@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,34 @@
|
|
|
1
|
+
import { forwardRef, type ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface SkipLinkProps extends ComponentProps<"a"> {
|
|
5
|
+
/** The id of the landmark to jump to. Defaults to "main-content". */
|
|
6
|
+
targetId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The first focusable element of an application: invisible until focused, then a
|
|
11
|
+
* token-styled pill pinned to the top-start corner. Give the target element
|
|
12
|
+
* `id={targetId}` and `tabIndex={-1}` so focus actually lands there.
|
|
13
|
+
*/
|
|
14
|
+
export const SkipLink = forwardRef<HTMLAnchorElement, SkipLinkProps>(function SkipLink(
|
|
15
|
+
{ targetId = "main-content", className, children, ...props },
|
|
16
|
+
ref,
|
|
17
|
+
) {
|
|
18
|
+
return (
|
|
19
|
+
<a
|
|
20
|
+
ref={ref}
|
|
21
|
+
data-slot="skip-link"
|
|
22
|
+
href={`#${targetId}`}
|
|
23
|
+
className={cn(
|
|
24
|
+
"sr-only focus-visible:not-sr-only focus-visible:absolute focus-visible:start-4 focus-visible:top-4 focus-visible:z-50",
|
|
25
|
+
"focus-visible:rounded-md focus-visible:bg-card focus-visible:px-3 focus-visible:py-2 focus-visible:text-body focus-visible:text-foreground focus-visible:shadow-ring-md",
|
|
26
|
+
"focus-ring",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{children ?? "Skip to main content"}
|
|
32
|
+
</a>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -124,7 +124,7 @@ export const Slider = forwardRef<ElementRef<typeof SliderPrimitive.Root>, Slider
|
|
|
124
124
|
aria-valuetext={ariaValueText}
|
|
125
125
|
{...safeThumbProps}
|
|
126
126
|
className={cn(
|
|
127
|
-
"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-
|
|
127
|
+
"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50",
|
|
128
128
|
safeThumbProps?.className,
|
|
129
129
|
)}
|
|
130
130
|
/>
|
|
@@ -109,7 +109,10 @@ export const DragThenReset: Story = {
|
|
|
109
109
|
thumb.focus();
|
|
110
110
|
await userEvent.keyboard("{ArrowRight}{ArrowRight}{ArrowRight}");
|
|
111
111
|
const numberInput = canvas.getByRole("spinbutton", { name: "Temperature" });
|
|
112
|
-
|
|
112
|
+
// Text spinbutton (locale-formatted): string display + numeric aria-valuenow.
|
|
113
|
+
await expect(numberInput).toHaveValue("0.3");
|
|
114
|
+
await expect(numberInput).toHaveAttribute("aria-valuenow", "0.3");
|
|
115
|
+
await expect(thumb).toHaveAttribute("aria-valuenow", "0.3");
|
|
113
116
|
|
|
114
117
|
const resetBtn = canvas.getByRole("button", { name: "Reset" });
|
|
115
118
|
await expect(resetBtn).not.toBeDisabled();
|
|
@@ -10,7 +10,7 @@ describe("SliderNumber", () => {
|
|
|
10
10
|
"aria-valuenow",
|
|
11
11
|
"0.5",
|
|
12
12
|
);
|
|
13
|
-
expect(screen.getByRole("spinbutton", { name: "Temperature" })).toHaveValue(0.5);
|
|
13
|
+
expect(screen.getByRole("spinbutton", { name: "Temperature" })).toHaveValue("0.5");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("rounds slider-driven and typed values identically (lockstep)", async () => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { Spinner } from "./spinner";
|
|
5
|
+
|
|
6
|
+
describe("Spinner", () => {
|
|
7
|
+
it("renders with its default accessible label", () => {
|
|
8
|
+
render(<Spinner />);
|
|
9
|
+
expect(screen.getByRole("status", { name: "Loading" })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("forwards its ref to the rendered svg", () => {
|
|
13
|
+
const ref = createRef<SVGSVGElement>();
|
|
14
|
+
render(<Spinner ref={ref} />);
|
|
15
|
+
expect(ref.current).toBeInstanceOf(SVGSVGElement);
|
|
16
|
+
expect(ref.current).toBe(screen.getByRole("status", { name: "Loading" }));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -1,19 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
1
4
|
import { Loader2 } from "lucide-react";
|
|
2
5
|
import { cn } from "../../lib/cn";
|
|
6
|
+
import { useLocale } from "../locale-provider";
|
|
3
7
|
|
|
4
8
|
export interface SpinnerProps extends React.SVGProps<SVGSVGElement> {
|
|
5
|
-
/** Accessible label; defaults to "Loading". */
|
|
9
|
+
/** Accessible label; defaults to the localized "Loading…" microcopy. */
|
|
6
10
|
label?: string;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
/** Indeterminate spinner with an accessible label. */
|
|
10
|
-
export
|
|
14
|
+
export const Spinner = forwardRef<SVGSVGElement, SpinnerProps>(function Spinner(
|
|
15
|
+
{ className, label, ...props },
|
|
16
|
+
ref,
|
|
17
|
+
) {
|
|
18
|
+
const { t } = useLocale();
|
|
11
19
|
return (
|
|
12
20
|
<Loader2
|
|
21
|
+
ref={ref}
|
|
22
|
+
data-slot="spinner"
|
|
13
23
|
role="status"
|
|
14
|
-
aria-label={label}
|
|
24
|
+
aria-label={label ?? t("ui.spinner.label")}
|
|
15
25
|
className={cn("size-4 animate-spin text-muted-foreground", className)}
|
|
16
26
|
{...props}
|
|
17
27
|
/>
|
|
18
28
|
);
|
|
19
|
-
}
|
|
29
|
+
});
|
|
@@ -4,7 +4,23 @@ import { SplitPanel } from "./split-panel";
|
|
|
4
4
|
const meta = {
|
|
5
5
|
title: "Layout/SplitPanel",
|
|
6
6
|
component: SplitPanel,
|
|
7
|
-
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "fullscreen",
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"A fixed two-pane layout with per-pane surface tones — panes the user can drag to " +
|
|
14
|
+
"resize are `Layout/Resizable`, see " +
|
|
15
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
16
|
+
'`SplitPanel` sizes the start pane with a CSS grid track (`startSize`, e.g. `"320px"`, ' +
|
|
17
|
+
'`"40%"`, `"1fr"`) rather than a percentage, so layout is predictable in tests and SSR ' +
|
|
18
|
+
"with no client-side measurement. The three-tone `plain`/`muted`/`card` ground-offset " +
|
|
19
|
+
"system on `startTone`/`endTone` is exported as `splitPaneVariants` — apply it to a " +
|
|
20
|
+
"`ResizablePanel` for the identical tiering on a draggable layout.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
8
24
|
} satisfies Meta<typeof SplitPanel>;
|
|
9
25
|
export default meta;
|
|
10
26
|
type Story = StoryObj<typeof meta>;
|
|
@@ -28,7 +44,7 @@ export const MasterDetail: Story = {
|
|
|
28
44
|
* the page itself is recessed below white; see research/structural-design 08 §H). A
|
|
29
45
|
* `muted` list well is a light-theme-only enhancement (in dark, `--surface-muted` reads
|
|
30
46
|
* lighter than `--card`), so this demo keeps the list `plain` to read correctly across
|
|
31
|
-
*
|
|
47
|
+
* every theme.
|
|
32
48
|
*/
|
|
33
49
|
export const Tiered: Story = {
|
|
34
50
|
render: () => (
|
|
@@ -19,6 +19,26 @@ describe("SplitPanel", () => {
|
|
|
19
19
|
expect(endPane.className).toMatch(/border-s/);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
// Issue #163 — WCAG 1.4.11: with both tones "plain" (the shipped default),
|
|
23
|
+
// the hairline is the ONLY cue between the two panes (no fill/elevation
|
|
24
|
+
// difference), so it must be the strong, ≥3:1 rung — `border-strong ≥ 3:1
|
|
25
|
+
// on --card/--background` is proven for every theme in
|
|
26
|
+
// `@elabs-ai/components-tokens`'s `themes-contrast.test.ts`; this test locks
|
|
27
|
+
// that `SplitPanel` actually reaches for that token in this configuration,
|
|
28
|
+
// not just the bare `border-s`/`border-t` class the previous assertion
|
|
29
|
+
// above already passed on the unfixed code.
|
|
30
|
+
it("plain/plain divider uses the strong (≥3:1) border rung, not the subtle default", () => {
|
|
31
|
+
const { getByText } = render(<SplitPanel start={<>List</>} end={<>Detail</>} />);
|
|
32
|
+
expect(getByText("Detail").className).toMatch(/border-border-strong/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("muted/card tones keep the subtle divider rung — the fill difference already carries the boundary", () => {
|
|
36
|
+
const { getByText } = render(
|
|
37
|
+
<SplitPanel startTone="muted" endTone="card" start={<>List</>} end={<>Detail</>} />,
|
|
38
|
+
);
|
|
39
|
+
expect(getByText("Detail").className).not.toMatch(/border-border-strong/);
|
|
40
|
+
});
|
|
41
|
+
|
|
22
42
|
it("applies the ground-offset tones: muted well + raised card", () => {
|
|
23
43
|
const { getByText } = render(
|
|
24
44
|
<SplitPanel
|
|
@@ -5,15 +5,20 @@ import { cn } from "../../lib/cn";
|
|
|
5
5
|
/**
|
|
6
6
|
* Per-pane surface tone — the ground-offset tiering axis (research 08 §H, #193).
|
|
7
7
|
* `plain` keeps the ambient ground (default; non-breaking). `card` RAISES the pane as
|
|
8
|
-
* a white `bg-card` + `shadow-sm` and is robust across
|
|
8
|
+
* a white `bg-card` + `shadow-sm` and is robust across every theme — a card is
|
|
9
9
|
* lighter than the page ground in BOTH light and dark, so it always reads as raised.
|
|
10
10
|
* `muted` (`bg-surface-muted`) reads as a recessed well ONLY in light themes; in dark
|
|
11
11
|
* themes `--surface-muted` is lighter than `--card`, so a `muted` pane reads as a faint
|
|
12
12
|
* raised tint, not a recess (dark-mode layering only goes UP — there is no surface below
|
|
13
13
|
* the page ground). For a guaranteed cross-theme raise/recess contrast, raise the focus
|
|
14
14
|
* pane with `card` and leave the other `plain`.
|
|
15
|
+
*
|
|
16
|
+
* Exported so the same tone system reaches a `Layout/Resizable` pane — a draggable
|
|
17
|
+
* `ResizablePanel` composes `splitPaneVariants({ tone })` for the identical ground-offset
|
|
18
|
+
* tiering `SplitPanel` gives its `startTone`/`endTone`, instead of a hand-rolled second
|
|
19
|
+
* copy of these three classes. See `Layout/Resizable`'s `Tiered` story.
|
|
15
20
|
*/
|
|
16
|
-
const
|
|
21
|
+
export const splitPaneVariants = cva("min-h-0 min-w-0 overflow-auto", {
|
|
17
22
|
variants: {
|
|
18
23
|
tone: {
|
|
19
24
|
plain: "",
|
|
@@ -24,7 +29,7 @@ const paneToneVariants = cva("min-h-0 min-w-0 overflow-auto", {
|
|
|
24
29
|
defaultVariants: { tone: "plain" },
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
export type SplitPanelTone = NonNullable<VariantProps<typeof
|
|
32
|
+
export type SplitPanelTone = NonNullable<VariantProps<typeof splitPaneVariants>["tone"]>;
|
|
28
33
|
|
|
29
34
|
export interface SplitPanelProps {
|
|
30
35
|
start: ReactNode;
|
|
@@ -49,8 +54,12 @@ export interface SplitPanelProps {
|
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
56
|
* Two-pane layout for master/detail, editor/preview and inspector workflows.
|
|
52
|
-
* Static sizing by design (predictable in tests/SSR)
|
|
53
|
-
*
|
|
57
|
+
* Static sizing by design (predictable in tests/SSR) — `startSize` is a CSS grid
|
|
58
|
+
* track (`"320px"`, `"40%"`, `"1fr"`), not a percentage. For panes the user can
|
|
59
|
+
* drag to resize, reach for `Layout/Resizable`'s `ResizablePanelGroup`/
|
|
60
|
+
* `ResizablePanel`/`ResizableHandle` instead and apply `splitPaneVariants({ tone })`
|
|
61
|
+
* to a panel for the same ground-offset tiering. See
|
|
62
|
+
* [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).
|
|
54
63
|
*
|
|
55
64
|
* For a master/detail surface where the detail should read as a raised card on a
|
|
56
65
|
* recessed list, use `startTone="muted" endTone="card"` (ground-offset tiering).
|
|
@@ -80,11 +89,22 @@ export function SplitPanel({
|
|
|
80
89
|
)}
|
|
81
90
|
style={style}
|
|
82
91
|
>
|
|
83
|
-
<div className={cn(
|
|
92
|
+
<div className={cn(splitPaneVariants({ tone: startTone }), startClassName)}>{start}</div>
|
|
84
93
|
<div
|
|
85
94
|
className={cn(
|
|
86
|
-
|
|
87
|
-
divider &&
|
|
95
|
+
splitPaneVariants({ tone: endTone }),
|
|
96
|
+
divider &&
|
|
97
|
+
(isHorizontal ? "border-s" : "border-t") +
|
|
98
|
+
// #163 — with both panes "plain" there is no fill/elevation
|
|
99
|
+
// difference between them, so the hairline is the ONLY cue and
|
|
100
|
+
// must clear the 3:1 non-text bar (WCAG 1.4.11):
|
|
101
|
+
// `border-border-strong` (`border-strong ≥ 3:1 on --card/
|
|
102
|
+
// --background` is proven for every theme in
|
|
103
|
+
// @elabs-ai/components-tokens's themes-contrast.test.ts).
|
|
104
|
+
// With either tone "muted"/"card" the fill change already
|
|
105
|
+
// carries the boundary, so the subtle default rung stays —
|
|
106
|
+
// the redundant-cue exemption in styling-and-tokens.md.
|
|
107
|
+
(startTone === "plain" && endTone === "plain" ? " border-border-strong" : ""),
|
|
88
108
|
endClassName,
|
|
89
109
|
)}
|
|
90
110
|
>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { Button } from "../button";
|
|
3
3
|
import { StatePanel } from "./state-panel";
|
|
4
|
+
import {
|
|
5
|
+
EmptyListIllustration,
|
|
6
|
+
NoAccessIllustration,
|
|
7
|
+
ErrorIllustration,
|
|
8
|
+
} from "../../illustrations";
|
|
4
9
|
|
|
5
10
|
const meta = {
|
|
6
11
|
title: "States/StatePanel",
|
|
@@ -78,3 +83,50 @@ export const EmptyNoIcon: Story = {
|
|
|
78
83
|
description: "Items will appear here once available.",
|
|
79
84
|
},
|
|
80
85
|
};
|
|
86
|
+
|
|
87
|
+
export const WithIllustration: Story = {
|
|
88
|
+
name: "Empty (with illustration, #24)",
|
|
89
|
+
args: {
|
|
90
|
+
kind: "empty",
|
|
91
|
+
title: "No projects yet",
|
|
92
|
+
description: "Create your first project to get started.",
|
|
93
|
+
illustration: <EmptyListIllustration />,
|
|
94
|
+
actions: <Button size="sm">New project</Button>,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const NoAccess: Story = {
|
|
99
|
+
name: "Empty (no-access illustration)",
|
|
100
|
+
args: {
|
|
101
|
+
// "No access" is a blocked/informational state, not a system failure —
|
|
102
|
+
// kind stays "empty" (no alarm-red "Error" eyebrow) rather than "error".
|
|
103
|
+
kind: "empty",
|
|
104
|
+
title: "You don’t have access",
|
|
105
|
+
description: "Ask a workspace admin to grant you permission.",
|
|
106
|
+
illustration: <NoAccessIllustration />,
|
|
107
|
+
actions: (
|
|
108
|
+
<Button size="sm" variant="outline">
|
|
109
|
+
Request access
|
|
110
|
+
</Button>
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const ErrorWithIllustration: Story = {
|
|
116
|
+
name: "Error (with illustration, #24 P0-2)",
|
|
117
|
+
args: {
|
|
118
|
+
// The one panel `kind` that re-tints the illustration slot (`text-destructive`)
|
|
119
|
+
// — pairing it with an illustration is what surfaces whether the accent
|
|
120
|
+
// follows the tint or clashes against it. See P0-2 in the fix-round review:
|
|
121
|
+
// no shipped story previously rendered an illustration inside `kind="error"`.
|
|
122
|
+
kind: "error",
|
|
123
|
+
title: "Failed to load data",
|
|
124
|
+
description: "The server returned an unexpected response.",
|
|
125
|
+
illustration: <ErrorIllustration />,
|
|
126
|
+
actions: (
|
|
127
|
+
<Button size="sm" variant="outline">
|
|
128
|
+
Try again
|
|
129
|
+
</Button>
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import { StatePanel } from "./state-panel";
|
|
4
|
+
import {
|
|
5
|
+
EmptyListIllustration,
|
|
6
|
+
ErrorIllustration,
|
|
7
|
+
ILLUSTRATION_ACCENT_VAR,
|
|
8
|
+
} from "../../illustrations";
|
|
4
9
|
|
|
5
10
|
describe("StatePanel", () => {
|
|
6
11
|
it("renders empty kind with title and description", () => {
|
|
@@ -24,6 +29,48 @@ describe("StatePanel", () => {
|
|
|
24
29
|
expect(alert).toHaveAttribute("data-kind", "error");
|
|
25
30
|
});
|
|
26
31
|
|
|
32
|
+
it("error eyebrow uses the destructive INK rung, not the fill rung (#40)", () => {
|
|
33
|
+
// The eyebrow is running TEXT on the panel's own `bg-destructive/5` wash, so
|
|
34
|
+
// it must reach for `text-destructive-text` (>= 4.5:1, the text-rung
|
|
35
|
+
// contract) — never the bare `text-destructive` fill rung, whose contract
|
|
36
|
+
// is only the 3:1 mark bar. See styling-and-tokens.md "Which status rung a
|
|
37
|
+
// graphical MARK reaches for" (#381).
|
|
38
|
+
render(<StatePanel kind="error" />);
|
|
39
|
+
const eyebrow = screen.getByText("Error");
|
|
40
|
+
expect(eyebrow.className).toContain("text-destructive-text");
|
|
41
|
+
expect(eyebrow.className).not.toMatch(/(?<!-)\btext-destructive\b(?!-)/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("error icon keeps the destructive FILL rung (a mark, not text)", () => {
|
|
45
|
+
// Inverse of the eyebrow lock above: an icon IS a mark, so it correctly
|
|
46
|
+
// stays on the fill rung — the fix must not "tidy" it onto -text too.
|
|
47
|
+
const { container } = render(<StatePanel kind="error" />);
|
|
48
|
+
const iconWrapper = container.querySelector("svg")?.parentElement;
|
|
49
|
+
expect(iconWrapper?.className ?? "").toMatch(/\btext-destructive\b/);
|
|
50
|
+
expect(iconWrapper?.className ?? "").not.toContain("text-destructive-text");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("error rail carries the width cue (border-s-4) AND a destructive-family colour, not the neutral border-strong grey (#71)", () => {
|
|
54
|
+
// The rail's WIDTH (4px vs 1px) is the monochrome-survivable structural
|
|
55
|
+
// cue (per styling-and-tokens.md's border/border-strong decision test);
|
|
56
|
+
// its HUE should match the rest of the destructive family, not fall back
|
|
57
|
+
// to the neutral `border-strong` rung — that reads as a CSS-specificity
|
|
58
|
+
// bug, a 4px grey rail beside three red hairlines, not a deliberate
|
|
59
|
+
// accent (#71).
|
|
60
|
+
const { container } = render(<StatePanel kind="error" />);
|
|
61
|
+
const root = container.firstChild as HTMLElement;
|
|
62
|
+
expect(root.className).toMatch(/\bborder-s-4\b/);
|
|
63
|
+
expect(root.className).toMatch(/\bborder-s-destructive\b/);
|
|
64
|
+
expect(root.className).not.toMatch(/\bborder-s-border-strong\b/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("empty kind's rail carries neither the error width nor its destructive colour", () => {
|
|
68
|
+
const { container } = render(<StatePanel kind="empty" />);
|
|
69
|
+
const root = container.firstChild as HTMLElement;
|
|
70
|
+
expect(root.className).not.toMatch(/\bborder-s-4\b/);
|
|
71
|
+
expect(root.className).not.toMatch(/\bborder-s-destructive\b/);
|
|
72
|
+
});
|
|
73
|
+
|
|
27
74
|
it("renders error kind with custom title", () => {
|
|
28
75
|
render(<StatePanel kind="error" title="Custom error" description="Custom description" />);
|
|
29
76
|
expect(screen.getByRole("alert")).toBeInTheDocument();
|
|
@@ -61,4 +108,82 @@ describe("StatePanel", () => {
|
|
|
61
108
|
);
|
|
62
109
|
expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
|
|
63
110
|
});
|
|
111
|
+
|
|
112
|
+
it("renders an illustration without the size-10 icon clamp, decoratively (#24)", () => {
|
|
113
|
+
const { container } = render(
|
|
114
|
+
<StatePanel kind="empty" title="No items" illustration={<EmptyListIllustration />} />,
|
|
115
|
+
);
|
|
116
|
+
const svg = container.querySelector("svg");
|
|
117
|
+
expect(svg).not.toBeNull();
|
|
118
|
+
expect(svg).toHaveAttribute("aria-hidden", "true");
|
|
119
|
+
// The illustration's own wrapper must NOT carry the `[&_svg]:size-10`
|
|
120
|
+
// clamp the default icon slot uses — its rem sizing governs instead.
|
|
121
|
+
const wrapper = svg?.parentElement;
|
|
122
|
+
expect(wrapper?.className ?? "").not.toContain("size-10");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("illustration path renders the title at the subtitle role, distinct from the icon path's text-body (#47)", () => {
|
|
126
|
+
// A 112px+ illustration needs a heavier title to group with — bumping the
|
|
127
|
+
// title to `text-subtitle` (styling-and-tokens.md's typography-scale
|
|
128
|
+
// roles) only on the illustration path, so the plain `icon` path (a
|
|
129
|
+
// fixed 40x40 glyph) stays byte-identical to before.
|
|
130
|
+
const { container: withIllustration } = render(
|
|
131
|
+
<StatePanel kind="empty" title="No items" illustration={<EmptyListIllustration />} />,
|
|
132
|
+
);
|
|
133
|
+
const titleWithIllustration = withIllustration.querySelector("h3");
|
|
134
|
+
expect(titleWithIllustration?.className ?? "").toContain("text-subtitle");
|
|
135
|
+
expect(titleWithIllustration?.className ?? "").not.toMatch(/\btext-body\b/);
|
|
136
|
+
|
|
137
|
+
const { container: withIcon } = render(
|
|
138
|
+
<StatePanel kind="empty" title="No items" icon={<span data-testid="i" />} />,
|
|
139
|
+
);
|
|
140
|
+
const titleWithIcon = withIcon.querySelector("h3");
|
|
141
|
+
expect(titleWithIcon?.className ?? "").toMatch(/\btext-body\b/);
|
|
142
|
+
expect(titleWithIcon?.className ?? "").not.toContain("text-subtitle");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("error-kind illustration retints the accent to the TEXT rung, distinct from the retinted subject's FILL rung (#48)", () => {
|
|
146
|
+
// StatePanel wraps the illustration slot in `text-destructive` (the FILL
|
|
147
|
+
// rung — a mark, per styling-and-tokens.md "which status rung a
|
|
148
|
+
// graphical MARK reaches for") for kind="error". If the ambient
|
|
149
|
+
// `--illustration-accent` override it sets ALSO resolved to
|
|
150
|
+
// `--destructive`, the accent would collapse onto the subject the moment
|
|
151
|
+
// both read the same token (#48 finding 2). It must resolve to the TEXT
|
|
152
|
+
// rung instead — matching the convention every other illustration's own
|
|
153
|
+
// default fallback already uses (`--primary-text`, `--success-text`).
|
|
154
|
+
const { container } = render(<StatePanel kind="error" illustration={<ErrorIllustration />} />);
|
|
155
|
+
const svg = container.querySelector("svg");
|
|
156
|
+
const wrapper = svg?.parentElement as HTMLElement;
|
|
157
|
+
expect(wrapper.className).toMatch(/\btext-destructive\b/);
|
|
158
|
+
expect(wrapper.style.getPropertyValue(ILLUSTRATION_ACCENT_VAR)).toBe("var(--destructive-text)");
|
|
159
|
+
// Sanity: ErrorIllustration's own fallback (used when no ambient override
|
|
160
|
+
// is set) also reaches for the TEXT rung, not the FILL token the subject
|
|
161
|
+
// above just read.
|
|
162
|
+
const accentRect = container.querySelector("rect");
|
|
163
|
+
expect(accentRect?.getAttribute("style") ?? "").toContain("--destructive-text");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("prefers illustration over icon when both are given", () => {
|
|
167
|
+
render(
|
|
168
|
+
<StatePanel
|
|
169
|
+
kind="empty"
|
|
170
|
+
title="Both given"
|
|
171
|
+
icon={<span data-testid="custom-icon">icon</span>}
|
|
172
|
+
illustration={<EmptyListIllustration />}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
expect(screen.queryByTestId("custom-icon")).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("titleAs renders the requested heading level (#385)", () => {
|
|
179
|
+
render(<StatePanel kind="empty" title="No data" titleAs="h2" />);
|
|
180
|
+
const heading = screen.getByRole("heading", { level: 2, name: "No data" });
|
|
181
|
+
expect(heading.tagName).toBe("H2");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("default titleAs is still h3 — unchanged output for every existing caller (#385)", () => {
|
|
185
|
+
render(<StatePanel kind="empty" title="No data" />);
|
|
186
|
+
const heading = screen.getByRole("heading", { level: 3, name: "No data" });
|
|
187
|
+
expect(heading.tagName).toBe("H3");
|
|
188
|
+
});
|
|
64
189
|
});
|