@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
|
@@ -109,7 +109,7 @@ export const DropdownMenuContent = forwardRef<
|
|
|
109
109
|
ref={ref}
|
|
110
110
|
sideOffset={sideOffset}
|
|
111
111
|
className={cn(
|
|
112
|
-
"z-50 min-w-[
|
|
112
|
+
"z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
113
113
|
"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",
|
|
114
114
|
"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",
|
|
115
115
|
className,
|
|
@@ -128,7 +128,7 @@ export const DropdownMenuItem = forwardRef<
|
|
|
128
128
|
<DropdownMenuPrimitive.Item
|
|
129
129
|
ref={ref}
|
|
130
130
|
className={cn(
|
|
131
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
131
|
+
"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",
|
|
132
132
|
"focus:bg-accent focus:text-accent-foreground",
|
|
133
133
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
134
134
|
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -147,7 +147,7 @@ export const DropdownMenuLabel = forwardRef<
|
|
|
147
147
|
return (
|
|
148
148
|
<DropdownMenuPrimitive.Label
|
|
149
149
|
ref={ref}
|
|
150
|
-
className={cn("px-2 py-1.5 text-
|
|
150
|
+
className={cn("px-2 py-1.5 text-meta font-semibold text-muted-foreground", className)}
|
|
151
151
|
{...props}
|
|
152
152
|
/>
|
|
153
153
|
);
|
|
@@ -172,7 +172,7 @@ export function DropdownMenuShortcut({
|
|
|
172
172
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
|
173
173
|
return (
|
|
174
174
|
<span
|
|
175
|
-
className={cn("ms-auto text-
|
|
175
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
176
176
|
{...props}
|
|
177
177
|
/>
|
|
178
178
|
);
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
1
4
|
import { describe, expect, it } from "vitest";
|
|
2
5
|
import { render, screen } from "@testing-library/react";
|
|
3
6
|
import { EmptyState } from "./empty-state";
|
|
4
7
|
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const source = readFileSync(join(__dirname, "empty-state.tsx"), "utf8");
|
|
10
|
+
|
|
5
11
|
describe("EmptyState", () => {
|
|
6
12
|
it("renders title and description", () => {
|
|
7
13
|
render(<EmptyState title="Empty" description="Nothing here" />);
|
|
8
14
|
expect(screen.getByText("Empty")).toBeInTheDocument();
|
|
9
15
|
expect(screen.getByText("Nothing here")).toBeInTheDocument();
|
|
10
16
|
});
|
|
17
|
+
|
|
18
|
+
it("clamps a passed icon to the fixed 40×40 slot, matching its documented behavior (#47)", () => {
|
|
19
|
+
// EmptyState only ever forwards `icon` to StatePanel, whose fixed-size
|
|
20
|
+
// icon slot clamps it via `[&_svg]:size-10` — it never forwards
|
|
21
|
+
// `illustration`, whose slot is unclamped. Lock the REAL behavior so it
|
|
22
|
+
// can't silently drift further from whatever the JSDoc claims.
|
|
23
|
+
render(<EmptyState title="X" icon={<svg data-testid="passed-icon" />} />);
|
|
24
|
+
const svg = screen.getByTestId("passed-icon");
|
|
25
|
+
expect(svg.parentElement?.className ?? "").toContain("size-10");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("JSDoc claims only what `icon` actually does — does not promise an unclamped illustration slot (#47)", () => {
|
|
29
|
+
// #47 finding C: the JSDoc said "icon/illustration" but the component
|
|
30
|
+
// only ever forwards `icon` (still clamped to 40x40). A reader passing a
|
|
31
|
+
// larger illustration through `icon` expecting it to render at full size
|
|
32
|
+
// would be surprised by the clamp. The doc must describe the clamp, not
|
|
33
|
+
// the capability the deprecated wrapper doesn't have.
|
|
34
|
+
expect(source).not.toMatch(/icon\/illustration/i);
|
|
35
|
+
expect(source).toMatch(/clamp/i);
|
|
36
|
+
});
|
|
11
37
|
});
|
|
@@ -8,7 +8,12 @@ import { StatePanel } from "../state-panel/state-panel";
|
|
|
8
8
|
export interface EmptyStateProps {
|
|
9
9
|
title: ReactNode;
|
|
10
10
|
description?: ReactNode;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Optional icon shown above the title, clamped to a fixed 40×40 (this
|
|
13
|
+
* wrapper only ever forwards `icon` to `StatePanel`, never `illustration`
|
|
14
|
+
* — for a larger, unclamped illustration use
|
|
15
|
+
* `StatePanel kind="empty" illustration={...}` directly).
|
|
16
|
+
*/
|
|
12
17
|
icon?: ReactNode;
|
|
13
18
|
/** Primary/secondary actions. */
|
|
14
19
|
actions?: ReactNode;
|
|
@@ -111,8 +111,7 @@ export const expandDialogPanesVariants = cva("grid min-h-0 gap-3 overflow-hidden
|
|
|
111
111
|
* and a pane holding only text has no focusable child to scroll from. Pass
|
|
112
112
|
* `tabIndex={-1}` when the pane always contains its own focusable content.
|
|
113
113
|
*/
|
|
114
|
-
const paneClass =
|
|
115
|
-
"min-h-0 min-w-0 overflow-auto overscroll-contain focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
114
|
+
const paneClass = "min-h-0 min-w-0 overflow-auto overscroll-contain focus-ring";
|
|
116
115
|
|
|
117
116
|
export interface ExpandDialogPanesProps
|
|
118
117
|
extends
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createContext, use } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared, lifted state for the `Field*` compound anatomy (`FieldRoot` +
|
|
5
|
+
* `FieldLabel`/`FieldControl`/`FieldDescription`/`FieldError`). `FieldRoot`
|
|
6
|
+
* is the only place this state is produced; every part reads the INTERFACE
|
|
7
|
+
* below rather than receiving positional props, so parts can be composed in
|
|
8
|
+
* any order/layout (compound-component + lifted-state convention — see
|
|
9
|
+
* `.claude/rules/component-api.md` "Composition patterns").
|
|
10
|
+
*/
|
|
11
|
+
export interface FieldContextValue {
|
|
12
|
+
/**
|
|
13
|
+
* The id `FieldLabel`'s `htmlFor` points to — the FIRST `FieldControl`
|
|
14
|
+
* mounted under this `FieldRoot`, by JSX order (self-registered; see
|
|
15
|
+
* `registerControl`). `undefined` until a control has registered.
|
|
16
|
+
*/
|
|
17
|
+
labelFor: string | undefined;
|
|
18
|
+
/** Registers a `FieldControl`'s resolved id (called while it is mounted). */
|
|
19
|
+
registerControl: (id: string) => void;
|
|
20
|
+
/** Unregisters a `FieldControl`'s id (called on unmount / when its id changes). */
|
|
21
|
+
unregisterControl: (id: string) => void;
|
|
22
|
+
/** `FieldRoot`'s `invalid` prop — drives `aria-invalid` on every `FieldControl` and error styling on `FieldLabel`. */
|
|
23
|
+
invalid: boolean;
|
|
24
|
+
/** `FieldRoot`'s `required` prop — drives `aria-required` on every `FieldControl`. */
|
|
25
|
+
required: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The composed `aria-describedby` value from every mounted `FieldDescription`/
|
|
28
|
+
* `FieldError` instance WITH content, descriptions first (in mount order),
|
|
29
|
+
* then errors (in mount order) — a fixed semantic order regardless of DOM
|
|
30
|
+
* position, so more than one of either part (or reordering them relative to
|
|
31
|
+
* `FieldControl`) never scrambles the announced order. `undefined` when
|
|
32
|
+
* nothing is registered.
|
|
33
|
+
*/
|
|
34
|
+
describedBy: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Registers one `FieldDescription` INSTANCE's own id (called while it has
|
|
37
|
+
* content). Each instance generates and registers its own id — this is not
|
|
38
|
+
* a single shared slot, so more than one `FieldDescription` under one
|
|
39
|
+
* `FieldRoot` is supported without id collisions.
|
|
40
|
+
*/
|
|
41
|
+
registerDescription: (id: string) => void;
|
|
42
|
+
/** Unregisters one `FieldDescription` instance's id (on unmount / when content is cleared). Removes only that instance's id. */
|
|
43
|
+
unregisterDescription: (id: string) => void;
|
|
44
|
+
/** Registers one `FieldError` instance's own id (called while it has content). Same per-instance contract as `registerDescription`. */
|
|
45
|
+
registerError: (id: string) => void;
|
|
46
|
+
/** Unregisters one `FieldError` instance's id (on unmount / when content is cleared). Removes only that instance's id. */
|
|
47
|
+
unregisterError: (id: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const FieldContext = createContext<FieldContextValue | null>(null);
|
|
51
|
+
|
|
52
|
+
/** Reads the shared `Field*` context; throws with the calling component's name when used outside a `FieldRoot`. */
|
|
53
|
+
export function useFieldContext(component: string): FieldContextValue {
|
|
54
|
+
const ctx = use(FieldContext);
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
throw new Error(`<${component}> must be rendered inside a <FieldRoot>.`);
|
|
57
|
+
}
|
|
58
|
+
return ctx;
|
|
59
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Input } from "../input";
|
|
4
|
+
import { FieldRoot, FieldLabel, FieldControl, FieldDescription, FieldError } from "./field";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/Field",
|
|
8
|
+
component: FieldRoot,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"A compound field anatomy — `FieldRoot` + `FieldLabel`/`FieldControl`/`FieldDescription`/`FieldError` — for callers who need to COMPOSE a field layout `FieldRow` (Forms/FieldRow) can't express: more than one control in one row, or a description placed before the control. Every part reads shared context (`FieldRoot` owns id generation and `aria-describedby` composition), so they can be arranged in any order/layout. `FieldRow` remains the convenience wrapper for the common single-control case and is unaffected (#43).",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof FieldRoot>;
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="w-72">
|
|
25
|
+
<FieldRoot>
|
|
26
|
+
<FieldLabel>Name</FieldLabel>
|
|
27
|
+
<FieldControl>
|
|
28
|
+
<Input placeholder="Jane Doe" />
|
|
29
|
+
</FieldControl>
|
|
30
|
+
</FieldRoot>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
play: async ({ canvas }) => {
|
|
34
|
+
const input = canvas.getByRole("textbox");
|
|
35
|
+
expect(input).not.toHaveAttribute("aria-describedby");
|
|
36
|
+
expect(input).toHaveAttribute("aria-invalid", "false");
|
|
37
|
+
expect(canvas.getByText("Name")).toHaveAttribute("for", input.id);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithDescriptionAndError: Story = {
|
|
42
|
+
name: "With description and error",
|
|
43
|
+
render: () => (
|
|
44
|
+
<div className="w-72">
|
|
45
|
+
<FieldRoot invalid required>
|
|
46
|
+
<FieldLabel>API key</FieldLabel>
|
|
47
|
+
<FieldControl>
|
|
48
|
+
<Input placeholder="sk-…" />
|
|
49
|
+
</FieldControl>
|
|
50
|
+
<FieldDescription>Found in your account settings.</FieldDescription>
|
|
51
|
+
<FieldError>Enter a valid API key.</FieldError>
|
|
52
|
+
</FieldRoot>
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
play: async ({ canvas }) => {
|
|
56
|
+
const input = canvas.getByRole("textbox");
|
|
57
|
+
expect(input).toHaveAttribute("aria-invalid", "true");
|
|
58
|
+
expect(input).toHaveAttribute("aria-required", "true");
|
|
59
|
+
const describedBy = input.getAttribute("aria-describedby")?.split(" ") ?? [];
|
|
60
|
+
expect(describedBy).toHaveLength(2);
|
|
61
|
+
expect(canvas.getByText("Found in your account settings.").id).toBe(describedBy[0]);
|
|
62
|
+
const alert = canvas.getByRole("alert");
|
|
63
|
+
expect(alert).toHaveTextContent("Enter a valid API key.");
|
|
64
|
+
expect(alert.id).toBe(describedBy[1]);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* #43's first motivating case: `FieldRow`'s `children: ReactElement` can only
|
|
70
|
+
* ever wrap ONE control, so a first/last-name row (or a control plus an
|
|
71
|
+
* inline unit suffix) can't be expressed. Here two `FieldControl`s share one
|
|
72
|
+
* `FieldRoot` — each gets its own independent `id` (given explicitly, the
|
|
73
|
+
* same way any two form controls would need distinct ids) while both read
|
|
74
|
+
* the SAME `invalid`/`aria-describedby` wiring from context. `FieldLabel`'s
|
|
75
|
+
* `htmlFor` can only point to ONE control (the first), so each `Input` also
|
|
76
|
+
* carries its own `aria-label` — a shared visual label plus a placeholder is
|
|
77
|
+
* not enough of an accessible name for the second control (axe:
|
|
78
|
+
* `label-title-only`).
|
|
79
|
+
*/
|
|
80
|
+
export const TwoControlsInOneRow: Story = {
|
|
81
|
+
name: "Two controls in one row",
|
|
82
|
+
render: () => (
|
|
83
|
+
<div className="w-96">
|
|
84
|
+
<FieldRoot invalid>
|
|
85
|
+
<FieldLabel>Name</FieldLabel>
|
|
86
|
+
<div className="flex gap-2">
|
|
87
|
+
<FieldControl>
|
|
88
|
+
<Input id="first-name" aria-label="First name" placeholder="First name" />
|
|
89
|
+
</FieldControl>
|
|
90
|
+
<FieldControl>
|
|
91
|
+
<Input id="last-name" aria-label="Last name" placeholder="Last name" />
|
|
92
|
+
</FieldControl>
|
|
93
|
+
</div>
|
|
94
|
+
<FieldError>Both first and last name are required.</FieldError>
|
|
95
|
+
</FieldRoot>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
play: async ({ canvas }) => {
|
|
99
|
+
const first = canvas.getByPlaceholderText("First name");
|
|
100
|
+
const last = canvas.getByPlaceholderText("Last name");
|
|
101
|
+
expect(first.id).toBe("first-name");
|
|
102
|
+
expect(last.id).toBe("last-name");
|
|
103
|
+
expect(first.id).not.toBe(last.id);
|
|
104
|
+
|
|
105
|
+
const alert = canvas.getByRole("alert");
|
|
106
|
+
expect(first).toHaveAttribute("aria-invalid", "true");
|
|
107
|
+
expect(last).toHaveAttribute("aria-invalid", "true");
|
|
108
|
+
expect(first.getAttribute("aria-describedby")).toBe(alert.id);
|
|
109
|
+
expect(last.getAttribute("aria-describedby")).toBe(alert.id);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* #43's second motivating case: `FieldRow` always renders description AFTER
|
|
115
|
+
* the control. Here `FieldDescription` is placed BEFORE `FieldControl` in
|
|
116
|
+
* JSX — the visible order follows the JSX order, but `aria-describedby`
|
|
117
|
+
* wiring is unaffected either way (`FieldRoot` composes it from context, not
|
|
118
|
+
* DOM position).
|
|
119
|
+
*/
|
|
120
|
+
export const DescriptionBeforeControl: Story = {
|
|
121
|
+
name: "Description between label and control",
|
|
122
|
+
render: () => (
|
|
123
|
+
<div className="w-72">
|
|
124
|
+
<FieldRoot>
|
|
125
|
+
<FieldLabel>Bio</FieldLabel>
|
|
126
|
+
<FieldDescription>Shown on your public profile.</FieldDescription>
|
|
127
|
+
<FieldControl>
|
|
128
|
+
<Input placeholder="Tell us about yourself…" />
|
|
129
|
+
</FieldControl>
|
|
130
|
+
</FieldRoot>
|
|
131
|
+
</div>
|
|
132
|
+
),
|
|
133
|
+
play: async ({ canvas }) => {
|
|
134
|
+
const input = canvas.getByRole("textbox");
|
|
135
|
+
const description = canvas.getByText("Shown on your public profile.");
|
|
136
|
+
expect(input.getAttribute("aria-describedby")).toBe(description.id);
|
|
137
|
+
// The description renders before the control in the DOM, matching JSX order.
|
|
138
|
+
expect(
|
|
139
|
+
description.compareDocumentPosition(input) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
140
|
+
).toBeTruthy();
|
|
141
|
+
},
|
|
142
|
+
};
|