diff --git a/.cursor/rules/v2-color-scale.mdc b/.cursor/rules/v2-color-scale.mdc new file mode 100644 index 000000000..9241445c0 --- /dev/null +++ b/.cursor/rules/v2-color-scale.mdc @@ -0,0 +1,19 @@ +--- +description: v2 UI color system — Radix numbered scale, no hardcoded colors, no manual dark mode +globs: "packages/ui/src/v2/**/*.{ts,tsx}" +alwaysApply: false +--- + +# v2 color scale + +Use the Radix 12-step numbered scale for all colors in v2 components: `sand`, `gold`, `red`, `green`, `amber`, `sky` (steps 1–12). + +## Rules + +- Use scale classes (`bg-sand-3`, `text-red-11`, `border-gold-7`) — never v1 semantic names (`txt-primary`, `border-low`, `bg-primary`, etc.) +- Never hardcode hex/rgb/oklch values for colors that exist in the scale +- Never use the `dark:` variant prefix for v2 color overrides — dark mode is handled by the `.dark` class on `` via Radix CSS imports +- Do not mix v1 and v2 color classes in the same component +- Respect step ranges: 1–2 backgrounds, 3–5 component bg, 6–8 borders, 9–10 solid bg, 11–12 text + +Full guide: `contrib/claude/v2-colors.md` diff --git a/AGENTS.md b/AGENTS.md index 5a178d58c..d56223331 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ Detailed guides for specific subsystems live in `contrib/claude/`: - [`contrib/claude/relay.md`](contrib/claude/relay.md) — Frontend Relay client (queries, fragments, mutations, pagination) - [`contrib/claude/react-components.md`](contrib/claude/react-components.md) — React component shape (file/export, props, configure vs data via hooks) - [`contrib/claude/ui.md`](contrib/claude/ui.md) — @probo/ui, Tailwind, tailwind-variants, folders, skeletons, compound components +- [`contrib/claude/v2-colors.md`](contrib/claude/v2-colors.md) — v2 color system (Radix scale, step usage, do/don't) - [`contrib/claude/config.md`](contrib/claude/config.md) — Configuration propagation (all files to update when config changes) - [`contrib/claude/file-naming.md`](contrib/claude/file-naming.md) — File naming conventions (template files, extensions) - [`contrib/claude/prompt-style.md`](contrib/claude/prompt-style.md) — Agent prompt template structure (role/task/instructions XML style) diff --git a/contrib/claude/v2-colors.md b/contrib/claude/v2-colors.md new file mode 100644 index 000000000..fcdbce28f --- /dev/null +++ b/contrib/claude/v2-colors.md @@ -0,0 +1,163 @@ +# v2 color system (Radix scale) + +The v2 UI kit uses [Radix Colors](https://www.radix-ui.com/colors) 12-step scales as its color primitive. Each hue provides 12 numbered steps designed for specific use cases. Components consume these through Tailwind utility classes (`bg-sand-3`, `text-red-11`, `border-gold-7`, …). + +Theme file: [`packages/ui/src/v2/theme.css`](../../packages/ui/src/v2/theme.css) + +## Available scales + +| Scale | Role | +|-------|------| +| **sand** | Neutral — primary UI chrome (backgrounds, borders, text) | +| **gold** | Warm accent neutral | +| **red** | Destructive / error | +| **green** | Success / positive | +| **amber** | Warning | +| **sky** | Informational | + +## Step-to-usage mapping + +Every scale follows the same 12-step structure: + +| Step | Use case | Tailwind example | +|------|----------|------------------| +| 1 | App background | `bg-sand-1` | +| 2 | Subtle background | `bg-sand-2` | +| 3 | UI element background | `bg-sand-3` | +| 4 | Hovered UI element background | `hover:bg-sand-4` | +| 5 | Active / selected UI element background | `bg-sand-5` | +| 6 | Subtle borders and separators | `border-sand-6` | +| 7 | UI element border and focus rings | `border-sand-7` | +| 8 | Hovered UI element border | `border-sand-8` | +| 9 | Solid backgrounds | `bg-green-9` | +| 10 | Hovered solid backgrounds | `hover:bg-green-10` | +| 11 | Low-contrast text | `text-sand-11` | +| 12 | High-contrast text | `text-sand-12` | + +### Quick mental model + +Three bands: **low = light/background**, **middle = borders**, **high = text/solid**. + +- **1–2** → backgrounds +- **3–5** → component backgrounds (normal → hover → active) +- **6–8** → borders (subtle → default → strong) +- **9–10** → solid backgrounds (normal → hover) +- **11–12** → text (low-contrast → high-contrast) + +## Choosing a color step + +1. **What am I styling?** + - Background → steps 1–5 (or 9–10 for solid fills) + - Border → steps 6–8 + - Text / icon → steps 11–12 +2. **What state?** + - Default → lower step in the range (3, 6, 9, 11) + - Hover → next step up (4, 7, 10) + - Active / pressed → one more (5, 8) +3. **Which hue?** + - Neutral UI → `sand` + - Semantic meaning → `red` (error), `green` (success), `amber` (warning), `sky` (info) + - Warm accent → `gold` + +## Neutral vs accent + +Use **sand** for all general UI chrome: page backgrounds, card backgrounds, borders, primary text. Use hue scales only when conveying semantic meaning: + +```tsx +// Neutral card +
+

Title

+

Description

+
+ +// Error state +
+

Something went wrong

+
+ +// Success badge +Approved +``` + +## Contrast guarantees + +Per the Radix spec, steps 11 and 12 are guaranteed to meet APCA contrast requirements on top of a step 1 or 2 background from the same scale. This means `text-sand-11` on `bg-sand-2` is always readable, and `text-sand-12` on `bg-sand-1` is always readable. + +## Dark mode + +**Never apply dark-mode color overrides in components.** The v2 theme imports `@radix-ui/colors` CSS files which handle light/dark switching automatically. Dark mode activates when a `.dark` class is present on ``: + +```ts +document.documentElement.classList.toggle("dark", isDark); +``` + +The same Tailwind classes (`bg-sand-1`, `text-red-11`, etc.) resolve to the correct dark values automatically because the `@theme inline` mappings reference the Radix variables (`var(--sand-1)`, etc.) which switch based on the `.dark` class. P3 wide-gamut colors are included for both light and dark modes on supported displays. + +This is independent of v1's dark mode which uses `@variant dark` / `prefers-color-scheme`. + +## `data-theme="v2"` boundary + +All v2 components must render inside a `[data-theme="v2"]` ancestor. This boundary scopes v2 token values (colors now, fonts/spacing/shadows later) so they don't collide with the v1 theme during progressive migration. + +```html +
+ +
+``` + +## Do / don't + +### Use the numbered scale + +```tsx +// Good — numbered scale step +
...
+ +// Bad — hardcoded hex +
...
+ +// Bad — v1 semantic color names in a v2 component +
...
+``` + +### Respect step ranges + +```tsx +// Good — step 3 for element background, step 11 for text + + +// Bad — step 11 is a text step, not a background step + +``` + +### Do not mix v1 and v2 colors + +```tsx +// Bad — mixing v1 (txt-primary) and v2 (sand-3) in one component +
...
+ +// Good — all v2 +
...
+``` + +### Let the theme handle dark mode + +```tsx +// Bad — manual dark: overrides for v2 colors +
...
+ +// Good — just use the scale; dark values come from the theme scope +
...
+``` + +### Solid backgrounds (steps 9–10) + +Steps 9 and 10 are designed for prominent, solid-color backgrounds (primary buttons, badges, banners). Most step 9 colors are designed for white foreground text. Exceptions: **sky**, **amber** are designed for dark foreground text on steps 9–10. + +```tsx +// Good — green solid button with white text + + +// Good — amber badge with dark text (amber 9-10 are light/bright) +Warning +``` diff --git a/package-lock.json b/package-lock.json index 8b650dc01..39e081321 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4229,6 +4229,12 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -22269,6 +22275,7 @@ "@probo/helpers": "1.0.0", "@probo/i18n": "1.0.0", "@probo/prosemirror": "1.0.0", + "@radix-ui/colors": "^3.0.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/packages/ui/package.json b/packages/ui/package.json index 8cbafc048..edbf10697 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,6 +18,7 @@ "@probo/helpers": "1.0.0", "@probo/i18n": "1.0.0", "@probo/prosemirror": "1.0.0", + "@radix-ui/colors": "^3.0.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/packages/ui/src/v2/theme.css b/packages/ui/src/v2/theme.css new file mode 100644 index 000000000..7cfdd3578 --- /dev/null +++ b/packages/ui/src/v2/theme.css @@ -0,0 +1,254 @@ +/* Copyright (c) 2026 Probo Inc . + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * v2 color system — Radix 12-step scale + * + * Each hue has 12 steps designed for specific use cases: + * + * Step Use case + * ────── ────────────────────────────────────── + * 1 App background + * 2 Subtle background + * 3 UI element background + * 4 Hovered UI element background + * 5 Active / selected UI element background + * 6 Subtle borders and separators + * 7 UI element border and focus rings + * 8 Hovered UI element border + * 9 Solid backgrounds + * 10 Hovered solid backgrounds + * 11 Low-contrast text + * 12 High-contrast text + * + * Scales: + * sand — neutral (primary UI chrome) + * gold — warm accent neutral + * red — destructive / error + * green — success / positive + * amber — warning + * sky — informational + * + * Dark mode: toggle .dark on . Radix dark imports handle the rest. + * P3 wide gamut: included automatically via @radix-ui/colors @supports blocks. + * + * See contrib/claude/v2-colors.md for the full usage guide. + */ + +/* --------------------------------------------------------------------------- + * Radix color imports (light → :root, dark → .dark) + * + * These define --sand-1 … --sand-12, --gold-1 … etc. as raw CSS variables. + * P3 wide-gamut overrides are included via @supports in each file. + * ------------------------------------------------------------------------- */ +@import "@radix-ui/colors/sand.css"; +@import "@radix-ui/colors/sand-dark.css"; +@import "@radix-ui/colors/gold.css"; +@import "@radix-ui/colors/gold-dark.css"; +@import "@radix-ui/colors/red.css"; +@import "@radix-ui/colors/red-dark.css"; +@import "@radix-ui/colors/green.css"; +@import "@radix-ui/colors/green-dark.css"; +@import "@radix-ui/colors/amber.css"; +@import "@radix-ui/colors/amber-dark.css"; +@import "@radix-ui/colors/sky.css"; +@import "@radix-ui/colors/sky-dark.css"; + +/* --------------------------------------------------------------------------- + * Register v2 color names with Tailwind (generates utility classes) + * + * Maps --sand-1 → --color-sand-1 so Tailwind generates bg-sand-1, text-sand-1, + * border-sand-1, etc. The `inline` keyword makes utilities reference + * var(--sand-1) directly, so light/dark switching at the Radix level + * propagates automatically. + * ------------------------------------------------------------------------- */ +@theme inline { + /* sand */ + --color-sand-1: var(--sand-1); + --color-sand-2: var(--sand-2); + --color-sand-3: var(--sand-3); + --color-sand-4: var(--sand-4); + --color-sand-5: var(--sand-5); + --color-sand-6: var(--sand-6); + --color-sand-7: var(--sand-7); + --color-sand-8: var(--sand-8); + --color-sand-9: var(--sand-9); + --color-sand-10: var(--sand-10); + --color-sand-11: var(--sand-11); + --color-sand-12: var(--sand-12); + + /* gold */ + --color-gold-1: var(--gold-1); + --color-gold-2: var(--gold-2); + --color-gold-3: var(--gold-3); + --color-gold-4: var(--gold-4); + --color-gold-5: var(--gold-5); + --color-gold-6: var(--gold-6); + --color-gold-7: var(--gold-7); + --color-gold-8: var(--gold-8); + --color-gold-9: var(--gold-9); + --color-gold-10: var(--gold-10); + --color-gold-11: var(--gold-11); + --color-gold-12: var(--gold-12); + + /* red */ + --color-red-1: var(--red-1); + --color-red-2: var(--red-2); + --color-red-3: var(--red-3); + --color-red-4: var(--red-4); + --color-red-5: var(--red-5); + --color-red-6: var(--red-6); + --color-red-7: var(--red-7); + --color-red-8: var(--red-8); + --color-red-9: var(--red-9); + --color-red-10: var(--red-10); + --color-red-11: var(--red-11); + --color-red-12: var(--red-12); + + /* green */ + --color-green-1: var(--green-1); + --color-green-2: var(--green-2); + --color-green-3: var(--green-3); + --color-green-4: var(--green-4); + --color-green-5: var(--green-5); + --color-green-6: var(--green-6); + --color-green-7: var(--green-7); + --color-green-8: var(--green-8); + --color-green-9: var(--green-9); + --color-green-10: var(--green-10); + --color-green-11: var(--green-11); + --color-green-12: var(--green-12); + + /* amber */ + --color-amber-1: var(--amber-1); + --color-amber-2: var(--amber-2); + --color-amber-3: var(--amber-3); + --color-amber-4: var(--amber-4); + --color-amber-5: var(--amber-5); + --color-amber-6: var(--amber-6); + --color-amber-7: var(--amber-7); + --color-amber-8: var(--amber-8); + --color-amber-9: var(--amber-9); + --color-amber-10: var(--amber-10); + --color-amber-11: var(--amber-11); + --color-amber-12: var(--amber-12); + + /* sky */ + --color-sky-1: var(--sky-1); + --color-sky-2: var(--sky-2); + --color-sky-3: var(--sky-3); + --color-sky-4: var(--sky-4); + --color-sky-5: var(--sky-5); + --color-sky-6: var(--sky-6); + --color-sky-7: var(--sky-7); + --color-sky-8: var(--sky-8); + --color-sky-9: var(--sky-9); + --color-sky-10: var(--sky-10); + --color-sky-11: var(--sky-11); + --color-sky-12: var(--sky-12); +} + +/* --------------------------------------------------------------------------- + * v2 boundary scope + * + * All v2 tokens resolve from this scope at runtime. Colors reference the Radix + * variables so light/dark switching is automatic. Future non-color tokens + * (fonts, spacing, shadows, radii) will go here too. + * ------------------------------------------------------------------------- */ +[data-theme="v2"] { + /* sand */ + --color-sand-1: var(--sand-1); + --color-sand-2: var(--sand-2); + --color-sand-3: var(--sand-3); + --color-sand-4: var(--sand-4); + --color-sand-5: var(--sand-5); + --color-sand-6: var(--sand-6); + --color-sand-7: var(--sand-7); + --color-sand-8: var(--sand-8); + --color-sand-9: var(--sand-9); + --color-sand-10: var(--sand-10); + --color-sand-11: var(--sand-11); + --color-sand-12: var(--sand-12); + + /* gold */ + --color-gold-1: var(--gold-1); + --color-gold-2: var(--gold-2); + --color-gold-3: var(--gold-3); + --color-gold-4: var(--gold-4); + --color-gold-5: var(--gold-5); + --color-gold-6: var(--gold-6); + --color-gold-7: var(--gold-7); + --color-gold-8: var(--gold-8); + --color-gold-9: var(--gold-9); + --color-gold-10: var(--gold-10); + --color-gold-11: var(--gold-11); + --color-gold-12: var(--gold-12); + + /* red */ + --color-red-1: var(--red-1); + --color-red-2: var(--red-2); + --color-red-3: var(--red-3); + --color-red-4: var(--red-4); + --color-red-5: var(--red-5); + --color-red-6: var(--red-6); + --color-red-7: var(--red-7); + --color-red-8: var(--red-8); + --color-red-9: var(--red-9); + --color-red-10: var(--red-10); + --color-red-11: var(--red-11); + --color-red-12: var(--red-12); + + /* green */ + --color-green-1: var(--green-1); + --color-green-2: var(--green-2); + --color-green-3: var(--green-3); + --color-green-4: var(--green-4); + --color-green-5: var(--green-5); + --color-green-6: var(--green-6); + --color-green-7: var(--green-7); + --color-green-8: var(--green-8); + --color-green-9: var(--green-9); + --color-green-10: var(--green-10); + --color-green-11: var(--green-11); + --color-green-12: var(--green-12); + + /* amber */ + --color-amber-1: var(--amber-1); + --color-amber-2: var(--amber-2); + --color-amber-3: var(--amber-3); + --color-amber-4: var(--amber-4); + --color-amber-5: var(--amber-5); + --color-amber-6: var(--amber-6); + --color-amber-7: var(--amber-7); + --color-amber-8: var(--amber-8); + --color-amber-9: var(--amber-9); + --color-amber-10: var(--amber-10); + --color-amber-11: var(--amber-11); + --color-amber-12: var(--amber-12); + + /* sky */ + --color-sky-1: var(--sky-1); + --color-sky-2: var(--sky-2); + --color-sky-3: var(--sky-3); + --color-sky-4: var(--sky-4); + --color-sky-5: var(--sky-5); + --color-sky-6: var(--sky-6); + --color-sky-7: var(--sky-7); + --color-sky-8: var(--sky-8); + --color-sky-9: var(--sky-9); + --color-sky-10: var(--sky-10); + --color-sky-11: var(--sky-11); + --color-sky-12: var(--sky-12); +}