@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,6 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import { LocaleProvider } from "../locale-provider";
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbEllipsis,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbList,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
} from "./breadcrumb";
|
|
4
11
|
describe("Breadcrumb", () => {
|
|
5
12
|
it("renders the current page", () => {
|
|
6
13
|
render(
|
|
@@ -15,3 +22,19 @@ describe("Breadcrumb", () => {
|
|
|
15
22
|
expect(screen.getByText("Here")).toHaveAttribute("aria-current", "page");
|
|
16
23
|
});
|
|
17
24
|
});
|
|
25
|
+
|
|
26
|
+
describe("BreadcrumbEllipsis — microcopy (#18)", () => {
|
|
27
|
+
it("keeps its shipped sr-only 'More' text with no LocaleProvider", () => {
|
|
28
|
+
render(<BreadcrumbEllipsis />);
|
|
29
|
+
expect(screen.getByText("More")).toHaveClass("sr-only");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("translates via the reused generic `more` key under a LocaleProvider", () => {
|
|
33
|
+
render(
|
|
34
|
+
<LocaleProvider locale="de-DE" messages={{ more: "Mehr" }}>
|
|
35
|
+
<BreadcrumbEllipsis />
|
|
36
|
+
</LocaleProvider>,
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText("Mehr")).toHaveClass("sr-only");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -2,10 +2,12 @@ import { forwardRef, type ComponentProps, type ReactNode } from "react";
|
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
|
+
import { useLocale } from "../locale-provider";
|
|
5
6
|
|
|
6
7
|
export const Breadcrumb = forwardRef<HTMLElement, ComponentProps<"nav">>(
|
|
7
8
|
function Breadcrumb(props, ref) {
|
|
8
|
-
|
|
9
|
+
const { t } = useLocale();
|
|
10
|
+
return <nav ref={ref} aria-label={t("ui.breadcrumb.label")} {...props} />;
|
|
9
11
|
},
|
|
10
12
|
);
|
|
11
13
|
export const BreadcrumbList = forwardRef<HTMLOListElement, ComponentProps<"ol">>(
|
|
@@ -14,7 +16,7 @@ export const BreadcrumbList = forwardRef<HTMLOListElement, ComponentProps<"ol">>
|
|
|
14
16
|
<ol
|
|
15
17
|
ref={ref}
|
|
16
18
|
className={cn(
|
|
17
|
-
"flex flex-wrap items-center gap-1.5 break-words text-
|
|
19
|
+
"flex flex-wrap items-center gap-1.5 break-words text-body text-muted-foreground",
|
|
18
20
|
className,
|
|
19
21
|
)}
|
|
20
22
|
{...props}
|
|
@@ -37,7 +39,10 @@ export const BreadcrumbLink = forwardRef<
|
|
|
37
39
|
return (
|
|
38
40
|
<Comp
|
|
39
41
|
ref={ref}
|
|
40
|
-
className={cn(
|
|
42
|
+
className={cn(
|
|
43
|
+
"rounded-sm transition-colors duration-fast hover:text-foreground focus-ring",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
41
46
|
{...props}
|
|
42
47
|
/>
|
|
43
48
|
);
|
|
@@ -69,6 +74,7 @@ export function BreadcrumbSeparator({ children, className, ...props }: Component
|
|
|
69
74
|
);
|
|
70
75
|
}
|
|
71
76
|
export function BreadcrumbEllipsis({ className, ...props }: ComponentProps<"span">) {
|
|
77
|
+
const { t } = useLocale();
|
|
72
78
|
return (
|
|
73
79
|
<span
|
|
74
80
|
role="presentation"
|
|
@@ -77,7 +83,7 @@ export function BreadcrumbEllipsis({ className, ...props }: ComponentProps<"span
|
|
|
77
83
|
{...props}
|
|
78
84
|
>
|
|
79
85
|
<MoreHorizontal className="size-4" />
|
|
80
|
-
<span className="sr-only">
|
|
86
|
+
<span className="sr-only">{t("more")}</span>
|
|
81
87
|
</span>
|
|
82
88
|
);
|
|
83
89
|
}
|
|
@@ -41,17 +41,30 @@ export const Ghost: Story = { args: { variant: "ghost" } };
|
|
|
41
41
|
// themes.css/Tailwind failed to load, this would be the transparent default
|
|
42
42
|
// `rgba(0, 0, 0, 0)` and fail — where a plain `toBeVisible()` would still pass
|
|
43
43
|
// on the unstyled element.
|
|
44
|
-
//
|
|
45
|
-
// The harness applies the shipped DEFAULT theme (`light`) to
|
|
46
|
-
// `document.documentElement` (see `apps/docs/.storybook/preview.tsx`'s
|
|
47
|
-
// `withTheme` decorator, #402), so `--primary` here is light's own
|
|
48
|
-
// value (`oklch(0.553 0.143 153)` in themes.css) — not the unbranded `:root`
|
|
49
|
-
// fallback this test asserted against before #402 fixed the harness to
|
|
50
|
-
// actually apply a theme.
|
|
51
44
|
export const CssCheck: Story = {
|
|
52
45
|
play: async ({ canvas }) => {
|
|
53
46
|
const btn = canvas.getByRole("button", { name: "Button" });
|
|
54
|
-
|
|
47
|
+
|
|
48
|
+
// Create a hidden reference element styled with the same token
|
|
49
|
+
// to derive the expected value dynamically, avoiding hardcoded snapshots
|
|
50
|
+
const ref = document.createElement("div");
|
|
51
|
+
ref.className = "bg-primary";
|
|
52
|
+
ref.style.visibility = "hidden";
|
|
53
|
+
ref.style.position = "absolute";
|
|
54
|
+
document.body.appendChild(ref);
|
|
55
|
+
|
|
56
|
+
const buttonBgColor = getComputedStyle(btn).backgroundColor;
|
|
57
|
+
const referenceBgColor = getComputedStyle(ref).backgroundColor;
|
|
58
|
+
|
|
59
|
+
// Verify the token system loaded by comparing button color to reference
|
|
60
|
+
await expect(buttonBgColor).toBe(referenceBgColor);
|
|
61
|
+
|
|
62
|
+
// Verify the token system actually loaded a non-transparent color
|
|
63
|
+
// (catches the case where themes.css/Tailwind failed to load)
|
|
64
|
+
await expect(buttonBgColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
65
|
+
|
|
66
|
+
// Clean up
|
|
67
|
+
ref.remove();
|
|
55
68
|
},
|
|
56
69
|
};
|
|
57
70
|
|
|
@@ -68,3 +81,98 @@ export const AllVariants: Story = {
|
|
|
68
81
|
</div>
|
|
69
82
|
),
|
|
70
83
|
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* #67 — the RENDERED lock on the compound focus indicator (ADR 0027 Amendment).
|
|
87
|
+
*
|
|
88
|
+
* A class-string assertion would be worthless here: `focus-ring` could name a
|
|
89
|
+
* single-layer ring and the class would still be present while the ratio stayed
|
|
90
|
+
* at 1.36:1, which is the exact failure this story exists to prevent. So this
|
|
91
|
+
* play function measures the SHIPPED pixels — it reads the computed indicator
|
|
92
|
+
* layers off a really-focused button, resolves the ground the indicator is
|
|
93
|
+
* actually drawn on by walking up to the first non-transparent ancestor, and
|
|
94
|
+
* asserts at least one layer clears WCAG 1.4.11 (3:1) against it.
|
|
95
|
+
*
|
|
96
|
+
* It is theme-agnostic on purpose: whichever theme the run resolves (the
|
|
97
|
+
* toolbar global, or `STORYBOOK_THEME=<slug>` headless — see
|
|
98
|
+
* @.claude/rules/storybook-mcp.md § Themes) the SAME assertion has to hold, so
|
|
99
|
+
* one story covers `light`, `dark` and any consumer theme a fork adds.
|
|
100
|
+
*/
|
|
101
|
+
export const CompoundFocusIndicator: Story = {
|
|
102
|
+
name: "Compound focus indicator (WCAG 1.4.11)",
|
|
103
|
+
play: async ({ canvas, userEvent }) => {
|
|
104
|
+
const btn = canvas.getByRole("button", { name: "Button" });
|
|
105
|
+
|
|
106
|
+
// Tab rather than .focus(): `:focus-visible` is what the utility keys on,
|
|
107
|
+
// and a programmatic focus does not reliably match it.
|
|
108
|
+
await userEvent.tab();
|
|
109
|
+
await expect(btn).toHaveFocus();
|
|
110
|
+
|
|
111
|
+
/** Resolve a token to the rgb() the browser actually paints. */
|
|
112
|
+
const resolve = (token: string) => {
|
|
113
|
+
const probe = document.createElement("div");
|
|
114
|
+
probe.style.cssText = `position:absolute;visibility:hidden;background-color:var(${token})`;
|
|
115
|
+
btn.parentElement?.appendChild(probe);
|
|
116
|
+
const value = getComputedStyle(probe).backgroundColor;
|
|
117
|
+
probe.remove();
|
|
118
|
+
return value;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Colours come back in whatever space the author wrote — Chromium echoes
|
|
122
|
+
// `oklch()` verbatim for an oklch token — so convert through a 1x1 canvas
|
|
123
|
+
// rather than a regex: the canvas does the same CSS Color 4 → sRGB
|
|
124
|
+
// conversion the compositor does, for any colour syntax a theme may use.
|
|
125
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
126
|
+
if (ctx == null) throw new Error("no 2d context");
|
|
127
|
+
const srgb = (value: string): [number, number, number] => {
|
|
128
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
129
|
+
ctx.fillStyle = value;
|
|
130
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
131
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
132
|
+
return [r as number, g as number, b as number];
|
|
133
|
+
};
|
|
134
|
+
const luminance = (value: string) => {
|
|
135
|
+
const [r, g, b] = srgb(value).map((c) => {
|
|
136
|
+
const s = c / 255;
|
|
137
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
138
|
+
}) as [number, number, number];
|
|
139
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
140
|
+
};
|
|
141
|
+
const ratio = (a: string, b: string) => {
|
|
142
|
+
const [la, lb] = [luminance(a), luminance(b)];
|
|
143
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// The ground the indicator is drawn ON — walked, not assumed, and started at
|
|
147
|
+
// the PARENT: the indicator sits outside the button, so the button's own
|
|
148
|
+
// `--primary` fill is not the surface it has to be legible against.
|
|
149
|
+
let ground = "rgba(0, 0, 0, 0)";
|
|
150
|
+
for (let el: HTMLElement | null = btn.parentElement; el != null; el = el.parentElement) {
|
|
151
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
152
|
+
if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
|
153
|
+
ground = bg;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await expect(ground).not.toBe("rgba(0, 0, 0, 0)");
|
|
158
|
+
|
|
159
|
+
// BOTH layers must actually be emitted — a compound indicator with one layer
|
|
160
|
+
// missing is the single-layer ring this issue was filed about.
|
|
161
|
+
const focused = getComputedStyle(btn);
|
|
162
|
+
await expect(focused.boxShadow).not.toBe("none");
|
|
163
|
+
await expect(focused.outlineStyle).toBe("solid");
|
|
164
|
+
await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
|
|
165
|
+
|
|
166
|
+
const ringLayer = resolve("--ring");
|
|
167
|
+
const contourLayer = focused.outlineColor;
|
|
168
|
+
// The contour layer really is the token, not currentColor or a fallback.
|
|
169
|
+
await expect(contourLayer).toBe(resolve("--ring-contour"));
|
|
170
|
+
|
|
171
|
+
const best = Math.max(ratio(ringLayer, ground), ratio(contourLayer, ground));
|
|
172
|
+
await expect(
|
|
173
|
+
best,
|
|
174
|
+
`focus indicator on ${ground}: ring ${ringLayer} = ${ratio(ringLayer, ground).toFixed(2)}, ` +
|
|
175
|
+
`contour ${contourLayer} = ${ratio(contourLayer, ground).toFixed(2)}`,
|
|
176
|
+
).toBeGreaterThanOrEqual(3);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
@@ -4,15 +4,21 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
5
|
|
|
6
6
|
export const buttonVariants = cva(
|
|
7
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap touch-manipulation rounded-
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap touch-manipulation rounded-control text-body font-control transition-[color,background-color,border-color,box-shadow,scale] duration-fast ease-standard active:scale-[0.98] motion-reduce:active:scale-100 focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
+
// Disabled treatment is ONE rule for every variant, inherited from the
|
|
12
|
+
// base string above (`disabled:pointer-events-none disabled:opacity-50`)
|
|
13
|
+
// — no per-variant fill swap. A solid-fill variant fading via opacity
|
|
14
|
+
// still reads as "off" against `--muted`/`--background` in both themes;
|
|
15
|
+
// keeping one mechanism (vs. a bg-muted swap on some variants only) is
|
|
16
|
+
// what made Input's disabled state internally inconsistent (#286).
|
|
11
17
|
default:
|
|
12
|
-
"bg-primary text-primary-foreground hover:bg-primary
|
|
18
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover active:bg-primary-active",
|
|
13
19
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
14
20
|
destructive:
|
|
15
|
-
"bg-destructive text-destructive-foreground hover:bg-destructive
|
|
21
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive-hover active:bg-destructive-active",
|
|
16
22
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
23
|
// The calm "outlined but quiet" rung (#194, research 02 §3a). `outline` uses
|
|
18
24
|
// the form-field `border-input` token, `outline-subtle` uses `border-border`.
|
|
@@ -24,17 +30,17 @@ export const buttonVariants = cva(
|
|
|
24
30
|
"outline-subtle":
|
|
25
31
|
"border border-border bg-background hover:bg-accent hover:text-accent-foreground",
|
|
26
32
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
27
|
-
// #399 — a link button is TEXT on the page
|
|
28
|
-
//
|
|
29
|
-
link: "text-
|
|
33
|
+
// #399 — a link button is TEXT on the page: `--link` defaults to the
|
|
34
|
+
// on-surface `--primary-text` rung; `bg-primary` above keeps the fill rung.
|
|
35
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
30
36
|
},
|
|
31
37
|
size: {
|
|
32
|
-
sm: "h-
|
|
33
|
-
default: "h-
|
|
34
|
-
lg: "h-
|
|
35
|
-
icon: "size-
|
|
36
|
-
"icon-sm": "size-
|
|
37
|
-
"icon-lg": "size-
|
|
38
|
+
sm: "h-control-sm px-3 text-meta",
|
|
39
|
+
default: "h-control px-4 py-2",
|
|
40
|
+
lg: "h-control-lg px-6",
|
|
41
|
+
icon: "size-control",
|
|
42
|
+
"icon-sm": "size-control-sm",
|
|
43
|
+
"icon-lg": "size-control-lg",
|
|
38
44
|
},
|
|
39
45
|
},
|
|
40
46
|
defaultVariants: { variant: "default", size: "default" },
|
|
@@ -44,7 +44,7 @@ export const ButtonGroupText = forwardRef<
|
|
|
44
44
|
<Comp
|
|
45
45
|
ref={ref}
|
|
46
46
|
className={cn(
|
|
47
|
-
"flex items-center gap-2 rounded-md border bg-muted px-4 text-
|
|
47
|
+
"flex items-center gap-2 rounded-md border bg-muted px-4 text-body font-medium shadow-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
48
48
|
className,
|
|
49
49
|
)}
|
|
50
50
|
{...props}
|
|
@@ -14,6 +14,7 @@ export function Calendar({
|
|
|
14
14
|
}: CalendarProps) {
|
|
15
15
|
return (
|
|
16
16
|
<DayPicker
|
|
17
|
+
data-slot="calendar"
|
|
17
18
|
showOutsideDays={showOutsideDays}
|
|
18
19
|
className={cn("p-3", className)}
|
|
19
20
|
classNames={{
|
|
@@ -26,7 +27,7 @@ export function Calendar({
|
|
|
26
27
|
// for a taller caption (e.g. `captionLayout="dropdown"`), and `px-8` keeps
|
|
27
28
|
// the label clear of the buttons overlaying either edge.
|
|
28
29
|
month_caption: "flex min-h-7 items-center justify-center px-8",
|
|
29
|
-
caption_label: "text-
|
|
30
|
+
caption_label: "text-body font-medium",
|
|
30
31
|
// Overlays the caption row and pushes the buttons to either edge; the
|
|
31
32
|
// buttons themselves stay in normal flow inside it.
|
|
32
33
|
nav: "absolute inset-x-0 top-0 flex h-7 items-center justify-between",
|
|
@@ -42,7 +43,7 @@ export function Calendar({
|
|
|
42
43
|
weekdays: "flex",
|
|
43
44
|
weekday: "w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground",
|
|
44
45
|
week: "mt-2 flex w-full",
|
|
45
|
-
day: "relative size-8 p-0 text-center text-
|
|
46
|
+
day: "relative size-8 p-0 text-center text-body focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-e-md first:[&:has([aria-selected])]:rounded-s-md last:[&:has([aria-selected])]:rounded-e-md",
|
|
46
47
|
day_button: cn(
|
|
47
48
|
buttonVariants({ variant: "ghost" }),
|
|
48
49
|
"size-8 p-0 font-normal aria-selected:opacity-100",
|
|
@@ -70,6 +70,11 @@ export const Default: Story = {
|
|
|
70
70
|
|
|
71
71
|
/** Opt-in hover-lift for clickable cards (gated by the motion system). */
|
|
72
72
|
export const Interactive: Story = {
|
|
73
|
+
// The suite runs under `prefers-reduced-motion: reduce`
|
|
74
|
+
// (`apps/docs/vitest.config.ts`, #125), so the gated `--t-base` this asserts
|
|
75
|
+
// would read as the floor. The lift is what this story exists to prove, so it
|
|
76
|
+
// opts back into full motion explicitly.
|
|
77
|
+
globals: { motionPref: "full" },
|
|
73
78
|
render: () => (
|
|
74
79
|
<Card interactive className="max-w-sm" tabIndex={0} role="button">
|
|
75
80
|
<CardHeader>
|
|
@@ -153,17 +158,17 @@ export const DetailSideHover: Story = {
|
|
|
153
158
|
</p>
|
|
154
159
|
<ul className="text-body space-y-1">
|
|
155
160
|
<li>
|
|
156
|
-
<button className="text-foreground hover:underline focus-
|
|
161
|
+
<button className="text-foreground hover:underline focus-ring rounded-sm px-1">
|
|
157
162
|
View report
|
|
158
163
|
</button>
|
|
159
164
|
</li>
|
|
160
165
|
<li>
|
|
161
|
-
<button className="text-foreground hover:underline focus-
|
|
166
|
+
<button className="text-foreground hover:underline focus-ring rounded-sm px-1">
|
|
162
167
|
Export CSV
|
|
163
168
|
</button>
|
|
164
169
|
</li>
|
|
165
170
|
<li>
|
|
166
|
-
<button className="text-foreground hover:underline focus-
|
|
171
|
+
<button className="text-foreground hover:underline focus-ring rounded-sm px-1">
|
|
167
172
|
Share
|
|
168
173
|
</button>
|
|
169
174
|
</li>
|
|
@@ -223,10 +228,10 @@ export const DetailBottomHover: Story = {
|
|
|
223
228
|
className="h-full"
|
|
224
229
|
detail={
|
|
225
230
|
<div className="flex items-center gap-3">
|
|
226
|
-
<button className="text-body text-foreground hover:underline focus-
|
|
231
|
+
<button className="text-body text-foreground hover:underline focus-ring rounded-sm px-1">
|
|
227
232
|
Drill down
|
|
228
233
|
</button>
|
|
229
|
-
<button className="text-body text-foreground hover:underline focus-
|
|
234
|
+
<button className="text-body text-foreground hover:underline focus-ring rounded-sm px-1">
|
|
230
235
|
Download
|
|
231
236
|
</button>
|
|
232
237
|
</div>
|
|
@@ -390,7 +395,7 @@ export const DetailHoverKeyboardReveal: Story = {
|
|
|
390
395
|
<div className="flex flex-col gap-2 pt-1">
|
|
391
396
|
<button
|
|
392
397
|
data-testid="detail-action"
|
|
393
|
-
className="text-body text-foreground hover:underline focus-
|
|
398
|
+
className="text-body text-foreground hover:underline focus-ring rounded-sm px-1 text-start"
|
|
394
399
|
>
|
|
395
400
|
Open details
|
|
396
401
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
3
|
import { Card, CardDescription, CardTitle } from "./card";
|
|
4
4
|
|
|
5
5
|
describe("Card", () => {
|
|
@@ -103,6 +103,28 @@ describe("Card", () => {
|
|
|
103
103
|
expect(region).toHaveAttribute("aria-hidden", "true");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
it("with `detail` + hover, cancels its pending conceal rAF on unmount", () => {
|
|
107
|
+
const rafSpy = vi.spyOn(window, "requestAnimationFrame").mockReturnValue(42);
|
|
108
|
+
const cafSpy = vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {});
|
|
109
|
+
const { unmount } = render(
|
|
110
|
+
<Card data-testid="card" detailReveal="hover" detail={<button>More</button>}>
|
|
111
|
+
main
|
|
112
|
+
</Card>,
|
|
113
|
+
);
|
|
114
|
+
const card = screen.getByTestId("card");
|
|
115
|
+
// focus-out schedules the conceal rAF this test is locking cleanup for.
|
|
116
|
+
fireEvent.focusOut(card);
|
|
117
|
+
expect(rafSpy).toHaveBeenCalled();
|
|
118
|
+
|
|
119
|
+
unmount();
|
|
120
|
+
// Without cancelling it, the scheduled frame is still pending after
|
|
121
|
+
// teardown and fires against a component that no longer exists.
|
|
122
|
+
expect(cafSpy).toHaveBeenCalledWith(42);
|
|
123
|
+
|
|
124
|
+
rafSpy.mockRestore();
|
|
125
|
+
cafSpy.mockRestore();
|
|
126
|
+
});
|
|
127
|
+
|
|
106
128
|
it("with `detail` + hover, detail region is labelled (landmark navigation, #141)", () => {
|
|
107
129
|
render(
|
|
108
130
|
<Card detailReveal="hover" detail={<span>info</span>} detailLabel="Card details">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
forwardRef,
|
|
5
|
+
useEffect,
|
|
5
6
|
useRef,
|
|
6
7
|
useState,
|
|
7
8
|
useCallback,
|
|
@@ -23,7 +24,7 @@ export const cardVariants = cva("rounded-lg border bg-card text-card-foreground
|
|
|
23
24
|
// duration-* utility; the transform is neutralized under OS reduced-motion
|
|
24
25
|
// (the shadow/border still respond — "reduced != none").
|
|
25
26
|
interactive: {
|
|
26
|
-
true: "cursor-pointer transition-[translate,box-shadow,border-color] duration-base ease-standard hover:-translate-y-1 hover:border-ring/40 hover:shadow-md focus-
|
|
27
|
+
true: "cursor-pointer transition-[translate,box-shadow,border-color] duration-base ease-standard hover:-translate-y-1 hover:border-ring/40 hover:shadow-md focus-ring motion-reduce:hover:translate-y-0",
|
|
27
28
|
false: "",
|
|
28
29
|
},
|
|
29
30
|
},
|
|
@@ -110,12 +111,22 @@ function CardWithHoverDetail({
|
|
|
110
111
|
}: CardProps) {
|
|
111
112
|
const [isRevealed, setIsRevealed] = useState(false);
|
|
112
113
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
114
|
+
const concealRafRef = useRef<number | null>(null);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
return () => {
|
|
118
|
+
if (concealRafRef.current !== null) cancelAnimationFrame(concealRafRef.current);
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
113
121
|
|
|
114
122
|
const reveal = useCallback(() => setIsRevealed(true), []);
|
|
115
123
|
const conceal = useCallback(() => {
|
|
116
124
|
// Only conceal if focus has truly left the card (not moved to detail).
|
|
117
125
|
// Use requestAnimationFrame so the new focusIn event fires first.
|
|
118
|
-
|
|
126
|
+
// Tracked in a ref so it can be cancelled if the card unmounts before the
|
|
127
|
+
// frame fires (a focus-out right before navigation away, say).
|
|
128
|
+
concealRafRef.current = requestAnimationFrame(() => {
|
|
129
|
+
concealRafRef.current = null;
|
|
119
130
|
if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
|
|
120
131
|
setIsRevealed(false);
|
|
121
132
|
}
|
|
@@ -142,6 +153,7 @@ function CardWithHoverDetail({
|
|
|
142
153
|
return (
|
|
143
154
|
<div
|
|
144
155
|
ref={containerRef}
|
|
156
|
+
data-slot="card"
|
|
145
157
|
className={cn(
|
|
146
158
|
cardVariants({ interactive }),
|
|
147
159
|
cardDetailVariants({ detailPlacement, detailReveal: "hover" }),
|
|
@@ -161,6 +173,7 @@ function CardWithHoverDetail({
|
|
|
161
173
|
role="region"
|
|
162
174
|
aria-label={detailLabel}
|
|
163
175
|
aria-hidden={!isRevealed}
|
|
176
|
+
data-slot="card-detail"
|
|
164
177
|
className={cn("overflow-hidden bg-background p-6", dividerEdge, dividerColor)}
|
|
165
178
|
>
|
|
166
179
|
{detail}
|
|
@@ -189,6 +202,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
|
189
202
|
return (
|
|
190
203
|
<div
|
|
191
204
|
ref={ref}
|
|
205
|
+
data-slot="card"
|
|
192
206
|
className={cn(cardVariants({ interactive }), className)}
|
|
193
207
|
style={style}
|
|
194
208
|
{...props}
|
|
@@ -230,6 +244,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
|
230
244
|
return (
|
|
231
245
|
<div
|
|
232
246
|
ref={ref}
|
|
247
|
+
data-slot="card"
|
|
233
248
|
className={cn(
|
|
234
249
|
cardVariants({ interactive }),
|
|
235
250
|
cardDetailVariants({ detailPlacement, detailReveal }),
|
|
@@ -243,6 +258,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
|
243
258
|
<div
|
|
244
259
|
role="region"
|
|
245
260
|
aria-label={detailLabel}
|
|
261
|
+
data-slot="card-detail"
|
|
246
262
|
className={cn("overflow-hidden bg-background p-6", dividerEdge, dividerColor)}
|
|
247
263
|
>
|
|
248
264
|
{detail}
|
|
@@ -253,7 +269,14 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
|
253
269
|
|
|
254
270
|
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
255
271
|
function CardHeader({ className, ...props }, ref) {
|
|
256
|
-
return
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
ref={ref}
|
|
275
|
+
data-slot="card-header"
|
|
276
|
+
className={cn("flex flex-col gap-1.5 p-6", className)}
|
|
277
|
+
{...props}
|
|
278
|
+
/>
|
|
279
|
+
);
|
|
257
280
|
},
|
|
258
281
|
);
|
|
259
282
|
|
|
@@ -276,7 +299,14 @@ export const CardTitle = forwardRef<HTMLDivElement, CardTitleProps>(function Car
|
|
|
276
299
|
{ className, as: Tag = "div", ...props },
|
|
277
300
|
ref,
|
|
278
301
|
) {
|
|
279
|
-
return
|
|
302
|
+
return (
|
|
303
|
+
<Tag
|
|
304
|
+
ref={ref}
|
|
305
|
+
data-slot="card-title"
|
|
306
|
+
className={cn("text-title leading-none", className)}
|
|
307
|
+
{...props}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
280
310
|
});
|
|
281
311
|
|
|
282
312
|
export interface CardDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {
|
|
@@ -293,11 +323,12 @@ export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionP
|
|
|
293
323
|
return (
|
|
294
324
|
<p
|
|
295
325
|
ref={ref}
|
|
326
|
+
data-slot="card-description"
|
|
296
327
|
// `text-balance` sets `text-wrap: balance`. #336's real defect was an
|
|
297
328
|
// invalid, misspelled utility name that emitted zero CSS (NOT a
|
|
298
329
|
// tailwind-merge conflict) — this is the real, registered class.
|
|
299
330
|
className={cn(
|
|
300
|
-
"text-
|
|
331
|
+
"text-body text-balance text-muted-foreground",
|
|
301
332
|
measure && "max-w-prose",
|
|
302
333
|
className,
|
|
303
334
|
)}
|
|
@@ -312,6 +343,7 @@ export const CardAction = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEleme
|
|
|
312
343
|
return (
|
|
313
344
|
<div
|
|
314
345
|
ref={ref}
|
|
346
|
+
data-slot="card-action"
|
|
315
347
|
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
316
348
|
{...props}
|
|
317
349
|
/>
|
|
@@ -321,12 +353,21 @@ export const CardAction = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEleme
|
|
|
321
353
|
|
|
322
354
|
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
323
355
|
function CardContent({ className, ...props }, ref) {
|
|
324
|
-
return
|
|
356
|
+
return (
|
|
357
|
+
<div ref={ref} data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />
|
|
358
|
+
);
|
|
325
359
|
},
|
|
326
360
|
);
|
|
327
361
|
|
|
328
362
|
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
329
363
|
function CardFooter({ className, ...props }, ref) {
|
|
330
|
-
return
|
|
364
|
+
return (
|
|
365
|
+
<div
|
|
366
|
+
ref={ref}
|
|
367
|
+
data-slot="card-footer"
|
|
368
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
369
|
+
{...props}
|
|
370
|
+
/>
|
|
371
|
+
);
|
|
331
372
|
},
|
|
332
373
|
);
|