diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts
index 0080307858..be45a915a8 100644
--- a/packages/client/src/components/index.ts
+++ b/packages/client/src/components/index.ts
@@ -4,6 +4,7 @@ export * from './Alert';
export * from './AlertDialog';
export * from './Breadcrumb';
export * from './Button';
+export * from './Chip';
export * from './Checkbox';
export * from './Dialog';
export * from './DropdownMenu';
@@ -30,6 +31,7 @@ export * from './Tooltip';
export * from './Pagination';
export * from './Progress';
export * from './InputOTP';
+export * from './IconButton';
export * from './MultiSearch';
export * from './Resizable';
export * from './Select';
diff --git a/packages/client/src/theme/README.md b/packages/client/src/theme/README.md
index 1fcdc57428..d9a2df3419 100644
--- a/packages/client/src/theme/README.md
+++ b/packages/client/src/theme/README.md
@@ -137,7 +137,9 @@ function MyComponent() {
return (
Hello World
- Submit
+
+ Submit
+
);
}
@@ -153,6 +155,7 @@ function MyComponent() {
- `text-text-tertiary` - Tertiary text color
- `text-text-warning` - Warning text color
- `text-text-destructive` - Destructive/error text color
+- `text-text-on-status` - Text color for strong status surfaces
### Surface Colors
@@ -162,6 +165,7 @@ function MyComponent() {
- `bg-surface-submit` - Submit button background
- `bg-surface-destructive` - Destructive action background
- `bg-surface-dialog` - Dialog/modal background
+- `bg-surface-overlay` - Dialog/modal scrim, adapted per theme
- `bg-surface-chat` - Chat interface background
### Border Colors
@@ -174,13 +178,15 @@ function MyComponent() {
### Status Colors
-Each status family has a foreground, a `-subtle` background, and a `-border`:
+Each status family has a foreground, a `-subtle` background, a `-border`, and a
+`-strong` surface for high-contrast notifications:
- `text-status-success` / `bg-status-success-subtle` / `border-status-success-border`
- `text-status-info` / `bg-status-info-subtle` / `border-status-info-border`
- `text-status-warning` / `bg-status-warning-subtle` / `border-status-warning-border`
- `text-status-error` / `bg-status-error-subtle` / `border-status-error-border`
- `text-status-neutral` / `bg-status-neutral-subtle` / `border-status-neutral-border`
+- `bg-status-success-strong` / `bg-status-info-strong` / `bg-status-warning-strong` / `bg-status-error-strong`
### Other Colors
diff --git a/packages/client/src/theme/semanticTokens.spec.ts b/packages/client/src/theme/semanticTokens.spec.ts
new file mode 100644
index 0000000000..883b668942
--- /dev/null
+++ b/packages/client/src/theme/semanticTokens.spec.ts
@@ -0,0 +1,28 @@
+import { join } from 'node:path';
+import { readFileSync } from 'node:fs';
+
+const sharedComponents = [
+ 'AnimatedSearchInput.tsx',
+ 'AlertDialog.tsx',
+ 'Button.tsx',
+ 'Chip.tsx',
+ 'Dialog.tsx',
+ 'IconButton.tsx',
+ 'Tag.tsx',
+ 'Toast.tsx',
+];
+
+describe('shared component color guardrail', () => {
+ it('keeps shared primitives free of direct palette utilities and hex colors', () => {
+ const directPalette =
+ /(?:bg|text|border|ring|from|via|to)-(?:gray|red|green|blue|purple|amber|yellow|orange|pink|indigo|violet|teal|cyan|slate|zinc|neutral|stone)-\d/;
+ const hexColor = /#[0-9a-f]{3,8}\b/i;
+
+ sharedComponents.forEach((component) => {
+ const source = readFileSync(join(__dirname, '..', 'components', component), 'utf8');
+
+ expect(source).not.toMatch(directPalette);
+ expect(source).not.toMatch(hexColor);
+ });
+ });
+});
diff --git a/packages/client/src/theme/themes/dark.ts b/packages/client/src/theme/themes/dark.ts
index 6d00ae5c5b..ec7b4b73ef 100644
--- a/packages/client/src/theme/themes/dark.ts
+++ b/packages/client/src/theme/themes/dark.ts
@@ -41,6 +41,7 @@ export const darkTheme: IThemeRGB = {
'rgb-surface-tertiary': '47 47 47', // #2f2f2f (gray-700)
'rgb-surface-tertiary-alt': '47 47 47', // #2f2f2f (gray-700)
'rgb-surface-dialog': '23 23 23', // #171717 (gray-850)
+ 'rgb-surface-overlay': '0 0 0', // #000 (black)
'rgb-surface-submit': '4 120 87', // #047857 (green-700)
'rgb-surface-submit-hover': '6 95 70', // #065f46 (green-800)
'rgb-surface-destructive': '153 27 27', // #991b1b (red-800)
@@ -65,18 +66,23 @@ export const darkTheme: IThemeRGB = {
'rgb-status-success': '110 231 183', // #6ee7b7 (green-300)
'rgb-status-success-subtle': '2 44 34', // #022c22 (green-950)
'rgb-status-success-border': '6 95 70', // #065f46 (green-800)
+ 'rgb-status-success-strong': '6 95 70', // #065f46 (green-800)
'rgb-status-info': '147 197 253', // #93c5fd (blue-300)
'rgb-status-info-subtle': '23 37 84', // #172554 (blue-950)
'rgb-status-info-border': '30 64 175', // #1e40af (blue-800)
+ 'rgb-status-info-strong': '66 66 66', // #424242 (gray-600)
'rgb-status-warning': '252 211 77', // #fcd34d (amber-300)
'rgb-status-warning-subtle': '69 26 3', // #451a03 (amber-950)
'rgb-status-warning-border': '146 64 14', // #92400e (amber-800)
+ 'rgb-status-warning-strong': '146 64 14', // #92400e (amber-800)
'rgb-status-error': '252 165 165', // #fca5a5 (red-300)
'rgb-status-error-subtle': '69 10 10', // #450a0a (red-950)
'rgb-status-error-border': '153 27 27', // #991b1b (red-800)
+ 'rgb-status-error-strong': '153 27 27', // #991b1b (red-800)
'rgb-status-neutral': '205 205 205', // #cdcdcd (gray-300)
'rgb-status-neutral-subtle': '33 33 33', // #212121 (gray-800)
'rgb-status-neutral-border': '47 47 47', // #2f2f2f (gray-700)
+ 'rgb-text-on-status': '255 255 255', // #fff (white)
// Brand colors
'rgb-brand-purple': '171 104 255', // #ab68ff
diff --git a/packages/client/src/theme/themes/default.ts b/packages/client/src/theme/themes/default.ts
index 2d131ad51f..3e2a4c75eb 100644
--- a/packages/client/src/theme/themes/default.ts
+++ b/packages/client/src/theme/themes/default.ts
@@ -41,6 +41,7 @@ export const defaultTheme: IThemeRGB = {
'rgb-surface-tertiary': '236 236 236', // #ececec (gray-100)
'rgb-surface-tertiary-alt': '255 255 255', // #fff (white)
'rgb-surface-dialog': '255 255 255', // #fff (white)
+ 'rgb-surface-overlay': '89 89 89', // #595959 (gray-500)
'rgb-surface-submit': '4 120 87', // #047857 (green-700)
'rgb-surface-submit-hover': '6 95 70', // #065f46 (green-800)
'rgb-surface-destructive': '185 28 28', // #b91c1c (red-700)
@@ -65,18 +66,23 @@ export const defaultTheme: IThemeRGB = {
'rgb-status-success': '5 150 105', // #059669 (green-600)
'rgb-status-success-subtle': '236 253 245', // #ecfdf5 (green-50)
'rgb-status-success-border': '110 231 183', // #6ee7b7 (green-300)
+ 'rgb-status-success-strong': '2 133 94', // #02855e
'rgb-status-info': '37 99 235', // #2563eb (blue-600)
'rgb-status-info-subtle': '239 246 255', // #eff6ff (blue-50)
'rgb-status-info-border': '147 197 253', // #93c5fd (blue-300)
+ 'rgb-status-info-strong': '89 89 89', // #595959 (gray-500)
'rgb-status-warning': '217 119 6', // #d97706 (amber-600)
'rgb-status-warning-subtle': '255 251 235', // #fffbeb (amber-50)
'rgb-status-warning-border': '252 211 77', // #fcd34d (amber-300)
+ 'rgb-status-warning-strong': '199 82 9', // #c75209
'rgb-status-error': '220 38 38', // #dc2626 (red-600)
'rgb-status-error-subtle': '254 242 242', // #fef2f2 (red-50)
'rgb-status-error-border': '252 165 165', // #fca5a5 (red-300)
+ 'rgb-status-error-strong': '224 47 31', // #e02f1f
'rgb-status-neutral': '66 66 66', // #424242 (gray-600)
'rgb-status-neutral-subtle': '236 236 236', // #ececec (gray-100)
'rgb-status-neutral-border': '205 205 205', // #cdcdcd (gray-300)
+ 'rgb-text-on-status': '255 255 255', // #fff (white)
// Brand colors
'rgb-brand-purple': '171 104 255', // #ab68ff
diff --git a/packages/client/src/theme/types/index.ts b/packages/client/src/theme/types/index.ts
index 2076793d94..15370055ab 100644
--- a/packages/client/src/theme/types/index.ts
+++ b/packages/client/src/theme/types/index.ts
@@ -39,6 +39,7 @@ export interface IThemeRGB {
'rgb-surface-tertiary'?: string;
'rgb-surface-tertiary-alt'?: string;
'rgb-surface-dialog'?: string;
+ 'rgb-surface-overlay'?: string;
'rgb-surface-submit'?: string;
'rgb-surface-submit-hover'?: string;
'rgb-surface-destructive'?: string;
@@ -63,18 +64,23 @@ export interface IThemeRGB {
'rgb-status-success'?: string;
'rgb-status-success-subtle'?: string;
'rgb-status-success-border'?: string;
+ 'rgb-status-success-strong'?: string;
'rgb-status-info'?: string;
'rgb-status-info-subtle'?: string;
'rgb-status-info-border'?: string;
+ 'rgb-status-info-strong'?: string;
'rgb-status-warning'?: string;
'rgb-status-warning-subtle'?: string;
'rgb-status-warning-border'?: string;
+ 'rgb-status-warning-strong'?: string;
'rgb-status-error'?: string;
'rgb-status-error-subtle'?: string;
'rgb-status-error-border'?: string;
+ 'rgb-status-error-strong'?: string;
'rgb-status-neutral'?: string;
'rgb-status-neutral-subtle'?: string;
'rgb-status-neutral-border'?: string;
+ 'rgb-text-on-status'?: string;
// Brand colors
'rgb-brand-purple'?: string;
@@ -114,6 +120,7 @@ export interface IThemeVariables {
'--surface-tertiary': string;
'--surface-tertiary-alt': string;
'--surface-dialog': string;
+ '--surface-overlay': string;
'--surface-submit': string;
'--surface-submit-hover': string;
'--surface-destructive': string;
@@ -138,18 +145,23 @@ export interface IThemeVariables {
'--status-success': string;
'--status-success-subtle': string;
'--status-success-border': string;
+ '--status-success-strong': string;
'--status-info': string;
'--status-info-subtle': string;
'--status-info-border': string;
+ '--status-info-strong': string;
'--status-warning': string;
'--status-warning-subtle': string;
'--status-warning-border': string;
+ '--status-warning-strong': string;
'--status-error': string;
'--status-error-subtle': string;
'--status-error-border': string;
+ '--status-error-strong': string;
'--status-neutral': string;
'--status-neutral-subtle': string;
'--status-neutral-border': string;
+ '--text-on-status': string;
'--brand-purple': string;
'--presentation': string;
}
@@ -185,6 +197,7 @@ export interface IThemeColors {
'surface-tertiary'?: string;
'surface-tertiary-alt'?: string;
'surface-dialog'?: string;
+ 'surface-overlay'?: string;
'surface-submit'?: string;
'surface-submit-hover'?: string;
'surface-destructive'?: string;
@@ -205,18 +218,23 @@ export interface IThemeColors {
'status-success'?: string;
'status-success-subtle'?: string;
'status-success-border'?: string;
+ 'status-success-strong'?: string;
'status-info'?: string;
'status-info-subtle'?: string;
'status-info-border'?: string;
+ 'status-info-strong'?: string;
'status-warning'?: string;
'status-warning-subtle'?: string;
'status-warning-border'?: string;
+ 'status-warning-strong'?: string;
'status-error'?: string;
'status-error-subtle'?: string;
'status-error-border'?: string;
+ 'status-error-strong'?: string;
'status-neutral'?: string;
'status-neutral-subtle'?: string;
'status-neutral-border'?: string;
+ 'text-on-status'?: string;
'brand-purple'?: string;
presentation?: string;
diff --git a/packages/client/src/theme/utils/applyTheme.spec.ts b/packages/client/src/theme/utils/applyTheme.spec.ts
index d5b139cecb..0b4c50ad82 100644
--- a/packages/client/src/theme/utils/applyTheme.spec.ts
+++ b/packages/client/src/theme/utils/applyTheme.spec.ts
@@ -12,6 +12,9 @@ const semanticProperties = [
'--status-success',
'--status-success-subtle',
'--status-success-border',
+ '--status-success-strong',
+ '--surface-overlay',
+ '--text-on-status',
'--status-error',
'--status-neutral-border',
];
@@ -46,8 +49,11 @@ describe('applyTheme', () => {
'rgb-status-success': '26 27 28',
'rgb-status-success-subtle': '29 30 31',
'rgb-status-success-border': '32 33 34',
+ 'rgb-status-success-strong': '33 34 35',
'rgb-status-error': '35 36 37',
'rgb-status-neutral-border': '38 39 40',
+ 'rgb-surface-overlay': '39 40 41',
+ 'rgb-text-on-status': '42 43 44',
});
const style = document.documentElement.style;
@@ -56,13 +62,17 @@ describe('applyTheme', () => {
expect(style.getPropertyValue('--status-success')).toBe('26 27 28');
expect(style.getPropertyValue('--status-success-subtle')).toBe('29 30 31');
expect(style.getPropertyValue('--status-success-border')).toBe('32 33 34');
+ expect(style.getPropertyValue('--status-success-strong')).toBe('33 34 35');
expect(style.getPropertyValue('--status-error')).toBe('35 36 37');
expect(style.getPropertyValue('--status-neutral-border')).toBe('38 39 40');
+ expect(style.getPropertyValue('--surface-overlay')).toBe('39 40 41');
+ expect(style.getPropertyValue('--text-on-status')).toBe('42 43 44');
});
it('ships status tokens in the bundled themes', () => {
applyTheme(defaultTheme);
expect(document.documentElement.style.getPropertyValue('--status-error')).toBe('220 38 38');
+ expect(document.documentElement.style.getPropertyValue('--surface-overlay')).toBe('89 89 89');
});
});
diff --git a/packages/client/src/theme/utils/applyTheme.ts b/packages/client/src/theme/utils/applyTheme.ts
index d1e73945f9..409bdd3ba9 100644
--- a/packages/client/src/theme/utils/applyTheme.ts
+++ b/packages/client/src/theme/utils/applyTheme.ts
@@ -53,6 +53,7 @@ function mapTheme(rgb: IThemeRGB): Partial
{
'rgb-surface-tertiary': '--surface-tertiary',
'rgb-surface-tertiary-alt': '--surface-tertiary-alt',
'rgb-surface-dialog': '--surface-dialog',
+ 'rgb-surface-overlay': '--surface-overlay',
'rgb-surface-submit': '--surface-submit',
'rgb-surface-submit-hover': '--surface-submit-hover',
'rgb-surface-destructive': '--surface-destructive',
@@ -73,18 +74,23 @@ function mapTheme(rgb: IThemeRGB): Partial {
'rgb-status-success': '--status-success',
'rgb-status-success-subtle': '--status-success-subtle',
'rgb-status-success-border': '--status-success-border',
+ 'rgb-status-success-strong': '--status-success-strong',
'rgb-status-info': '--status-info',
'rgb-status-info-subtle': '--status-info-subtle',
'rgb-status-info-border': '--status-info-border',
+ 'rgb-status-info-strong': '--status-info-strong',
'rgb-status-warning': '--status-warning',
'rgb-status-warning-subtle': '--status-warning-subtle',
'rgb-status-warning-border': '--status-warning-border',
+ 'rgb-status-warning-strong': '--status-warning-strong',
'rgb-status-error': '--status-error',
'rgb-status-error-subtle': '--status-error-subtle',
'rgb-status-error-border': '--status-error-border',
+ 'rgb-status-error-strong': '--status-error-strong',
'rgb-status-neutral': '--status-neutral',
'rgb-status-neutral-subtle': '--status-neutral-subtle',
'rgb-status-neutral-border': '--status-neutral-border',
+ 'rgb-text-on-status': '--text-on-status',
'rgb-brand-purple': '--brand-purple',
'rgb-presentation': '--presentation',
};
diff --git a/packages/client/src/theme/utils/createTailwindColors.js b/packages/client/src/theme/utils/createTailwindColors.js
index 6bc3dc1b4d..7892e152ed 100644
--- a/packages/client/src/theme/utils/createTailwindColors.js
+++ b/packages/client/src/theme/utils/createTailwindColors.js
@@ -96,6 +96,7 @@ function createTailwindColors() {
'surface-tertiary': cssVar('--surface-tertiary'),
'surface-tertiary-alt': cssVar('--surface-tertiary-alt'),
'surface-dialog': cssVar('--surface-dialog'),
+ 'surface-overlay': cssVar('--surface-overlay'),
'surface-submit': cssVar('--surface-submit'),
'surface-submit-hover': cssVar('--surface-submit-hover'),
'surface-destructive': cssVar('--surface-destructive'),
@@ -118,18 +119,23 @@ function createTailwindColors() {
'status-success': cssVar('--status-success'),
'status-success-subtle': cssVar('--status-success-subtle'),
'status-success-border': cssVar('--status-success-border'),
+ 'status-success-strong': cssVar('--status-success-strong'),
'status-info': cssVar('--status-info'),
'status-info-subtle': cssVar('--status-info-subtle'),
'status-info-border': cssVar('--status-info-border'),
+ 'status-info-strong': cssVar('--status-info-strong'),
'status-warning': cssVar('--status-warning'),
'status-warning-subtle': cssVar('--status-warning-subtle'),
'status-warning-border': cssVar('--status-warning-border'),
+ 'status-warning-strong': cssVar('--status-warning-strong'),
'status-error': cssVar('--status-error'),
'status-error-subtle': cssVar('--status-error-subtle'),
'status-error-border': cssVar('--status-error-border'),
+ 'status-error-strong': cssVar('--status-error-strong'),
'status-neutral': cssVar('--status-neutral'),
'status-neutral-subtle': cssVar('--status-neutral-subtle'),
'status-neutral-border': cssVar('--status-neutral-border'),
+ 'text-on-status': cssVar('--text-on-status'),
'switch-unchecked': hslVar('--switch-unchecked'),
};
diff --git a/packages/client/src/theme/utils/createTailwindColors.spec.js b/packages/client/src/theme/utils/createTailwindColors.spec.js
index 0acac8c1b4..a626edf59d 100644
--- a/packages/client/src/theme/utils/createTailwindColors.spec.js
+++ b/packages/client/src/theme/utils/createTailwindColors.spec.js
@@ -1,6 +1,16 @@
const { createTailwindColors } = require('./createTailwindColors');
describe('createTailwindColors', () => {
+ it('exposes semantic overlay and strong status colors', () => {
+ const colors = createTailwindColors();
+
+ expect(colors['surface-overlay']).toBe('rgb(var(--surface-overlay) / )');
+ expect(colors['status-success-strong']).toBe(
+ 'rgb(var(--status-success-strong) / )',
+ );
+ expect(colors['text-on-status']).toBe('rgb(var(--text-on-status) / )');
+ });
+
it('combines intrinsic border alpha with Tailwind opacity modifiers', () => {
const colors = createTailwindColors();