@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// a2ui.exposed: yes
|
|
2
4
|
import {
|
|
3
5
|
forwardRef,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
} from "react";
|
|
11
13
|
import { cn } from "../../lib/cn";
|
|
12
14
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
15
|
+
import { useLocale } from "../locale-provider";
|
|
13
16
|
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// Token vocabulary
|
|
@@ -120,7 +123,7 @@ const Swatch = forwardRef<HTMLButtonElement, SwatchProps>(function Swatch(
|
|
|
120
123
|
selected
|
|
121
124
|
? "border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background"
|
|
122
125
|
: "border-transparent hover:border-foreground/40",
|
|
123
|
-
"focus-
|
|
126
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
124
127
|
className,
|
|
125
128
|
)}
|
|
126
129
|
{...props}
|
|
@@ -194,12 +197,14 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
194
197
|
onValueChange,
|
|
195
198
|
swatches = COLOR_TOKENS,
|
|
196
199
|
allowCustom = false,
|
|
197
|
-
"aria-label": ariaLabel
|
|
200
|
+
"aria-label": ariaLabel,
|
|
198
201
|
className,
|
|
199
202
|
...props
|
|
200
203
|
},
|
|
201
204
|
ref,
|
|
202
205
|
) {
|
|
206
|
+
const { t } = useLocale();
|
|
207
|
+
const resolvedAriaLabel = ariaLabel ?? t("ui.colorPicker.pickColor");
|
|
203
208
|
const isControlled = valueProp !== undefined;
|
|
204
209
|
const [internalValue, setInternalValue] = useState<ColorToken | string>(
|
|
205
210
|
defaultValue ?? "chart-1",
|
|
@@ -259,19 +264,19 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
259
264
|
<PopoverTrigger asChild>
|
|
260
265
|
<button
|
|
261
266
|
type="button"
|
|
262
|
-
aria-label={
|
|
267
|
+
aria-label={resolvedAriaLabel}
|
|
263
268
|
aria-haspopup="dialog"
|
|
264
269
|
aria-expanded={open}
|
|
265
270
|
className={cn(
|
|
266
|
-
"inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-
|
|
271
|
+
"inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-body shadow-sm",
|
|
267
272
|
"transition-[color,box-shadow] duration-fast",
|
|
268
273
|
"hover:bg-accent hover:text-accent-foreground",
|
|
269
|
-
"focus-
|
|
274
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
270
275
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
271
276
|
)}
|
|
272
277
|
>
|
|
273
278
|
<TriggerSwatch value={current} />
|
|
274
|
-
<span className="text-muted-foreground">{current || "
|
|
279
|
+
<span className="text-muted-foreground">{current || t("ui.colorPicker.none")}</span>
|
|
275
280
|
</button>
|
|
276
281
|
</PopoverTrigger>
|
|
277
282
|
|
|
@@ -279,7 +284,7 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
279
284
|
<div
|
|
280
285
|
ref={groupRef}
|
|
281
286
|
role="radiogroup"
|
|
282
|
-
aria-label="
|
|
287
|
+
aria-label={t("ui.colorPicker.swatches")}
|
|
283
288
|
className="flex flex-wrap gap-1.5"
|
|
284
289
|
onKeyDown={handleGroupKeyDown}
|
|
285
290
|
>
|
|
@@ -296,17 +301,19 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
296
301
|
{allowCustom && (
|
|
297
302
|
<div className="mt-3 border-t border-border pt-3">
|
|
298
303
|
{/* Custom hex is NOT theme-aware — it will not adapt to theme changes. */}
|
|
299
|
-
<p className="mb-1.5 text-
|
|
300
|
-
|
|
304
|
+
<p className="mb-1.5 text-meta text-muted-foreground">
|
|
305
|
+
{/* #124: inline running text — ink rung, not the 3:1 mark rung. */}
|
|
306
|
+
{t("ui.colorPicker.customHexLabel")}{" "}
|
|
307
|
+
<span className="text-warning-text">{t("ui.colorPicker.notThemeAware")}</span>
|
|
301
308
|
</p>
|
|
302
309
|
<div className="flex gap-2">
|
|
303
310
|
<input
|
|
304
311
|
type="text"
|
|
305
312
|
value={customHex}
|
|
306
|
-
placeholder="#3b82f6"
|
|
313
|
+
placeholder="#3b82f6" // i18n-exempt: example hex value, not prose
|
|
307
314
|
maxLength={7}
|
|
308
315
|
spellCheck={false}
|
|
309
|
-
aria-label="
|
|
316
|
+
aria-label={t("ui.colorPicker.customHex")}
|
|
310
317
|
onChange={(e) => setCustomHex(e.target.value)}
|
|
311
318
|
onKeyDown={(e) => {
|
|
312
319
|
if (e.key === "Enter") {
|
|
@@ -315,8 +322,8 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
315
322
|
}
|
|
316
323
|
}}
|
|
317
324
|
className={cn(
|
|
318
|
-
"h-8 w-28 rounded-md border border-input bg-background px-2 text-
|
|
319
|
-
"focus-
|
|
325
|
+
"h-8 w-28 rounded-md border border-input bg-background px-2 text-body",
|
|
326
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
320
327
|
"placeholder:text-muted-foreground",
|
|
321
328
|
)}
|
|
322
329
|
/>
|
|
@@ -324,9 +331,9 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
324
331
|
type="button"
|
|
325
332
|
onClick={handleCustomSubmit}
|
|
326
333
|
className={cn(
|
|
327
|
-
"h-8 rounded-md border border-input bg-background px-2 text-
|
|
334
|
+
"h-8 rounded-md border border-input bg-background px-2 text-body",
|
|
328
335
|
"hover:bg-accent hover:text-accent-foreground",
|
|
329
|
-
"focus-
|
|
336
|
+
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
330
337
|
)}
|
|
331
338
|
>
|
|
332
339
|
Apply
|
|
@@ -5,6 +5,16 @@ const meta = {
|
|
|
5
5
|
title: "Forms/Combobox",
|
|
6
6
|
component: Combobox,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
|
+
// The trigger fills its container by default (no hard-fixed width) — cap
|
|
9
|
+
// the docs canvas to a realistic form-field width instead of letting it
|
|
10
|
+
// stretch the full autodocs page.
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="w-full max-w-md">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
8
18
|
argTypes: {
|
|
9
19
|
options: {
|
|
10
20
|
description: "Array of `{ label, value }` options.",
|
|
@@ -66,6 +66,60 @@ describe("Combobox", () => {
|
|
|
66
66
|
);
|
|
67
67
|
expect(screen.getByRole("combobox")).toHaveTextContent("Custom Value");
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
71
|
+
render(<Combobox options={[{ label: "A", value: "a" }]} />);
|
|
72
|
+
expect(screen.getByRole("combobox").className).not.toMatch(/\bw-56\b/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// jsdom doesn't implement Element.scrollIntoView, which cmdk calls internally
|
|
77
|
+
// to keep the auto-highlighted item visible — see the comment on the
|
|
78
|
+
// `allowCustomValue` describe below for the full explanation.
|
|
79
|
+
describe("Combobox (options interaction)", () => {
|
|
80
|
+
let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
84
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// #reviewed 1.7 — a hand-rolled `onValueChange ? onValueChange(v) : setInternal(v)`
|
|
92
|
+
// never updates internal state once a listener is attached, so an
|
|
93
|
+
// uncontrolled Combobox with an onValueChange handler never shows its own
|
|
94
|
+
// selection on the trigger.
|
|
95
|
+
it("updates the trigger's own displayed value when uncontrolled even with an onValueChange listener", async () => {
|
|
96
|
+
const onValueChange = vi.fn();
|
|
97
|
+
render(<Combobox options={[{ label: "Alpha", value: "a" }]} onValueChange={onValueChange} />);
|
|
98
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
99
|
+
await userEvent.click(await screen.findByRole("option", { name: "Alpha" }));
|
|
100
|
+
expect(onValueChange).toHaveBeenCalledWith("a");
|
|
101
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Alpha");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Duplicate labels used to collide because CommandItem's `value` (cmdk's
|
|
105
|
+
// identity/scoring key) was the LABEL, not the unique option value.
|
|
106
|
+
it("distinguishes options that share the same label", async () => {
|
|
107
|
+
const onValueChange = vi.fn();
|
|
108
|
+
render(
|
|
109
|
+
<Combobox
|
|
110
|
+
options={[
|
|
111
|
+
{ label: "Production", value: "prod-us" },
|
|
112
|
+
{ label: "Production", value: "prod-eu" },
|
|
113
|
+
]}
|
|
114
|
+
onValueChange={onValueChange}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
118
|
+
const options = await screen.findAllByRole("option", { name: "Production" });
|
|
119
|
+
expect(options).toHaveLength(2);
|
|
120
|
+
await userEvent.click(options[1]!);
|
|
121
|
+
expect(onValueChange).toHaveBeenCalledWith("prod-eu");
|
|
122
|
+
});
|
|
69
123
|
});
|
|
70
124
|
|
|
71
125
|
// #359 — allowCustomValue. A nested describe because these tests TYPE into the
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
|
|
2
4
|
import { defaultFilter } from "cmdk";
|
|
3
5
|
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
|
4
6
|
import { cn } from "../../lib/cn";
|
|
7
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
5
8
|
import { Button } from "../button";
|
|
6
9
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
7
10
|
import { useLocale } from "../locale-provider";
|
|
@@ -20,6 +23,12 @@ export interface ComboboxOption {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface ComboboxProps {
|
|
26
|
+
/**
|
|
27
|
+
* The option list — every option is mounted in the DOM (cmdk has no built-in
|
|
28
|
+
* windowing), so this is fine for typical form/menu-sized lists but not for
|
|
29
|
+
* open-ended data. For thousands of options, use `VirtualSelect` instead,
|
|
30
|
+
* which windows rendering via `@tanstack/react-virtual`.
|
|
31
|
+
*/
|
|
23
32
|
options: ComboboxOption[];
|
|
24
33
|
value?: string;
|
|
25
34
|
onValueChange?: (value: string) => void;
|
|
@@ -97,8 +106,8 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
97
106
|
options,
|
|
98
107
|
value,
|
|
99
108
|
onValueChange,
|
|
100
|
-
placeholder
|
|
101
|
-
searchPlaceholder
|
|
109
|
+
placeholder,
|
|
110
|
+
searchPlaceholder,
|
|
102
111
|
emptyText,
|
|
103
112
|
className,
|
|
104
113
|
disabled = false,
|
|
@@ -111,11 +120,11 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
111
120
|
ref,
|
|
112
121
|
) {
|
|
113
122
|
const { t } = useLocale();
|
|
123
|
+
const resolvedPlaceholder = placeholder ?? t("ui.combobox.placeholder");
|
|
124
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? t("ui.combobox.searchPlaceholder");
|
|
114
125
|
const [open, setOpen] = useState(false);
|
|
115
|
-
const [
|
|
126
|
+
const [selected, setValue] = useControllableState(value, "", onValueChange);
|
|
116
127
|
const [search, setSearch] = useState("");
|
|
117
|
-
const selected = value ?? internal;
|
|
118
|
-
const setValue = (v: string) => (onValueChange ? onValueChange(v) : setInternal(v));
|
|
119
128
|
const current = options.find((o) => o.value === selected);
|
|
120
129
|
|
|
121
130
|
const trimmedSearch = search.trim();
|
|
@@ -150,30 +159,35 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
150
159
|
aria-expanded={open}
|
|
151
160
|
disabled={disabled}
|
|
152
161
|
data-slot="combobox"
|
|
153
|
-
className={cn("w-
|
|
162
|
+
className={cn("w-full justify-between font-normal", triggerProps?.className, className)}
|
|
154
163
|
>
|
|
155
|
-
{current ? current.label : selected ||
|
|
164
|
+
{current ? current.label : selected || resolvedPlaceholder}
|
|
156
165
|
<ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" />
|
|
157
166
|
</Button>
|
|
158
167
|
</PopoverTrigger>
|
|
159
168
|
<PopoverContent
|
|
160
|
-
className="w-56 p-0"
|
|
169
|
+
className="w-[var(--radix-popover-trigger-width)] min-w-56 p-0"
|
|
161
170
|
data-slot="combobox-content"
|
|
162
171
|
// Radix Popover.Content renders role="dialog"; without a name axe's
|
|
163
172
|
// aria-dialog-name rule (rightly) flags it. Reuse the trigger's own
|
|
164
173
|
// purpose label when the consumer set one, else fall back to the
|
|
165
174
|
// (already-translatable) search placeholder — no new hardcoded copy.
|
|
166
|
-
aria-label={ariaLabel ??
|
|
175
|
+
aria-label={ariaLabel ?? resolvedSearchPlaceholder}
|
|
167
176
|
>
|
|
168
177
|
<Command filter={allowCustomValue ? customValueFilter : undefined}>
|
|
169
|
-
<CommandInput
|
|
178
|
+
<CommandInput
|
|
179
|
+
placeholder={resolvedSearchPlaceholder}
|
|
180
|
+
value={search}
|
|
181
|
+
onValueChange={setSearch}
|
|
182
|
+
/>
|
|
170
183
|
<CommandList>
|
|
171
184
|
<CommandEmpty>{resolvedEmptyText}</CommandEmpty>
|
|
172
185
|
<CommandGroup>
|
|
173
186
|
{options.map((o) => (
|
|
174
187
|
<CommandItem
|
|
175
188
|
key={o.value}
|
|
176
|
-
value={o.
|
|
189
|
+
value={o.value}
|
|
190
|
+
keywords={[o.label]}
|
|
177
191
|
onSelect={() => {
|
|
178
192
|
setValue(o.value === selected ? "" : o.value);
|
|
179
193
|
setOpen(false);
|
|
@@ -3,7 +3,6 @@ import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent }
|
|
|
3
3
|
import { expect, waitFor, within } from "storybook/test";
|
|
4
4
|
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react";
|
|
5
5
|
import { Button } from "../button";
|
|
6
|
-
import { DialogTitle } from "../dialog";
|
|
7
6
|
import {
|
|
8
7
|
Command,
|
|
9
8
|
CommandDialog,
|
|
@@ -19,6 +18,20 @@ const meta = {
|
|
|
19
18
|
title: "Overlays/Command",
|
|
20
19
|
component: Command,
|
|
21
20
|
tags: ["autodocs"],
|
|
21
|
+
parameters: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
"A grouped, searchable, keyboard-driven list (cmdk). Render it inline, or wrap it " +
|
|
26
|
+
"in `CommandDialog` for a full ⌘K palette — pass `CommandDialog`'s optional `title` " +
|
|
27
|
+
"for the dialog's `sr-only` accessible name.\n\n" +
|
|
28
|
+
"**Choosing between the two model/target pickers.** A full ⌘K palette → compose " +
|
|
29
|
+
"`CommandDialog` + `Command*` from `@elabs-ai/components-ui` directly; there is no " +
|
|
30
|
+
"separate selector component. An inline pill in a composer footer that opens the " +
|
|
31
|
+
"same grouped list anchored under itself → `ModelPicker`.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
22
35
|
argTypes: {
|
|
23
36
|
label: {
|
|
24
37
|
description: "Accessible label for the command palette (cmdk `label` prop).",
|
|
@@ -113,10 +126,7 @@ function CommandPaletteDemo() {
|
|
|
113
126
|
Open command palette
|
|
114
127
|
</Button>
|
|
115
128
|
|
|
116
|
-
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
117
|
-
{/* CommandDialog doesn't ship a built-in title, so supply an sr-only one
|
|
118
|
-
for the dialog's accessible name. */}
|
|
119
|
-
<DialogTitle className="sr-only">Command palette</DialogTitle>
|
|
129
|
+
<CommandDialog open={open} onOpenChange={setOpen} title="Command palette">
|
|
120
130
|
<CommandInput placeholder="Type a command or search…" />
|
|
121
131
|
<CommandList>
|
|
122
132
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
@@ -291,7 +301,7 @@ function ExternalInputComboboxDemo() {
|
|
|
291
301
|
role="combobox"
|
|
292
302
|
onKeyDown={forwardKey}
|
|
293
303
|
placeholder="Type @ to mention…"
|
|
294
|
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-body
|
|
304
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-body focus-ring"
|
|
295
305
|
/>
|
|
296
306
|
<Command
|
|
297
307
|
id="mention-list"
|
|
@@ -4,12 +4,16 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import userEvent from "@testing-library/user-event";
|
|
5
5
|
import {
|
|
6
6
|
Command,
|
|
7
|
+
CommandDialog,
|
|
8
|
+
CommandEmpty,
|
|
7
9
|
CommandGroup,
|
|
8
10
|
CommandInput,
|
|
9
11
|
CommandItem,
|
|
10
12
|
CommandList,
|
|
13
|
+
CommandSeparator,
|
|
11
14
|
useCommandActiveItemId,
|
|
12
15
|
} from "./command";
|
|
16
|
+
import { DialogTitle } from "../dialog";
|
|
13
17
|
|
|
14
18
|
// jsdom does not implement `Element.prototype.scrollIntoView`, and `cmdk`
|
|
15
19
|
// calls it unconditionally (not feature-detected — it's a third-party
|
|
@@ -278,3 +282,87 @@ describe("Command — external combobox input wiring (integration)", () => {
|
|
|
278
282
|
});
|
|
279
283
|
});
|
|
280
284
|
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* RM-010 — `CommandDialog` grew a `title` prop when the `ModelSelector` alias
|
|
288
|
+
* shell in `@elabs-ai/components-ai` was deleted; the `sr-only` `DialogTitle`
|
|
289
|
+
* was the one capability that shell had and `ui` lacked.
|
|
290
|
+
*
|
|
291
|
+
* It MUST stay optional. `@elabs-ai/components-ai`'s `VoiceSelectorDialog`
|
|
292
|
+
* spreads `ComponentProps<typeof CommandDialog>` straight through, and callers
|
|
293
|
+
* that already render their own `DialogTitle` in `children` must not be forced
|
|
294
|
+
* to move it. Both branches are asserted so a later "make it required" change
|
|
295
|
+
* fails here rather than in a downstream package.
|
|
296
|
+
*/
|
|
297
|
+
describe("CommandDialog — optional title (RM-010)", () => {
|
|
298
|
+
it("names the dialog from `title`, rendered sr-only", () => {
|
|
299
|
+
render(
|
|
300
|
+
<CommandDialog open title="Command palette">
|
|
301
|
+
<CommandList>
|
|
302
|
+
<CommandItem>Calendar</CommandItem>
|
|
303
|
+
</CommandList>
|
|
304
|
+
</CommandDialog>,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const dialog = screen.getByRole("dialog");
|
|
308
|
+
expect(dialog).toHaveAccessibleName("Command palette");
|
|
309
|
+
expect(screen.getByText("Command palette")).toHaveClass("sr-only");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("renders no title of its own when `title` is omitted — a caller's own DialogTitle still wins", () => {
|
|
313
|
+
render(
|
|
314
|
+
<CommandDialog open>
|
|
315
|
+
<DialogTitle>Caller-owned title</DialogTitle>
|
|
316
|
+
<CommandList>
|
|
317
|
+
<CommandItem>Calendar</CommandItem>
|
|
318
|
+
</CommandList>
|
|
319
|
+
</CommandDialog>,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(screen.getByRole("dialog")).toHaveAccessibleName("Caller-owned title");
|
|
323
|
+
// Exactly one heading — CommandDialog did not add a second, competing one.
|
|
324
|
+
expect(screen.getAllByText(/title/i)).toHaveLength(1);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/** #157 — `CommandSeparator` is `cmdk`'s divider between groups, rendered as a
|
|
329
|
+
* direct child of `CommandList`'s `role="listbox"`. A listbox may only own
|
|
330
|
+
* `option`/`group` children (WCAG 1.3.1); a bare `role="separator"` there is
|
|
331
|
+
* neither. */
|
|
332
|
+
describe("Command — CommandSeparator a11y (#157)", () => {
|
|
333
|
+
it("stays in the DOM for sighted users but is removed from the accessibility tree", () => {
|
|
334
|
+
render(
|
|
335
|
+
<Command>
|
|
336
|
+
<CommandList>
|
|
337
|
+
<CommandGroup heading="Fruit">
|
|
338
|
+
<CommandItem value="apple">Apple</CommandItem>
|
|
339
|
+
</CommandGroup>
|
|
340
|
+
<CommandSeparator />
|
|
341
|
+
<CommandGroup heading="Veg">
|
|
342
|
+
<CommandItem value="carrot">Carrot</CommandItem>
|
|
343
|
+
</CommandGroup>
|
|
344
|
+
</CommandList>
|
|
345
|
+
</Command>,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Still rendered — cmdk owns the visual divider.
|
|
349
|
+
expect(document.querySelector("[cmdk-separator]")).not.toBeNull();
|
|
350
|
+
// Never exposed with its `role="separator"` — a listbox can't own one.
|
|
351
|
+
expect(screen.queryByRole("separator")).toBeNull();
|
|
352
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("CommandEmpty — merges className instead of replacing it", () => {
|
|
357
|
+
it("keeps the base py-6/text-center classes alongside a caller className", () => {
|
|
358
|
+
render(
|
|
359
|
+
<Command>
|
|
360
|
+
<CommandList>
|
|
361
|
+
<CommandEmpty className="custom-empty">No results</CommandEmpty>
|
|
362
|
+
</CommandList>
|
|
363
|
+
</Command>,
|
|
364
|
+
);
|
|
365
|
+
const empty = screen.getByText("No results");
|
|
366
|
+
expect(empty).toHaveClass("py-6", "text-center", "custom-empty");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createContext,
|
|
3
5
|
forwardRef,
|
|
@@ -16,7 +18,7 @@ import { Command as CommandPrimitive, useCommandState } from "cmdk";
|
|
|
16
18
|
import { Search } from "lucide-react";
|
|
17
19
|
import { cn } from "../../lib/cn";
|
|
18
20
|
import { mergeRefs } from "../../lib/merge-refs";
|
|
19
|
-
import { Dialog, DialogContent } from "../dialog";
|
|
21
|
+
import { Dialog, DialogContent, DialogTitle } from "../dialog";
|
|
20
22
|
|
|
21
23
|
/** cmdk's own marker attribute on an item row — the scope for the id lookup. */
|
|
22
24
|
const CMDK_ITEM_SELECTOR = '[cmdk-item=""][aria-selected="true"]';
|
|
@@ -149,17 +151,34 @@ export function useCommandActiveItemId(): string | undefined {
|
|
|
149
151
|
return id === NO_COMMAND_PROVIDER ? undefined : id;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
export
|
|
153
|
-
children,
|
|
154
|
-
filter,
|
|
155
|
-
...props
|
|
156
|
-
}: ComponentPropsWithoutRef<typeof Dialog> & {
|
|
154
|
+
export type CommandDialogProps = ComponentPropsWithoutRef<typeof Dialog> & {
|
|
157
155
|
/** Custom cmdk ranking (e.g. strict substring instead of fuzzy). */
|
|
158
156
|
filter?: ComponentPropsWithoutRef<typeof CommandPrimitive>["filter"];
|
|
159
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The dialog's accessible name, rendered `sr-only`.
|
|
159
|
+
*
|
|
160
|
+
* OPTIONAL, and deliberately so: callers that already render their own
|
|
161
|
+
* `DialogTitle` in `children` must not be forced to move it, and
|
|
162
|
+
* `@elabs-ai/components-ai`'s `VoiceSelectorDialog` passes this whole prop
|
|
163
|
+
* type straight through. Supply it when you have no visible title — a
|
|
164
|
+
* `Dialog` with no accessible name is a Radix warning and an axe violation.
|
|
165
|
+
*/
|
|
166
|
+
title?: ReactNode;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A ⌘K command palette in a modal `Dialog`.
|
|
171
|
+
*
|
|
172
|
+
* This IS the palette presentation of `Command` — there is no separate
|
|
173
|
+
* "selector" component. For an inline pill that opens the same grouped,
|
|
174
|
+
* searchable list anchored under itself (a composer footer), reach for
|
|
175
|
+
* `ModelPicker` instead.
|
|
176
|
+
*/
|
|
177
|
+
export function CommandDialog({ children, filter, title, ...props }: CommandDialogProps) {
|
|
160
178
|
return (
|
|
161
179
|
<Dialog {...props}>
|
|
162
180
|
<DialogContent className="overflow-hidden p-0">
|
|
181
|
+
{title === undefined ? null : <DialogTitle className="sr-only">{title}</DialogTitle>}
|
|
163
182
|
<Command
|
|
164
183
|
filter={filter}
|
|
165
184
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3"
|
|
@@ -182,7 +201,7 @@ export const CommandInput = forwardRef<
|
|
|
182
201
|
ref={ref}
|
|
183
202
|
data-slot="command-input"
|
|
184
203
|
className={cn(
|
|
185
|
-
"flex h-10 w-full rounded-md bg-transparent py-3 text-
|
|
204
|
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-body outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
186
205
|
className,
|
|
187
206
|
)}
|
|
188
207
|
{...props}
|
|
@@ -208,12 +227,12 @@ export const CommandList = forwardRef<
|
|
|
208
227
|
export const CommandEmpty = forwardRef<
|
|
209
228
|
ElementRef<typeof CommandPrimitive.Empty>,
|
|
210
229
|
ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
211
|
-
>(function CommandEmpty(props, ref) {
|
|
230
|
+
>(function CommandEmpty({ className, ...props }, ref) {
|
|
212
231
|
return (
|
|
213
232
|
<CommandPrimitive.Empty
|
|
214
233
|
ref={ref}
|
|
215
234
|
data-slot="command-empty"
|
|
216
|
-
className="py-6 text-center text-
|
|
235
|
+
className={cn("py-6 text-center text-body", className)}
|
|
217
236
|
{...props}
|
|
218
237
|
/>
|
|
219
238
|
);
|
|
@@ -228,7 +247,7 @@ export const CommandGroup = forwardRef<
|
|
|
228
247
|
ref={ref}
|
|
229
248
|
data-slot="command-group"
|
|
230
249
|
className={cn(
|
|
231
|
-
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-
|
|
250
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-meta [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
232
251
|
className,
|
|
233
252
|
)}
|
|
234
253
|
{...props}
|
|
@@ -244,6 +263,12 @@ export const CommandSeparator = forwardRef<
|
|
|
244
263
|
<CommandPrimitive.Separator
|
|
245
264
|
ref={ref}
|
|
246
265
|
data-slot="command-separator"
|
|
266
|
+
// `cmdk` renders this as a direct child of its `role="listbox"`
|
|
267
|
+
// (`CommandList`), which may only own `option`/`group` (WCAG 1.3.1,
|
|
268
|
+
// #157) — a purely visual divider between groups, so `aria-hidden`
|
|
269
|
+
// removes it from the accessibility tree instead of asking the listbox
|
|
270
|
+
// to own a role it can't. `{...props}` still lets a caller override it.
|
|
271
|
+
aria-hidden="true"
|
|
247
272
|
className={cn("-mx-1 h-px bg-border", className)}
|
|
248
273
|
{...props}
|
|
249
274
|
/>
|
|
@@ -282,7 +307,7 @@ export const CommandItem = forwardRef<
|
|
|
282
307
|
ref={ref}
|
|
283
308
|
data-slot="command-item"
|
|
284
309
|
className={cn(
|
|
285
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
310
|
+
"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 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4",
|
|
286
311
|
className,
|
|
287
312
|
)}
|
|
288
313
|
{...props}
|
|
@@ -294,7 +319,7 @@ export function CommandShortcut({ className, ...props }: HTMLAttributes<HTMLSpan
|
|
|
294
319
|
return (
|
|
295
320
|
<span
|
|
296
321
|
data-slot="command-shortcut"
|
|
297
|
-
className={cn("ms-auto text-
|
|
322
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
298
323
|
{...props}
|
|
299
324
|
/>
|
|
300
325
|
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
|
+
import { CommandTrigger } from "./command-trigger";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Navigation/CommandTrigger",
|
|
7
|
+
component: CommandTrigger,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
"The command-palette opener a top bar reaches for: shaped like a search field, " +
|
|
13
|
+
"collapsing to just the icon under `sm`. The visible label and the shortcut `Kbd` are " +
|
|
14
|
+
"both decorative — the accessible name is authored with `aria-label` so the shortcut " +
|
|
15
|
+
"glyph never concatenates into it (#117).",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
tags: ["autodocs"],
|
|
20
|
+
} satisfies Meta<typeof CommandTrigger>;
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
/** Default label and platform-detected shortcut. */
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
play: async ({ canvasElement }) => {
|
|
27
|
+
const button = within(canvasElement).getByRole("button");
|
|
28
|
+
// #117: the Kbd's shortcut glyph must not pollute the computed name.
|
|
29
|
+
await expect(button).toHaveAccessibleName("Search");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** A caller-supplied label and shortcut — useful when the platform guess is wrong. */
|
|
34
|
+
export const WithCustomShortcut: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
label: "Find anything",
|
|
37
|
+
shortcut: "Ctrl K",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { CommandTrigger } from "./command-trigger";
|
|
4
|
+
|
|
5
|
+
describe("CommandTrigger", () => {
|
|
6
|
+
it("announces only its label — the shortcut glyph must not pollute the accessible name", () => {
|
|
7
|
+
render(<CommandTrigger />);
|
|
8
|
+
// #117: a Kbd inside a control concatenates into the computed name. Assert the
|
|
9
|
+
// EXACT name; a regex match would happily pass on the polluted string.
|
|
10
|
+
expect(screen.getByRole("button")).toHaveAccessibleName("Search");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("takes a custom label and shortcut", () => {
|
|
14
|
+
render(<CommandTrigger label="Find anything" shortcut="Ctrl K" />);
|
|
15
|
+
expect(screen.getByRole("button")).toHaveAccessibleName("Find anything");
|
|
16
|
+
expect(screen.getByText("Ctrl K")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
});
|