@elabs-ai/components-ui 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/chunk-FCH4ZN6G.js +57 -0
- package/dist/chunk-FCH4ZN6G.js.map +1 -0
- package/dist/form.d.ts +28 -0
- package/dist/form.js +123 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2597 -188
- package/dist/index.js +12855 -3578
- package/dist/index.js.map +1 -1
- package/dist/lib/cn.js +10 -1
- package/dist/lib/cn.js.map +1 -1
- package/package.json +21 -7
- package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
- package/src/__contract__/badge.contract.test.tsx +49 -0
- package/src/__contract__/bounded-number.contract.test.tsx +49 -0
- package/src/__contract__/button.contract.test.tsx +49 -0
- package/src/__contract__/color-picker.contract.test.tsx +49 -0
- package/src/__contract__/combobox.contract.test.tsx +49 -0
- package/src/__contract__/command-trigger.contract.test.tsx +49 -0
- package/src/__contract__/copyable-value.contract.test.tsx +49 -0
- package/src/__contract__/date-picker.contract.test.tsx +49 -0
- package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
- package/src/__contract__/empty-state.contract.test.tsx +49 -0
- package/src/__contract__/error-state.contract.test.tsx +49 -0
- package/src/__contract__/icon-button.contract.test.tsx +49 -0
- package/src/__contract__/input.contract.test.tsx +49 -0
- package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
- package/src/__contract__/list-editor.contract.test.tsx +49 -0
- package/src/__contract__/loading-state.contract.test.tsx +49 -0
- package/src/__contract__/metric-card.contract.test.tsx +49 -0
- package/src/__contract__/model-picker.contract.test.tsx +49 -0
- package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
- package/src/__contract__/number-input.contract.test.tsx +49 -0
- package/src/__contract__/rating.contract.test.tsx +49 -0
- package/src/__contract__/schema-form.contract.test.tsx +49 -0
- package/src/__contract__/section-header.contract.test.tsx +49 -0
- package/src/__contract__/segmented-field.contract.test.tsx +49 -0
- package/src/__contract__/slider-number.contract.test.tsx +49 -0
- package/src/__contract__/status-badge.contract.test.tsx +49 -0
- package/src/__contract__/tag-input.contract.test.tsx +49 -0
- package/src/__contract__/text.contract.test.tsx +49 -0
- package/src/__contract__/textarea.contract.test.tsx +49 -0
- package/src/__contract__/timeline.contract.test.tsx +49 -0
- package/src/__contract__/toggle.contract.test.tsx +49 -0
- package/src/__contract__/transfer.contract.test.tsx +49 -0
- package/src/__contract__/tree-select.contract.test.tsx +49 -0
- package/src/__contract__/tree.contract.test.tsx +49 -0
- package/src/__contract__/virtual-select.contract.test.tsx +49 -0
- package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +13 -4
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +15 -4
- package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
- package/src/components/alert-dialog/alert-dialog.tsx +28 -6
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.test.tsx +99 -0
- package/src/components/app-shell/app-shell.tsx +56 -4
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +3 -1
- package/src/components/attribution-panel/attributions.generated.ts +83 -79
- package/src/components/avatar/avatar.tsx +4 -1
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.test.tsx +8 -0
- package/src/components/badge/badge.tsx +15 -5
- package/src/components/bento-grid/bento-grid.test.tsx +24 -3
- package/src/components/bento-grid/bento-grid.tsx +11 -1
- package/src/components/bounded-number/bounded-number.test.tsx +1 -1
- package/src/components/bounded-number/bounded-number.tsx +78 -75
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +10 -4
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +18 -12
- package/src/components/button-group/button-group.tsx +1 -1
- package/src/components/calendar/calendar.tsx +3 -2
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.test.tsx +24 -2
- package/src/components/card/card.tsx +48 -7
- package/src/components/carousel/carousel.test.tsx +169 -0
- package/src/components/carousel/carousel.tsx +93 -32
- package/src/components/change-review/change-review.stories.tsx +145 -1
- package/src/components/change-review/change-review.test.tsx +143 -0
- package/src/components/change-review/change-review.tsx +214 -24
- package/src/components/checkbox/checkbox.tsx +2 -1
- package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
- package/src/components/color-picker/color-picker.tsx +22 -15
- package/src/components/combobox/combobox.stories.tsx +10 -0
- package/src/components/combobox/combobox.test.tsx +54 -0
- package/src/components/combobox/combobox.tsx +25 -11
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +88 -0
- package/src/components/command/command.tsx +38 -13
- package/src/components/command/index.ts +1 -0
- package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
- package/src/components/command-trigger/command-trigger.test.tsx +18 -0
- package/src/components/command-trigger/command-trigger.tsx +70 -0
- package/src/components/command-trigger/index.ts +1 -0
- package/src/components/context-menu/context-menu.tsx +16 -7
- package/src/components/context-rail/context-rail.stories.tsx +285 -0
- package/src/components/context-rail/context-rail.test.tsx +263 -0
- package/src/components/context-rail/context-rail.tsx +678 -0
- package/src/components/context-rail/index.ts +1 -0
- package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
- package/src/components/copyable-value/copyable-value.test.tsx +28 -1
- package/src/components/copyable-value/copyable-value.tsx +13 -2
- package/src/components/date-picker/date-picker.stories.tsx +9 -0
- package/src/components/date-picker/date-picker.test.tsx +52 -0
- package/src/components/date-picker/date-picker.tsx +13 -15
- package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
- package/src/components/date-range-picker/date-range-picker.tsx +39 -28
- package/src/components/descriptions/descriptions.tsx +1 -1
- package/src/components/dialog/dialog.test.tsx +8 -3
- package/src/components/dialog/dialog.tsx +14 -6
- package/src/components/drawer/drawer.tsx +23 -6
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
- package/src/components/empty-state/empty-state.test.tsx +26 -0
- package/src/components/empty-state/empty-state.tsx +6 -1
- package/src/components/expand-dialog/expand-dialog.tsx +1 -2
- package/src/components/field/field-context.ts +59 -0
- package/src/components/field/field.stories.tsx +142 -0
- package/src/components/field/field.test.tsx +415 -0
- package/src/components/field/field.tsx +342 -0
- package/src/components/field/index.ts +9 -0
- package/src/components/field-row/field-row.stories.tsx +85 -1
- package/src/components/field-row/field-row.test.tsx +145 -0
- package/src/components/field-row/field-row.tsx +50 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +90 -12
- package/src/components/file-upload/file-upload.tsx +164 -31
- package/src/components/form/form.tsx +8 -3
- package/src/components/hover-card/hover-card.tsx +1 -0
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +3 -3
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +8 -4
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +18 -9
- package/src/components/input-otp/input-otp.tsx +8 -2
- package/src/components/keyboard-shortcuts/index.ts +6 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
- package/src/components/label/label.tsx +1 -1
- package/src/components/link-preview/link-preview.tsx +1 -1
- package/src/components/list-editor/list-editor.test.tsx +24 -0
- package/src/components/list-editor/list-editor.tsx +126 -52
- package/src/components/locale-provider/index.ts +6 -1
- package/src/components/locale-provider/locale-keys.test.ts +134 -0
- package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
- package/src/components/locale-provider/locale-provider.test.tsx +257 -1
- package/src/components/locale-provider/locale-provider.tsx +63 -8
- package/src/components/locale-provider/messages.ts +822 -2
- package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
- package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
- package/src/components/mention-input/mention-input.stories.tsx +3 -3
- package/src/components/mention-input/mention-input.test.tsx +51 -1
- package/src/components/mention-input/mention-input.tsx +66 -46
- package/src/components/mention-input/mention-value.ts +8 -2
- package/src/components/menubar/menubar.tsx +16 -6
- package/src/components/metric-card/metric-card.stories.tsx +5 -3
- package/src/components/metric-card/metric-card.tsx +21 -2
- package/src/components/model-picker/model-picker.stories.tsx +63 -3
- package/src/components/model-picker/model-picker.test.tsx +63 -0
- package/src/components/model-picker/model-picker.tsx +62 -36
- package/src/components/nav-main/nav-main.tsx +8 -4
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
- package/src/components/nav-notifications/nav-notifications.tsx +67 -9
- package/src/components/nav-user/nav-user.tsx +5 -5
- package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
- package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
- package/src/components/navigation-menu/navigation-menu.tsx +204 -15
- package/src/components/number-input/number-input.stories.tsx +7 -3
- package/src/components/number-input/number-input.test.tsx +47 -0
- package/src/components/number-input/number-input.tsx +65 -24
- package/src/components/page-shell/page-shell.stories.tsx +163 -0
- package/src/components/page-shell/page-shell.test.tsx +229 -0
- package/src/components/page-shell/page-shell.tsx +108 -25
- package/src/components/pagination/pagination.test.tsx +109 -0
- package/src/components/pagination/pagination.tsx +27 -19
- package/src/components/popover/popover.tsx +1 -0
- package/src/components/progress/progress.tsx +2 -0
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +3 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.test.tsx +76 -0
- package/src/components/resizable/resizable.tsx +32 -6
- package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
- package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
- package/src/components/revision-timeline/revision-timeline.tsx +53 -5
- package/src/components/schema-form/from-json-schema.test.ts +390 -0
- package/src/components/schema-form/from-json-schema.ts +375 -0
- package/src/components/schema-form/index.ts +81 -0
- package/src/components/schema-form/schema-form-spec.ts +698 -0
- package/src/components/schema-form/schema-form-store.ts +506 -0
- package/src/components/schema-form/schema-form.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +1021 -0
- package/src/components/schema-form/schema-form.tsx +1381 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +14 -3
- package/src/components/select/select.tsx +13 -7
- package/src/components/separator/separator.tsx +1 -0
- package/src/components/sheet/sheet.test.tsx +39 -0
- package/src/components/sheet/sheet.tsx +40 -9
- package/src/components/side-dock/index.ts +1 -0
- package/src/components/side-dock/side-dock.stories.tsx +137 -0
- package/src/components/side-dock/side-dock.test.tsx +299 -0
- package/src/components/side-dock/side-dock.tsx +515 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
- package/src/components/sidebar/sidebar.stories.tsx +421 -1
- package/src/components/sidebar/sidebar.test.tsx +71 -2
- package/src/components/sidebar/sidebar.tsx +264 -38
- package/src/components/skeleton/skeleton.tsx +1 -0
- package/src/components/skip-link/index.ts +1 -0
- package/src/components/skip-link/skip-link.stories.tsx +106 -0
- package/src/components/skip-link/skip-link.test.tsx +20 -0
- package/src/components/skip-link/skip-link.tsx +34 -0
- package/src/components/slider/slider.tsx +1 -1
- package/src/components/slider-number/slider-number.stories.tsx +4 -1
- package/src/components/slider-number/slider-number.test.tsx +1 -1
- package/src/components/sonner/sonner.tsx +2 -0
- package/src/components/spinner/spinner.test.tsx +18 -0
- package/src/components/spinner/spinner.tsx +14 -4
- package/src/components/split-panel/index.ts +6 -1
- package/src/components/split-panel/split-panel.stories.tsx +18 -2
- package/src/components/split-panel/split-panel.test.tsx +20 -0
- package/src/components/split-panel/split-panel.tsx +28 -8
- package/src/components/state-panel/state-panel.stories.tsx +52 -0
- package/src/components/state-panel/state-panel.test.tsx +125 -0
- package/src/components/state-panel/state-panel.tsx +110 -24
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +3 -1
- package/src/components/table/table.stories.tsx +64 -0
- package/src/components/table/table.test.tsx +149 -1
- package/src/components/table/table.tsx +71 -2
- package/src/components/tabs/index.ts +10 -1
- package/src/components/tabs/tabs.stories.tsx +48 -0
- package/src/components/tabs/tabs.test.tsx +67 -0
- package/src/components/tabs/tabs.tsx +176 -72
- package/src/components/tag-input/tag-input.tsx +6 -3
- package/src/components/team-switcher/team-switcher.tsx +35 -6
- package/src/components/textarea/textarea.tsx +2 -2
- package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
- package/src/components/theme-switcher/theme-switcher.tsx +140 -12
- package/src/components/theme-switcher/use-theme-transition.ts +2 -2
- package/src/components/timeline/timeline.stories.tsx +27 -0
- package/src/components/timeline/timeline.tsx +10 -3
- package/src/components/toggle/toggle.tsx +7 -2
- package/src/components/toggle-group/toggle-group.tsx +2 -0
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tooltip/tooltip.tsx +2 -1
- package/src/components/top-nav/top-nav.tsx +1 -1
- package/src/components/transfer/transfer.tsx +35 -30
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.test.tsx +41 -0
- package/src/components/tree/tree.tsx +91 -31
- package/src/components/tree/use-tree-keyboard.ts +37 -14
- package/src/components/tree-select/tree-select.stories.tsx +9 -0
- package/src/components/tree-select/tree-select.test.tsx +5 -0
- package/src/components/tree-select/tree-select.tsx +11 -14
- package/src/components/typography/prose.test.tsx +3 -2
- package/src/components/typography/prose.tsx +3 -3
- package/src/components/typography/typography.stories.tsx +228 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
- package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
- package/src/components/view-toolbar/view-toolbar.tsx +37 -4
- package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
- package/src/components/virtual-select/virtual-select.test.tsx +29 -3
- package/src/components/virtual-select/virtual-select.tsx +56 -18
- package/src/components/wizard/wizard.test.tsx +9 -0
- package/src/components/wizard/wizard.tsx +13 -5
- package/src/components/workspace-picker/index.ts +3 -0
- package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
- package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
- package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
- package/src/components/workspace-picker/workspace-picker.tsx +172 -0
- package/src/illustrations/empty-list-illustration.tsx +26 -0
- package/src/illustrations/error-illustration.tsx +60 -0
- package/src/illustrations/first-run-illustration.tsx +27 -0
- package/src/illustrations/illustration-base.tsx +108 -0
- package/src/illustrations/illustrations.stories.tsx +94 -0
- package/src/illustrations/illustrations.test.tsx +98 -0
- package/src/illustrations/index.ts +16 -0
- package/src/illustrations/no-access-illustration.tsx +45 -0
- package/src/illustrations/no-results-illustration.tsx +46 -0
- package/src/illustrations/offline-illustration.tsx +37 -0
- package/src/illustrations/success-illustration.tsx +40 -0
- package/src/index.ts +80 -2
- package/src/lib/agent-event-model.test.ts +12 -0
- package/src/lib/agent-event-model.ts +42 -0
- package/src/lib/approval-option.test.ts +17 -0
- package/src/lib/approval-option.ts +53 -0
- package/src/lib/check-result.ts +33 -0
- package/src/lib/cn.ts +9 -0
- package/src/lib/csv.ts +48 -0
- package/src/lib/diff-line.test.ts +36 -0
- package/src/lib/diff-line.ts +63 -0
- package/src/lib/diff-rows.test.ts +76 -0
- package/src/lib/diff-rows.ts +94 -0
- package/src/lib/format-duration.test.ts +22 -0
- package/src/lib/format-duration.ts +30 -0
- package/src/lib/has-renderable-content.test.ts +49 -0
- package/src/lib/has-renderable-content.ts +32 -0
- package/src/lib/merge-refs.ts +2 -0
- package/src/lib/operating-mode.test.ts +36 -0
- package/src/lib/operating-mode.ts +81 -0
- package/src/lib/optional-peer.ts +59 -0
- package/src/lib/session-launch.ts +37 -0
- package/src/lib/slash-command.test.ts +44 -0
- package/src/lib/slash-command.ts +45 -0
- package/src/lib/trigger-query.test.ts +94 -0
- package/src/lib/trigger-query.ts +88 -0
- package/src/lib/use-controllable-state.ts +47 -0
- package/src/lib/use-mobile.test.tsx +47 -0
- package/src/lib/use-mobile.ts +34 -12
- package/src/motion.stories.tsx +6 -0
- package/src/templates-object-detail-hub.stories.tsx +2 -2
- package/src/templates-screen-states.stories.tsx +5 -5
- package/src/templates-settings.stories.tsx +1 -1
- package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
- package/src/blocks/sidebar-02/logo.tsx +0 -18
- package/src/blocks/sidebar-02/nav-main.tsx +0 -13
- package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
- package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
- package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
- package/src/blocks/sidebar-04/mail-context.tsx +0 -29
- package/src/blocks/sidebar-04/nav-user.tsx +0 -6
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
- package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
- package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BUILT_IN_THEME_DEFINITIONS,
|
|
8
8
|
defineTheme,
|
|
9
9
|
ThemeProvider,
|
|
10
|
+
useTheme,
|
|
10
11
|
} from "@elabs-ai/components-tokens";
|
|
11
12
|
|
|
12
13
|
import { ThemeSwitcher } from "./theme-switcher";
|
|
@@ -251,3 +252,184 @@ describe("ThemeSwitcher — consumer themes (ADR 0029)", () => {
|
|
|
251
252
|
);
|
|
252
253
|
});
|
|
253
254
|
});
|
|
255
|
+
|
|
256
|
+
describe("ThemeSwitcher — theme families (ADR 0036)", () => {
|
|
257
|
+
const oceanLight = defineTheme({
|
|
258
|
+
value: "ocean-light",
|
|
259
|
+
label: "Ocean light",
|
|
260
|
+
dark: false,
|
|
261
|
+
family: "ocean",
|
|
262
|
+
familyLabel: "Ocean",
|
|
263
|
+
});
|
|
264
|
+
const oceanDark = defineTheme({
|
|
265
|
+
value: "ocean-dark",
|
|
266
|
+
label: "Ocean dark",
|
|
267
|
+
dark: true,
|
|
268
|
+
family: "ocean",
|
|
269
|
+
});
|
|
270
|
+
const dusk = defineTheme({
|
|
271
|
+
value: "dusk",
|
|
272
|
+
label: "Dusk",
|
|
273
|
+
dark: true,
|
|
274
|
+
family: "dusk",
|
|
275
|
+
familyLabel: "Dusk",
|
|
276
|
+
});
|
|
277
|
+
const midnight = defineTheme({ value: "midnight", label: "Midnight", dark: true });
|
|
278
|
+
const daylight = defineTheme({ value: "daylight", label: "Daylight", dark: false });
|
|
279
|
+
const REGISTRY = [...BUILT_IN_THEME_DEFINITIONS, oceanLight, oceanDark, dusk];
|
|
280
|
+
|
|
281
|
+
async function openMenu() {
|
|
282
|
+
await userEvent.click(screen.getByRole("button", { name: "Theme" }));
|
|
283
|
+
return screen.findByRole("menu");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
it("BACKWARD COMPAT: the default provider still renders the cycle toggle", () => {
|
|
287
|
+
setup(<ThemeSwitcher />);
|
|
288
|
+
expect(screen.getByRole("button", { name: /theme:/i })).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("BACKWARD COMPAT: built-ins plus one undeclared theme stays a flat dropdown", async () => {
|
|
292
|
+
render(
|
|
293
|
+
<ThemeProvider themes={[...BUILT_IN_THEME_DEFINITIONS, midnight]}>
|
|
294
|
+
<ThemeSwitcher />
|
|
295
|
+
</ThemeProvider>,
|
|
296
|
+
);
|
|
297
|
+
const menu = await openMenu();
|
|
298
|
+
expect(within(menu).queryAllByRole("menuitemradio")).toHaveLength(0);
|
|
299
|
+
expect(within(menu).getByText("Midnight")).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("BACKWARD COMPAT: an undeclared two-theme pair stays a light/dark toggle", () => {
|
|
303
|
+
render(
|
|
304
|
+
<ThemeProvider themes={[daylight, midnight]} defaultTheme="daylight">
|
|
305
|
+
<ThemeSwitcher showSystem={false} />
|
|
306
|
+
</ThemeProvider>,
|
|
307
|
+
);
|
|
308
|
+
expect(screen.getByRole("button", { name: /theme:/i })).toHaveAccessibleName(
|
|
309
|
+
"Theme: Daylight. Activate to switch to Midnight.",
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("offers families and a separate mode group once two families are declared", async () => {
|
|
314
|
+
render(
|
|
315
|
+
<ThemeProvider themes={REGISTRY} defaultTheme="light">
|
|
316
|
+
<ThemeSwitcher />
|
|
317
|
+
</ThemeProvider>,
|
|
318
|
+
);
|
|
319
|
+
const menu = await openMenu();
|
|
320
|
+
const radios = within(menu).getAllByRole("menuitemradio");
|
|
321
|
+
expect(radios.map((r) => r.textContent)).toEqual([
|
|
322
|
+
"Default",
|
|
323
|
+
"Ocean",
|
|
324
|
+
"Dusk",
|
|
325
|
+
"Light",
|
|
326
|
+
"Dark",
|
|
327
|
+
"System",
|
|
328
|
+
]);
|
|
329
|
+
expect(within(menu).getByRole("menuitemradio", { name: "Default" })).toBeChecked();
|
|
330
|
+
expect(within(menu).getByRole("menuitemradio", { name: "Light" })).toBeChecked();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("switching family keeps the chosen mode", async () => {
|
|
334
|
+
render(
|
|
335
|
+
<ThemeProvider themes={REGISTRY} defaultTheme="dark">
|
|
336
|
+
<ThemeSwitcher />
|
|
337
|
+
</ThemeProvider>,
|
|
338
|
+
);
|
|
339
|
+
await openMenu();
|
|
340
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
|
|
341
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("switching mode stays within the family", async () => {
|
|
345
|
+
render(
|
|
346
|
+
<ThemeProvider themes={REGISTRY} defaultTheme="ocean-light">
|
|
347
|
+
<ThemeSwitcher />
|
|
348
|
+
</ThemeProvider>,
|
|
349
|
+
);
|
|
350
|
+
await openMenu();
|
|
351
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "Dark" }));
|
|
352
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("hides the mode group for a single-mode family", async () => {
|
|
356
|
+
render(
|
|
357
|
+
<ThemeProvider themes={REGISTRY} defaultTheme="dusk">
|
|
358
|
+
<ThemeSwitcher />
|
|
359
|
+
</ThemeProvider>,
|
|
360
|
+
);
|
|
361
|
+
const menu = await openMenu();
|
|
362
|
+
expect(within(menu).queryByRole("menuitemradio", { name: "Light" })).not.toBeInTheDocument();
|
|
363
|
+
expect(within(menu).queryByText("Mode")).not.toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("remembers dark across a light-only family", async () => {
|
|
367
|
+
const sand = defineTheme({ value: "sand", label: "Sand", dark: false, family: "sand" });
|
|
368
|
+
render(
|
|
369
|
+
<ThemeProvider themes={[...REGISTRY, sand]} defaultTheme="dark">
|
|
370
|
+
<ThemeSwitcher />
|
|
371
|
+
</ThemeProvider>,
|
|
372
|
+
);
|
|
373
|
+
await openMenu();
|
|
374
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "sand" }));
|
|
375
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("sand");
|
|
376
|
+
await openMenu();
|
|
377
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
|
|
378
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("checks the right family when a second same-scheme variant is active", async () => {
|
|
382
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
383
|
+
const oceanDeep = defineTheme({
|
|
384
|
+
value: "ocean-deep",
|
|
385
|
+
label: "Ocean deep",
|
|
386
|
+
dark: true,
|
|
387
|
+
family: "ocean",
|
|
388
|
+
});
|
|
389
|
+
render(
|
|
390
|
+
<ThemeProvider themes={[...REGISTRY, oceanDeep]} defaultTheme="ocean-deep">
|
|
391
|
+
<ThemeSwitcher />
|
|
392
|
+
</ThemeProvider>,
|
|
393
|
+
);
|
|
394
|
+
const menu = await openMenu();
|
|
395
|
+
expect(within(menu).getByRole("menuitemradio", { name: "Ocean" })).toBeChecked();
|
|
396
|
+
expect(within(menu).getByRole("menuitemradio", { name: "Default" })).not.toBeChecked();
|
|
397
|
+
warn.mockRestore();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("forgets a remembered mode once another control changes the theme", async () => {
|
|
401
|
+
function ExternalLight() {
|
|
402
|
+
const { setTheme } = useTheme();
|
|
403
|
+
return (
|
|
404
|
+
<button type="button" onClick={() => setTheme("light")}>
|
|
405
|
+
External light
|
|
406
|
+
</button>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
render(
|
|
410
|
+
<ThemeProvider themes={REGISTRY} defaultTheme="light">
|
|
411
|
+
<ThemeSwitcher />
|
|
412
|
+
<ExternalLight />
|
|
413
|
+
</ThemeProvider>,
|
|
414
|
+
);
|
|
415
|
+
await openMenu();
|
|
416
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "Dark" }));
|
|
417
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
418
|
+
await userEvent.click(screen.getByRole("button", { name: "External light" }));
|
|
419
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
420
|
+
await openMenu();
|
|
421
|
+
await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
|
|
422
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-light");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("forced dropdown mode keeps the flat list even with families", async () => {
|
|
426
|
+
render(
|
|
427
|
+
<ThemeProvider themes={REGISTRY}>
|
|
428
|
+
<ThemeSwitcher mode="dropdown" />
|
|
429
|
+
</ThemeProvider>,
|
|
430
|
+
);
|
|
431
|
+
const menu = await openMenu();
|
|
432
|
+
expect(within(menu).queryAllByRole("menuitemradio")).toHaveLength(0);
|
|
433
|
+
expect(within(menu).getByText("Ocean dark")).toBeInTheDocument();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { forwardRef, useCallback, useEffect, useState, type HTMLAttributes } from "react";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useRef, useState, type HTMLAttributes } from "react";
|
|
4
4
|
import { Check, Monitor, Moon, Sun } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
groupThemeFamilies,
|
|
7
|
+
resolveThemeVariant,
|
|
8
|
+
themeFamilyIdOf,
|
|
9
|
+
themeSchemeOf,
|
|
10
|
+
useTheme,
|
|
11
|
+
type ThemeDefinition,
|
|
12
|
+
type ThemeName,
|
|
13
|
+
type ThemeScheme,
|
|
14
|
+
} from "@elabs-ai/components-tokens";
|
|
6
15
|
|
|
7
16
|
import { Button } from "../button";
|
|
8
17
|
import {
|
|
9
18
|
DropdownMenu,
|
|
10
19
|
DropdownMenuContent,
|
|
11
20
|
DropdownMenuItem,
|
|
21
|
+
DropdownMenuLabel,
|
|
22
|
+
DropdownMenuRadioGroup,
|
|
23
|
+
DropdownMenuRadioItem,
|
|
24
|
+
DropdownMenuSeparator,
|
|
12
25
|
DropdownMenuTrigger,
|
|
13
26
|
} from "../dropdown-menu";
|
|
14
27
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
28
|
+
import { useLocale } from "../locale-provider";
|
|
15
29
|
import { useThemeTransition, type ThemeTransitionEffect } from "./use-theme-transition";
|
|
16
30
|
|
|
17
31
|
const SYSTEM_STORAGE_KEY = "brand-ui-theme-system";
|
|
@@ -107,6 +121,7 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
107
121
|
ref,
|
|
108
122
|
) {
|
|
109
123
|
const { theme, themeDefinitions, setTheme } = useTheme();
|
|
124
|
+
const { t } = useLocale();
|
|
110
125
|
const switchTheme = useThemeTransition(effect);
|
|
111
126
|
const isControlled = preference !== undefined;
|
|
112
127
|
const [uncontrolledIsSystem, setUncontrolledIsSystem] = useState(false);
|
|
@@ -127,16 +142,43 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
127
142
|
const labelOf = (name: ThemeName) => offered.find((d) => d.value === name)?.label ?? name;
|
|
128
143
|
const isDark = (name: ThemeName) => offered.find((d) => d.value === name)?.dark ?? false;
|
|
129
144
|
|
|
130
|
-
// `?? theme` (the active theme), never a "light"/"dark" literal: with an open
|
|
131
|
-
// registry those names may not exist at all in this app.
|
|
132
|
-
const lightTheme: ThemeName = pickByDarkness(offered, false) ?? theme;
|
|
133
|
-
const darkTheme: ThemeName = pickByDarkness(offered, true) ?? theme;
|
|
134
|
-
|
|
135
145
|
// The concrete theme to DISPLAY as current: the controlled preference when
|
|
136
146
|
// it names one, otherwise the theme actually applied via the provider.
|
|
137
147
|
const resolvedTheme: ThemeName =
|
|
138
148
|
isControlled && preference !== undefined && preference !== "system" ? preference : theme;
|
|
139
149
|
|
|
150
|
+
// ADR 0036 — the family layout is OPT-IN: only when the offered list (not the
|
|
151
|
+
// provider's `families`, so the `themes` prop still narrows) holds two or
|
|
152
|
+
// more DECLARED families. An undeclared `[daylight, midnight]` pair keeps
|
|
153
|
+
// today's toggle byte-for-byte.
|
|
154
|
+
const families = groupThemeFamilies(offered);
|
|
155
|
+
const useFamilies = mode === "auto" && families.filter((f) => f.declared).length >= 2;
|
|
156
|
+
// Match by the active definition's declared family, not the retained
|
|
157
|
+
// light/dark slots: a second same-scheme variant is still reachable via
|
|
158
|
+
// `setTheme` but is not in either slot.
|
|
159
|
+
const activeDefinition = offered.find((d) => d.value === resolvedTheme);
|
|
160
|
+
const activeFamily = useFamilies
|
|
161
|
+
? ((activeDefinition && families.find((f) => f.id === themeFamilyIdOf(activeDefinition))) ??
|
|
162
|
+
families[0])
|
|
163
|
+
: undefined;
|
|
164
|
+
// "System" is scoped to the active family in the family layout.
|
|
165
|
+
const scope: readonly ThemeDefinition[] = activeFamily
|
|
166
|
+
? [activeFamily.light, activeFamily.dark].filter((d): d is ThemeDefinition => d !== undefined)
|
|
167
|
+
: offered;
|
|
168
|
+
|
|
169
|
+
// `?? theme` (the active theme), never a "light"/"dark" literal: with an open
|
|
170
|
+
// registry those names may not exist at all in this app.
|
|
171
|
+
const lightTheme: ThemeName = pickByDarkness(scope, false) ?? theme;
|
|
172
|
+
const darkTheme: ThemeName = pickByDarkness(scope, true) ?? theme;
|
|
173
|
+
|
|
174
|
+
// The scheme the user last chose, so dark → a light-only family → a
|
|
175
|
+
// two-scheme family lands on dark again (mirrors the provider's `setFamily`).
|
|
176
|
+
// Remembered together with the theme it produced: once something else changes
|
|
177
|
+
// the theme (another control calling `setTheme`), the memory no longer applies.
|
|
178
|
+
const intendedSchemeRef = useRef<{ scheme: ThemeScheme; theme: ThemeName } | undefined>(
|
|
179
|
+
undefined,
|
|
180
|
+
);
|
|
181
|
+
|
|
140
182
|
// Hydrate the "system" choice from localStorage and apply it on mount —
|
|
141
183
|
// uncontrolled mode only. Controlled mode's "system" state comes from the
|
|
142
184
|
// `preference` prop and never touches this component's own storage key.
|
|
@@ -193,6 +235,90 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
193
235
|
}, [switchTheme, darkTheme, lightTheme, isControlled, onPreferenceChange]);
|
|
194
236
|
|
|
195
237
|
const TriggerIcon = isSystem ? Monitor : isDark(resolvedTheme) ? Moon : Sun;
|
|
238
|
+
|
|
239
|
+
// ---- Family layout (≥2 declared families): family group + scheme group ----
|
|
240
|
+
if (activeFamily) {
|
|
241
|
+
const activeScheme = activeDefinition;
|
|
242
|
+
const remembered = intendedSchemeRef.current;
|
|
243
|
+
const intended =
|
|
244
|
+
remembered && remembered.theme === resolvedTheme
|
|
245
|
+
? remembered.scheme
|
|
246
|
+
: activeScheme
|
|
247
|
+
? themeSchemeOf(activeScheme)
|
|
248
|
+
: "light";
|
|
249
|
+
const pickFamily = (familyId: string) => {
|
|
250
|
+
// System re-resolves against the OS immediately on a family change
|
|
251
|
+
// instead of waiting for the next `change` event.
|
|
252
|
+
const scheme: ThemeScheme = isSystem ? (prefersDark() ? "dark" : "light") : intended;
|
|
253
|
+
const next = resolveThemeVariant(families, familyId, scheme);
|
|
254
|
+
if (next === undefined) return;
|
|
255
|
+
intendedSchemeRef.current = { scheme: intended, theme: next };
|
|
256
|
+
if (isSystem) switchTheme(next);
|
|
257
|
+
else pickTheme(next);
|
|
258
|
+
};
|
|
259
|
+
const pickScheme = (value: string) => {
|
|
260
|
+
if (value === "system") {
|
|
261
|
+
pickSystem();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const scheme = value as ThemeScheme;
|
|
265
|
+
const next = activeFamily[scheme]?.value;
|
|
266
|
+
if (next === undefined) return;
|
|
267
|
+
intendedSchemeRef.current = { scheme, theme: next };
|
|
268
|
+
pickTheme(next);
|
|
269
|
+
};
|
|
270
|
+
const schemeValue = isSystem ? "system" : activeScheme ? themeSchemeOf(activeScheme) : "";
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<DropdownMenu>
|
|
274
|
+
<DropdownMenuTrigger asChild>
|
|
275
|
+
<Button
|
|
276
|
+
ref={ref}
|
|
277
|
+
variant="outline"
|
|
278
|
+
size={ICON_SIZE[size]}
|
|
279
|
+
aria-label={t("ui.themeSwitcher.theme")}
|
|
280
|
+
className={className}
|
|
281
|
+
{...props}
|
|
282
|
+
>
|
|
283
|
+
<TriggerIcon />
|
|
284
|
+
</Button>
|
|
285
|
+
</DropdownMenuTrigger>
|
|
286
|
+
<DropdownMenuContent align="end">
|
|
287
|
+
<DropdownMenuLabel>{t("ui.themeSwitcher.theme")}</DropdownMenuLabel>
|
|
288
|
+
<DropdownMenuRadioGroup value={activeFamily.id} onValueChange={pickFamily}>
|
|
289
|
+
{families.map((f) => (
|
|
290
|
+
<DropdownMenuRadioItem key={f.id} value={f.id}>
|
|
291
|
+
{f.label}
|
|
292
|
+
</DropdownMenuRadioItem>
|
|
293
|
+
))}
|
|
294
|
+
</DropdownMenuRadioGroup>
|
|
295
|
+
{activeFamily.schemes.length === 2 ? (
|
|
296
|
+
<>
|
|
297
|
+
<DropdownMenuSeparator />
|
|
298
|
+
<DropdownMenuLabel>{t("ui.themeSwitcher.mode")}</DropdownMenuLabel>
|
|
299
|
+
<DropdownMenuRadioGroup value={schemeValue} onValueChange={pickScheme}>
|
|
300
|
+
<DropdownMenuRadioItem value="light">
|
|
301
|
+
<Sun aria-hidden="true" />
|
|
302
|
+
{t("ui.themeSwitcher.light")}
|
|
303
|
+
</DropdownMenuRadioItem>
|
|
304
|
+
<DropdownMenuRadioItem value="dark">
|
|
305
|
+
<Moon aria-hidden="true" />
|
|
306
|
+
{t("ui.themeSwitcher.dark")}
|
|
307
|
+
</DropdownMenuRadioItem>
|
|
308
|
+
{showSystem ? (
|
|
309
|
+
<DropdownMenuRadioItem value="system">
|
|
310
|
+
<Monitor aria-hidden="true" />
|
|
311
|
+
{t("ui.themeSwitcher.system")}
|
|
312
|
+
</DropdownMenuRadioItem>
|
|
313
|
+
) : null}
|
|
314
|
+
</DropdownMenuRadioGroup>
|
|
315
|
+
</>
|
|
316
|
+
) : null}
|
|
317
|
+
</DropdownMenuContent>
|
|
318
|
+
</DropdownMenu>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
196
322
|
const useDropdown = mode === "dropdown" || (mode === "auto" && safeThemes.length > 2);
|
|
197
323
|
|
|
198
324
|
// ---- Dropdown mode (>2 themes) ----
|
|
@@ -204,7 +330,7 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
204
330
|
ref={ref}
|
|
205
331
|
variant="outline"
|
|
206
332
|
size={ICON_SIZE[size]}
|
|
207
|
-
aria-label="
|
|
333
|
+
aria-label={t("ui.themeSwitcher.theme")}
|
|
208
334
|
className={className}
|
|
209
335
|
{...props}
|
|
210
336
|
>
|
|
@@ -215,15 +341,15 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
215
341
|
{showSystem ? (
|
|
216
342
|
<DropdownMenuItem onSelect={() => pickSystem()}>
|
|
217
343
|
<Monitor />
|
|
218
|
-
<span>
|
|
219
|
-
{isSystem ? <Check className="
|
|
344
|
+
<span>{t("ui.themeSwitcher.system")}</span>
|
|
345
|
+
{isSystem ? <Check className="ms-auto" /> : null}
|
|
220
346
|
</DropdownMenuItem>
|
|
221
347
|
) : null}
|
|
222
348
|
{offered.map((d) => (
|
|
223
349
|
<DropdownMenuItem key={d.value} onSelect={() => pickTheme(d.value)}>
|
|
224
350
|
{d.dark ? <Moon /> : <Sun />}
|
|
225
351
|
<span>{d.label}</span>
|
|
226
|
-
{!isSystem && resolvedTheme === d.value ? <Check className="
|
|
352
|
+
{!isSystem && resolvedTheme === d.value ? <Check className="ms-auto" /> : null}
|
|
227
353
|
</DropdownMenuItem>
|
|
228
354
|
))}
|
|
229
355
|
</DropdownMenuContent>
|
|
@@ -254,7 +380,9 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
|
254
380
|
// `role` is optional only because `order[i]` is an indexed read under
|
|
255
381
|
// `noUncheckedIndexedAccess`; the modulo above makes it always present.
|
|
256
382
|
const roleLabel = (role: "light" | "dark" | "system" | undefined) =>
|
|
257
|
-
role === "system"
|
|
383
|
+
role === "system"
|
|
384
|
+
? t("ui.themeSwitcher.system")
|
|
385
|
+
: labelOf(role === "dark" ? darkTheme : lightTheme);
|
|
258
386
|
|
|
259
387
|
return (
|
|
260
388
|
<TooltipProvider>
|
|
@@ -15,8 +15,8 @@ type ViewTransitionDocument = Document & {
|
|
|
15
15
|
* Transitions API (the chanhdai.com "theme toggle effect", adapted to brand-ui).
|
|
16
16
|
*
|
|
17
17
|
* - Direction-aware: sets `data-vt="to-dark|to-light"` on `<html>` from the
|
|
18
|
-
* target theme's `dark` flag, so the clip-path wipes the right way for ANY
|
|
19
|
-
*
|
|
18
|
+
* target theme's `dark` flag, so the clip-path wipes the right way for ANY
|
|
19
|
+
* theme (not just light/dark).
|
|
20
20
|
* - `data-vt-effect` selects the reveal shape (default "polygon").
|
|
21
21
|
* - Motion-aware: under `useReducedMotion()` (OS or in-app preference) — or when
|
|
22
22
|
* the browser lacks `startViewTransition` — it falls back to an instant,
|
|
@@ -11,6 +11,9 @@ const meta = {
|
|
|
11
11
|
docs: {
|
|
12
12
|
description: {
|
|
13
13
|
component:
|
|
14
|
+
"The GENERIC ordered-steps rail; an agent execution trace is `AI/AgentTimeline` " +
|
|
15
|
+
"and git history is `Data/RevisionTimeline` — see " +
|
|
16
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
14
17
|
"Ordered steps with a vertical connector and status-colored nodes — THE shared " +
|
|
15
18
|
"rail spine (#190). Two front doors over one rail: the array `items` API (the " +
|
|
16
19
|
"editor's `:::timeline` target, `done|active|pending`) and the compound " +
|
|
@@ -78,6 +81,30 @@ export const Compound: Story = {
|
|
|
78
81
|
},
|
|
79
82
|
};
|
|
80
83
|
|
|
84
|
+
export const TabularTimestamps: Story = {
|
|
85
|
+
// #386 — a right-aligned timestamp column of differing digit widths
|
|
86
|
+
// ("09:14" vs "11:47" vs "9:02") must not jitter: the span needs
|
|
87
|
+
// `tabular-nums` so every digit occupies equal width.
|
|
88
|
+
args: { items: [] },
|
|
89
|
+
render: () => (
|
|
90
|
+
<TimelineRoot aria-label="Run with numeric timestamps">
|
|
91
|
+
<TimelineItem status="complete" timestamp="09:14">
|
|
92
|
+
Searched financial filings
|
|
93
|
+
</TimelineItem>
|
|
94
|
+
<TimelineItem status="complete" timestamp="11:47">
|
|
95
|
+
Queried finance.revenue
|
|
96
|
+
</TimelineItem>
|
|
97
|
+
<TimelineItem status="pending" timestamp="9:02">
|
|
98
|
+
Draft the board note
|
|
99
|
+
</TimelineItem>
|
|
100
|
+
</TimelineRoot>
|
|
101
|
+
),
|
|
102
|
+
play: async ({ canvas }) => {
|
|
103
|
+
const timestamp = await waitFor(() => canvas.getByText("09:14"));
|
|
104
|
+
await expect(getComputedStyle(timestamp).fontVariantNumeric).toContain("tabular-nums");
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
81
108
|
export const CanonicalStatuses: Story = {
|
|
82
109
|
args: { items: [] },
|
|
83
110
|
render: () => (
|
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
* parts. Keeps the editor-facing `done|active|pending` API unchanged by
|
|
16
16
|
* mapping in via `fromTimelineStatus` at the boundary.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Deliberately NOT on this rail (a different grammar, not an oversight —
|
|
19
|
+
* RM-014, #133): `RevisionTimeline` (@elabs-ai/components-ui). Its rail is a
|
|
20
|
+
* multi-lane commit DAG whose node colour is LANE IDENTITY (the chart ramp),
|
|
21
|
+
* not `Status`, and whose fork/merge joins are cross-row beziers living in one
|
|
22
|
+
* SVG coordinate space spanning the whole list — neither of which a
|
|
23
|
+
* status-keyed node or an item-local `w-px` connector can express. Carrying
|
|
24
|
+
* both grammars here would cost ~5 opt-in props, four of them switching this
|
|
25
|
+
* rail's own node/connector/geometry/status announcement off. The four-point
|
|
26
|
+
* reason is in that component's docblock; reopen #133 before converging them.
|
|
20
27
|
*/
|
|
21
28
|
import { forwardRef, type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from "react";
|
|
22
29
|
import { cn } from "../../lib/cn";
|
|
@@ -141,7 +148,7 @@ export const TimelineItem = forwardRef<HTMLLIElement, TimelineItemProps>(functio
|
|
|
141
148
|
{children}
|
|
142
149
|
</span>
|
|
143
150
|
{timestamp ? (
|
|
144
|
-
<span className="shrink-0 text-meta text-muted-foreground">{timestamp}</span>
|
|
151
|
+
<span className="shrink-0 text-meta text-muted-foreground tabular-nums">{timestamp}</span>
|
|
145
152
|
) : null}
|
|
146
153
|
</div>
|
|
147
154
|
{description ? <p className="mt-0.5 text-body text-muted-foreground">{description}</p> : null}
|
|
@@ -21,7 +21,7 @@ export const toggleVariants = cva(
|
|
|
21
21
|
// asChild wrapper overwrites data-state="on" with "closed" — aria-pressed
|
|
22
22
|
// (Toggle) and aria-checked (ToggleGroupItem) are set by the toggle primitive
|
|
23
23
|
// alone and survive the merge. (#214)
|
|
24
|
-
"inline-flex items-center justify-center gap-2 rounded-
|
|
24
|
+
"inline-flex items-center justify-center gap-2 rounded-control text-body font-control transition-colors duration-fast ease-standard hover:bg-muted hover:text-muted-foreground focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-semibold aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:font-semibold aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-semibold [&_svg]:size-4 [&_svg]:shrink-0",
|
|
25
25
|
{
|
|
26
26
|
variants: {
|
|
27
27
|
variant: {
|
|
@@ -44,7 +44,11 @@ export const toggleVariants = cva(
|
|
|
44
44
|
segmented:
|
|
45
45
|
"border-0 bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground aria-checked:bg-surface-elevated aria-checked:text-foreground aria-checked:font-medium aria-checked:shadow-sm aria-pressed:bg-surface-elevated aria-pressed:text-foreground aria-pressed:font-medium aria-pressed:shadow-sm data-[state=on]:bg-surface-elevated data-[state=on]:text-foreground data-[state=on]:font-medium data-[state=on]:shadow-sm",
|
|
46
46
|
},
|
|
47
|
-
size: {
|
|
47
|
+
size: {
|
|
48
|
+
default: "h-control px-2.5 min-w-control",
|
|
49
|
+
sm: "h-control-sm px-2 min-w-control-sm",
|
|
50
|
+
lg: "h-control-lg px-3 min-w-control-lg",
|
|
51
|
+
},
|
|
48
52
|
},
|
|
49
53
|
defaultVariants: { variant: "default", size: "default" },
|
|
50
54
|
},
|
|
@@ -57,6 +61,7 @@ export const Toggle = forwardRef<
|
|
|
57
61
|
return (
|
|
58
62
|
<TogglePrimitive.Root
|
|
59
63
|
ref={ref}
|
|
64
|
+
data-slot="toggle"
|
|
60
65
|
className={cn(toggleVariants({ variant, size }), className)}
|
|
61
66
|
{...props}
|
|
62
67
|
/>
|
|
@@ -28,10 +28,18 @@ import {
|
|
|
28
28
|
* expect there.
|
|
29
29
|
*/
|
|
30
30
|
const meta = {
|
|
31
|
-
title: "
|
|
31
|
+
title: "Layout/Toolbar",
|
|
32
32
|
component: Toolbar,
|
|
33
33
|
tags: ["autodocs"],
|
|
34
|
-
parameters: {
|
|
34
|
+
parameters: {
|
|
35
|
+
layout: "padded",
|
|
36
|
+
docs: {
|
|
37
|
+
description: {
|
|
38
|
+
component:
|
|
39
|
+
"The one row that genuinely claims the `toolbar` ARIA role: Radix delegates a roving tabindex to it, so ten controls cost one tab stop instead of ten. The row above a list, table or board — filters left, actions right — is `Layout/ViewToolbar`, which deliberately does NOT claim that role because its controls are ordinary tab stops. See [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
35
43
|
args: { "aria-label": "Formatting" },
|
|
36
44
|
} satisfies Meta<typeof Toolbar>;
|
|
37
45
|
|
|
@@ -14,9 +14,10 @@ export const TooltipContent = forwardRef<
|
|
|
14
14
|
<TooltipPrimitive.Portal>
|
|
15
15
|
<TooltipPrimitive.Content
|
|
16
16
|
ref={ref}
|
|
17
|
+
data-slot="tooltip-content"
|
|
17
18
|
sideOffset={sideOffset}
|
|
18
19
|
className={cn(
|
|
19
|
-
"z-50 overflow-hidden rounded-md bg-foreground px-2.5 py-1.5 text-
|
|
20
|
+
"z-50 overflow-hidden rounded-md bg-foreground px-2.5 py-1.5 text-meta text-background shadow-md",
|
|
20
21
|
"data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95",
|
|
21
22
|
className,
|
|
22
23
|
)}
|
|
@@ -17,7 +17,7 @@ export const TopNav = forwardRef<HTMLElement, TopNavProps>(function TopNav(
|
|
|
17
17
|
<header
|
|
18
18
|
ref={ref}
|
|
19
19
|
className={cn(
|
|
20
|
-
"sticky top-0 z-30 flex h-
|
|
20
|
+
"sticky top-0 z-30 flex h-header items-center gap-4 border-b bg-surface-elevated/80 px-4 backdrop-blur",
|
|
21
21
|
className,
|
|
22
22
|
)}
|
|
23
23
|
{...props}
|