diff --git a/apps/showcase/src/demos/container-management/components/ContainerDetail.tsx b/apps/showcase/src/demos/container-management/components/ContainerDetail.tsx index 1169222..a98043b 100644 --- a/apps/showcase/src/demos/container-management/components/ContainerDetail.tsx +++ b/apps/showcase/src/demos/container-management/components/ContainerDetail.tsx @@ -44,7 +44,7 @@ export function ContainerDetail({ container, onBack }: ContainerDetailProps) { @@ -101,7 +101,7 @@ export function ContainerDetail({ container, onBack }: ContainerDetailProps) { toast(`Deleting ${container.name}`, { severity: 'warning' })} @@ -114,20 +114,16 @@ export function ContainerDetail({ container, onBack }: ContainerDetailProps) { {/* Tabs */} - - + }> Logs - - + }> Inspect - - + }> Stats - - + }> Files diff --git a/apps/showcase/src/demos/web-browser/components/BrowserToolbar.tsx b/apps/showcase/src/demos/web-browser/components/BrowserToolbar.tsx index b9caa68..e190fad 100644 --- a/apps/showcase/src/demos/web-browser/components/BrowserToolbar.tsx +++ b/apps/showcase/src/demos/web-browser/components/BrowserToolbar.tsx @@ -5,7 +5,7 @@ import { LuRotateCw, LuLock, } from 'react-icons/lu'; -import { IconButton, Input } from '@omniview/base-ui'; +import { IconButton, Input, Toolbar } from '@omniview/base-ui'; import { ensureProtocol, NEW_TAB_URL } from '../data'; import styles from '../index.module.css'; @@ -47,36 +47,35 @@ export function BrowserToolbar({ ); return ( -
- - - - - - - - - + + + + + + + + + + + + } @@ -87,6 +86,6 @@ export function BrowserToolbar({ placeholder="Enter a URL…" /> -
+ ); } diff --git a/apps/showcase/src/demos/web-browser/index.module.css b/apps/showcase/src/demos/web-browser/index.module.css index b14ac87..145a3bd 100644 --- a/apps/showcase/src/demos/web-browser/index.module.css +++ b/apps/showcase/src/demos/web-browser/index.module.css @@ -31,9 +31,6 @@ /* Toolbar: nav buttons + address bar */ .toolbar { - display: flex; - align-items: center; - gap: 2px; padding: 4px 8px; background: var(--ov-color-bg-surface); } diff --git a/packages/base-ui/.storybook/preview.tsx b/packages/base-ui/.storybook/preview.tsx index 6384aa3..34b6b3a 100644 --- a/packages/base-ui/.storybook/preview.tsx +++ b/packages/base-ui/.storybook/preview.tsx @@ -9,7 +9,7 @@ const preview: Preview = { defaultValue: 'dark', toolbar: { icon: 'paintbrush', - items: ['dark', 'light', 'high-contrast-dark', 'high-contrast-light'], + items: ['dark', 'light', 'high-contrast-dark', 'high-contrast-light', 'obsidian', 'carbon', 'void'], }, }, density: { diff --git a/packages/base-ui/src/components/_consistency/ConsistencyCheck.stories.tsx b/packages/base-ui/src/components/_consistency/ConsistencyCheck.stories.tsx new file mode 100644 index 0000000..c4d3a0d --- /dev/null +++ b/packages/base-ui/src/components/_consistency/ConsistencyCheck.stories.tsx @@ -0,0 +1,534 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { LuChevronDown } from 'react-icons/lu'; +import { Badge } from '../badge'; +import { Button } from '../button'; +import { Checkbox } from '../checkbox'; +import { Chip } from '../chip'; +import { IconButton } from '../icon-button'; +import { Input } from '../input'; +import { Radio } from '../radio'; +import { RadioGroup } from '../radio-group'; +import { Select } from '../select'; +import { Switch } from '../switch'; +import type { ComponentColor, ComponentSize, ComponentVariant } from '../../system/types'; + +// ─── Shared icon ────────────────────────────────────────────────────────────── + +const SearchIcon = () => ( + + + + +); + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const SIZES: ComponentSize[] = ['xs', 'sm', 'md', 'lg', 'xl']; + +// Expected pixel heights per size tier (informational labels) +const SIZE_HEIGHTS: Record = { + xs: '18px', + sm: '22px', + md: '26px', + lg: '32px', + xl: '40px', +}; + +const COLORS: ComponentColor[] = [ + 'neutral', + 'brand', + 'success', + 'warning', + 'danger', + 'info', + 'discovery', + 'secondary', +]; + +const VARIANTS: ComponentVariant[] = ['solid', 'soft', 'outline', 'ghost']; + +// ─── Section header helper ──────────────────────────────────────────────────── + +function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) { + return ( +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ ); +} + +// ─── Section 1: Size Consistency Grid ──────────────────────────────────────── + +function SizeConsistencyGrid() { + return ( +
+ + + {/* Column headers */} +
+ {['', 'Button', 'IconButton', 'Input', 'Select', 'Chip', 'Checkbox\n(control)', 'Radio\n(control)', 'Switch', 'Badge'].map( + (label) => ( +
+ {label} +
+ ), + )} +
+ + {/* Size rows */} + {SIZES.map((size) => ( +
+ {/* Reference line through vertical midpoint */} +
+ + {/* Row label */} +
+ {size} + + {SIZE_HEIGHTS[size]} + +
+ + {/* Button */} +
+ +
+ + {/* IconButton */} +
+ + + +
+ + {/* Input */} +
+ + + +
+ + {/* Select */} +
+ + + + + + + + + + + + + Option A + + + Option B + + + + + + +
+ + {/* Chip */} +
+ + Tag + +
+ + {/* Checkbox (control only — no label, to measure control height) */} +
+ +
+ + {/* Radio (control only — no label) */} +
+ + + +
+ + {/* Switch */} +
+ +
+ + {/* Badge */} +
+ +
+ +
+
+ ))} +
+ ); +} + +// ─── Section 2: Color × Variant Matrix ─────────────────────────────────────── + +function ColorVariantMatrix() { + return ( +
+ + + {/* Variant column headers */} +
+ {['', ...VARIANTS].map((label) => ( +
+ {label} +
+ ))} +
+ + {/* Dark background section */} +
+
+ Dark background +
+ {COLORS.map((color) => ( +
+
+ {color} +
+ {VARIANTS.map((variant) => ( +
+ +
+ ))} +
+ ))} +
+ + {/* Light background section */} +
+
+ Light background +
+ {COLORS.map((color) => ( +
+
+ {color} +
+ {VARIANTS.map((variant) => ( +
+ +
+ ))} +
+ ))} +
+
+ ); +} + +// ─── Section 3: IconButton Squareness Check ─────────────────────────────────── + +function IconButtonSquarenessCheck() { + return ( +
+ + +
+ {SIZES.map((size) => ( +
+
+ {/* Visible square background to expose non-square dimensions */} +
+ + + +
+ + {size} + +
+ ))} +
+
+ ); +} + +// ─── Story scaffold ─────────────────────────────────────────────────────────── + +function AuditPage() { + return ( +
+
+

+ Size & Color Audit +

+

+ Visual reference tool — not interactive. Resize the panel to stress-test layouts. +

+
+ + + + +
+ ); +} + +const meta = { + title: 'Consistency/Size & Color Audit', + component: AuditPage, + parameters: { + controls: { hideNoControlsWarning: true }, + docs: { disable: true }, + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Audit: Story = { + parameters: { + controls: { disable: true }, + }, +}; diff --git a/packages/base-ui/src/components/accordion/Accordion.stories.tsx b/packages/base-ui/src/components/accordion/Accordion.stories.tsx index 3b3238e..1a8d911 100644 --- a/packages/base-ui/src/components/accordion/Accordion.stories.tsx +++ b/packages/base-ui/src/components/accordion/Accordion.stories.tsx @@ -16,7 +16,7 @@ const meta = { exclusive: { control: 'boolean' }, defaultExpanded: { control: 'object' }, animation: { control: 'inline-radio', options: ['default', 'fast', 'none'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, } satisfies Meta; diff --git a/packages/base-ui/src/components/action-list/ActionList.module.css b/packages/base-ui/src/components/action-list/ActionList.module.css index 6601ef7..d00cbaa 100644 --- a/packages/base-ui/src/components/action-list/ActionList.module.css +++ b/packages/base-ui/src/components/action-list/ActionList.module.css @@ -8,6 +8,12 @@ min-inline-size: 220px; } +.Root[data-ov-size='xs'] { + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-xs); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-xs); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-xs); +} + .Root[data-ov-size='sm'] { --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-sm); --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-sm); @@ -20,6 +26,12 @@ --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-lg); } +.Root[data-ov-size='xl'] { + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-xl); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-xl); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-xl); +} + .Item { --_ov-item-height: var(--ov-size-action-list-item-height-md); --_ov-item-padding-inline: var(--ov-size-action-list-item-padding-inline-md); @@ -33,6 +45,14 @@ text-align: left; } +.Item[data-ov-size='xs'] { + --_ov-item-height: var(--ov-size-action-list-item-height-xs); + --_ov-item-padding-inline: var(--ov-size-action-list-item-padding-inline-xs); + --_ov-item-icon-size: var(--ov-size-action-list-icon-size-xs); + --_ov-item-shortcut-min-width: var(--ov-size-action-list-shortcut-min-width-xs); + --_ov-content-gap: var(--ov-space-stack-sm); +} + .Item[data-ov-size='sm'] { --_ov-item-height: var(--ov-size-action-list-item-height-sm); --_ov-item-padding-inline: var(--ov-size-action-list-item-padding-inline-sm); @@ -49,6 +69,14 @@ --_ov-content-gap: var(--ov-space-inline-control); } +.Item[data-ov-size='xl'] { + --_ov-item-height: var(--ov-size-action-list-item-height-xl); + --_ov-item-padding-inline: var(--ov-size-action-list-item-padding-inline-xl); + --_ov-item-icon-size: var(--ov-size-action-list-icon-size-xl); + --_ov-item-shortcut-min-width: var(--ov-size-action-list-shortcut-min-width-xl); + --_ov-content-gap: var(--ov-space-inline-control); +} + .Item :where([data-ov-slot='start-decorator'], [data-ov-slot='end-decorator']) { display: inline-flex; align-items: center; @@ -126,6 +154,10 @@ margin-block-end: var(--_ov-group-label-gap-after); } +.GroupLabel[data-ov-size='xs'] { + padding-inline: var(--ov-size-action-list-item-padding-inline-xs); +} + .GroupLabel[data-ov-size='sm'] { padding-inline: var(--ov-size-action-list-item-padding-inline-sm); } @@ -133,3 +165,7 @@ .GroupLabel[data-ov-size='lg'] { padding-inline: var(--ov-size-action-list-item-padding-inline-lg); } + +.GroupLabel[data-ov-size='xl'] { + padding-inline: var(--ov-size-action-list-item-padding-inline-xl); +} diff --git a/packages/base-ui/src/components/action-list/ActionList.stories.tsx b/packages/base-ui/src/components/action-list/ActionList.stories.tsx index e216ca2..e6ecdf5 100644 --- a/packages/base-ui/src/components/action-list/ActionList.stories.tsx +++ b/packages/base-ui/src/components/action-list/ActionList.stories.tsx @@ -12,8 +12,8 @@ const meta = { itemVariant: 'ghost', }, argTypes: { - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, itemVariant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, }, parameters: { diff --git a/packages/base-ui/src/components/action-list/ActionList.test.tsx b/packages/base-ui/src/components/action-list/ActionList.test.tsx index 71ea3f7..7e8af9a 100644 --- a/packages/base-ui/src/components/action-list/ActionList.test.tsx +++ b/packages/base-ui/src/components/action-list/ActionList.test.tsx @@ -19,6 +19,42 @@ describe('ActionList', () => { expect(screen.getByText('Open the selected workspace')).toBeVisible(); }); + it('applies xs size data attribute', () => { + renderWithTheme( + + Item + , + ); + const button = screen.getByRole('button', { name: 'Item' }); + expect(button).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size data attribute', () => { + renderWithTheme( + + Item + , + ); + const button = screen.getByRole('button', { name: 'Item' }); + expect(button).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery and secondary color data attributes', () => { + const { rerender } = renderWithTheme( + + Item + , + ); + expect(screen.getByRole('button', { name: 'Item' })).toHaveAttribute('data-ov-color', 'discovery'); + + rerender( + + Item + , + ); + expect(screen.getByRole('button', { name: 'Item' })).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('renders separator and group label', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/alert-dialog/AlertDialog.module.css b/packages/base-ui/src/components/alert-dialog/AlertDialog.module.css index b0141ce..f6cba52 100644 --- a/packages/base-ui/src/components/alert-dialog/AlertDialog.module.css +++ b/packages/base-ui/src/components/alert-dialog/AlertDialog.module.css @@ -53,6 +53,18 @@ --_ov-accent-soft: var(--ov-color-danger-soft); } +.Popup[data-ov-color='info'] { + --_ov-accent-soft: var(--ov-color-info-soft); +} + +.Popup[data-ov-color='discovery'] { + --_ov-accent-soft: var(--ov-color-discovery-soft); +} + +.Popup[data-ov-color='secondary'] { + --_ov-accent-soft: var(--ov-color-secondary-soft); +} + .Title { margin: 0; color: var(--_ov-title); @@ -128,6 +140,39 @@ --_ov-bg-pressed: color-mix(in srgb, var(--ov-color-danger) 78%, black 22%); } +.Trigger[data-ov-color='info'], +.Close[data-ov-color='info'] { + --_ov-bg: var(--ov-color-info); + --_ov-border: var(--ov-color-info); + --_ov-fg: var(--ov-color-fg-inverse); + --_ov-bg-hover: color-mix(in srgb, var(--ov-color-info) 88%, white 12%); + --_ov-bg-pressed: color-mix(in srgb, var(--ov-color-info) 78%, black 22%); +} + +.Trigger[data-ov-color='discovery'], +.Close[data-ov-color='discovery'] { + --_ov-bg: var(--ov-color-discovery); + --_ov-border: var(--ov-color-discovery); + --_ov-fg: var(--ov-color-fg-inverse); + --_ov-bg-hover: color-mix(in srgb, var(--ov-color-discovery) 88%, white 12%); + --_ov-bg-pressed: color-mix(in srgb, var(--ov-color-discovery) 78%, black 22%); +} + +.Trigger[data-ov-color='secondary'], +.Close[data-ov-color='secondary'] { + --_ov-bg: var(--ov-color-secondary); + --_ov-border: var(--ov-color-secondary); + --_ov-fg: var(--ov-color-fg-inverse); + --_ov-bg-hover: color-mix(in srgb, var(--ov-color-secondary) 88%, white 12%); + --_ov-bg-pressed: color-mix(in srgb, var(--ov-color-secondary) 78%, black 22%); +} + +.Trigger[data-ov-variant='solid'], +.Close[data-ov-variant='solid'] { + --_ov-bg: var(--ov-color-bg-surface-raised); + --_ov-border: var(--ov-color-border-default); +} + .Trigger[data-ov-variant='soft'], .Close[data-ov-variant='soft'] { --_ov-bg: var(--ov-color-state-selected); diff --git a/packages/base-ui/src/components/alert-dialog/AlertDialog.stories.tsx b/packages/base-ui/src/components/alert-dialog/AlertDialog.stories.tsx index 78f10d9..3ebd0f0 100644 --- a/packages/base-ui/src/components/alert-dialog/AlertDialog.stories.tsx +++ b/packages/base-ui/src/components/alert-dialog/AlertDialog.stories.tsx @@ -12,8 +12,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, parameters: { controls: { diff --git a/packages/base-ui/src/components/alert-dialog/AlertDialog.test.tsx b/packages/base-ui/src/components/alert-dialog/AlertDialog.test.tsx index 62587c2..b028208 100644 --- a/packages/base-ui/src/components/alert-dialog/AlertDialog.test.tsx +++ b/packages/base-ui/src/components/alert-dialog/AlertDialog.test.tsx @@ -4,6 +4,38 @@ import { renderWithTheme } from '../../test/render'; import { AlertDialog } from './AlertDialog'; describe('AlertDialog', () => { + it('renders discovery color on popup', () => { + renderWithTheme( + + Open + + + + Discovery Dialog + + + , + ); + const popup = screen.getByText('Discovery Dialog').closest('[data-ov-color]'); + expect(popup).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('renders secondary color on popup', () => { + renderWithTheme( + + Open + + + + Secondary Dialog + + + , + ); + const popup = screen.getByText('Secondary Dialog').closest('[data-ov-color]'); + expect(popup).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('renders themed popup when opened', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/autocomplete/Autocomplete.module.css b/packages/base-ui/src/components/autocomplete/Autocomplete.module.css index 6a88fdd..eee9767 100644 --- a/packages/base-ui/src/components/autocomplete/Autocomplete.module.css +++ b/packages/base-ui/src/components/autocomplete/Autocomplete.module.css @@ -34,6 +34,11 @@ box-shadow: 0 0 0 1px var(--ov-color-state-focus-ring); } +.Input[data-ov-size='xs'] { + --_ov-control-height: var(--ov-control-height-xs); + --_ov-font-size: var(--ov-font-size-caption); +} + .Input[data-ov-size='sm'] { --_ov-control-height: var(--ov-control-height-sm); --_ov-font-size: var(--ov-font-size-caption); @@ -43,6 +48,15 @@ --_ov-control-height: var(--ov-control-height-lg); } +.Input[data-ov-size='xl'] { + --_ov-control-height: var(--ov-control-height-xl); +} + +.Input[data-ov-color='neutral'] { + --_ov-focus: var(--ov-color-border-default); + --_ov-accent-soft: var(--ov-color-state-selected); +} + .Input[data-ov-color='brand'] { --_ov-focus: var(--ov-color-brand-400); --_ov-accent-soft: var(--ov-color-accent-soft); @@ -63,6 +77,21 @@ --_ov-accent-soft: var(--ov-color-danger-soft); } +.Input[data-ov-color='info'] { + --_ov-focus: var(--ov-color-info); + --_ov-accent-soft: var(--ov-color-info-soft); +} + +.Input[data-ov-color='discovery'] { + --_ov-focus: var(--ov-color-discovery); + --_ov-accent-soft: var(--ov-color-discovery-soft); +} + +.Input[data-ov-color='secondary'] { + --_ov-focus: var(--ov-color-secondary); + --_ov-accent-soft: var(--ov-color-secondary-soft); +} + .Input[data-ov-variant='solid'] { --_ov-bg: color-mix(in srgb, var(--ov-color-bg-surface) 86%, var(--_ov-accent-soft) 14%); } @@ -76,6 +105,11 @@ --_ov-border: transparent; } +.Input[data-ov-variant='soft'] { + --_ov-bg: var(--_ov-accent-soft); + --_ov-border: transparent; +} + .Positioner { z-index: var(--ov-z-popup, 120); } @@ -104,6 +138,16 @@ padding: var(--_ov-popup-padding); } +.Popup[data-ov-size='xs'] { + --_ov-popup-padding: calc(var(--ov-size-list-item-gap) - 2px); + --_ov-list-gap: calc(var(--ov-size-list-item-gap) - 2px); + --_ov-item-padding-inline: var(--ov-size-list-item-padding-inline-xs); + --_ov-item-height: var(--ov-size-list-item-height-xs); + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-sm); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-sm); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-sm); +} + .Popup[data-ov-size='sm'] { --_ov-popup-padding: calc(var(--ov-size-list-item-gap) - 1px); --_ov-list-gap: calc(var(--ov-size-list-item-gap) - 1px); @@ -124,6 +168,20 @@ --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-lg); } +.Popup[data-ov-size='xl'] { + --_ov-popup-padding: calc(var(--ov-size-list-item-gap) + 2px); + --_ov-list-gap: calc(var(--ov-size-list-item-gap) + 2px); + --_ov-item-padding-inline: var(--ov-size-list-item-padding-inline-xl); + --_ov-item-height: var(--ov-size-list-item-height-xl); + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-lg); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-lg); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-lg); +} + +.Popup[data-ov-color='neutral'] { + --_ov-selected-bg: var(--ov-color-state-selected); +} + .Popup[data-ov-color='brand'] { --_ov-selected-bg: var(--ov-color-accent-soft); } @@ -140,6 +198,18 @@ --_ov-selected-bg: var(--ov-color-danger-soft); } +.Popup[data-ov-color='info'] { + --_ov-selected-bg: var(--ov-color-info-soft); +} + +.Popup[data-ov-color='discovery'] { + --_ov-selected-bg: var(--ov-color-discovery-soft); +} + +.Popup[data-ov-color='secondary'] { + --_ov-selected-bg: var(--ov-color-secondary-soft); +} + .List { margin: 0; padding: 0; @@ -235,6 +305,12 @@ color var(--ov-duration-interactive) var(--ov-ease-standard); } +.Trigger[data-ov-size='xs'], +.Clear[data-ov-size='xs'] { + --_ov-control-height: var(--ov-control-height-xs); + --_ov-icon-size: var(--ov-size-icon-button-icon-xs); +} + .Trigger[data-ov-size='sm'], .Clear[data-ov-size='sm'] { --_ov-control-height: var(--ov-control-height-sm); @@ -247,6 +323,17 @@ --_ov-icon-size: var(--ov-size-icon-button-icon-lg); } +.Trigger[data-ov-size='xl'], +.Clear[data-ov-size='xl'] { + --_ov-control-height: var(--ov-control-height-xl); + --_ov-icon-size: var(--ov-size-icon-button-icon-xl); +} + +.Trigger[data-ov-color='neutral'], +.Clear[data-ov-color='neutral'] { + --_ov-fg: var(--ov-color-fg-muted); +} + .Trigger[data-ov-color='brand'], .Clear[data-ov-color='brand'] { --_ov-fg: var(--ov-color-brand-400); @@ -267,6 +354,21 @@ --_ov-fg: var(--ov-color-danger); } +.Trigger[data-ov-color='info'], +.Clear[data-ov-color='info'] { + --_ov-fg: var(--ov-color-info); +} + +.Trigger[data-ov-color='discovery'], +.Clear[data-ov-color='discovery'] { + --_ov-fg: var(--ov-color-discovery); +} + +.Trigger[data-ov-color='secondary'], +.Clear[data-ov-color='secondary'] { + --_ov-fg: var(--ov-color-secondary); +} + .Trigger[data-ov-variant='solid'], .Clear[data-ov-variant='solid'] { --_ov-bg: var(--ov-color-state-selected); @@ -284,6 +386,12 @@ --_ov-border: transparent; } +.Trigger[data-ov-variant='soft'], +.Clear[data-ov-variant='soft'] { + --_ov-bg: var(--ov-color-state-selected); + --_ov-border: transparent; +} + .Trigger:focus-visible, .Clear:focus-visible { outline: none; diff --git a/packages/base-ui/src/components/autocomplete/Autocomplete.stories.tsx b/packages/base-ui/src/components/autocomplete/Autocomplete.stories.tsx index 19d11f4..448df2c 100644 --- a/packages/base-ui/src/components/autocomplete/Autocomplete.stories.tsx +++ b/packages/base-ui/src/components/autocomplete/Autocomplete.stories.tsx @@ -79,8 +79,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, parameters: { controls: { diff --git a/packages/base-ui/src/components/autocomplete/Autocomplete.test.tsx b/packages/base-ui/src/components/autocomplete/Autocomplete.test.tsx index dd61b00..d9a273b 100644 --- a/packages/base-ui/src/components/autocomplete/Autocomplete.test.tsx +++ b/packages/base-ui/src/components/autocomplete/Autocomplete.test.tsx @@ -41,4 +41,100 @@ describe('Autocomplete', () => { expect(screen.getByText('feature')).toBeInTheDocument(); }); + + it('applies xs size data attribute to input', () => { + renderWithTheme( + + + + + + + {(tag: Tag) => ( + + {tag.value} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Search tags xs'); + expect(input).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size data attribute to input', () => { + renderWithTheme( + + + + + + + {(tag: Tag) => ( + + {tag.value} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Search tags xl'); + expect(input).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery color data attribute to input', () => { + renderWithTheme( + + + + + + + {(tag: Tag) => ( + + {tag.value} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Search tags discovery'); + expect(input).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('applies secondary color data attribute to input', () => { + renderWithTheme( + + + + + + + {(tag: Tag) => ( + + {tag.value} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Search tags secondary'); + expect(input).toHaveAttribute('data-ov-color', 'secondary'); + }); }); diff --git a/packages/base-ui/src/components/avatar-group/AvatarGroup.stories.tsx b/packages/base-ui/src/components/avatar-group/AvatarGroup.stories.tsx index 1d0e808..e743088 100644 --- a/packages/base-ui/src/components/avatar-group/AvatarGroup.stories.tsx +++ b/packages/base-ui/src/components/avatar-group/AvatarGroup.stories.tsx @@ -25,10 +25,10 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, shape: { control: 'inline-radio', options: ['circle', 'rounded'] }, - overlap: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + overlap: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, deterministic: { control: 'boolean' }, max: { control: { type: 'number', min: 1, max: 8, step: 1 } }, total: { control: { type: 'number', min: 1, max: 99, step: 1 } }, diff --git a/packages/base-ui/src/components/avatar/Avatar.module.css b/packages/base-ui/src/components/avatar/Avatar.module.css index c1cd8db..85f4f68 100644 --- a/packages/base-ui/src/components/avatar/Avatar.module.css +++ b/packages/base-ui/src/components/avatar/Avatar.module.css @@ -27,6 +27,12 @@ border-radius: var(--ov-size-avatar-radius-rounded); } +.Root[data-ov-size='xs'] { + --_ov-size: var(--ov-size-avatar-xs, var(--ov-size-avatar-sm)); + --_ov-font-size: var(--ov-size-avatar-font-size-xs, var(--ov-size-avatar-font-size-sm)); + --_ov-icon-size: var(--ov-size-avatar-icon-size-xs, var(--ov-size-avatar-icon-size-sm)); +} + .Root[data-ov-size='sm'] { --_ov-size: var(--ov-size-avatar-sm); --_ov-font-size: var(--ov-size-avatar-font-size-sm); @@ -39,6 +45,12 @@ --_ov-icon-size: var(--ov-size-avatar-icon-size-lg); } +.Root[data-ov-size='xl'] { + --_ov-size: var(--ov-size-avatar-xl, var(--ov-size-avatar-lg)); + --_ov-font-size: var(--ov-size-avatar-font-size-xl, var(--ov-size-avatar-font-size-lg)); + --_ov-icon-size: var(--ov-size-avatar-icon-size-xl, var(--ov-size-avatar-icon-size-lg)); +} + .Root[data-ov-color='neutral'] { --_ov-accent-bg: var(--ov-color-bg-surface-raised); --_ov-accent-fg: var(--ov-color-fg-default); @@ -79,6 +91,30 @@ --_ov-accent-soft-fg: var(--ov-color-danger); } +.Root[data-ov-color='info'] { + --_ov-accent-bg: var(--ov-color-info); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-info); + --_ov-accent-soft-bg: var(--ov-color-info-soft); + --_ov-accent-soft-fg: var(--ov-color-info); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent-bg: var(--ov-color-discovery); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-discovery); + --_ov-accent-soft-bg: var(--ov-color-discovery-soft); + --_ov-accent-soft-fg: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent-bg: var(--ov-color-secondary); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-secondary); + --_ov-accent-soft-bg: var(--ov-color-secondary-soft); + --_ov-accent-soft-fg: var(--ov-color-secondary); +} + .Root[data-ov-variant='solid'] { --_ov-bg: var(--_ov-accent-bg); --_ov-fg: var(--_ov-accent-fg); diff --git a/packages/base-ui/src/components/avatar/Avatar.stories.tsx b/packages/base-ui/src/components/avatar/Avatar.stories.tsx index 98ae388..e14b53f 100644 --- a/packages/base-ui/src/components/avatar/Avatar.stories.tsx +++ b/packages/base-ui/src/components/avatar/Avatar.stories.tsx @@ -56,8 +56,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, shape: { control: 'inline-radio', options: ['circle', 'rounded'] }, deterministic: { control: 'boolean' }, name: { control: 'text' }, diff --git a/packages/base-ui/src/components/avatar/Avatar.test.tsx b/packages/base-ui/src/components/avatar/Avatar.test.tsx index 0e634d4..55772bd 100644 --- a/packages/base-ui/src/components/avatar/Avatar.test.tsx +++ b/packages/base-ui/src/components/avatar/Avatar.test.tsx @@ -38,6 +38,26 @@ describe('Avatar', () => { expect(screen.getByTestId('avatar-icon')).toBeInTheDocument(); }); + it('renders xs and xl sizes', () => { + const { rerender } = renderWithTheme(); + let root = screen.getByText('AB').closest('[data-ov-avatar-root="true"]'); + expect(root).toHaveAttribute('data-ov-size', 'xs'); + + rerender(); + root = screen.getByText('AB').closest('[data-ov-avatar-root="true"]'); + expect(root).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('renders discovery and secondary colors', () => { + const { rerender } = renderWithTheme(); + let root = screen.getByText('CD').closest('[data-ov-avatar-root="true"]'); + expect(root).toHaveAttribute('data-ov-color', 'discovery'); + + rerender(); + root = screen.getByText('CD').closest('[data-ov-avatar-root="true"]'); + expect(root).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('supports non-deterministic fallback mode', () => { renderWithTheme(); diff --git a/packages/base-ui/src/components/badge/Badge.module.css b/packages/base-ui/src/components/badge/Badge.module.css index 17a2c5f..0468b62 100644 --- a/packages/base-ui/src/components/badge/Badge.module.css +++ b/packages/base-ui/src/components/badge/Badge.module.css @@ -55,6 +55,21 @@ } /* ── Color variants ── */ +.Badge[data-ov-color='info'] { + --_ov-bg: var(--ov-color-info); + --_ov-fg: var(--ov-color-fg-inverse); +} + +.Badge[data-ov-color='discovery'] { + --_ov-bg: var(--ov-color-discovery); + --_ov-fg: var(--ov-color-fg-inverse); +} + +.Badge[data-ov-color='secondary'] { + --_ov-bg: var(--ov-color-secondary); + --_ov-fg: var(--ov-color-fg-inverse); +} + .Badge[data-ov-color='neutral'] { --_ov-bg: var(--ov-color-bg-surface-raised); --_ov-fg: var(--ov-color-fg-default); @@ -80,6 +95,16 @@ --_ov-fg: var(--ov-color-fg-inverse); } +/* ── Size: xs ── */ +.Badge[data-ov-size='xs'] { + --_ov-badge-min-w: 14px; + --_ov-badge-h: 14px; + --_ov-badge-pad: 0 3px; + --_ov-badge-radius: 7px; + --_ov-badge-font: 0.5rem; + --_ov-dot-size: 5px; +} + /* ── Size: sm ── */ .Badge[data-ov-size='sm'] { --_ov-badge-min-w: 16px; @@ -100,6 +125,16 @@ --_ov-dot-size: 14px; } +/* ── Size: xl ── */ +.Badge[data-ov-size='xl'] { + --_ov-badge-min-w: 26px; + --_ov-badge-h: 26px; + --_ov-badge-pad: 0 7px; + --_ov-badge-radius: 13px; + --_ov-badge-font: 0.875rem; + --_ov-dot-size: 16px; +} + /* ── Position variants (rectangular) ── */ .Badge[data-ov-position='top-right'] { top: 0; diff --git a/packages/base-ui/src/components/badge/Badge.stories.tsx b/packages/base-ui/src/components/badge/Badge.stories.tsx index 41b8a55..52d99d0 100644 --- a/packages/base-ui/src/components/badge/Badge.stories.tsx +++ b/packages/base-ui/src/components/badge/Badge.stories.tsx @@ -21,12 +21,12 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['standard', 'dot'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, position: { control: 'select', options: ['top-right', 'top-left', 'bottom-right', 'bottom-left'], }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, overlap: { control: 'inline-radio', options: ['rectangular', 'circular'] }, max: { control: 'number' }, content: { control: 'text' }, diff --git a/packages/base-ui/src/components/badge/Badge.test.tsx b/packages/base-ui/src/components/badge/Badge.test.tsx index a6a3e05..712f9f9 100644 --- a/packages/base-ui/src/components/badge/Badge.test.tsx +++ b/packages/base-ui/src/components/badge/Badge.test.tsx @@ -147,6 +147,41 @@ describe('Badge', () => { expect(badge).toBeInTheDocument(); }); + it('xs size prop applies data attribute', () => { + renderWithTheme( + + Icon + , + ); + expect(document.querySelector('[data-ov-size="xs"]')).toBeInTheDocument(); + }); + + it('xl size prop applies data attribute', () => { + renderWithTheme( + + Icon + , + ); + expect(document.querySelector('[data-ov-size="xl"]')).toBeInTheDocument(); + }); + + it('discovery and secondary color variants apply data attributes', () => { + const { unmount } = renderWithTheme( + + Icon + , + ); + expect(document.querySelector('[data-ov-color="discovery"]')).toBeInTheDocument(); + unmount(); + + renderWithTheme( + + Icon + , + ); + expect(document.querySelector('[data-ov-color="secondary"]')).toBeInTheDocument(); + }); + it('overlap prop applies data attribute for circular', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/badge/Badge.tsx b/packages/base-ui/src/components/badge/Badge.tsx index 9f8b684..d24786f 100644 --- a/packages/base-ui/src/components/badge/Badge.tsx +++ b/packages/base-ui/src/components/badge/Badge.tsx @@ -4,8 +4,8 @@ import styles from './Badge.module.css'; export type BadgeVariant = 'standard' | 'dot'; export type BadgePosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; -export type BadgeColor = 'neutral' | 'brand' | 'success' | 'warning' | 'danger'; -export type BadgeSize = 'sm' | 'md' | 'lg'; +export type BadgeColor = 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'discovery' | 'secondary'; +export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export type BadgeOverlap = 'rectangular' | 'circular'; export interface BadgeProps extends Omit, 'color' | 'content'> { diff --git a/packages/base-ui/src/components/banner/Banner.module.css b/packages/base-ui/src/components/banner/Banner.module.css index 602d6f6..f649b43 100644 --- a/packages/base-ui/src/components/banner/Banner.module.css +++ b/packages/base-ui/src/components/banner/Banner.module.css @@ -22,6 +22,15 @@ font-size: var(--_ov-body-font-size); } +.Root[data-ov-size='xs'] { + --_ov-padding-inline: var(--ov-size-banner-padding-inline-xs); + --_ov-padding-block: var(--ov-size-banner-padding-block-xs); + --_ov-gap: var(--ov-size-banner-gap-xs); + --_ov-icon-size: var(--ov-size-banner-icon-size-xs); + --_ov-title-font-size: var(--ov-size-banner-title-font-size-xs); + --_ov-body-font-size: var(--ov-size-banner-body-font-size-xs); +} + .Root[data-ov-size='sm'] { --_ov-padding-inline: var(--ov-size-banner-padding-inline-sm); --_ov-padding-block: var(--ov-size-banner-padding-block-sm); @@ -40,6 +49,15 @@ --_ov-body-font-size: var(--ov-size-banner-body-font-size-lg); } +.Root[data-ov-size='xl'] { + --_ov-padding-inline: var(--ov-size-banner-padding-inline-xl); + --_ov-padding-block: var(--ov-size-banner-padding-block-xl); + --_ov-gap: var(--ov-size-banner-gap-xl); + --_ov-icon-size: var(--ov-size-banner-icon-size-xl); + --_ov-title-font-size: var(--ov-size-banner-title-font-size-xl); + --_ov-body-font-size: var(--ov-size-banner-body-font-size-xl); +} + /* Color mappings */ .Root[data-ov-color='neutral'] { --_ov-accent-bg: var(--ov-color-bg-surface-raised); @@ -83,6 +101,20 @@ --_ov-accent-soft-bg: var(--ov-color-info-soft); } +.Root[data-ov-color='discovery'] { + --_ov-accent-bg: var(--ov-color-discovery); + --_ov-accent-fg: var(--ov-color-discovery); + --_ov-accent-border: var(--ov-color-discovery); + --_ov-accent-soft-bg: var(--ov-color-discovery-soft); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent-bg: var(--ov-color-secondary); + --_ov-accent-fg: var(--ov-color-secondary); + --_ov-accent-border: var(--ov-color-secondary); + --_ov-accent-soft-bg: var(--ov-color-secondary-soft); +} + /* Variant mappings */ .Root[data-ov-variant='solid'] { --_ov-bg: var(--_ov-accent-bg); diff --git a/packages/base-ui/src/components/banner/Banner.stories.tsx b/packages/base-ui/src/components/banner/Banner.stories.tsx index 569d487..0b6b95f 100644 --- a/packages/base-ui/src/components/banner/Banner.stories.tsx +++ b/packages/base-ui/src/components/banner/Banner.stories.tsx @@ -31,9 +31,9 @@ const meta = { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, color: { control: 'select', - options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info'], + options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'], }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, render: (args) => ( diff --git a/packages/base-ui/src/components/banner/Banner.test.tsx b/packages/base-ui/src/components/banner/Banner.test.tsx index 12ee867..42d71b7 100644 --- a/packages/base-ui/src/components/banner/Banner.test.tsx +++ b/packages/base-ui/src/components/banner/Banner.test.tsx @@ -52,4 +52,44 @@ describe('Banner', () => { ); expect(screen.getByRole('alert')).toHaveAttribute('data-ov-color', 'info'); }); + + it('applies xs and xl size data attributes', () => { + const { rerender } = renderWithTheme( + + + Title + + , + ); + expect(screen.getByRole('alert')).toHaveAttribute('data-ov-size', 'xs'); + + rerender( + + + Title + + , + ); + expect(screen.getByRole('alert')).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery and secondary color data attributes', () => { + const { rerender } = renderWithTheme( + + + Discovery + + , + ); + expect(screen.getByRole('alert')).toHaveAttribute('data-ov-color', 'discovery'); + + rerender( + + + Secondary + + , + ); + expect(screen.getByRole('alert')).toHaveAttribute('data-ov-color', 'secondary'); + }); }); diff --git a/packages/base-ui/src/components/basic-list/BasicList.stories.tsx b/packages/base-ui/src/components/basic-list/BasicList.stories.tsx index 9192f7a..c37e7a4 100644 --- a/packages/base-ui/src/components/basic-list/BasicList.stories.tsx +++ b/packages/base-ui/src/components/basic-list/BasicList.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { argTypes: { size: { control: 'inline-radio', - options: ['sm', 'md', 'lg'], + options: ['xs', 'sm', 'md', 'lg', 'xl'], description: 'Controls the overall size of list items (font size, icon size, spacing).', table: { defaultValue: { summary: 'md' } }, }, diff --git a/packages/base-ui/src/components/breadcrumbs/Breadcrumbs.stories.tsx b/packages/base-ui/src/components/breadcrumbs/Breadcrumbs.stories.tsx index 87b6235..5334d50 100644 --- a/packages/base-ui/src/components/breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/base-ui/src/components/breadcrumbs/Breadcrumbs.stories.tsx @@ -20,7 +20,7 @@ const meta = { maxItems: { control: 'number' }, itemsBeforeCollapse: { control: 'number' }, itemsAfterCollapse: { control: 'number' }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, } satisfies Meta; diff --git a/packages/base-ui/src/components/button-group/ButtonGroup.stories.tsx b/packages/base-ui/src/components/button-group/ButtonGroup.stories.tsx index 9a1596f..fed1f44 100644 --- a/packages/base-ui/src/components/button-group/ButtonGroup.stories.tsx +++ b/packages/base-ui/src/components/button-group/ButtonGroup.stories.tsx @@ -14,8 +14,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, orientation: { control: 'inline-radio', options: ['horizontal', 'vertical'] }, attached: { control: 'boolean' }, }, diff --git a/packages/base-ui/src/components/button/Button.module.css b/packages/base-ui/src/components/button/Button.module.css index 37c53ea..456819f 100644 --- a/packages/base-ui/src/components/button/Button.module.css +++ b/packages/base-ui/src/components/button/Button.module.css @@ -53,6 +53,14 @@ cursor: not-allowed; } +.Root[data-ov-size='xs'] { + --_ov-control-height: var(--ov-size-button-height-xs); + --_ov-padding-inline: var(--ov-size-button-padding-inline-xs); + --_ov-font-size: var(--ov-size-button-font-size-xs); + --_ov-content-gap: var(--ov-space-stack-xs); + --_ov-decorator-size: var(--ov-size-icon-button-icon-xs); +} + .Root[data-ov-size='sm'] { --_ov-control-height: var(--ov-size-button-height-sm); --_ov-padding-inline: var(--ov-size-button-padding-inline-sm); @@ -69,6 +77,14 @@ --_ov-decorator-size: var(--ov-size-icon-button-icon-lg); } +.Root[data-ov-size='xl'] { + --_ov-control-height: var(--ov-size-button-height-xl); + --_ov-padding-inline: var(--ov-size-button-padding-inline-xl); + --_ov-font-size: var(--ov-size-button-font-size-xl); + --_ov-content-gap: var(--ov-space-inline-control); + --_ov-decorator-size: var(--ov-size-icon-button-icon-xl); +} + .Root[data-ov-color='neutral'] { --_ov-accent-bg: var(--ov-color-bg-surface-raised); --_ov-accent-fg: var(--ov-color-fg-default); @@ -114,6 +130,33 @@ --_ov-accent-ghost-fg: var(--ov-color-danger); } +.Root[data-ov-color='info'] { + --_ov-accent-bg: var(--ov-color-info); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-info); + --_ov-accent-soft-bg: var(--ov-color-info-soft); + --_ov-accent-soft-fg: var(--ov-color-info); + --_ov-accent-ghost-fg: var(--ov-color-info); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent-bg: var(--ov-color-discovery); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-discovery); + --_ov-accent-soft-bg: var(--ov-color-discovery-soft); + --_ov-accent-soft-fg: var(--ov-color-discovery); + --_ov-accent-ghost-fg: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent-bg: var(--ov-color-secondary); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-secondary); + --_ov-accent-soft-bg: var(--ov-color-secondary-soft); + --_ov-accent-soft-fg: var(--ov-color-secondary); + --_ov-accent-ghost-fg: var(--ov-color-secondary); +} + .Root[data-ov-variant='solid'] { --_ov-bg: var(--_ov-accent-bg); --_ov-fg: var(--_ov-accent-fg); diff --git a/packages/base-ui/src/components/button/Button.stories.tsx b/packages/base-ui/src/components/button/Button.stories.tsx index e26bea7..ecebb5f 100644 --- a/packages/base-ui/src/components/button/Button.stories.tsx +++ b/packages/base-ui/src/components/button/Button.stories.tsx @@ -68,8 +68,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, startIcon: { control: 'select', options: DECORATOR_ICON_OPTIONS }, endIcon: { control: 'select', options: DECORATOR_ICON_OPTIONS }, startDecorator: { control: false, table: { disable: true } }, diff --git a/packages/base-ui/src/components/button/Button.test.tsx b/packages/base-ui/src/components/button/Button.test.tsx index 7a23ff1..9325c1b 100644 --- a/packages/base-ui/src/components/button/Button.test.tsx +++ b/packages/base-ui/src/components/button/Button.test.tsx @@ -36,4 +36,28 @@ describe('Button', () => { expect(button.querySelector('[data-ov-slot="start-decorator"]')).toBeInTheDocument(); expect(button.querySelector('[data-ov-slot="end-decorator"]')).toBeInTheDocument(); }); + + it('renders with xs size', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: 'Tiny' }); + expect(button).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('renders with xl size', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: 'Hero' }); + expect(button).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('renders with discovery color', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: 'New' }); + expect(button).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('renders with secondary color', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: 'Meta' }); + expect(button).toHaveAttribute('data-ov-color', 'secondary'); + }); }); diff --git a/packages/base-ui/src/components/card/Card.module.css b/packages/base-ui/src/components/card/Card.module.css index 1ffdf0b..c210afa 100644 --- a/packages/base-ui/src/components/card/Card.module.css +++ b/packages/base-ui/src/components/card/Card.module.css @@ -43,6 +43,23 @@ overflow: visible; } +.Root[data-ov-size='xs'] { + --_ov-padding-inline: var(--ov-size-card-padding-inline-xs, var(--ov-size-card-padding-inline-sm)); + --_ov-padding-block: var(--ov-size-card-padding-block-xs, var(--ov-size-card-padding-block-sm)); + --_ov-gap: var(--ov-size-card-gap-xs, var(--ov-size-card-gap-sm)); + --_ov-footer-padding-block: var(--ov-size-card-footer-padding-block-xs, var(--ov-size-card-footer-padding-block-sm)); + --_ov-footer-gap: var(--ov-size-card-footer-gap-xs, var(--ov-size-card-footer-gap-sm)); + --_ov-action-height: var(--ov-size-card-action-height-xs, var(--ov-size-card-action-height-sm)); + --_ov-action-padding-inline: var(--ov-size-card-action-padding-inline-xs, var(--ov-size-card-action-padding-inline-sm)); + --_ov-action-font-size: var(--ov-size-card-action-font-size-xs, var(--ov-size-card-action-font-size-sm)); + --_ov-title-font-size: var(--ov-size-card-title-font-size-xs, var(--ov-size-card-title-font-size-sm)); + --_ov-body-font-size: var(--ov-size-card-body-font-size-xs, var(--ov-size-card-body-font-size-sm)); + --_ov-description-font-size: var(--ov-size-card-description-font-size-xs, var(--ov-size-card-description-font-size-sm)); + --_ov-stat-font-size: var(--ov-size-card-stat-font-size-xs, var(--ov-size-card-stat-font-size-sm)); + --_ov-eyebrow-font-size: var(--ov-size-card-eyebrow-font-size-xs, var(--ov-size-card-eyebrow-font-size-sm)); + --_ov-indicator-dot: var(--ov-size-card-indicator-dot-xs, var(--ov-size-card-indicator-dot-sm)); +} + .Root[data-ov-size='sm'] { --_ov-padding-inline: var(--ov-size-card-padding-inline-sm); --_ov-padding-block: var(--ov-size-card-padding-block-sm); @@ -77,6 +94,23 @@ --_ov-indicator-dot: var(--ov-size-card-indicator-dot-lg); } +.Root[data-ov-size='xl'] { + --_ov-padding-inline: var(--ov-size-card-padding-inline-xl, var(--ov-size-card-padding-inline-lg)); + --_ov-padding-block: var(--ov-size-card-padding-block-xl, var(--ov-size-card-padding-block-lg)); + --_ov-gap: var(--ov-size-card-gap-xl, var(--ov-size-card-gap-lg)); + --_ov-footer-padding-block: var(--ov-size-card-footer-padding-block-xl, var(--ov-size-card-footer-padding-block-lg)); + --_ov-footer-gap: var(--ov-size-card-footer-gap-xl, var(--ov-size-card-footer-gap-lg)); + --_ov-action-height: var(--ov-size-card-action-height-xl, var(--ov-size-card-action-height-lg)); + --_ov-action-padding-inline: var(--ov-size-card-action-padding-inline-xl, var(--ov-size-card-action-padding-inline-lg)); + --_ov-action-font-size: var(--ov-size-card-action-font-size-xl, var(--ov-size-card-action-font-size-lg)); + --_ov-title-font-size: var(--ov-size-card-title-font-size-xl, var(--ov-size-card-title-font-size-lg)); + --_ov-body-font-size: var(--ov-size-card-body-font-size-xl, var(--ov-size-card-body-font-size-lg)); + --_ov-description-font-size: var(--ov-size-card-description-font-size-xl, var(--ov-size-card-description-font-size-lg)); + --_ov-stat-font-size: var(--ov-size-card-stat-font-size-xl, var(--ov-size-card-stat-font-size-lg)); + --_ov-eyebrow-font-size: var(--ov-size-card-eyebrow-font-size-xl, var(--ov-size-card-eyebrow-font-size-lg)); + --_ov-indicator-dot: var(--ov-size-card-indicator-dot-xl, var(--ov-size-card-indicator-dot-lg)); +} + /* ─── Surface levels ─── */ .Root[data-ov-surface='base'] { @@ -142,6 +176,24 @@ --_ov-accent-fg: var(--ov-color-danger); } +.Root[data-ov-color='info'] { + --_ov-accent: var(--ov-color-info); + --_ov-accent-soft: var(--ov-color-info-soft); + --_ov-accent-fg: var(--ov-color-info); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent: var(--ov-color-discovery); + --_ov-accent-soft: var(--ov-color-discovery-soft); + --_ov-accent-fg: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent: var(--ov-color-secondary); + --_ov-accent-soft: var(--ov-color-secondary-soft); + --_ov-accent-fg: var(--ov-color-secondary); +} + .Root[data-ov-variant='solid'] { --_ov-bg: color-mix(in srgb, var(--_ov-surface-bg) 86%, var(--_ov-accent) 14%); --_ov-border: color-mix( diff --git a/packages/base-ui/src/components/card/Card.stories.tsx b/packages/base-ui/src/components/card/Card.stories.tsx index c04de77..ac3e4af 100644 --- a/packages/base-ui/src/components/card/Card.stories.tsx +++ b/packages/base-ui/src/components/card/Card.stories.tsx @@ -15,8 +15,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, elevation: { control: 'inline-radio', options: [0, 1, 2, 3] }, surface: { control: 'select', diff --git a/packages/base-ui/src/components/card/Card.test.tsx b/packages/base-ui/src/components/card/Card.test.tsx index d8139f9..471617e 100644 --- a/packages/base-ui/src/components/card/Card.test.tsx +++ b/packages/base-ui/src/components/card/Card.test.tsx @@ -290,6 +290,46 @@ describe('Card', () => { expect(group).toHaveAttribute('data-ov-gap', 'lg'); }); + it('renders xs size', () => { + renderWithTheme( + + Content + , + ); + const card = screen.getByText('Content').closest('section'); + expect(card).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('renders xl size', () => { + renderWithTheme( + + Content + , + ); + const card = screen.getByText('Content').closest('section'); + expect(card).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('renders discovery color', () => { + renderWithTheme( + + Content + , + ); + const card = screen.getByText('Content').closest('section'); + expect(card).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('renders secondary color', () => { + renderWithTheme( + + Content + , + ); + const card = screen.getByText('Content').closest('section'); + expect(card).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('renders Group with auto columns', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/checkbox-group/CheckboxGroup.stories.tsx b/packages/base-ui/src/components/checkbox-group/CheckboxGroup.stories.tsx index 6f5562d..f465cd4 100644 --- a/packages/base-ui/src/components/checkbox-group/CheckboxGroup.stories.tsx +++ b/packages/base-ui/src/components/checkbox-group/CheckboxGroup.stories.tsx @@ -14,8 +14,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, orientation: { control: 'inline-radio', options: ['vertical', 'horizontal'] }, }, render: (args) => ( diff --git a/packages/base-ui/src/components/checkbox/Checkbox.module.css b/packages/base-ui/src/components/checkbox/Checkbox.module.css index 56d3e08..3d14ab6 100644 --- a/packages/base-ui/src/components/checkbox/Checkbox.module.css +++ b/packages/base-ui/src/components/checkbox/Checkbox.module.css @@ -54,6 +54,12 @@ column-gap: 0; } +.Root[data-ov-size='xs'] { + --_ov-control-size: var(--ov-size-choice-control-xs); + --_ov-indicator-size: var(--ov-size-choice-indicator-xs); + --_ov-gap: var(--ov-size-choice-gap-xs); +} + .Root[data-ov-size='sm'] { --_ov-control-size: var(--ov-size-choice-control-sm); --_ov-indicator-size: var(--ov-size-choice-indicator-sm); @@ -66,6 +72,12 @@ --_ov-gap: var(--ov-size-choice-gap-lg); } +.Root[data-ov-size='xl'] { + --_ov-control-size: var(--ov-size-choice-control-xl); + --_ov-indicator-size: var(--ov-size-choice-indicator-xl); + --_ov-gap: var(--ov-size-choice-gap-xl); +} + .Root[data-ov-color='neutral'] { --_ov-accent-bg: var(--ov-color-brand-500); --_ov-accent-fg: var(--ov-color-fg-inverse); @@ -110,6 +122,33 @@ --_ov-focus-border: var(--ov-color-danger); } +.Root[data-ov-color='info'] { + --_ov-accent-bg: var(--ov-color-info); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-info); + --_ov-accent-soft-bg: var(--ov-color-info-soft); + --_ov-accent-soft-fg: var(--ov-color-info); + --_ov-focus-border: var(--ov-color-info); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent-bg: var(--ov-color-discovery); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-discovery); + --_ov-accent-soft-bg: var(--ov-color-discovery-soft); + --_ov-accent-soft-fg: var(--ov-color-discovery); + --_ov-focus-border: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent-bg: var(--ov-color-secondary); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-secondary); + --_ov-accent-soft-bg: var(--ov-color-secondary-soft); + --_ov-accent-soft-fg: var(--ov-color-secondary); + --_ov-focus-border: var(--ov-color-secondary); +} + .Root[data-ov-variant='solid'] { --_ov-control-bg-checked: var(--_ov-accent-bg); --_ov-control-border-checked: color-mix(in srgb, var(--_ov-accent-bg) 84%, black 16%); @@ -318,12 +357,20 @@ line-height: 1.4; } +.Root[data-ov-size='xs'] .Label { + font-size: var(--ov-font-size-caption); +} + .Root[data-ov-size='sm'] .Label { font-size: var(--ov-font-size-caption); } .Root[data-ov-size='lg'] .Label { - font-size: 1rem; /* restores original 16px — body token resolves to 14px which removes the size progression */ + font-size: var(--ov-size-button-font-size-lg); +} + +.Root[data-ov-size='xl'] .Label { + font-size: var(--ov-size-button-font-size-lg); } .Item { diff --git a/packages/base-ui/src/components/checkbox/Checkbox.stories.tsx b/packages/base-ui/src/components/checkbox/Checkbox.stories.tsx index 04987bd..0537a74 100644 --- a/packages/base-ui/src/components/checkbox/Checkbox.stories.tsx +++ b/packages/base-ui/src/components/checkbox/Checkbox.stories.tsx @@ -21,8 +21,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, defaultChecked: { control: 'boolean' }, indeterminate: { control: 'boolean' }, labelPosition: { control: 'inline-radio', options: ['start', 'end'] }, diff --git a/packages/base-ui/src/components/checkbox/Checkbox.test.tsx b/packages/base-ui/src/components/checkbox/Checkbox.test.tsx index 7066f2d..c582a48 100644 --- a/packages/base-ui/src/components/checkbox/Checkbox.test.tsx +++ b/packages/base-ui/src/components/checkbox/Checkbox.test.tsx @@ -72,4 +72,38 @@ describe('Checkbox', () => { expect(checkbox).toHaveAttribute('data-ov-label-position', 'start'); expect(checkbox).toHaveAttribute('data-ov-layout', 'spread'); }); + + it('applies xs size attribute', () => { + renderWithTheme(Tiny option); + const checkbox = screen.getByRole('checkbox', { name: 'Tiny option' }); + expect(checkbox).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size attribute', () => { + renderWithTheme(Large option); + const checkbox = screen.getByRole('checkbox', { name: 'Large option' }); + expect(checkbox).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery color attribute', () => { + renderWithTheme( + + Discovery feature + , + ); + const checkbox = screen.getByRole('checkbox', { name: 'Discovery feature' }); + expect(checkbox).toHaveAttribute('data-ov-color', 'discovery'); + expect(checkbox).toHaveAttribute('data-checked'); + }); + + it('applies secondary color attribute', () => { + renderWithTheme( + + Secondary option + , + ); + const checkbox = screen.getByRole('checkbox', { name: 'Secondary option' }); + expect(checkbox).toHaveAttribute('data-ov-color', 'secondary'); + expect(checkbox).toHaveAttribute('data-checked'); + }); }); diff --git a/packages/base-ui/src/components/chip/Chip.module.css b/packages/base-ui/src/components/chip/Chip.module.css index 2dc4003..540ac1b 100644 --- a/packages/base-ui/src/components/chip/Chip.module.css +++ b/packages/base-ui/src/components/chip/Chip.module.css @@ -74,6 +74,16 @@ padding-inline-end: var(--_ov-decorator-inset); } +.Root[data-ov-size='xs'] { + --_ov-height: var(--ov-size-chip-height-xs); + --_ov-padding-inline: var(--ov-size-chip-padding-inline-xs); + --_ov-font-size: var(--ov-size-chip-font-size-xs); + --_ov-gap: var(--ov-size-chip-gap-xs); + --_ov-icon-size: var(--ov-size-chip-icon-size-xs); + --_ov-radius: var(--ov-size-chip-radius-xs); + --_ov-decorator-inset: 3px; +} + .Root[data-ov-size='sm'] { --_ov-height: var(--ov-size-chip-height-sm); --_ov-padding-inline: var(--ov-size-chip-padding-inline-sm); @@ -94,6 +104,16 @@ --_ov-decorator-inset: 6px; /* half of 12px text-side padding */ } +.Root[data-ov-size='xl'] { + --_ov-height: var(--ov-size-chip-height-xl); + --_ov-padding-inline: var(--ov-size-chip-padding-inline-xl); + --_ov-font-size: var(--ov-size-chip-font-size-xl); + --_ov-gap: var(--ov-size-chip-gap-xl); + --_ov-icon-size: var(--ov-size-chip-icon-size-xl); + --_ov-radius: var(--ov-size-chip-radius-xl); + --_ov-decorator-inset: 8px; +} + .Root[data-ov-color='neutral'] { --_ov-accent-bg: var(--ov-color-bg-surface-raised); --_ov-accent-fg: var(--ov-color-fg-default); @@ -139,6 +159,33 @@ --_ov-accent-ghost-fg: var(--ov-color-danger); } +.Root[data-ov-color='info'] { + --_ov-accent-bg: var(--ov-color-info); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-info); + --_ov-accent-soft-bg: var(--ov-color-info-soft); + --_ov-accent-soft-fg: var(--ov-color-info); + --_ov-accent-ghost-fg: var(--ov-color-info); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent-bg: var(--ov-color-discovery); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-discovery); + --_ov-accent-soft-bg: var(--ov-color-discovery-soft); + --_ov-accent-soft-fg: var(--ov-color-discovery); + --_ov-accent-ghost-fg: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent-bg: var(--ov-color-secondary); + --_ov-accent-fg: var(--ov-color-fg-inverse); + --_ov-accent-border: var(--ov-color-secondary); + --_ov-accent-soft-bg: var(--ov-color-secondary-soft); + --_ov-accent-soft-fg: var(--ov-color-secondary); + --_ov-accent-ghost-fg: var(--ov-color-secondary); +} + .Root[data-ov-variant='solid'] { --_ov-bg: var(--_ov-accent-bg); --_ov-fg: var(--_ov-accent-fg); diff --git a/packages/base-ui/src/components/chip/Chip.stories.tsx b/packages/base-ui/src/components/chip/Chip.stories.tsx index b5af310..de6eced 100644 --- a/packages/base-ui/src/components/chip/Chip.stories.tsx +++ b/packages/base-ui/src/components/chip/Chip.stories.tsx @@ -55,8 +55,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, mono: { control: 'boolean' }, clickable: { control: 'boolean' }, startIcon: { control: 'select', options: DECORATOR_ICON_OPTIONS }, diff --git a/packages/base-ui/src/components/chip/Chip.test.tsx b/packages/base-ui/src/components/chip/Chip.test.tsx index ace89e0..e37c28d 100644 --- a/packages/base-ui/src/components/chip/Chip.test.tsx +++ b/packages/base-ui/src/components/chip/Chip.test.tsx @@ -60,6 +60,30 @@ describe('Chip', () => { expect(chip).toHaveAttribute('data-ov-clickable', 'true'); }); + it('applies xs size data attribute', () => { + renderWithTheme(Tiny); + const chip = screen.getByText('Tiny').closest('[data-ov-size]'); + expect(chip).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size data attribute', () => { + renderWithTheme(Large); + const chip = screen.getByText('Large').closest('[data-ov-size]'); + expect(chip).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery color data attribute', () => { + renderWithTheme(Discover); + const chip = screen.getByText('Discover').closest('[data-ov-color]'); + expect(chip).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('applies secondary color data attribute', () => { + renderWithTheme(Secondary); + const chip = screen.getByText('Secondary').closest('[data-ov-color]'); + expect(chip).toHaveAttribute('data-ov-color', 'secondary'); + }); + describe('Chip.Group', () => { it('renders children in a wrapping row by default', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/code-block/CodeBlock.module.css b/packages/base-ui/src/components/code-block/CodeBlock.module.css index 82d4341..035b2a9 100644 --- a/packages/base-ui/src/components/code-block/CodeBlock.module.css +++ b/packages/base-ui/src/components/code-block/CodeBlock.module.css @@ -51,6 +51,18 @@ --_ov-selected-bg: var(--ov-color-danger-soft); } +.Root[data-ov-color='info'] { + --_ov-selected-bg: var(--ov-color-info-soft); +} + +.Root[data-ov-color='discovery'] { + --_ov-selected-bg: var(--ov-color-discovery-soft); +} + +.Root[data-ov-color='secondary'] { + --_ov-selected-bg: var(--ov-color-secondary-soft); +} + .Root[data-ov-variant='solid'] { --_ov-bg: color-mix(in srgb, var(--ov-color-bg-surface-overlay) 84%, var(--_ov-selected-bg) 16%); } diff --git a/packages/base-ui/src/components/code-block/CodeBlock.stories.tsx b/packages/base-ui/src/components/code-block/CodeBlock.stories.tsx index 31c812f..e956dc3 100644 --- a/packages/base-ui/src/components/code-block/CodeBlock.stories.tsx +++ b/packages/base-ui/src/components/code-block/CodeBlock.stories.tsx @@ -52,8 +52,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, lineNumbers: { control: 'boolean' }, copyable: { control: 'boolean' }, wrap: { control: 'boolean' }, diff --git a/packages/base-ui/src/components/code-block/CodeBlock.test.tsx b/packages/base-ui/src/components/code-block/CodeBlock.test.tsx index bc6b4a6..a7ccd6c 100644 --- a/packages/base-ui/src/components/code-block/CodeBlock.test.tsx +++ b/packages/base-ui/src/components/code-block/CodeBlock.test.tsx @@ -47,4 +47,15 @@ describe('CodeBlock', () => { const root = screen.getByText('a-very-long-line').closest('[data-ov-wrap]'); expect(root).toHaveAttribute('data-ov-wrap', 'true'); }); + + it('renders discovery and secondary colors', () => { + const { rerender } = renderWithTheme( + , + ); + const root = screen.getByText('hello').closest('[data-ov-color]'); + expect(root).toHaveAttribute('data-ov-color', 'discovery'); + + rerender(); + expect(screen.getByText('hello').closest('[data-ov-color]')).toHaveAttribute('data-ov-color', 'secondary'); + }); }); diff --git a/packages/base-ui/src/components/combobox/Combobox.module.css b/packages/base-ui/src/components/combobox/Combobox.module.css index 09d51dc..1c3f370 100644 --- a/packages/base-ui/src/components/combobox/Combobox.module.css +++ b/packages/base-ui/src/components/combobox/Combobox.module.css @@ -34,6 +34,11 @@ box-shadow: 0 0 0 1px var(--ov-color-state-focus-ring); } +.Input[data-ov-size='xs'] { + --_ov-control-height: var(--ov-control-height-xs); + --_ov-font-size: var(--ov-font-size-caption); +} + .Input[data-ov-size='sm'] { --_ov-control-height: var(--ov-control-height-sm); --_ov-font-size: var(--ov-font-size-caption); @@ -43,6 +48,15 @@ --_ov-control-height: var(--ov-control-height-lg); } +.Input[data-ov-size='xl'] { + --_ov-control-height: var(--ov-control-height-xl); +} + +.Input[data-ov-color='neutral'] { + --_ov-focus: var(--ov-color-border-default); + --_ov-accent-soft: var(--ov-color-state-selected); +} + .Input[data-ov-color='brand'] { --_ov-focus: var(--ov-color-brand-400); --_ov-accent-soft: var(--ov-color-accent-soft); @@ -63,6 +77,21 @@ --_ov-accent-soft: var(--ov-color-danger-soft); } +.Input[data-ov-color='info'] { + --_ov-focus: var(--ov-color-info); + --_ov-accent-soft: var(--ov-color-info-soft); +} + +.Input[data-ov-color='discovery'] { + --_ov-focus: var(--ov-color-discovery); + --_ov-accent-soft: var(--ov-color-discovery-soft); +} + +.Input[data-ov-color='secondary'] { + --_ov-focus: var(--ov-color-secondary); + --_ov-accent-soft: var(--ov-color-secondary-soft); +} + .Input[data-ov-variant='solid'] { --_ov-bg: color-mix(in srgb, var(--ov-color-bg-surface) 86%, var(--_ov-accent-soft) 14%); } @@ -76,6 +105,11 @@ --_ov-border: transparent; } +.Input[data-ov-variant='soft'] { + --_ov-bg: var(--_ov-accent-soft); + --_ov-border: transparent; +} + .Positioner { z-index: var(--ov-z-popup, 120); } @@ -106,6 +140,17 @@ padding: var(--_ov-popup-padding); } +.Popup[data-ov-size='xs'] { + --_ov-popup-padding: calc(var(--ov-size-list-item-gap) - 2px); + --_ov-list-gap: calc(var(--ov-size-list-item-gap) - 2px); + --_ov-item-padding-inline: var(--ov-size-list-item-padding-inline-xs); + --_ov-item-height: var(--ov-size-list-item-height-xs); + --_ov-decorator-size: var(--ov-size-icon-button-icon-xs); + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-sm); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-sm); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-sm); +} + .Popup[data-ov-size='sm'] { --_ov-popup-padding: calc(var(--ov-size-list-item-gap) - 1px); --_ov-list-gap: calc(var(--ov-size-list-item-gap) - 1px); @@ -128,6 +173,21 @@ --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-lg); } +.Popup[data-ov-size='xl'] { + --_ov-popup-padding: calc(var(--ov-size-list-item-gap) + 2px); + --_ov-list-gap: calc(var(--ov-size-list-item-gap) + 2px); + --_ov-item-padding-inline: var(--ov-size-list-item-padding-inline-xl); + --_ov-item-height: var(--ov-size-list-item-height-xl); + --_ov-decorator-size: var(--ov-size-icon-button-icon-xl); + --_ov-group-label-font-size: var(--ov-size-list-group-label-font-size-lg); + --_ov-group-label-gap-top: var(--ov-size-list-group-label-gap-top-lg); + --_ov-group-label-gap-after: var(--ov-size-list-group-label-gap-after-lg); +} + +.Popup[data-ov-color='neutral'] { + --_ov-selected-bg: var(--ov-color-state-selected); +} + .Popup[data-ov-color='brand'] { --_ov-selected-bg: var(--ov-color-accent-soft); } @@ -144,6 +204,18 @@ --_ov-selected-bg: var(--ov-color-danger-soft); } +.Popup[data-ov-color='info'] { + --_ov-selected-bg: var(--ov-color-info-soft); +} + +.Popup[data-ov-color='discovery'] { + --_ov-selected-bg: var(--ov-color-discovery-soft); +} + +.Popup[data-ov-color='secondary'] { + --_ov-selected-bg: var(--ov-color-secondary-soft); +} + .List { margin: 0; padding: 0; @@ -268,6 +340,11 @@ color var(--ov-duration-interactive) var(--ov-ease-standard); } +.Trigger[data-ov-size='xs'], +.Clear[data-ov-size='xs'] { + --_ov-control-height: var(--ov-control-height-xs); +} + .Trigger[data-ov-size='sm'], .Clear[data-ov-size='sm'] { --_ov-control-height: var(--ov-control-height-sm); @@ -278,6 +355,16 @@ --_ov-control-height: var(--ov-control-height-lg); } +.Trigger[data-ov-size='xl'], +.Clear[data-ov-size='xl'] { + --_ov-control-height: var(--ov-control-height-xl); +} + +.Trigger[data-ov-color='neutral'], +.Clear[data-ov-color='neutral'] { + --_ov-fg: var(--ov-color-fg-muted); +} + .Trigger[data-ov-color='brand'], .Clear[data-ov-color='brand'] { --_ov-fg: var(--ov-color-brand-400); @@ -298,6 +385,21 @@ --_ov-fg: var(--ov-color-danger); } +.Trigger[data-ov-color='info'], +.Clear[data-ov-color='info'] { + --_ov-fg: var(--ov-color-info); +} + +.Trigger[data-ov-color='discovery'], +.Clear[data-ov-color='discovery'] { + --_ov-fg: var(--ov-color-discovery); +} + +.Trigger[data-ov-color='secondary'], +.Clear[data-ov-color='secondary'] { + --_ov-fg: var(--ov-color-secondary); +} + .Trigger[data-ov-variant='solid'], .Clear[data-ov-variant='solid'] { --_ov-bg: var(--ov-color-state-selected); @@ -315,6 +417,12 @@ --_ov-border: transparent; } +.Trigger[data-ov-variant='soft'], +.Clear[data-ov-variant='soft'] { + --_ov-bg: var(--ov-color-state-selected); + --_ov-border: transparent; +} + .Trigger:focus-visible, .Clear:focus-visible, .ChipRemove:focus-visible { diff --git a/packages/base-ui/src/components/combobox/Combobox.stories.tsx b/packages/base-ui/src/components/combobox/Combobox.stories.tsx index 2328865..b0ee666 100644 --- a/packages/base-ui/src/components/combobox/Combobox.stories.tsx +++ b/packages/base-ui/src/components/combobox/Combobox.stories.tsx @@ -88,8 +88,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, parameters: { controls: { diff --git a/packages/base-ui/src/components/combobox/Combobox.test.tsx b/packages/base-ui/src/components/combobox/Combobox.test.tsx index 50e99c8..fb828cb 100644 --- a/packages/base-ui/src/components/combobox/Combobox.test.tsx +++ b/packages/base-ui/src/components/combobox/Combobox.test.tsx @@ -49,4 +49,124 @@ describe('Combobox', () => { expect(screen.getByText('Local Runtime')).toBeInTheDocument(); }); + + it('applies xs size data attribute to input', () => { + renderWithTheme( + item.label} + itemToStringValue={(item: Runtime) => item.id} + items={runtimes} + > + + + + + + {(runtime: Runtime) => ( + + {runtime.label} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Select runtime xs'); + expect(input).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size data attribute to input', () => { + renderWithTheme( + item.label} + itemToStringValue={(item: Runtime) => item.id} + items={runtimes} + > + + + + + + {(runtime: Runtime) => ( + + {runtime.label} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Select runtime xl'); + expect(input).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery color data attribute to input', () => { + renderWithTheme( + item.label} + itemToStringValue={(item: Runtime) => item.id} + items={runtimes} + > + + + + + + {(runtime: Runtime) => ( + + {runtime.label} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Select runtime discovery'); + expect(input).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('applies secondary color data attribute to input', () => { + renderWithTheme( + item.label} + itemToStringValue={(item: Runtime) => item.id} + items={runtimes} + > + + + + + + {(runtime: Runtime) => ( + + {runtime.label} + + )} + + + + + , + ); + + const input = screen.getByPlaceholderText('Select runtime secondary'); + expect(input).toHaveAttribute('data-ov-color', 'secondary'); + }); }); diff --git a/packages/base-ui/src/components/confirm-button/ConfirmButton.stories.tsx b/packages/base-ui/src/components/confirm-button/ConfirmButton.stories.tsx index 44f03b6..9e9dd0a 100644 --- a/packages/base-ui/src/components/confirm-button/ConfirmButton.stories.tsx +++ b/packages/base-ui/src/components/confirm-button/ConfirmButton.stories.tsx @@ -21,11 +21,11 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, confirmColor: { control: 'select', - options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info'], + options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'], }, confirmTimeout: { control: { type: 'number', min: 500, max: 10000, step: 500 } }, onConfirm: { action: 'onConfirm' }, diff --git a/packages/base-ui/src/components/context-menu/ContextMenu.module.css b/packages/base-ui/src/components/context-menu/ContextMenu.module.css index 9ac0cd7..f5d9193 100644 --- a/packages/base-ui/src/components/context-menu/ContextMenu.module.css +++ b/packages/base-ui/src/components/context-menu/ContextMenu.module.css @@ -75,6 +75,35 @@ --_ov-selected-bg: var(--ov-color-danger-soft); } +.Popup[data-ov-color='info'] { + --_ov-selected-bg: var(--ov-color-info-soft); +} + +.Popup[data-ov-color='discovery'] { + --_ov-selected-bg: var(--ov-color-discovery-soft); +} + +.Popup[data-ov-color='secondary'] { + --_ov-selected-bg: var(--ov-color-secondary-soft); +} + +.Popup[data-ov-variant='solid'] { + --_ov-bg: color-mix(in srgb, var(--ov-color-bg-surface-overlay) 86%, var(--_ov-selected-bg) 14%); +} + +.Popup[data-ov-variant='soft'] { + --_ov-bg: color-mix(in srgb, var(--ov-color-bg-surface-overlay) 90%, var(--_ov-selected-bg) 10%); +} + +.Popup[data-ov-variant='outline'] { + --_ov-border: color-mix(in srgb, var(--ov-color-border-default) 50%, var(--_ov-selected-bg) 50%); +} + +.Popup[data-ov-variant='ghost'] { + --_ov-bg: var(--ov-color-bg-surface-overlay); + --_ov-border: transparent; +} + .GroupLabel { padding-inline: var(--_ov-item-padding-inline); margin-block-start: var(--_ov-group-label-gap-top); diff --git a/packages/base-ui/src/components/context-menu/ContextMenu.stories.tsx b/packages/base-ui/src/components/context-menu/ContextMenu.stories.tsx index c34fd94..24762ba 100644 --- a/packages/base-ui/src/components/context-menu/ContextMenu.stories.tsx +++ b/packages/base-ui/src/components/context-menu/ContextMenu.stories.tsx @@ -13,8 +13,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, render: (args) => ( diff --git a/packages/base-ui/src/components/context-menu/ContextMenu.test.tsx b/packages/base-ui/src/components/context-menu/ContextMenu.test.tsx index 1411203..cb20822 100644 --- a/packages/base-ui/src/components/context-menu/ContextMenu.test.tsx +++ b/packages/base-ui/src/components/context-menu/ContextMenu.test.tsx @@ -4,6 +4,40 @@ import { renderWithTheme } from '../../test/render'; import { ContextMenu } from './ContextMenu'; describe('ContextMenu', () => { + it('renders discovery color on menu items', () => { + renderWithTheme( + + Surface + + + + Action + + + + , + ); + fireEvent.contextMenu(screen.getByText('Surface')); + expect(screen.getByText('Action')).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('renders secondary color on menu items', () => { + renderWithTheme( + + Surface + + + + Action + + + + , + ); + fireEvent.contextMenu(screen.getByText('Surface')); + expect(screen.getByText('Action')).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('opens on right click and applies themed attributes to menu items', () => { renderWithTheme( diff --git a/packages/base-ui/src/components/data-table/DataTable.Nested.stories.tsx b/packages/base-ui/src/components/data-table/DataTable.Nested.stories.tsx index e9a99c8..f525e36 100644 --- a/packages/base-ui/src/components/data-table/DataTable.Nested.stories.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.Nested.stories.tsx @@ -350,7 +350,7 @@ const podColumns: ColumnDef[] = [ enableResizing: false, meta: { align: 'center' }, cell: () => ( - + ), diff --git a/packages/base-ui/src/components/data-table/DataTable.Pinning.stories.tsx b/packages/base-ui/src/components/data-table/DataTable.Pinning.stories.tsx index 2040430..9d54b77 100644 --- a/packages/base-ui/src/components/data-table/DataTable.Pinning.stories.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.Pinning.stories.tsx @@ -67,7 +67,7 @@ const actionsColumn: ColumnDef = { id: 'actions', header: '', cell: () => ( - + ), diff --git a/packages/base-ui/src/components/data-table/DataTable.Selection.stories.tsx b/packages/base-ui/src/components/data-table/DataTable.Selection.stories.tsx index d58cad0..8a7a8a2 100644 --- a/packages/base-ui/src/components/data-table/DataTable.Selection.stories.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.Selection.stories.tsx @@ -72,8 +72,8 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, }; diff --git a/packages/base-ui/src/components/data-table/DataTable.Virtualized.stories.tsx b/packages/base-ui/src/components/data-table/DataTable.Virtualized.stories.tsx index 3b73149..bb1e3dc 100644 --- a/packages/base-ui/src/components/data-table/DataTable.Virtualized.stories.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.Virtualized.stories.tsx @@ -84,7 +84,7 @@ const actionsColumn: ColumnDef = { id: 'actions', header: '', cell: () => ( - + ), diff --git a/packages/base-ui/src/components/data-table/DataTable.module.css b/packages/base-ui/src/components/data-table/DataTable.module.css index f2a778e..ad6b922 100644 --- a/packages/base-ui/src/components/data-table/DataTable.module.css +++ b/packages/base-ui/src/components/data-table/DataTable.module.css @@ -52,6 +52,14 @@ /* --- Size variants --- */ +.Root[data-ov-size='xs'] { + --_ov-cell-padding-inline: var(--ov-size-table-cell-padding-inline-xs); + --_ov-cell-padding-block: var(--ov-size-table-cell-padding-block-xs); + --_ov-cell-font-size: var(--ov-size-table-cell-font-size-xs); + --_ov-header-font-size: var(--ov-size-table-header-font-size-xs); + --_ov-row-height: var(--ov-size-table-row-height-xs); +} + .Root[data-ov-size='sm'] { --_ov-cell-padding-inline: var(--ov-size-table-cell-padding-inline-sm); --_ov-cell-padding-block: var(--ov-size-table-cell-padding-block-sm); @@ -68,6 +76,14 @@ --_ov-row-height: var(--ov-size-table-row-height-lg); } +.Root[data-ov-size='xl'] { + --_ov-cell-padding-inline: var(--ov-size-table-cell-padding-inline-xl); + --_ov-cell-padding-block: var(--ov-size-table-cell-padding-block-xl); + --_ov-cell-font-size: var(--ov-size-table-cell-font-size-xl); + --_ov-header-font-size: var(--ov-size-table-header-font-size-xl); + --_ov-row-height: var(--ov-size-table-row-height-xl); +} + /* --- Color variants --- */ .Root[data-ov-color='neutral'] { @@ -95,6 +111,21 @@ --_ov-accent-soft: var(--ov-color-danger-soft); } +.Root[data-ov-color='info'] { + --_ov-accent: var(--ov-color-info); + --_ov-accent-soft: var(--ov-color-info-soft); +} + +.Root[data-ov-color='discovery'] { + --_ov-accent: var(--ov-color-discovery); + --_ov-accent-soft: var(--ov-color-discovery-soft); +} + +.Root[data-ov-color='secondary'] { + --_ov-accent: var(--ov-color-secondary); + --_ov-accent-soft: var(--ov-color-secondary-soft); +} + /* --- Visual variants --- */ .Root[data-ov-variant='solid'] { diff --git a/packages/base-ui/src/components/data-table/DataTable.stories.tsx b/packages/base-ui/src/components/data-table/DataTable.stories.tsx index a1648cd..f2b6ce1 100644 --- a/packages/base-ui/src/components/data-table/DataTable.stories.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.stories.tsx @@ -56,8 +56,8 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, }; diff --git a/packages/base-ui/src/components/data-table/DataTable.test.tsx b/packages/base-ui/src/components/data-table/DataTable.test.tsx index 44181bb..c5f8820 100644 --- a/packages/base-ui/src/components/data-table/DataTable.test.tsx +++ b/packages/base-ui/src/components/data-table/DataTable.test.tsx @@ -27,8 +27,8 @@ const columns: ColumnDef[] = [ function TestTable(props: { variant?: 'solid' | 'soft' | 'outline' | 'ghost'; - color?: 'neutral' | 'brand' | 'success' | 'warning' | 'danger'; - size?: 'sm' | 'md' | 'lg'; + color?: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'discovery' | 'secondary'; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; hoverable?: boolean; stickyHeader?: boolean; striped?: boolean; @@ -66,6 +66,26 @@ describe('DataTable', () => { expect(root).toHaveAttribute('data-ov-size', 'sm'); }); + it('applies xs size data attribute', () => { + renderWithTheme(); + expect(screen.getByTestId('dt-root')).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('applies xl size data attribute', () => { + renderWithTheme(); + expect(screen.getByTestId('dt-root')).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery color data attribute', () => { + renderWithTheme(); + expect(screen.getByTestId('dt-root')).toHaveAttribute('data-ov-color', 'discovery'); + }); + + it('applies secondary color data attribute', () => { + renderWithTheme(); + expect(screen.getByTestId('dt-root')).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('supports hoverable, stickyHeader, and striped flags', () => { renderWithTheme(); const root = screen.getByTestId('dt-root'); diff --git a/packages/base-ui/src/components/data-table/DataTableColumnVisibility.tsx b/packages/base-ui/src/components/data-table/DataTableColumnVisibility.tsx index 07e12a5..a67b487 100644 --- a/packages/base-ui/src/components/data-table/DataTableColumnVisibility.tsx +++ b/packages/base-ui/src/components/data-table/DataTableColumnVisibility.tsx @@ -21,7 +21,6 @@ export const DataTableColumnVisibility = forwardRef
; diff --git a/packages/base-ui/src/components/editor-tabs/EditorTabs.module.css b/packages/base-ui/src/components/editor-tabs/EditorTabs.module.css index 1cf5589..4677bd1 100644 --- a/packages/base-ui/src/components/editor-tabs/EditorTabs.module.css +++ b/packages/base-ui/src/components/editor-tabs/EditorTabs.module.css @@ -142,10 +142,25 @@ --_ov-tab-active-border: var(--ov-color-info); } +.Root[data-ov-color='discovery'] { + --_ov-tab-active-border: var(--ov-color-discovery); +} + +.Root[data-ov-color='secondary'] { + --_ov-tab-active-border: var(--ov-color-secondary); +} + /* --------------------------------------------------------------------------- Size overrides --------------------------------------------------------------------------- */ +.Root[data-ov-size='xs'] { + --_ov-tab-height: 22px; + --_ov-tab-font-size: var(--ov-font-size-caption); + --_ov-tab-padding-x: 6px; + --_ov-tab-padding-trailing: 3px; +} + .Root[data-ov-size='sm'] { --_ov-tab-height: 28px; --_ov-tab-font-size: var(--ov-font-size-caption); /* 0.6875rem (11px) original; caption (12px) is closest semantic token */ @@ -158,6 +173,11 @@ --_ov-tab-padding-x: 14px; } +.Root[data-ov-size='xl'] { + --_ov-tab-height: 48px; + --_ov-tab-padding-x: 18px; +} + .Viewport { display: flex; flex: 1 1 0%; @@ -230,7 +250,7 @@ } .Tab[data-active] { - background: var(--_ov-tab-active-bg); + background: var(--ov-color-bg-surface-raised); color: var(--_ov-tab-active-fg); border-bottom-color: var(--_ov-tab-active-border); } diff --git a/packages/base-ui/src/components/editor-tabs/EditorTabs.stories.tsx b/packages/base-ui/src/components/editor-tabs/EditorTabs.stories.tsx index 5a35290..59ac0f2 100644 --- a/packages/base-ui/src/components/editor-tabs/EditorTabs.stories.tsx +++ b/packages/base-ui/src/components/editor-tabs/EditorTabs.stories.tsx @@ -46,12 +46,12 @@ const meta: Meta = { }, color: { control: 'select', - options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info'], + options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'], description: 'Active indicator color', }, size: { control: 'select', - options: ['sm', 'md', 'lg'], + options: ['xs', 'sm', 'md', 'lg', 'xl'], description: 'Tab size', }, activeId: { control: 'text', description: 'Currently active tab ID (controlled)' }, diff --git a/packages/base-ui/src/components/editor-tabs/EditorTabs.test.tsx b/packages/base-ui/src/components/editor-tabs/EditorTabs.test.tsx index a318be9..01de3fe 100644 --- a/packages/base-ui/src/components/editor-tabs/EditorTabs.test.tsx +++ b/packages/base-ui/src/components/editor-tabs/EditorTabs.test.tsx @@ -249,6 +249,24 @@ describe('EditorTabs', () => { expect(root).toHaveAttribute('data-ov-variant', 'pill'); }); + it('applies xs and xl size data attributes', () => { + const tabs: TabDescriptor[] = [{ id: '1', title: 'Tab 1' }]; + const { rerender } = renderWithTheme(); + expect(screen.getByRole('tablist')).toHaveAttribute('data-ov-size', 'xs'); + + rerender(); + expect(screen.getByRole('tablist')).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('applies discovery and secondary color data attributes', () => { + const tabs: TabDescriptor[] = [{ id: '1', title: 'Tab 1' }]; + const { rerender } = renderWithTheme(); + expect(screen.getByRole('tablist')).toHaveAttribute('data-ov-color', 'discovery'); + + rerender(); + expect(screen.getByRole('tablist')).toHaveAttribute('data-ov-color', 'secondary'); + }); + it('detachable tab wires onDetachCommit through without crashing', () => { // Full pointer drag simulation is not feasible in jsdom (dnd-kit requires // setPointerCapture, getBoundingClientRect, etc.). The useTabDetach hook diff --git a/packages/base-ui/src/components/empty-state/EmptyState.stories.tsx b/packages/base-ui/src/components/empty-state/EmptyState.stories.tsx index 1062cc1..fd321df 100644 --- a/packages/base-ui/src/components/empty-state/EmptyState.stories.tsx +++ b/packages/base-ui/src/components/empty-state/EmptyState.stories.tsx @@ -12,7 +12,7 @@ const meta = { size: 'md', }, argTypes: { - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, title: { control: 'text' }, description: { control: 'text' }, }, diff --git a/packages/base-ui/src/components/filter-bar/FilterBar.stories.tsx b/packages/base-ui/src/components/filter-bar/FilterBar.stories.tsx index 4e520e7..271205c 100644 --- a/packages/base-ui/src/components/filter-bar/FilterBar.stories.tsx +++ b/packages/base-ui/src/components/filter-bar/FilterBar.stories.tsx @@ -10,7 +10,7 @@ const meta = { size: 'md', }, argTypes: { - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, } satisfies Meta; diff --git a/packages/base-ui/src/components/filter-bar/FilterBar.tsx b/packages/base-ui/src/components/filter-bar/FilterBar.tsx index 96d9cc7..016edf6 100644 --- a/packages/base-ui/src/components/filter-bar/FilterBar.tsx +++ b/packages/base-ui/src/components/filter-bar/FilterBar.tsx @@ -85,7 +85,6 @@ const FilterBarChip = forwardRef( @@ -121,7 +120,6 @@ const FilterBarAdd = forwardRef( ref={ref as never} variant="ghost" size={size} - dense aria-label="Add filter" {...props} > @@ -151,7 +149,6 @@ const FilterBarClear = forwardRef( ref={ref as never} variant="ghost" size={size} - dense aria-label={typeof children === 'string' ? children : 'Clear all'} {...props} > diff --git a/packages/base-ui/src/components/find-bar/FindBar.tsx b/packages/base-ui/src/components/find-bar/FindBar.tsx index 15517b4..e6dba36 100644 --- a/packages/base-ui/src/components/find-bar/FindBar.tsx +++ b/packages/base-ui/src/components/find-bar/FindBar.tsx @@ -246,7 +246,6 @@ export const FindBar = forwardRef(function FindBar (function FindBar @@ -20,9 +19,8 @@ const meta = { }, argTypes: { variant: { control: 'inline-radio', options: ['solid', 'soft', 'outline', 'ghost'] }, - color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger'] }, - size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] }, - dense: { control: 'boolean' }, + color: { control: 'select', options: ['neutral', 'brand', 'success', 'warning', 'danger', 'info', 'discovery', 'secondary'] }, + size: { control: 'inline-radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, }, } satisfies Meta; diff --git a/packages/base-ui/src/components/icon-button/IconButton.test.tsx b/packages/base-ui/src/components/icon-button/IconButton.test.tsx index 891c6cb..0ae9a9b 100644 --- a/packages/base-ui/src/components/icon-button/IconButton.test.tsx +++ b/packages/base-ui/src/components/icon-button/IconButton.test.tsx @@ -3,19 +3,39 @@ import { describe, expect, it } from 'vitest'; import { renderWithTheme } from '../../test/render'; import { IconButton } from './IconButton'; +const TestIcon = () => ( + +); + describe('IconButton', () => { - it('renders icon-only button with style attributes', () => { + it('renders with style attributes', () => { renderWithTheme( - - - + , ); - const button = screen.getByRole('button', { name: 'Search' }); expect(button).toHaveAttribute('data-ov-size', 'lg'); expect(button).toHaveAttribute('data-ov-variant', 'outline'); expect(button).toHaveAttribute('data-ov-color', 'brand'); }); + + it('renders with xs size', () => { + renderWithTheme( + , + ); + expect(screen.getByRole('button', { name: 'Tiny' })).toHaveAttribute('data-ov-size', 'xs'); + }); + + it('renders with xl size', () => { + renderWithTheme( + , + ); + expect(screen.getByRole('button', { name: 'Big' })).toHaveAttribute('data-ov-size', 'xl'); + }); + + it('does not accept dense prop', () => { + // @ts-expect-error dense prop was removed + renderWithTheme(); + }); }); diff --git a/packages/base-ui/src/components/icon-button/IconButton.tsx b/packages/base-ui/src/components/icon-button/IconButton.tsx index 499da4d..d7098df 100644 --- a/packages/base-ui/src/components/icon-button/IconButton.tsx +++ b/packages/base-ui/src/components/icon-button/IconButton.tsx @@ -5,18 +5,16 @@ import styles from './IconButton.module.css'; export interface IconButtonProps extends Omit { children: ReactNode; - dense?: boolean; } export const IconButton = forwardRef(function IconButton( - { className, dense = false, ...props }, + { className, ...props }, ref, ) { return (