@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
|
@@ -76,6 +76,73 @@ describe("Tabs", () => {
|
|
|
76
76
|
// scroll-into-view/320px-container behavior is layout-dependent and is
|
|
77
77
|
// covered by the `OverflowScrollable` Storybook interaction test instead
|
|
78
78
|
// (jsdom doesn't lay out real scroll geometry).
|
|
79
|
+
it("creates one shared observer pair per list, not one per trigger (#387)", () => {
|
|
80
|
+
const originalMutationObserver = globalThis.MutationObserver;
|
|
81
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
82
|
+
let mutationObserverCount = 0;
|
|
83
|
+
let resizeObserverCount = 0;
|
|
84
|
+
|
|
85
|
+
class CountingMutationObserver extends originalMutationObserver {
|
|
86
|
+
constructor(...args: ConstructorParameters<typeof MutationObserver>) {
|
|
87
|
+
super(...args);
|
|
88
|
+
mutationObserverCount += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class CountingResizeObserver extends originalResizeObserver {
|
|
92
|
+
constructor(...args: ConstructorParameters<typeof ResizeObserver>) {
|
|
93
|
+
super(...args);
|
|
94
|
+
resizeObserverCount += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
globalThis.MutationObserver = CountingMutationObserver as typeof MutationObserver;
|
|
98
|
+
globalThis.ResizeObserver = CountingResizeObserver as typeof ResizeObserver;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
render(
|
|
102
|
+
<Tabs defaultValue="a">
|
|
103
|
+
<TabsList>
|
|
104
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
105
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
106
|
+
<TabsTrigger value="c">C</TabsTrigger>
|
|
107
|
+
<TabsTrigger value="d">D</TabsTrigger>
|
|
108
|
+
</TabsList>
|
|
109
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
110
|
+
</Tabs>,
|
|
111
|
+
);
|
|
112
|
+
// 4 triggers in the strip. Before the fix, each trigger owned its own
|
|
113
|
+
// MutationObserver AND ResizeObserver (4 of each here — the review's
|
|
114
|
+
// "20 callbacks per resize" was measured on a 20-tab strip).
|
|
115
|
+
const fourTriggerMutationCount = mutationObserverCount;
|
|
116
|
+
const fourTriggerResizeCount = resizeObserverCount;
|
|
117
|
+
expect(fourTriggerResizeCount).toBeGreaterThan(0);
|
|
118
|
+
|
|
119
|
+
mutationObserverCount = 0;
|
|
120
|
+
resizeObserverCount = 0;
|
|
121
|
+
render(
|
|
122
|
+
<Tabs defaultValue="a">
|
|
123
|
+
<TabsList>
|
|
124
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
125
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
126
|
+
<TabsTrigger value="c">C</TabsTrigger>
|
|
127
|
+
<TabsTrigger value="d">D</TabsTrigger>
|
|
128
|
+
<TabsTrigger value="e">E</TabsTrigger>
|
|
129
|
+
<TabsTrigger value="f">F</TabsTrigger>
|
|
130
|
+
<TabsTrigger value="g">G</TabsTrigger>
|
|
131
|
+
<TabsTrigger value="h">H</TabsTrigger>
|
|
132
|
+
</TabsList>
|
|
133
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
134
|
+
</Tabs>,
|
|
135
|
+
);
|
|
136
|
+
// The list owns a FIXED number of observers regardless of trigger
|
|
137
|
+
// count — doubling the trigger count must not double it.
|
|
138
|
+
expect(mutationObserverCount).toBe(fourTriggerMutationCount);
|
|
139
|
+
expect(resizeObserverCount).toBe(fourTriggerResizeCount);
|
|
140
|
+
} finally {
|
|
141
|
+
globalThis.MutationObserver = originalMutationObserver;
|
|
142
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
79
146
|
it("renders TabsList as a bounded scroll container with safe centering", () => {
|
|
80
147
|
render(
|
|
81
148
|
<Tabs defaultValue="a">
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
4
|
+
createContext,
|
|
2
5
|
forwardRef,
|
|
6
|
+
use,
|
|
3
7
|
useEffect,
|
|
4
8
|
useMemo,
|
|
5
9
|
useRef,
|
|
@@ -7,6 +11,7 @@ import {
|
|
|
7
11
|
type ElementRef,
|
|
8
12
|
} from "react";
|
|
9
13
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
14
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
10
15
|
import { useReducedMotion } from "@elabs-ai/components-tokens";
|
|
11
16
|
import { cn } from "../../lib/cn";
|
|
12
17
|
import { mergeRefs } from "../../lib/merge-refs";
|
|
@@ -68,85 +73,184 @@ function scrollTriggerIntoStrip(trigger: HTMLElement, behavior: ScrollBehavior)
|
|
|
68
73
|
* start-alignment on overflow while staying visually identical when the
|
|
69
74
|
* strip fits (see #344).
|
|
70
75
|
*/
|
|
71
|
-
export const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
export const tabsListVariants = cva(
|
|
77
|
+
"max-w-full items-center overflow-x-auto text-muted-foreground",
|
|
78
|
+
{
|
|
79
|
+
variants: {
|
|
80
|
+
variant: {
|
|
81
|
+
// Recessed pill track with a raised active segment.
|
|
82
|
+
segmented: "inline-flex h-9 justify-center-safe rounded-lg bg-muted p-1",
|
|
83
|
+
// Line tabs: a transparent start-aligned row over a 1px rule; the active
|
|
84
|
+
// tab's 2px underline sits on top of it (see `tabsTriggerVariants`).
|
|
85
|
+
underline: "flex w-full justify-start border-b border-rule",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
defaultVariants: { variant: "segmented" },
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const tabsTriggerVariants = cva(
|
|
93
|
+
"inline-flex items-center justify-center whitespace-nowrap text-body font-control transition-colors duration-fast disabled:pointer-events-none disabled:opacity-50",
|
|
94
|
+
{
|
|
95
|
+
variants: {
|
|
96
|
+
variant: {
|
|
97
|
+
segmented: cn(
|
|
98
|
+
"rounded-control px-3 py-1",
|
|
99
|
+
// `ring-offset-1`, not `-2`: the compound indicator's reach is
|
|
100
|
+
// `ring-offset` + 2px ring + 1px contour, and `TabsList` is an
|
|
101
|
+
// `overflow-x-auto` strip with `p-1` (4px). At offset 2 the reach is 5px
|
|
102
|
+
// and the scroll box clips the contour on three sides (#67 fix round 2,
|
|
103
|
+
// measured on `patterns-templates-object-detail-hub--default` tab stop
|
|
104
|
+
// 9); at offset 1 it is exactly 4px and the loop closes.
|
|
105
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
106
|
+
"data-[state=active]:bg-surface-elevated data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
107
|
+
),
|
|
108
|
+
underline: cn(
|
|
109
|
+
// The underline is a bottom BORDER inside the fixed height, transparent
|
|
110
|
+
// at rest, so activation never shifts layout. No negative margin to
|
|
111
|
+
// overlap the list's rule: the list scrolls (`overflow-x-auto`), which
|
|
112
|
+
// would clip anything hanging past its padding box. The strip has no
|
|
113
|
+
// padding for an outer ring either, so the indicator is drawn inset.
|
|
114
|
+
"h-12 rounded-none border-b-2 border-transparent px-4 hover:text-foreground",
|
|
115
|
+
"focus-ring-inset",
|
|
116
|
+
"data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
defaultVariants: { variant: "segmented" },
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export type TabsVariant = NonNullable<VariantProps<typeof tabsListVariants>["variant"]>;
|
|
125
|
+
|
|
126
|
+
// The list owns the variant; its triggers read it, so a strip never mixes two.
|
|
127
|
+
const TabsVariantContext = createContext<TabsVariant>("segmented");
|
|
128
|
+
|
|
129
|
+
export interface TabsListProps
|
|
130
|
+
extends
|
|
131
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.List>,
|
|
132
|
+
VariantProps<typeof tabsListVariants> {}
|
|
133
|
+
|
|
134
|
+
export const TabsList = forwardRef<ElementRef<typeof TabsPrimitive.List>, TabsListProps>(
|
|
135
|
+
function TabsList({ className, variant, ...props }, ref) {
|
|
136
|
+
const resolved = variant ?? "segmented";
|
|
137
|
+
const innerRef = useRef<HTMLDivElement | null>(null);
|
|
138
|
+
const mergedRef = useMemo(() => mergeRefs(ref, innerRef), [ref]);
|
|
139
|
+
// Honours the in-app tri-state motion preference on `ThemeProvider` as well
|
|
140
|
+
// as the OS setting, and degrades to OS-only outside a provider.
|
|
141
|
+
const reducedMotion = useReducedMotion();
|
|
142
|
+
|
|
143
|
+
// Keep the tab that BECOMES active inside the strip's visible bounds.
|
|
144
|
+
//
|
|
145
|
+
// ONE set of observers per LIST, not one per trigger (#387 perf finding:
|
|
146
|
+
// N triggers used to mean N independent `MutationObserver`s (each
|
|
147
|
+
// watching only its own node) plus N independent `ResizeObserver`s (each
|
|
148
|
+
// ALSO watching this same shared strip) — a resize of a 20-tab strip fired
|
|
149
|
+
// 20 redundant callbacks. Watching the list's subtree in one callback
|
|
150
|
+
// gets the same behaviour for the cost of one.
|
|
151
|
+
//
|
|
152
|
+
// Watching attributes rather than reacting to a render is still
|
|
153
|
+
// load-bearing: Radix flips `data-state` on the trigger it renders, and
|
|
154
|
+
// only ITS OWN context consumer re-renders — this component does not, so
|
|
155
|
+
// a render-driven effect would never observe an activation at all
|
|
156
|
+
// (measured: 8 effect runs for 8 triggers at mount, zero on any later
|
|
157
|
+
// activation). A MutationObserver fires on a genuine inactive → active
|
|
158
|
+
// TRANSITION and never at mount, which is what stops a freshly-mounted
|
|
159
|
+
// Tabs from scrolling anything (#344 shipped a version that scrolled the
|
|
160
|
+
// page 900px on every mount).
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const node = innerRef.current;
|
|
163
|
+
if (!node) return;
|
|
164
|
+
|
|
165
|
+
const scrollActiveIntoView = () => {
|
|
166
|
+
const active = node.querySelector<HTMLElement>(
|
|
167
|
+
'[data-slot="tabs-trigger"][data-state="active"]',
|
|
168
|
+
);
|
|
169
|
+
if (active) scrollTriggerIntoStrip(active, reducedMotion ? "auto" : "smooth");
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const activationObserver = new MutationObserver((mutations) => {
|
|
173
|
+
for (const mutation of mutations) {
|
|
174
|
+
const el = mutation.target as HTMLElement;
|
|
175
|
+
if (el.getAttribute("data-state") === "active") {
|
|
176
|
+
scrollTriggerIntoStrip(el, reducedMotion ? "auto" : "smooth");
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
activationObserver.observe(node, {
|
|
182
|
+
subtree: true,
|
|
183
|
+
attributes: true,
|
|
184
|
+
attributeFilter: ["data-state"],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// A relayout AFTER the scroll invalidates the target it was computed
|
|
188
|
+
// from: a webfont swapping in on a cold load (measured — a
|
|
189
|
+
// mono-everything theme's wider mono face left the last tab 45px
|
|
190
|
+
// outside the strip), or the strip itself being resized. One
|
|
191
|
+
// `ResizeObserver` instance covers both — it watches the strip AND
|
|
192
|
+
// every trigger inside it, not a second instance per trigger.
|
|
193
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
194
|
+
if (typeof ResizeObserver === "function") {
|
|
195
|
+
resizeObserver = new ResizeObserver(scrollActiveIntoView);
|
|
196
|
+
resizeObserver.observe(node);
|
|
197
|
+
for (const trigger of node.querySelectorAll<HTMLElement>('[data-slot="tabs-trigger"]')) {
|
|
198
|
+
resizeObserver.observe(trigger);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A dynamic tab list can add/remove triggers later — keep the single
|
|
203
|
+
// `ResizeObserver`'s target set in sync instead of spinning up a new
|
|
204
|
+
// observer per trigger mount.
|
|
205
|
+
const childListObserver = new MutationObserver((mutations) => {
|
|
206
|
+
if (!resizeObserver) return;
|
|
207
|
+
for (const mutation of mutations) {
|
|
208
|
+
mutation.addedNodes.forEach((added) => {
|
|
209
|
+
if (added instanceof HTMLElement && added.matches('[data-slot="tabs-trigger"]')) {
|
|
210
|
+
resizeObserver!.observe(added);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
mutation.removedNodes.forEach((removed) => {
|
|
214
|
+
if (removed instanceof HTMLElement && removed.matches('[data-slot="tabs-trigger"]')) {
|
|
215
|
+
resizeObserver!.unobserve(removed);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
childListObserver.observe(node, { childList: true, subtree: true });
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
activationObserver.disconnect();
|
|
224
|
+
resizeObserver?.disconnect();
|
|
225
|
+
childListObserver.disconnect();
|
|
226
|
+
};
|
|
227
|
+
}, [reducedMotion]);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<TabsVariantContext.Provider value={resolved}>
|
|
231
|
+
<TabsPrimitive.List
|
|
232
|
+
ref={mergedRef}
|
|
233
|
+
data-slot="tabs-list"
|
|
234
|
+
data-variant={resolved}
|
|
235
|
+
className={cn(tabsListVariants({ variant: resolved }), className)}
|
|
236
|
+
{...props}
|
|
237
|
+
/>
|
|
238
|
+
</TabsVariantContext.Provider>
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
);
|
|
86
242
|
|
|
87
243
|
export const TabsTrigger = forwardRef<
|
|
88
244
|
ElementRef<typeof TabsPrimitive.Trigger>,
|
|
89
245
|
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
90
246
|
>(function TabsTrigger({ className, ...props }, ref) {
|
|
91
|
-
const
|
|
92
|
-
const mergedRef = useMemo(() => mergeRefs(ref, innerRef), [ref]);
|
|
93
|
-
// Honours the in-app tri-state motion preference on `ThemeProvider` as well
|
|
94
|
-
// as the OS setting, and degrades to OS-only outside a provider.
|
|
95
|
-
const reducedMotion = useReducedMotion();
|
|
96
|
-
|
|
97
|
-
// Keep the tab that BECOMES active inside the strip's visible bounds.
|
|
98
|
-
//
|
|
99
|
-
// Watching the attribute rather than reacting to a render is load-bearing on
|
|
100
|
-
// both counts:
|
|
101
|
-
// - Radix flips `data-state` on the trigger it renders INSIDE this wrapper,
|
|
102
|
-
// and only its own context consumer re-renders — this component does not.
|
|
103
|
-
// A render-driven effect therefore never observes an activation at all
|
|
104
|
-
// (measured: 8 effect runs for 8 triggers at mount, zero on any later
|
|
105
|
-
// activation), so the scroll only ever ran when the CONSUMER's own state
|
|
106
|
-
// changed.
|
|
107
|
-
// - A MutationObserver fires on a genuine inactive → active TRANSITION and
|
|
108
|
-
// never at mount, which is what stops a freshly-mounted Tabs from
|
|
109
|
-
// scrolling anything (#344 shipped a version that scrolled the page 900px
|
|
110
|
-
// on every mount).
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
const node = innerRef.current;
|
|
113
|
-
if (!node) return;
|
|
114
|
-
|
|
115
|
-
const scrollIfActive = () => {
|
|
116
|
-
if (node.getAttribute("data-state") !== "active") return;
|
|
117
|
-
scrollTriggerIntoStrip(node, reducedMotion ? "auto" : "smooth");
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const observer = new MutationObserver(scrollIfActive);
|
|
121
|
-
observer.observe(node, { attributes: true, attributeFilter: ["data-state"] });
|
|
122
|
-
|
|
123
|
-
// A relayout AFTER the scroll invalidates the target it was computed from:
|
|
124
|
-
// a webfont swapping in on a cold load (measured — a mono-everything theme's
|
|
125
|
-
// wider mono face left the last tab 45px outside the strip), or the strip
|
|
126
|
-
// being resized. Re-measure whenever the tab or the strip changes size;
|
|
127
|
-
// the delta check makes every unaffected pass a no-op.
|
|
128
|
-
const resizeObserver =
|
|
129
|
-
typeof ResizeObserver === "function" ? new ResizeObserver(scrollIfActive) : undefined;
|
|
130
|
-
resizeObserver?.observe(node);
|
|
131
|
-
const strip = node.closest<HTMLElement>('[role="tablist"]');
|
|
132
|
-
if (strip) resizeObserver?.observe(strip);
|
|
133
|
-
|
|
134
|
-
return () => {
|
|
135
|
-
observer.disconnect();
|
|
136
|
-
resizeObserver?.disconnect();
|
|
137
|
-
};
|
|
138
|
-
}, [reducedMotion]);
|
|
247
|
+
const variant = use(TabsVariantContext);
|
|
139
248
|
|
|
140
249
|
return (
|
|
141
250
|
<TabsPrimitive.Trigger
|
|
142
|
-
ref={
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
146
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
147
|
-
"data-[state=active]:bg-surface-elevated data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
148
|
-
className,
|
|
149
|
-
)}
|
|
251
|
+
ref={ref}
|
|
252
|
+
data-slot="tabs-trigger"
|
|
253
|
+
className={cn(tabsTriggerVariants({ variant }), className)}
|
|
150
254
|
{...props}
|
|
151
255
|
/>
|
|
152
256
|
);
|
|
@@ -160,7 +264,7 @@ export const TabsContent = forwardRef<
|
|
|
160
264
|
<TabsPrimitive.Content
|
|
161
265
|
ref={ref}
|
|
162
266
|
className={cn(
|
|
163
|
-
"mt-2 focus-
|
|
267
|
+
"mt-2 focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
164
268
|
"data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:[--tw-ease:var(--ease-entrance)]",
|
|
165
269
|
className,
|
|
166
270
|
)}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// a2ui.exposed: yes
|
|
2
4
|
import {
|
|
3
5
|
forwardRef,
|
|
@@ -208,7 +210,7 @@ export const TagInput = forwardRef<HTMLDivElement, TagInputProps>(function TagIn
|
|
|
208
210
|
}}
|
|
209
211
|
className={cn(
|
|
210
212
|
"rounded-full p-0.5 transition-colors",
|
|
211
|
-
"hover:bg-secondary-foreground/20 focus-
|
|
213
|
+
"hover:bg-secondary-foreground/20 focus-ring",
|
|
212
214
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
213
215
|
)}
|
|
214
216
|
>
|
|
@@ -229,7 +231,7 @@ export const TagInput = forwardRef<HTMLDivElement, TagInputProps>(function TagIn
|
|
|
229
231
|
aria-invalid={error ? "true" : undefined}
|
|
230
232
|
data-slot="input-group-control"
|
|
231
233
|
className={cn(
|
|
232
|
-
"min-w-[6rem] flex-1 bg-transparent text-
|
|
234
|
+
"min-w-[6rem] flex-1 bg-transparent text-body outline-none",
|
|
233
235
|
"placeholder:text-muted-foreground",
|
|
234
236
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
235
237
|
)}
|
|
@@ -240,7 +242,8 @@ export const TagInput = forwardRef<HTMLDivElement, TagInputProps>(function TagIn
|
|
|
240
242
|
/>
|
|
241
243
|
</InputGroup>
|
|
242
244
|
{error && (
|
|
243
|
-
|
|
245
|
+
// #124: running text (the validation message) — ink rung.
|
|
246
|
+
<p role="alert" className="text-meta text-destructive-text">
|
|
244
247
|
{error}
|
|
245
248
|
</p>
|
|
246
249
|
)}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DropdownMenuTrigger,
|
|
13
13
|
} from "../dropdown-menu";
|
|
14
14
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "../sidebar";
|
|
15
|
+
import { useLocale } from "../locale-provider";
|
|
15
16
|
import { cn } from "../../lib/cn";
|
|
16
17
|
|
|
17
18
|
export type TeamSwitcherTeam = {
|
|
@@ -37,6 +38,7 @@ export interface TeamSwitcherProps {
|
|
|
37
38
|
*/
|
|
38
39
|
export function TeamSwitcher({ teams, logoClassName, className }: TeamSwitcherProps) {
|
|
39
40
|
const { isMobile } = useSidebar();
|
|
41
|
+
const { t } = useLocale();
|
|
40
42
|
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
|
41
43
|
if (!activeTeam) return null;
|
|
42
44
|
const Logo = activeTeam.logo;
|
|
@@ -48,21 +50,44 @@ export function TeamSwitcher({ teams, logoClassName, className }: TeamSwitcherPr
|
|
|
48
50
|
<DropdownMenuTrigger asChild>
|
|
49
51
|
<SidebarMenuButton
|
|
50
52
|
size="lg"
|
|
53
|
+
// The name is written out rather than left to the button's own
|
|
54
|
+
// contents, because the contents cannot carry it in every state:
|
|
55
|
+
// collapsed to the 48px rail the trigger is one glyph, and the
|
|
56
|
+
// label beside it is either gone or clipped out of view by the
|
|
57
|
+
// button's `overflow-hidden` — either way axe reads a nameless
|
|
58
|
+
// button (`button-name`, critical). The string is exactly the
|
|
59
|
+
// visible label, in reading order, so WCAG 2.5.3 (label in name)
|
|
60
|
+
// holds expanded, where that label IS on screen. Verified by
|
|
61
|
+
// deleting this line and watching three dashboard stories fail
|
|
62
|
+
// `button-name` (critical).
|
|
63
|
+
aria-label={`${activeTeam.name} ${activeTeam.plan}`}
|
|
51
64
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
52
65
|
>
|
|
66
|
+
{/* Collapsed, this row is ONE ICON in a 48px rail and has to sit
|
|
67
|
+
on the same centre line as every nav entry above and below it.
|
|
68
|
+
Measured before this fix: the 32px plate did not fit the
|
|
69
|
+
button's 16px content box, so it overflowed (clipped by the
|
|
70
|
+
button's own `overflow-hidden`) and its glyph landed 8px to the
|
|
71
|
+
trailing side of every other icon in the rail. So the plate
|
|
72
|
+
becomes the glyph's own 16px box — no fill, no radius, nothing
|
|
73
|
+
to overflow — and the label and chevron stand down: at 48px
|
|
74
|
+
there is no room to read them, and they are what crowds the
|
|
75
|
+
plate. Expanded, all three come back unchanged. */}
|
|
53
76
|
<div
|
|
54
77
|
className={cn(
|
|
55
|
-
"flex aspect-square size-8 items-center justify-center rounded-lg",
|
|
78
|
+
"flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg",
|
|
56
79
|
logoClassName ?? "bg-sidebar-accent text-sidebar-accent-foreground",
|
|
80
|
+
"group-data-[collapsible=icon]:size-4 group-data-[collapsible=icon]:rounded-none group-data-[collapsible=icon]:bg-transparent group-data-[collapsible=icon]:text-sidebar-foreground",
|
|
57
81
|
)}
|
|
58
82
|
>
|
|
59
83
|
<Logo className="size-4" />
|
|
60
84
|
</div>
|
|
61
|
-
<div className="grid flex-1 text-start text-
|
|
85
|
+
<div className="grid flex-1 text-start text-body leading-tight group-data-[collapsible=icon]:hidden">
|
|
62
86
|
<span className="truncate font-semibold">{activeTeam.name}</span>
|
|
63
|
-
<span className="truncate text-
|
|
87
|
+
<span className="truncate text-meta">{activeTeam.plan}</span>
|
|
64
88
|
</div>
|
|
65
|
-
|
|
89
|
+
{/* Decorative, so it simply goes. */}
|
|
90
|
+
<ChevronsUpDown className="ms-auto group-data-[collapsible=icon]:hidden" />
|
|
66
91
|
</SidebarMenuButton>
|
|
67
92
|
</DropdownMenuTrigger>
|
|
68
93
|
<DropdownMenuContent
|
|
@@ -71,7 +96,9 @@ export function TeamSwitcher({ teams, logoClassName, className }: TeamSwitcherPr
|
|
|
71
96
|
side={isMobile ? "bottom" : "right"}
|
|
72
97
|
sideOffset={4}
|
|
73
98
|
>
|
|
74
|
-
<DropdownMenuLabel className="text-
|
|
99
|
+
<DropdownMenuLabel className="text-meta text-muted-foreground">
|
|
100
|
+
{t("ui.teamSwitcher.label")}
|
|
101
|
+
</DropdownMenuLabel>
|
|
75
102
|
{teams.map((team, index) => (
|
|
76
103
|
<DropdownMenuItem
|
|
77
104
|
key={team.name}
|
|
@@ -90,7 +117,9 @@ export function TeamSwitcher({ teams, logoClassName, className }: TeamSwitcherPr
|
|
|
90
117
|
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
|
91
118
|
<Plus className="size-4" />
|
|
92
119
|
</div>
|
|
93
|
-
<div className="font-medium text-muted-foreground">
|
|
120
|
+
<div className="font-medium text-muted-foreground">
|
|
121
|
+
{t("ui.teamSwitcher.addTeam")}
|
|
122
|
+
</div>
|
|
94
123
|
</DropdownMenuItem>
|
|
95
124
|
</DropdownMenuContent>
|
|
96
125
|
</DropdownMenu>
|
|
@@ -11,9 +11,9 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function
|
|
|
11
11
|
<textarea
|
|
12
12
|
ref={ref}
|
|
13
13
|
className={cn(
|
|
14
|
-
"flex min-h-20 w-full rounded-
|
|
14
|
+
"flex min-h-20 w-full rounded-control border border-input bg-input-background px-3 py-2 text-body shadow-input transition-[color,background-color,border-color,box-shadow] duration-fast ease-standard",
|
|
15
15
|
"placeholder:text-muted-foreground",
|
|
16
|
-
"focus-
|
|
16
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
17
17
|
"disabled:cursor-not-allowed disabled:opacity-50 disabled:border-border",
|
|
18
18
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
|
|
19
19
|
className,
|
|
@@ -144,3 +144,79 @@ export const RestrictedProvider: Story = {
|
|
|
144
144
|
await waitFor(() => expect(body.queryByRole("menu")).not.toBeInTheDocument());
|
|
145
145
|
},
|
|
146
146
|
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Theme families (ADR 0036). Once two or more families declare `family`, the
|
|
150
|
+
* switcher offers a **Theme** group and — for a family with both schemes — a
|
|
151
|
+
* separate **Mode** group (Light / Dark / System). `ocean-*` are the
|
|
152
|
+
* downloadable family in repo-root `themes/ocean/`, whose CSS this Storybook
|
|
153
|
+
* imports.
|
|
154
|
+
*/
|
|
155
|
+
const oceanThemes = [
|
|
156
|
+
defineTheme({
|
|
157
|
+
value: "ocean-light",
|
|
158
|
+
label: "Ocean Light",
|
|
159
|
+
dark: false,
|
|
160
|
+
family: "ocean",
|
|
161
|
+
familyLabel: "Ocean",
|
|
162
|
+
}),
|
|
163
|
+
defineTheme({ value: "ocean-dark", label: "Ocean Dark", dark: true, family: "ocean" }),
|
|
164
|
+
];
|
|
165
|
+
const FAMILY_REGISTRY = [...BUILT_IN_THEME_DEFINITIONS, ...oceanThemes];
|
|
166
|
+
|
|
167
|
+
export const WithThemeFamilies: Story = {
|
|
168
|
+
decorators: [
|
|
169
|
+
(Story) => (
|
|
170
|
+
<ThemeProvider themes={FAMILY_REGISTRY} defaultTheme="light" storageKey={null}>
|
|
171
|
+
<div className="flex min-h-32 items-center justify-center">
|
|
172
|
+
<Story />
|
|
173
|
+
</div>
|
|
174
|
+
</ThemeProvider>
|
|
175
|
+
),
|
|
176
|
+
],
|
|
177
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
178
|
+
const root = canvasElement.ownerDocument.documentElement;
|
|
179
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
180
|
+
await userEvent.click(canvas.getByRole("button", { name: "Theme" }));
|
|
181
|
+
await userEvent.click(await body.findByRole("menuitemradio", { name: "Ocean" }));
|
|
182
|
+
await waitFor(() => expect(root).toHaveAttribute("data-theme", "ocean-light"));
|
|
183
|
+
// The closing menu keeps the canvas aria-hidden until it unmounts; reopen only after.
|
|
184
|
+
await waitFor(() => expect(body.queryByRole("menu")).not.toBeInTheDocument());
|
|
185
|
+
await userEvent.click(await canvas.findByRole("button", { name: "Theme" }));
|
|
186
|
+
await userEvent.click(await body.findByRole("menuitemradio", { name: "Dark" }));
|
|
187
|
+
await waitFor(() => expect(root).toHaveAttribute("data-theme", "ocean-dark"));
|
|
188
|
+
await waitFor(() => expect(body.queryByRole("menu")).not.toBeInTheDocument());
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A family that ships only one scheme shows no Mode group. `Dusk` borrows the
|
|
194
|
+
* reference `dark` block as its only variant.
|
|
195
|
+
*/
|
|
196
|
+
const duskTheme = defineTheme({
|
|
197
|
+
value: "dark",
|
|
198
|
+
label: "Dusk",
|
|
199
|
+
dark: true,
|
|
200
|
+
family: "dusk",
|
|
201
|
+
familyLabel: "Dusk (dark only)",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export const SingleModeFamily: Story = {
|
|
205
|
+
decorators: [
|
|
206
|
+
(Story) => (
|
|
207
|
+
<ThemeProvider themes={[...oceanThemes, duskTheme]} defaultTheme="dark" storageKey={null}>
|
|
208
|
+
<div className="flex min-h-32 items-center justify-center">
|
|
209
|
+
<Story />
|
|
210
|
+
</div>
|
|
211
|
+
</ThemeProvider>
|
|
212
|
+
),
|
|
213
|
+
],
|
|
214
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
215
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
216
|
+
await userEvent.click(canvas.getByRole("button", { name: "Theme" }));
|
|
217
|
+
await expect(await body.findByRole("menuitemradio", { name: /Dusk/ })).toBeChecked();
|
|
218
|
+
await expect(body.queryByRole("menuitemradio", { name: "Light" })).not.toBeInTheDocument();
|
|
219
|
+
await userEvent.keyboard("{Escape}");
|
|
220
|
+
await waitFor(() => expect(body.queryByRole("menu")).not.toBeInTheDocument());
|
|
221
|
+
},
|
|
222
|
+
};
|