diff --git a/web/oss/src/styles/theme/controlScale.ts b/web/oss/src/styles/theme/controlScale.ts new file mode 100644 index 0000000000..f6c09a53bb --- /dev/null +++ b/web/oss/src/styles/theme/controlScale.ts @@ -0,0 +1,89 @@ +/** + * Control geometry scale — the ONE definition of control dimensions. + * + * Shared because two toolchains consume it: web/oss (Tailwind v3, JS config) spreads it into + * `theme.extend`, and web/mobile (Tailwind v4, CSS-first) emits it as `@theme` custom + * properties. Keeping it in the v3 config forced mobile to copy the pixel values by hand, + * which drifts silently — a shared component would then render at different sizes per app. + */ +// ── Control scale ──────────────────────────────────────────────── +// Geometry + typography for the shadcn control primitives (Button/Input/Badge). +// The single place to retune control sizing: components reference these names and never +// raw pixels. Namespaced (`control-*`, `btn-*`, `input-*`) and purely ADDITIVE, exactly +// like the `tremor-*` scale below, so no existing `h-*`/`rounded-*`/`text-*` changes. +// +// Values are theme-invariant (they don't flip light/dark), which is why they live here +// rather than in palette.ts. They currently encode antd's measured geometry so the +// migration is visually neutral — retune HERE once antd is gone, not in the components. +// +// v4 note: this whole object becomes a `@theme` block; the class names do not change. +export const controlScale = { + height: { + "control-sm": "24px", + control: "28px", + "control-lg": "34px", + // antd Switch track (default/small) + handle. Own dims — not the control heights. + switch: "22px", + "switch-sm": "16px", + "switch-thumb": "18px", + "switch-thumb-sm": "12px", + // antd Checkbox/Radio box (16px) + Radio inner dot (8px) + Checkbox indeterminate dash (7px). + "control-check": "16px", + "control-dot": "8px", + "control-check-dash": "7px", + }, + width: { + "control-sm": "24px", + control: "28px", + "control-lg": "34px", + // antd Switch min track width (default/small) + handle. + switch: "44px", + "switch-sm": "28px", + "switch-thumb": "18px", + "switch-thumb-sm": "12px", + // antd Checkbox/Radio box (16px) + Radio inner dot (8px) + Checkbox indeterminate dash (7px). + "control-check": "16px", + "control-dot": "8px", + "control-check-dash": "7px", + }, + borderRadius: { + "control-sm": "6px", + control: "8px", + "control-lg": "10px", + // antd shape="circle". 50%, not 9999px — they render identically on a square but + // the parity gate compares computed values. + "control-round": "50%", + }, + spacing: { + // Horizontal padding differs between buttons and inputs at md/lg, so they are + // separate families rather than one fudged scale. + "btn-sm": "7px", + btn: "15px", + "btn-lg": "15px", + "input-sm": "7px", + input: "11px", + "input-lg": "11px", + // Vertical padding for inputs: heights are padding + line-height derived (antd's + // model), not fixed. `ghost` adds 1px to compensate for its missing border. + "input-y-sm": "0px", + "input-y": "4px", + "input-y-lg": "7px", + "input-y-ghost-sm": "1px", + "input-y-ghost": "5px", + "input-y-ghost-lg": "8px", + }, + // Type ramps. Buttons and fields differ at `sm` (12px vs 10px), so they are separate + // ramps rather than one averaged scale. Names avoid every colour-token name, because + // `text-*` is shared between font-size and text-colour utilities. + // Exact ratios, not rounded decimals: 1.8667 computes to 22.4004px where antd renders + // 22.4 and the parity gate flags the difference. + fontSize: { + "btn-sm": ["12px", {lineHeight: "normal"}] as [string, {lineHeight: string}], + "btn-md": ["12px", {lineHeight: "normal"}] as [string, {lineHeight: string}], + "btn-lg": ["14px", {lineHeight: "normal"}] as [string, {lineHeight: string}], + "field-sm": ["10px", {lineHeight: "1.6666666666666667"}] as [string, {lineHeight: string}], + "field-md": ["12px", {lineHeight: "1.6666666666666667"}] as [string, {lineHeight: string}], + "field-lg": ["14px", {lineHeight: "1.5714285714285714"}] as [string, {lineHeight: string}], + "badge-md": ["12px", {lineHeight: "1.8666666666666667"}] as [string, {lineHeight: string}], + }, +} diff --git a/web/oss/src/styles/theme/shadcnTokens.ts b/web/oss/src/styles/theme/shadcnTokens.ts new file mode 100644 index 0000000000..a9f9365243 --- /dev/null +++ b/web/oss/src/styles/theme/shadcnTokens.ts @@ -0,0 +1,119 @@ +/** + * Token contract for the shadcn primitives in @agenta/ui — utility name → CSS variable. + * + * Three consumers compile this: web/oss (Tailwind v3 `theme.extend.colors`), Storybook, and + * web/mobile (Tailwind v4, via scripts/generate-shadcn-tokens.ts). Shared so a component + * written against `bg-btn-default-bg` resolves identically everywhere, instead of each app + * maintaining its own alias table and drifting. + */ +// ── shadcn token bridge ────────────────────────────────────────── +// Semantic token names consumed by the shadcn components in +// @agenta/ui, mapped to the palette-derived --ag-* layer (theme-variables.css, +// generated from palette.ts). ONE shared definition for the app + Storybook: flip +// the underlying layer and every component follows; when antd is removed nothing here +// changes. Purely ADDITIVE — the app uses `colorX`-named + default Tailwind classes, +// never these shadcn names, so nothing is overridden. Radius is intentionally NOT +// bridged (theme-invariant; overriding it would change every `rounded-*` in the app). +export const shadcnTokens = { + background: "var(--ag-colorBgContainer)", + foreground: "var(--ag-colorText)", + border: "var(--ag-colorBorder)", + input: "var(--ag-colorBorder)", + ring: "var(--ag-colorPrimary)", + // antd's keyboard focus ring is `colorPrimaryBorder` (4px, offset 1px). NOTE: it equals + // colorInfoBorder in LIGHT (both #d6dee6) but diverges in DARK (olive vs navy) — needs its + // own token, which the forced-state gate caught. + "focus-ring": "var(--ag-colorPrimaryBorder)", + primary: { + DEFAULT: "var(--ag-colorPrimary)", + // antd fills a CHECKED control with colorPrimaryHover on hover (Radio/Checkbox). + hover: "var(--ag-colorPrimaryHover)", + foreground: "var(--ag-colorTextLightSolid)", + }, + secondary: {DEFAULT: "var(--ag-colorFillSecondary)", foreground: "var(--ag-colorText)"}, + muted: {DEFAULT: "var(--ag-colorFillTertiary)", foreground: "var(--ag-colorTextSecondary)"}, + accent: {DEFAULT: "var(--ag-colorFillSecondary)", foreground: "var(--ag-colorText)"}, + destructive: {DEFAULT: "var(--ag-colorError)", foreground: "var(--ag-colorTextLightSolid)"}, + popover: {DEFAULT: "var(--ag-colorBgElevated)", foreground: "var(--ag-colorText)"}, + card: {DEFAULT: "var(--ag-colorBgContainer)", foreground: "var(--ag-colorText)"}, + placeholder: "var(--ag-colorTextPlaceholder)", + // Disabled + interaction states used by the control primitives. + disabled: { + DEFAULT: "var(--ag-colorTextDisabled)", + bg: "var(--ag-colorBgContainerDisabled)", + border: "var(--ag-input-disabled-border)", + }, + // Semantic status families (Badge/Tag/status chips): {DEFAULT = text, bg, border}. + success: { + DEFAULT: "var(--ag-colorSuccess)", + bg: "var(--ag-colorSuccessBg)", + border: "var(--ag-colorSuccessBorder)", + }, + warning: { + DEFAULT: "var(--ag-colorWarningText)", + bg: "var(--ag-colorWarningBg)", + border: "var(--ag-colorWarningBorder)", + }, + error: { + DEFAULT: "var(--ag-colorError)", + bg: "var(--ag-colorErrorBg)", + border: "var(--ag-colorErrorBorder)", + hover: "var(--ag-colorErrorHover)", + active: "var(--ag-colorErrorActive)", + }, + info: { + DEFAULT: "var(--ag-colorInfo)", + bg: "var(--ag-colorInfoBg)", + border: "var(--ag-colorInfoBorder)", + }, + draft: { + DEFAULT: "var(--ag-draft-text)", + bg: "var(--ag-draft-bg)", + border: "var(--ag-draft-border)", + }, + // antd preset Tag colors (color="blue"/"green"). Namespaced `tag-*` to avoid + // clobbering Tailwind's default `blue`/`green` scales (used across the app). + "tag-blue": {DEFAULT: "var(--ag-preset-blue-text)", bg: "var(--ag-preset-blue-bg)"}, + "tag-green": {DEFAULT: "var(--ag-preset-green-text)", bg: "var(--ag-preset-green-bg)"}, + "tag-orange": {DEFAULT: "var(--ag-preset-orange-text)", bg: "var(--ag-preset-orange-bg)"}, + "tag-red": {DEFAULT: "var(--ag-preset-red-text)", bg: "var(--ag-preset-red-bg)"}, + "tag-purple": {DEFAULT: "var(--ag-preset-purple-text)", bg: "var(--ag-preset-purple-bg)"}, + "tag-pink": {DEFAULT: "var(--ag-preset-pink-text)", bg: "var(--ag-preset-pink-bg)"}, + "tag-yellow": {DEFAULT: "var(--ag-preset-yellow-text)", bg: "var(--ag-preset-yellow-bg)"}, + "tag-volcano": {DEFAULT: "var(--ag-preset-volcano-text)", bg: "var(--ag-preset-volcano-bg)"}, + "tag-geekblue": {DEFAULT: "var(--ag-preset-geekblue-text)", bg: "var(--ag-preset-geekblue-bg)"}, + "tag-lime": {DEFAULT: "var(--ag-preset-lime-text)", bg: "var(--ag-preset-lime-bg)"}, + "tag-cyan": {DEFAULT: "var(--ag-preset-cyan-text)", bg: "var(--ag-preset-cyan-bg)"}, + "tag-magenta": {DEFAULT: "var(--ag-preset-magenta-text)", bg: "var(--ag-preset-magenta-bg)"}, + "tag-gold": {DEFAULT: "var(--ag-preset-gold-text)", bg: "var(--ag-preset-gold-bg)"}, + // Button state tokens. + btn: { + "primary-fg": "var(--ag-btn-primary-text)", + "primary-hover": "var(--ag-btn-primary-hover)", + "primary-active": "var(--ag-btn-primary-active)", + "default-bg": "var(--ag-btn-default-bg)", + "default-hover-bg": "var(--ag-btn-default-hover-bg)", + "default-active-bg": "var(--ag-btn-default-active-bg)", + "text-hover-bg": "var(--ag-btn-text-hover-bg)", + "text-active-bg": "var(--ag-btn-text-active-bg)", + link: "var(--ag-btn-link)", + "link-hover": "var(--ag-btn-link-hover)", + "link-active": "var(--ag-btn-link-active)", + }, + // antd Empty's DEFAULT illustration greys (opaque — see palette.ts `emptyImage`). + empty: { + shadow: "var(--ag-empty-shadow)", + sheet: "var(--ag-empty-sheet)", + flap: "var(--ag-empty-flap)", + front: "var(--ag-empty-front)", + hole: "var(--ag-empty-hole)", + }, + // Fills. + chip: "var(--ag-colorFillChip)", + fill: { + DEFAULT: "var(--ag-colorFill)", + secondary: "var(--ag-colorFillSecondary)", + tertiary: "var(--ag-colorFillTertiary)", + quaternary: "var(--ag-colorFillQuaternary)", + }, +} diff --git a/web/oss/tailwind.config.ts b/web/oss/tailwind.config.ts index 76f0cca1d9..c97964af27 100644 --- a/web/oss/tailwind.config.ts +++ b/web/oss/tailwind.config.ts @@ -3,6 +3,8 @@ import type {Config} from "tailwindcss" import colors from "tailwindcss/colors" import antdTailwind from "./src/styles/tokens/antd-tailwind.json" +import {controlScale} from "./src/styles/theme/controlScale" +import {shadcnTokens} from "./src/styles/theme/shadcnTokens" const token = theme.getDesignToken() // Theme-aware colors backed by CSS variables defined in styles/theme-variables.css. @@ -127,200 +129,6 @@ const themeAwareColors = { colorWhite: v("colorWhite"), } -// ── Control scale ──────────────────────────────────────────────── -// Geometry + typography for the shadcn control primitives (Button/Input/Badge). -// The single place to retune control sizing: components reference these names and never -// raw pixels. Namespaced (`control-*`, `btn-*`, `input-*`) and purely ADDITIVE, exactly -// like the `tremor-*` scale below, so no existing `h-*`/`rounded-*`/`text-*` changes. -// -// Values are theme-invariant (they don't flip light/dark), which is why they live here -// rather than in palette.ts. They currently encode antd's measured geometry so the -// migration is visually neutral — retune HERE once antd is gone, not in the components. -// -// v4 note: this whole object becomes a `@theme` block; the class names do not change. -const controlScale = { - height: { - "control-sm": "24px", - control: "28px", - "control-lg": "34px", - // antd Switch track (default/small) + handle. Own dims — not the control heights. - switch: "22px", - "switch-sm": "16px", - "switch-thumb": "18px", - "switch-thumb-sm": "12px", - // antd Checkbox/Radio box (16px) + Radio inner dot (8px) + Checkbox indeterminate dash (7px). - "control-check": "16px", - "control-dot": "8px", - "control-check-dash": "7px", - }, - width: { - "control-sm": "24px", - control: "28px", - "control-lg": "34px", - // antd Switch min track width (default/small) + handle. - switch: "44px", - "switch-sm": "28px", - "switch-thumb": "18px", - "switch-thumb-sm": "12px", - // antd Checkbox/Radio box (16px) + Radio inner dot (8px) + Checkbox indeterminate dash (7px). - "control-check": "16px", - "control-dot": "8px", - "control-check-dash": "7px", - }, - borderRadius: { - "control-sm": "6px", - control: "8px", - "control-lg": "10px", - // antd shape="circle". 50%, not 9999px — they render identically on a square but - // the parity gate compares computed values. - "control-round": "50%", - }, - spacing: { - // Horizontal padding differs between buttons and inputs at md/lg, so they are - // separate families rather than one fudged scale. - "btn-sm": "7px", - btn: "15px", - "btn-lg": "15px", - "input-sm": "7px", - input: "11px", - "input-lg": "11px", - // Vertical padding for inputs: heights are padding + line-height derived (antd's - // model), not fixed. `ghost` adds 1px to compensate for its missing border. - "input-y-sm": "0px", - "input-y": "4px", - "input-y-lg": "7px", - "input-y-ghost-sm": "1px", - "input-y-ghost": "5px", - "input-y-ghost-lg": "8px", - }, - // Type ramps. Buttons and fields differ at `sm` (12px vs 10px), so they are separate - // ramps rather than one averaged scale. Names avoid every colour-token name, because - // `text-*` is shared between font-size and text-colour utilities. - // Exact ratios, not rounded decimals: 1.8667 computes to 22.4004px where antd renders - // 22.4 and the parity gate flags the difference. - fontSize: { - "btn-sm": ["12px", {lineHeight: "normal"}] as [string, {lineHeight: string}], - "btn-md": ["12px", {lineHeight: "normal"}] as [string, {lineHeight: string}], - "btn-lg": ["14px", {lineHeight: "normal"}] as [string, {lineHeight: string}], - "field-sm": ["10px", {lineHeight: "1.6666666666666667"}] as [string, {lineHeight: string}], - "field-md": ["12px", {lineHeight: "1.6666666666666667"}] as [string, {lineHeight: string}], - "field-lg": ["14px", {lineHeight: "1.5714285714285714"}] as [string, {lineHeight: string}], - "badge-md": ["12px", {lineHeight: "1.8666666666666667"}] as [string, {lineHeight: string}], - }, -} - -// ── shadcn token bridge ────────────────────────────────────────── -// Semantic token names consumed by the shadcn components in -// @agenta/ui, mapped to the palette-derived --ag-* layer (theme-variables.css, -// generated from palette.ts). ONE shared definition for the app + Storybook: flip -// the underlying layer and every component follows; when antd is removed nothing here -// changes. Purely ADDITIVE — the app uses `colorX`-named + default Tailwind classes, -// never these shadcn names, so nothing is overridden. Radius is intentionally NOT -// bridged (theme-invariant; overriding it would change every `rounded-*` in the app). -const shadcnTokens = { - background: "var(--ag-colorBgContainer)", - foreground: "var(--ag-colorText)", - border: "var(--ag-colorBorder)", - input: "var(--ag-colorBorder)", - ring: "var(--ag-colorPrimary)", - // antd's keyboard focus ring is `colorPrimaryBorder` (4px, offset 1px). NOTE: it equals - // colorInfoBorder in LIGHT (both #d6dee6) but diverges in DARK (olive vs navy) — needs its - // own token, which the forced-state gate caught. - "focus-ring": "var(--ag-colorPrimaryBorder)", - primary: { - DEFAULT: "var(--ag-colorPrimary)", - // antd fills a CHECKED control with colorPrimaryHover on hover (Radio/Checkbox). - hover: "var(--ag-colorPrimaryHover)", - foreground: "var(--ag-colorTextLightSolid)", - }, - secondary: {DEFAULT: "var(--ag-colorFillSecondary)", foreground: "var(--ag-colorText)"}, - muted: {DEFAULT: "var(--ag-colorFillTertiary)", foreground: "var(--ag-colorTextSecondary)"}, - accent: {DEFAULT: "var(--ag-colorFillSecondary)", foreground: "var(--ag-colorText)"}, - destructive: {DEFAULT: "var(--ag-colorError)", foreground: "var(--ag-colorTextLightSolid)"}, - popover: {DEFAULT: "var(--ag-colorBgElevated)", foreground: "var(--ag-colorText)"}, - card: {DEFAULT: "var(--ag-colorBgContainer)", foreground: "var(--ag-colorText)"}, - placeholder: "var(--ag-colorTextPlaceholder)", - // Disabled + interaction states used by the control primitives. - disabled: { - DEFAULT: "var(--ag-colorTextDisabled)", - bg: "var(--ag-colorBgContainerDisabled)", - border: "var(--ag-input-disabled-border)", - }, - // Semantic status families (Badge/Tag/status chips): {DEFAULT = text, bg, border}. - success: { - DEFAULT: "var(--ag-colorSuccess)", - bg: "var(--ag-colorSuccessBg)", - border: "var(--ag-colorSuccessBorder)", - }, - warning: { - DEFAULT: "var(--ag-colorWarningText)", - bg: "var(--ag-colorWarningBg)", - border: "var(--ag-colorWarningBorder)", - }, - error: { - DEFAULT: "var(--ag-colorError)", - bg: "var(--ag-colorErrorBg)", - border: "var(--ag-colorErrorBorder)", - hover: "var(--ag-colorErrorHover)", - active: "var(--ag-colorErrorActive)", - }, - info: { - DEFAULT: "var(--ag-colorInfo)", - bg: "var(--ag-colorInfoBg)", - border: "var(--ag-colorInfoBorder)", - }, - draft: { - DEFAULT: "var(--ag-draft-text)", - bg: "var(--ag-draft-bg)", - border: "var(--ag-draft-border)", - }, - // antd preset Tag colors (color="blue"/"green"). Namespaced `tag-*` to avoid - // clobbering Tailwind's default `blue`/`green` scales (used across the app). - "tag-blue": {DEFAULT: "var(--ag-preset-blue-text)", bg: "var(--ag-preset-blue-bg)"}, - "tag-green": {DEFAULT: "var(--ag-preset-green-text)", bg: "var(--ag-preset-green-bg)"}, - "tag-orange": {DEFAULT: "var(--ag-preset-orange-text)", bg: "var(--ag-preset-orange-bg)"}, - "tag-red": {DEFAULT: "var(--ag-preset-red-text)", bg: "var(--ag-preset-red-bg)"}, - "tag-purple": {DEFAULT: "var(--ag-preset-purple-text)", bg: "var(--ag-preset-purple-bg)"}, - "tag-pink": {DEFAULT: "var(--ag-preset-pink-text)", bg: "var(--ag-preset-pink-bg)"}, - "tag-yellow": {DEFAULT: "var(--ag-preset-yellow-text)", bg: "var(--ag-preset-yellow-bg)"}, - "tag-volcano": {DEFAULT: "var(--ag-preset-volcano-text)", bg: "var(--ag-preset-volcano-bg)"}, - "tag-geekblue": {DEFAULT: "var(--ag-preset-geekblue-text)", bg: "var(--ag-preset-geekblue-bg)"}, - "tag-lime": {DEFAULT: "var(--ag-preset-lime-text)", bg: "var(--ag-preset-lime-bg)"}, - "tag-cyan": {DEFAULT: "var(--ag-preset-cyan-text)", bg: "var(--ag-preset-cyan-bg)"}, - "tag-magenta": {DEFAULT: "var(--ag-preset-magenta-text)", bg: "var(--ag-preset-magenta-bg)"}, - "tag-gold": {DEFAULT: "var(--ag-preset-gold-text)", bg: "var(--ag-preset-gold-bg)"}, - // Button state tokens. - btn: { - "primary-fg": "var(--ag-btn-primary-text)", - "primary-hover": "var(--ag-btn-primary-hover)", - "primary-active": "var(--ag-btn-primary-active)", - "default-bg": "var(--ag-btn-default-bg)", - "default-hover-bg": "var(--ag-btn-default-hover-bg)", - "default-active-bg": "var(--ag-btn-default-active-bg)", - "text-hover-bg": "var(--ag-btn-text-hover-bg)", - "text-active-bg": "var(--ag-btn-text-active-bg)", - link: "var(--ag-btn-link)", - "link-hover": "var(--ag-btn-link-hover)", - "link-active": "var(--ag-btn-link-active)", - }, - // antd Empty's DEFAULT illustration greys (opaque — see palette.ts `emptyImage`). - empty: { - shadow: "var(--ag-empty-shadow)", - sheet: "var(--ag-empty-sheet)", - flap: "var(--ag-empty-flap)", - front: "var(--ag-empty-front)", - hole: "var(--ag-empty-hole)", - }, - // Fills. - chip: "var(--ag-colorFillChip)", - fill: { - DEFAULT: "var(--ag-colorFill)", - secondary: "var(--ag-colorFillSecondary)", - tertiary: "var(--ag-colorFillTertiary)", - quaternary: "var(--ag-colorFillQuaternary)", - }, -} - export const createConfig = (content: string[] = []): Config => { return { darkMode: "selector",