@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { forwardRef, type ComponentProps } from "react";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Kbd } from "../kbd";
|
|
5
|
+
|
|
6
|
+
export interface CommandTriggerProps extends ComponentProps<"button"> {
|
|
7
|
+
/** The visible and announced label. Defaults to "Search". */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** The shortcut hint. Defaults to the platform-correct ⌘ K / Ctrl K. */
|
|
10
|
+
shortcut?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function platformShortcut(): string {
|
|
14
|
+
if (typeof navigator === "undefined") return "Ctrl K";
|
|
15
|
+
// `userAgentData.platform` where available, else the legacy string. Both are
|
|
16
|
+
// hints, not guarantees — the trigger is a hint too, so a wrong guess is cosmetic.
|
|
17
|
+
const platform =
|
|
18
|
+
(navigator as { userAgentData?: { platform?: string } }).userAgentData?.platform ??
|
|
19
|
+
navigator.platform ??
|
|
20
|
+
"";
|
|
21
|
+
return /mac|iphone|ipad/i.test(platform) ? "⌘ K" : "Ctrl K";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The command-palette opener, shaped like a search field: label left, shortcut
|
|
26
|
+
* pinned right, collapsing to the icon alone under `sm`.
|
|
27
|
+
*
|
|
28
|
+
* The button’s accessible name comes entirely from `aria-label` — that is what
|
|
29
|
+
* stops the `Kbd`’s shortcut glyph from concatenating into it (#117); a
|
|
30
|
+
* non-empty `aria-label` short-circuits accessible-name computation before the
|
|
31
|
+
* DOM is ever consulted. The visible label and the `Kbd` are ALSO marked
|
|
32
|
+
* `aria-hidden`, per `accessibility.md`, as defence in depth: if `aria-label`
|
|
33
|
+
* is ever dropped, that turns a silent regression into a loud one — an empty
|
|
34
|
+
* accessible name rather than a plausible wrong one. They do not do the work
|
|
35
|
+
* themselves.
|
|
36
|
+
*
|
|
37
|
+
* `shortcut` reads `navigator` at render time when omitted, which can differ
|
|
38
|
+
* between server and client — a consumer that server-renders this component
|
|
39
|
+
* should pass `shortcut` explicitly to avoid a hydration mismatch.
|
|
40
|
+
*/
|
|
41
|
+
export const CommandTrigger = forwardRef<HTMLButtonElement, CommandTriggerProps>(
|
|
42
|
+
function CommandTrigger({ label = "Search", shortcut, className, ...props }, ref) {
|
|
43
|
+
const hint = shortcut ?? platformShortcut();
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
ref={ref}
|
|
47
|
+
type="button"
|
|
48
|
+
data-slot="command-trigger"
|
|
49
|
+
aria-label={label}
|
|
50
|
+
className={cn(
|
|
51
|
+
"inline-flex h-8 items-center gap-2 rounded-md border border-input bg-background px-2 text-body text-muted-foreground",
|
|
52
|
+
"hover:bg-accent hover:text-accent-foreground focus-ring",
|
|
53
|
+
"sm:w-56 sm:justify-between sm:ps-2 sm:pe-1.5",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
59
|
+
<Search aria-hidden="true" className="size-4 shrink-0" />
|
|
60
|
+
<span aria-hidden="true" className="hidden truncate sm:inline">
|
|
61
|
+
{label}
|
|
62
|
+
</span>
|
|
63
|
+
</span>
|
|
64
|
+
<Kbd aria-hidden="true" className="hidden sm:inline-flex">
|
|
65
|
+
{hint}
|
|
66
|
+
</Kbd>
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./command-trigger";
|
|
@@ -21,8 +21,9 @@ export const ContextMenuSubTrigger = forwardRef<
|
|
|
21
21
|
return (
|
|
22
22
|
<ContextMenuPrimitive.SubTrigger
|
|
23
23
|
ref={ref}
|
|
24
|
+
data-slot="context-menu-sub-trigger"
|
|
24
25
|
className={cn(
|
|
25
|
-
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-
|
|
26
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
|
|
26
27
|
inset && "ps-8",
|
|
27
28
|
className,
|
|
28
29
|
)}
|
|
@@ -41,6 +42,7 @@ export const ContextMenuSubContent = forwardRef<
|
|
|
41
42
|
return (
|
|
42
43
|
<ContextMenuPrimitive.SubContent
|
|
43
44
|
ref={ref}
|
|
45
|
+
data-slot="context-menu-sub-content"
|
|
44
46
|
className={cn(
|
|
45
47
|
"z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
46
48
|
className,
|
|
@@ -58,8 +60,9 @@ export const ContextMenuContent = forwardRef<
|
|
|
58
60
|
<ContextMenuPrimitive.Portal>
|
|
59
61
|
<ContextMenuPrimitive.Content
|
|
60
62
|
ref={ref}
|
|
63
|
+
data-slot="context-menu-content"
|
|
61
64
|
className={cn(
|
|
62
|
-
"z-50 min-w-[
|
|
65
|
+
"z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
63
66
|
className,
|
|
64
67
|
)}
|
|
65
68
|
{...props}
|
|
@@ -75,8 +78,9 @@ export const ContextMenuItem = forwardRef<
|
|
|
75
78
|
return (
|
|
76
79
|
<ContextMenuPrimitive.Item
|
|
77
80
|
ref={ref}
|
|
81
|
+
data-slot="context-menu-item"
|
|
78
82
|
className={cn(
|
|
79
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
83
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4",
|
|
80
84
|
inset && "ps-8",
|
|
81
85
|
className,
|
|
82
86
|
)}
|
|
@@ -92,9 +96,10 @@ export const ContextMenuCheckboxItem = forwardRef<
|
|
|
92
96
|
return (
|
|
93
97
|
<ContextMenuPrimitive.CheckboxItem
|
|
94
98
|
ref={ref}
|
|
99
|
+
data-slot="context-menu-checkbox-item"
|
|
95
100
|
checked={checked}
|
|
96
101
|
className={cn(
|
|
97
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
102
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
98
103
|
className,
|
|
99
104
|
)}
|
|
100
105
|
{...props}
|
|
@@ -116,8 +121,9 @@ export const ContextMenuRadioItem = forwardRef<
|
|
|
116
121
|
return (
|
|
117
122
|
<ContextMenuPrimitive.RadioItem
|
|
118
123
|
ref={ref}
|
|
124
|
+
data-slot="context-menu-radio-item"
|
|
119
125
|
className={cn(
|
|
120
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
126
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
121
127
|
className,
|
|
122
128
|
)}
|
|
123
129
|
{...props}
|
|
@@ -139,8 +145,9 @@ export const ContextMenuLabel = forwardRef<
|
|
|
139
145
|
return (
|
|
140
146
|
<ContextMenuPrimitive.Label
|
|
141
147
|
ref={ref}
|
|
148
|
+
data-slot="context-menu-label"
|
|
142
149
|
className={cn(
|
|
143
|
-
"px-2 py-1.5 text-
|
|
150
|
+
"px-2 py-1.5 text-meta font-semibold text-muted-foreground",
|
|
144
151
|
inset && "ps-8",
|
|
145
152
|
className,
|
|
146
153
|
)}
|
|
@@ -156,6 +163,7 @@ export const ContextMenuSeparator = forwardRef<
|
|
|
156
163
|
return (
|
|
157
164
|
<ContextMenuPrimitive.Separator
|
|
158
165
|
ref={ref}
|
|
166
|
+
data-slot="context-menu-separator"
|
|
159
167
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
160
168
|
{...props}
|
|
161
169
|
/>
|
|
@@ -165,7 +173,8 @@ export const ContextMenuSeparator = forwardRef<
|
|
|
165
173
|
export function ContextMenuShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
166
174
|
return (
|
|
167
175
|
<span
|
|
168
|
-
|
|
176
|
+
data-slot="context-menu-shortcut"
|
|
177
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
169
178
|
{...props}
|
|
170
179
|
/>
|
|
171
180
|
);
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { FileText, History, MessageSquare } from "lucide-react";
|
|
3
|
+
import { expect, screen, waitFor } from "storybook/test";
|
|
4
|
+
|
|
5
|
+
import { ContextRail, type ContextRailSection } from "./context-rail";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Layout/ContextRail",
|
|
9
|
+
component: ContextRail,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "fullscreen",
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"A right-hand rail whose collapsed state is a 48px icon strip that doubles as its own section switcher — click a switcher entry to make it active, click the ACTIVE entry again to collapse/expand. Below `overlayBreakpoint` (default 768) it renders the strip plus a `Sheet` overlay for the expanded body instead of a fixed-width panel. See `docs/ADR/0035-context-rail-and-side-dock.md`.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} satisfies Meta<typeof ContextRail>;
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const sections: ContextRailSection[] = [
|
|
25
|
+
{
|
|
26
|
+
id: "sources",
|
|
27
|
+
label: "Sources",
|
|
28
|
+
icon: <FileText />,
|
|
29
|
+
count: 4,
|
|
30
|
+
content: (
|
|
31
|
+
<ul className="flex flex-col gap-2 text-body">
|
|
32
|
+
<li className="rounded-md border border-border p-2">quarterly-report.pdf — p.12</li>
|
|
33
|
+
<li className="rounded-md border border-border p-2">design-notes.md — §3</li>
|
|
34
|
+
<li className="rounded-md border border-border p-2">onboarding-flow.fig</li>
|
|
35
|
+
<li className="rounded-md border border-border p-2">support-thread-4821</li>
|
|
36
|
+
</ul>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "comments",
|
|
41
|
+
label: "Comments",
|
|
42
|
+
icon: <MessageSquare />,
|
|
43
|
+
count: 2,
|
|
44
|
+
content: (
|
|
45
|
+
<ul className="flex flex-col gap-2 text-body">
|
|
46
|
+
<li className="rounded-md border border-border p-2">
|
|
47
|
+
“Can we tighten this copy?” — Priya
|
|
48
|
+
</li>
|
|
49
|
+
<li className="rounded-md border border-border p-2">
|
|
50
|
+
“Looks good, shipping.” — Sam
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "activity",
|
|
57
|
+
label: "Activity",
|
|
58
|
+
icon: <History />,
|
|
59
|
+
content: (
|
|
60
|
+
<ul className="flex flex-col gap-2 text-body text-muted-foreground">
|
|
61
|
+
<li>Created 2 hours ago</li>
|
|
62
|
+
<li>Edited by Priya, 40 minutes ago</li>
|
|
63
|
+
<li>Reviewed by Sam, 5 minutes ago</li>
|
|
64
|
+
</ul>
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Expanded (`open`, default), three believable sections, the first active. */
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="flex h-[560px] w-full">
|
|
73
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
74
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" />
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
play: async ({ canvas }) => {
|
|
78
|
+
await expect(canvas.getByRole("button", { name: "Sources 4 items" })).toBeInTheDocument();
|
|
79
|
+
await expect(canvas.getByRole("button", { name: "Comments 2 items" })).toBeInTheDocument();
|
|
80
|
+
await expect(canvas.getByRole("button", { name: "Activity" })).toBeInTheDocument();
|
|
81
|
+
await expect(canvas.getByText("Sources")).toBeInTheDocument();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** `defaultOpen={false}` — the collapsed 48px icon strip, still fully operable as a switcher (uncontrolled, so the click-to-expand in the play function actually moves state). */
|
|
86
|
+
export const Collapsed: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="flex h-[560px] w-full">
|
|
89
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
90
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" defaultOpen={false} />
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
play: async ({ canvas, userEvent }) => {
|
|
94
|
+
const sourcesEntry = await canvas.findByRole("button", { name: "Sources 4 items" });
|
|
95
|
+
await expect(sourcesEntry).toBeInTheDocument();
|
|
96
|
+
// Content stays mounted while collapsed (only the ACTIVE section's
|
|
97
|
+
// content is ever mounted at all — collapsing hides it visually via
|
|
98
|
+
// CSS, it doesn't unmount it) — so assert visibility, not presence.
|
|
99
|
+
await expect(canvas.getByText("quarterly-report.pdf — p.12")).not.toBeVisible();
|
|
100
|
+
|
|
101
|
+
// Clicking the ACTIVE entry while collapsed expands the rail again.
|
|
102
|
+
await userEvent.click(sourcesEntry);
|
|
103
|
+
await expect(canvas.getByText("quarterly-report.pdf — p.12")).toBeVisible();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** No sections — the default localized empty state. */
|
|
108
|
+
export const Empty: Story = {
|
|
109
|
+
render: () => (
|
|
110
|
+
<div className="flex h-[560px] w-full">
|
|
111
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
112
|
+
<ContextRail sections={[]} />
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
play: async ({ canvas }) => {
|
|
116
|
+
await expect(await canvas.findByText("No sections")).toBeInTheDocument();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Below `overlayBreakpoint` the rail mounts its own strip plus a `Sheet`
|
|
122
|
+
* instead of `Sidebar`. A viewport-size story parameter cannot force this
|
|
123
|
+
* branch under a headless run (the composed story's `window.innerWidth`
|
|
124
|
+
* doesn't track the addon's viewport there), so this story forces it
|
|
125
|
+
* deterministically with a very high `overlayBreakpoint` instead — reliable
|
|
126
|
+
* both interactively and under `vitest --project storybook`.
|
|
127
|
+
*
|
|
128
|
+
* Known constraint (task-11f-brief.md Finding 5): this story's own fixed
|
|
129
|
+
* `h-[560px]` host is shorter than the browser viewport, so the expanded
|
|
130
|
+
* body's `Sheet` (always viewport-height) visibly outruns the persistent
|
|
131
|
+
* strip below it — see the seam this reproduces in `ContextRail`'s own doc
|
|
132
|
+
* comment.
|
|
133
|
+
*/
|
|
134
|
+
export const Narrow: Story = {
|
|
135
|
+
// The sheet's real entrance/exit transition is load-bearing here: the
|
|
136
|
+
// occlusion hit-test is taken against the SETTLED panel, and the final
|
|
137
|
+
// assertion reads the panel's `data-state="closed"` while its exit animation
|
|
138
|
+
// is still running (Radix unmounts only afterwards). The suite otherwise runs
|
|
139
|
+
// under `prefers-reduced-motion: reduce` (`apps/docs/vitest.config.ts`, #125),
|
|
140
|
+
// which removes that exit window entirely — so this story opts back into full
|
|
141
|
+
// motion rather than the lock being rewritten around a missing animation.
|
|
142
|
+
globals: { motionPref: "full" },
|
|
143
|
+
render: () => (
|
|
144
|
+
<div className="flex h-[560px] w-full">
|
|
145
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
146
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" overlayBreakpoint={2000} />
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
150
|
+
await expect(canvas.getByRole("button", { name: "Sources 4 items" })).toBeInTheDocument();
|
|
151
|
+
await expect(canvas.getByRole("button", { name: "Comments 2 items" })).toBeInTheDocument();
|
|
152
|
+
await expect(canvasElement.querySelector('[data-slot="sidebar"]')).toBeNull();
|
|
153
|
+
await expect(canvasElement.querySelector('[data-slot="context-rail"]')).not.toBeNull();
|
|
154
|
+
|
|
155
|
+
// `ContextRail` has no `open`/`defaultOpen` here, so `SidebarProvider`'s
|
|
156
|
+
// own default (`true`) drives the Sheet `ContextRailNarrow` wires to that
|
|
157
|
+
// same `open` state — the panel renders OPEN at the same time as the
|
|
158
|
+
// persistent 48px strip, which is exactly the configuration the
|
|
159
|
+
// panel-occludes-the-strip regression (fix round 1, task-9b-fix-1.md)
|
|
160
|
+
// needs to reproduce. `userEvent.click` only checks `pointer-events:
|
|
161
|
+
// none` — it does NOT hit-test — so an occluded-but-visible button would
|
|
162
|
+
// still report a passing click. Assert the browser's REAL hit-test
|
|
163
|
+
// (`document.elementFromPoint`) instead, for an INACTIVE entry and for
|
|
164
|
+
// the ACTIVE/toggle entry — the reviewer's raw-coordinate repro landed
|
|
165
|
+
// on the sheet's own close button from exactly the active entry's
|
|
166
|
+
// position, so both are asserted, not just the easy one.
|
|
167
|
+
async function assertHitTestable(name: string) {
|
|
168
|
+
const button = canvas.getByRole("button", { name });
|
|
169
|
+
// The sheet's own entrance/exit transition (`duration-slow`, 380ms —
|
|
170
|
+
// `themes.css`) animates the panel in via `slide-in-from-right`. The
|
|
171
|
+
// panel is `position: fixed`, so mid-transition it sits translated by
|
|
172
|
+
// roughly its own width — a hit-test taken the instant after the click
|
|
173
|
+
// that opened it would see that IN-FLIGHT position (measured: right
|
|
174
|
+
// edge past the viewport, not its resting inset-by-the-strip-width
|
|
175
|
+
// position) and fail for a reason that has nothing to do with the
|
|
176
|
+
// Blocker this lock exists to catch. Wait for the REAL, settled
|
|
177
|
+
// hit-test to succeed instead of sleeping a fixed amount — this reads
|
|
178
|
+
// as fast as the animation actually is, and still fails for real
|
|
179
|
+
// (after timing out) if the panel keeps covering the strip once
|
|
180
|
+
// settled, which is the only case task-9b-fix-1.md's regression is.
|
|
181
|
+
await waitFor(
|
|
182
|
+
() => {
|
|
183
|
+
const rect = button.getBoundingClientRect();
|
|
184
|
+
if (rect.width === 0) {
|
|
185
|
+
throw new Error(`"${name}" has not been laid out yet (zero width)`);
|
|
186
|
+
}
|
|
187
|
+
const x = rect.left + rect.width / 2;
|
|
188
|
+
const y = rect.top + rect.height / 2;
|
|
189
|
+
const hit = document.elementFromPoint(x, y);
|
|
190
|
+
const hitDescription =
|
|
191
|
+
hit == null
|
|
192
|
+
? "nothing"
|
|
193
|
+
: `<${hit.tagName.toLowerCase()}${
|
|
194
|
+
hit.getAttribute("data-slot")
|
|
195
|
+
? ` data-slot="${hit.getAttribute("data-slot")}"`
|
|
196
|
+
: ""
|
|
197
|
+
}>`;
|
|
198
|
+
expect(
|
|
199
|
+
hit != null && button.contains(hit),
|
|
200
|
+
`expected the centre of "${name}" (${x}, ${y}) to hit-test to that button, but it ` +
|
|
201
|
+
`hit ${hitDescription} instead — the sheet panel is likely covering the strip`,
|
|
202
|
+
).toBe(true);
|
|
203
|
+
},
|
|
204
|
+
{ timeout: 1000 },
|
|
205
|
+
);
|
|
206
|
+
return button;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The switched-to section's content renders inside `SheetContent`, which
|
|
210
|
+
// Radix portals to `document.body` — a real DOM sibling of
|
|
211
|
+
// `canvasElement`, not a descendant of it — so it must be queried via
|
|
212
|
+
// `screen` (bound to `document.body`), not `canvas` (scoped to
|
|
213
|
+
// `canvasElement`).
|
|
214
|
+
|
|
215
|
+
// Inactive entry: clicking it changes the active section.
|
|
216
|
+
const commentsEntry = await assertHitTestable("Comments 2 items");
|
|
217
|
+
await userEvent.click(commentsEntry);
|
|
218
|
+
await expect(screen.getByText("“Looks good, shipping.” — Sam")).toBeVisible();
|
|
219
|
+
|
|
220
|
+
// Active/toggle entry: clicking it collapses the rail (closes the Sheet)
|
|
221
|
+
// rather than changing the section. Radix flips the panel's own
|
|
222
|
+
// `data-state` synchronously with the click (the exit ANIMATION, and the
|
|
223
|
+
// eventual unmount, follow after) — asserting that attribute is the
|
|
224
|
+
// immediate, non-flaky signal that the toggle actually fired, rather
|
|
225
|
+
// than racing the animation to observe visibility.
|
|
226
|
+
const commentsToggleEntry = await assertHitTestable("Comments 2 items");
|
|
227
|
+
await userEvent.click(commentsToggleEntry);
|
|
228
|
+
await expect(screen.getByRole("dialog", { hidden: true })).toHaveAttribute(
|
|
229
|
+
"data-state",
|
|
230
|
+
"closed",
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Eight sections in the default 20rem rail — the count at which the header's
|
|
237
|
+
* two occupants genuinely compete for the band. `sections` is caller-supplied
|
|
238
|
+
* and unbounded, and the switcher is a fixed-height row of 32px buttons, so
|
|
239
|
+
* left uncapped it reserves its full width before the heading gets any: the
|
|
240
|
+
* active section's label was measured down to roughly 20px of usable width
|
|
241
|
+
* (its own padding included), which is no label at all. The cap keeps the
|
|
242
|
+
* heading's half of the band and moves the overflow into the switcher, which
|
|
243
|
+
* scrolls.
|
|
244
|
+
*/
|
|
245
|
+
export const ManySections: Story = {
|
|
246
|
+
render: () => (
|
|
247
|
+
<div className="flex h-[560px] w-full">
|
|
248
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
249
|
+
<ContextRail
|
|
250
|
+
sections={[
|
|
251
|
+
...sections,
|
|
252
|
+
{ id: "files", label: "Files", icon: <FileText />, content: <p>Files</p> },
|
|
253
|
+
{ id: "notes", label: "Notes", icon: <MessageSquare />, content: <p>Notes</p> },
|
|
254
|
+
{ id: "audit", label: "Audit", icon: <History />, content: <p>Audit</p> },
|
|
255
|
+
{ id: "links", label: "Links", icon: <FileText />, content: <p>Links</p> },
|
|
256
|
+
{ id: "tasks", label: "Tasks", icon: <MessageSquare />, content: <p>Tasks</p> },
|
|
257
|
+
]}
|
|
258
|
+
defaultActiveSectionId="sources"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
),
|
|
262
|
+
play: async ({ canvasElement }) => {
|
|
263
|
+
const header = canvasElement.querySelector<HTMLElement>('[data-slot="context-rail-header"]');
|
|
264
|
+
const heading = canvasElement.querySelector<HTMLElement>('[data-slot="context-rail-heading"]');
|
|
265
|
+
const switcher = canvasElement.querySelector<HTMLElement>(
|
|
266
|
+
'[data-slot="context-rail-switcher"]',
|
|
267
|
+
);
|
|
268
|
+
await expect(header).not.toBeNull();
|
|
269
|
+
await expect(heading).not.toBeNull();
|
|
270
|
+
await expect(switcher).not.toBeNull();
|
|
271
|
+
|
|
272
|
+
const headerWidth = header!.getBoundingClientRect().width;
|
|
273
|
+
const switcherWidth = switcher!.getBoundingClientRect().width;
|
|
274
|
+
const headingWidth = heading!.getBoundingClientRect().width;
|
|
275
|
+
|
|
276
|
+
// The switcher never takes more than half the band…
|
|
277
|
+
await expect(switcherWidth).toBeLessThanOrEqual(headerWidth / 2 + 0.5);
|
|
278
|
+
// …so the heading keeps a width a label can actually render in. The
|
|
279
|
+
// regression this locks measured ~20px here.
|
|
280
|
+
await expect(headingWidth).toBeGreaterThan(80);
|
|
281
|
+
// And the header itself never outgrows the rail (the overflow lives
|
|
282
|
+
// inside the switcher, which scrolls).
|
|
283
|
+
await expect(switcher!.scrollWidth).toBeGreaterThan(switcher!.clientWidth);
|
|
284
|
+
},
|
|
285
|
+
};
|