@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,11 +1,21 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { Toaster } from "../sonner";
|
|
3
4
|
import { CopyableValue } from "./copyable-value";
|
|
4
5
|
|
|
5
6
|
const meta = {
|
|
6
7
|
title: "Display/CopyableValue",
|
|
7
8
|
component: CopyableValue,
|
|
8
9
|
tags: ["autodocs"],
|
|
10
|
+
// The copy toast needs a host; apps mount one `<Toaster />` at their root.
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<>
|
|
14
|
+
<Story />
|
|
15
|
+
<Toaster />
|
|
16
|
+
</>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
9
19
|
parameters: {
|
|
10
20
|
docs: {
|
|
11
21
|
description: {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
import { describe, expect, it, vi } from "vitest";
|
|
12
12
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
13
13
|
import userEvent from "@testing-library/user-event";
|
|
14
|
-
import {
|
|
14
|
+
import { toast } from "../sonner";
|
|
15
|
+
import { CopyableValue, COPYABLE_VALUE_TOAST_MS } from "./copyable-value";
|
|
16
|
+
|
|
17
|
+
vi.mock("../sonner", () => ({ toast: vi.fn() }));
|
|
15
18
|
|
|
16
19
|
/** Install a clipboard stub for one test; returns the writeText spy. */
|
|
17
20
|
function stubClipboard(impl: () => Promise<void> = () => Promise.resolve()) {
|
|
@@ -38,6 +41,30 @@ describe("CopyableValue", () => {
|
|
|
38
41
|
});
|
|
39
42
|
});
|
|
40
43
|
|
|
44
|
+
it("shows a one-second toast once the copy succeeds", async () => {
|
|
45
|
+
vi.mocked(toast).mockClear();
|
|
46
|
+
stubClipboard();
|
|
47
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
48
|
+
|
|
49
|
+
fireEvent.click(screen.getByRole("button"));
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
expect(toast).toHaveBeenCalledWith("Copied", { duration: COPYABLE_VALUE_TOAST_MS });
|
|
52
|
+
});
|
|
53
|
+
expect(COPYABLE_VALUE_TOAST_MS).toBe(1000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("shows no toast when the copy fails", async () => {
|
|
57
|
+
vi.mocked(toast).mockClear();
|
|
58
|
+
stubClipboard(() => Promise.reject(new Error("denied")));
|
|
59
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
60
|
+
|
|
61
|
+
fireEvent.click(screen.getByRole("button"));
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(screen.getByRole("status")).toHaveTextContent("");
|
|
64
|
+
});
|
|
65
|
+
expect(toast).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
41
68
|
it("falls back to the exact value as its own display", () => {
|
|
42
69
|
render(<CopyableValue value="1234" />);
|
|
43
70
|
expect(screen.getByRole("button")).toHaveTextContent("1234");
|
|
@@ -19,6 +19,10 @@ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
|
19
19
|
import { cn } from "../../lib/cn";
|
|
20
20
|
import { useCopyToClipboard } from "../../lib/use-copy-to-clipboard";
|
|
21
21
|
import { useLocale } from "../locale-provider";
|
|
22
|
+
import { toast } from "../sonner";
|
|
23
|
+
|
|
24
|
+
/** ms the copy-confirmation toast stays on screen. */
|
|
25
|
+
export const COPYABLE_VALUE_TOAST_MS = 1000;
|
|
22
26
|
|
|
23
27
|
export interface CopyableValueProps extends Omit<
|
|
24
28
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
@@ -72,14 +76,21 @@ export const CopyableValue = forwardRef<HTMLButtonElement, CopyableValueProps>(
|
|
|
72
76
|
// only resting affordance, so a value inside a heading or a KPI
|
|
73
77
|
// still looks like a value, not a link.
|
|
74
78
|
"inline-flex items-baseline gap-1 rounded-sm underline decoration-dotted decoration-muted-foreground underline-offset-4",
|
|
75
|
-
"hover:decoration-foreground focus-
|
|
79
|
+
"hover:decoration-foreground focus-ring",
|
|
76
80
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
77
81
|
className,
|
|
78
82
|
)}
|
|
79
83
|
onClick={(event) => {
|
|
80
84
|
onClick?.(event);
|
|
81
85
|
if (!event.defaultPrevented) {
|
|
82
|
-
|
|
86
|
+
// The toast is the visible confirmation; it needs an app-mounted
|
|
87
|
+
// `<Toaster />` and is a silent no-op without one, which is why
|
|
88
|
+
// the live region below stays as the always-present fallback.
|
|
89
|
+
void copy(value).then((ok) => {
|
|
90
|
+
if (ok) {
|
|
91
|
+
toast(t("ui.copyableValue.copied"), { duration: COPYABLE_VALUE_TOAST_MS });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
83
94
|
}
|
|
84
95
|
}}
|
|
85
96
|
>
|
|
@@ -5,6 +5,15 @@ const meta = {
|
|
|
5
5
|
title: "Forms/DatePicker",
|
|
6
6
|
component: DatePicker,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
|
+
// The trigger fills its container by default (no hard-fixed width) — cap
|
|
9
|
+
// the docs canvas to a realistic form-field width.
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div className="w-full max-w-xs">
|
|
13
|
+
<Story />
|
|
14
|
+
</div>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
8
17
|
argTypes: {
|
|
9
18
|
value: {
|
|
10
19
|
description: "Controlled selected date.",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen, within } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { DatePicker } from "./date-picker";
|
|
6
|
+
import { LocaleProvider } from "../locale-provider";
|
|
7
|
+
|
|
8
|
+
describe("DatePicker", () => {
|
|
9
|
+
it("forwards the ref to the trigger button", () => {
|
|
10
|
+
const ref = createRef<HTMLButtonElement>();
|
|
11
|
+
render(<DatePicker ref={ref} />);
|
|
12
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
13
|
+
expect(ref.current).toBe(screen.getByRole("button"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
17
|
+
render(<DatePicker />);
|
|
18
|
+
expect(screen.getByRole("button").className).not.toMatch(/\bw-56\b/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// #reviewed 1.7 — same `value ?? internal` / ternary-setter bug as Combobox:
|
|
22
|
+
// an uncontrolled DatePicker with an onValueChange listener never updated
|
|
23
|
+
// its own displayed date.
|
|
24
|
+
it("updates its own displayed date when uncontrolled even with an onValueChange listener", async () => {
|
|
25
|
+
const onValueChange = vi.fn();
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
render(<DatePicker onValueChange={onValueChange} placeholder="Pick a date" />);
|
|
28
|
+
const trigger = screen.getByRole("button", { name: /pick a date/i });
|
|
29
|
+
await user.click(trigger);
|
|
30
|
+
const grid = await screen.findByRole("grid");
|
|
31
|
+
const dayButtons = within(grid)
|
|
32
|
+
.getAllByRole("button")
|
|
33
|
+
.filter((b) => !/previous|next/i.test(b.getAttribute("aria-label") ?? ""));
|
|
34
|
+
const firstEnabled = dayButtons.find((b) => !(b as HTMLButtonElement).disabled)!;
|
|
35
|
+
await user.click(firstEnabled);
|
|
36
|
+
expect(onValueChange).toHaveBeenCalled();
|
|
37
|
+
expect(trigger).not.toHaveTextContent("Pick a date");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("formats the displayed date using the active locale", () => {
|
|
41
|
+
const date = new Date(2024, 0, 15); // Jan 15, 2024
|
|
42
|
+
render(
|
|
43
|
+
<LocaleProvider locale="de-DE">
|
|
44
|
+
<DatePicker value={date} />
|
|
45
|
+
</LocaleProvider>,
|
|
46
|
+
);
|
|
47
|
+
// de-DE "medium" dateStyle renders like "15. Jan. 2024" — distinct from
|
|
48
|
+
// the en-US default ("Jan 15, 2024").
|
|
49
|
+
expect(screen.getByRole("button")).toHaveTextContent("2024");
|
|
50
|
+
expect(screen.getByRole("button").textContent).not.toMatch(/Jan 15, 2024/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
2
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
4
5
|
import { Button } from "../button";
|
|
5
6
|
import { Calendar } from "../calendar";
|
|
7
|
+
import { useLocale } from "../locale-provider";
|
|
6
8
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
7
9
|
|
|
8
10
|
export interface DatePickerProps {
|
|
@@ -13,26 +15,22 @@ export interface DatePickerProps {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Single-date picker (Popover + Calendar). Controlled or uncontrolled. */
|
|
16
|
-
export function DatePicker(
|
|
17
|
-
value,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const date = value ?? internal;
|
|
24
|
-
const setDate = (d?: Date) => (onValueChange ? onValueChange(d) : setInternal(d));
|
|
25
|
-
const label = date
|
|
26
|
-
? new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date)
|
|
27
|
-
: placeholder;
|
|
18
|
+
export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker(
|
|
19
|
+
{ value, onValueChange, placeholder = "Pick a date", className },
|
|
20
|
+
ref,
|
|
21
|
+
) {
|
|
22
|
+
const { formatDate } = useLocale();
|
|
23
|
+
const [date, setDate] = useControllableState<Date | undefined>(value, undefined, onValueChange);
|
|
24
|
+
const label = date ? formatDate(date, { dateStyle: "medium" }) : placeholder;
|
|
28
25
|
|
|
29
26
|
return (
|
|
30
27
|
<Popover>
|
|
31
28
|
<PopoverTrigger asChild>
|
|
32
29
|
<Button
|
|
30
|
+
ref={ref}
|
|
33
31
|
variant="outline"
|
|
34
32
|
className={cn(
|
|
35
|
-
"w-
|
|
33
|
+
"w-full justify-start gap-2 text-start font-normal",
|
|
36
34
|
!date && "text-muted-foreground",
|
|
37
35
|
className,
|
|
38
36
|
)}
|
|
@@ -46,4 +44,4 @@ export function DatePicker({
|
|
|
46
44
|
</PopoverContent>
|
|
47
45
|
</Popover>
|
|
48
46
|
);
|
|
49
|
-
}
|
|
47
|
+
});
|
|
@@ -12,6 +12,15 @@ const meta = {
|
|
|
12
12
|
parameters: {
|
|
13
13
|
layout: "centered",
|
|
14
14
|
},
|
|
15
|
+
// The trigger fills its container by default (no hard-fixed width) — cap
|
|
16
|
+
// the docs canvas to a realistic form-field width.
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="w-80">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
15
24
|
argTypes: {
|
|
16
25
|
value: {
|
|
17
26
|
description: "Controlled date range `{ from, to }`.",
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* select interaction and accessibility are covered by the Storybook stories
|
|
7
7
|
* (run via `pnpm --filter @elabs-ai/components-docs test-storybook` or the MCP story tests).
|
|
8
8
|
*/
|
|
9
|
+
import { useState } from "react";
|
|
9
10
|
import { describe, expect, it } from "vitest";
|
|
10
11
|
import { render, screen } from "@testing-library/react";
|
|
11
|
-
import
|
|
12
|
+
import userEvent from "@testing-library/user-event";
|
|
13
|
+
import { DateRangePicker, type DateRange } from "./date-range-picker";
|
|
14
|
+
import { LocaleProvider } from "../locale-provider";
|
|
12
15
|
|
|
13
16
|
describe("DateRangePicker", () => {
|
|
14
17
|
it("renders the placeholder on the trigger when no range is set", () => {
|
|
@@ -59,4 +62,76 @@ describe("DateRangePicker", () => {
|
|
|
59
62
|
const button = screen.getByRole("button");
|
|
60
63
|
expect(button.className).toContain("my-custom-class");
|
|
61
64
|
});
|
|
65
|
+
|
|
66
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
67
|
+
render(<DateRangePicker />);
|
|
68
|
+
expect(screen.getByRole("button").className).not.toMatch(/\bw-72\b/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("spreads extra props (e.g. data-testid) onto the trigger button", () => {
|
|
72
|
+
render(<DateRangePicker data-testid="range-trigger" />);
|
|
73
|
+
expect(screen.getByTestId("range-trigger")).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("formats the range label using the active locale", () => {
|
|
77
|
+
const from = new Date(2024, 0, 5);
|
|
78
|
+
const to = new Date(2024, 0, 20);
|
|
79
|
+
render(
|
|
80
|
+
<LocaleProvider locale="de-DE">
|
|
81
|
+
<DateRangePicker value={{ from, to }} />
|
|
82
|
+
</LocaleProvider>,
|
|
83
|
+
);
|
|
84
|
+
// de-DE renders "5. Jan. 2024" style — distinct from the en-US default.
|
|
85
|
+
expect(screen.getByRole("button").textContent).not.toMatch(/Jan 5, 2024/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// #reviewed 1.7 — clearing a controlled range to `undefined` must not fall
|
|
89
|
+
// through to a stale uncontrolled `internal` value: once controlled, the
|
|
90
|
+
// component must stay controlled for its whole lifetime.
|
|
91
|
+
it("collapses to a single month below the sm breakpoint (phone-width popover)", async () => {
|
|
92
|
+
const originalWidth = window.innerWidth;
|
|
93
|
+
Object.defineProperty(window, "innerWidth", {
|
|
94
|
+
writable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
value: 375,
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
render(<DateRangePicker />);
|
|
101
|
+
await user.click(screen.getByRole("button"));
|
|
102
|
+
const grids = await screen.findAllByRole("grid");
|
|
103
|
+
expect(grids).toHaveLength(1);
|
|
104
|
+
} finally {
|
|
105
|
+
Object.defineProperty(window, "innerWidth", {
|
|
106
|
+
writable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
value: originalWidth,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("shows the placeholder (not a stale value) when a controlled range is cleared to undefined", async () => {
|
|
114
|
+
function Harness() {
|
|
115
|
+
const [range, setRange] = useState<DateRange | undefined>({
|
|
116
|
+
from: new Date(2024, 0, 5),
|
|
117
|
+
to: new Date(2024, 0, 20),
|
|
118
|
+
});
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<DateRangePicker value={range} onValueChange={setRange} placeholder="Pick a range" />
|
|
122
|
+
<button type="button" onClick={() => setRange(undefined)}>
|
|
123
|
+
Clear
|
|
124
|
+
</button>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
render(<Harness />);
|
|
130
|
+
const trigger = screen.getByRole("button", { name: /–/ });
|
|
131
|
+
expect(trigger.textContent).not.toBe("Pick a range");
|
|
132
|
+
|
|
133
|
+
await user.click(screen.getByRole("button", { name: "Clear" }));
|
|
134
|
+
|
|
135
|
+
expect(trigger).toHaveTextContent("Pick a range");
|
|
136
|
+
});
|
|
62
137
|
});
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// a2ui.exposed: yes
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { forwardRef,
|
|
4
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
5
5
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
6
6
|
import type { DateRange } from "react-day-picker";
|
|
7
7
|
import { cn } from "../../lib/cn";
|
|
8
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
9
|
+
import { useIsMobile } from "../../lib/use-mobile";
|
|
8
10
|
import { Button } from "../button";
|
|
9
11
|
import { Calendar } from "../calendar";
|
|
12
|
+
import { useLocale } from "../locale-provider";
|
|
10
13
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
11
14
|
|
|
15
|
+
/** Below this width, the calendar collapses to a single month (Tailwind `sm`). */
|
|
16
|
+
const NARROW_BREAKPOINT = 640;
|
|
17
|
+
|
|
12
18
|
export type { DateRange };
|
|
13
19
|
|
|
14
20
|
export interface DateRangePreset {
|
|
@@ -16,7 +22,10 @@ export interface DateRangePreset {
|
|
|
16
22
|
getRange: () => DateRange;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export interface DateRangePickerProps
|
|
25
|
+
export interface DateRangePickerProps extends Omit<
|
|
26
|
+
HTMLAttributes<HTMLButtonElement>,
|
|
27
|
+
"onSelect" | "children" | "value" | "defaultValue"
|
|
28
|
+
> {
|
|
20
29
|
value?: DateRange;
|
|
21
30
|
defaultValue?: DateRange;
|
|
22
31
|
onValueChange?: (range?: DateRange) => void;
|
|
@@ -26,16 +35,6 @@ export interface DateRangePickerProps {
|
|
|
26
35
|
className?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
const fmt = (date: Date) =>
|
|
30
|
-
new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date);
|
|
31
|
-
|
|
32
|
-
function formatRange(range: DateRange | undefined, placeholder: string): string {
|
|
33
|
-
if (!range) return placeholder;
|
|
34
|
-
if (range.from && range.to) return `${fmt(range.from)} – ${fmt(range.to)}`;
|
|
35
|
-
if (range.from) return `${fmt(range.from)} – …`;
|
|
36
|
-
return placeholder;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
38
|
/**
|
|
40
39
|
* Date range picker (Popover + Calendar in range mode).
|
|
41
40
|
* Supports optional preset quick-picks, controlled or uncontrolled.
|
|
@@ -50,24 +49,31 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
50
49
|
numberOfMonths = 2,
|
|
51
50
|
placeholder = "Pick a date range",
|
|
52
51
|
className,
|
|
52
|
+
...props
|
|
53
53
|
},
|
|
54
54
|
ref,
|
|
55
55
|
) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const { formatDate } = useLocale();
|
|
57
|
+
const [range, setRange] = useControllableState<DateRange | undefined>(
|
|
58
|
+
value,
|
|
59
|
+
defaultValue,
|
|
60
|
+
onValueChange,
|
|
61
|
+
);
|
|
60
62
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const fmt = (date: Date) => formatDate(date, { dateStyle: "medium" });
|
|
64
|
+
const formatRange = (r: DateRange | undefined): string => {
|
|
65
|
+
if (!r) return placeholder;
|
|
66
|
+
if (r.from && r.to) return `${fmt(r.from)} – ${fmt(r.to)}`;
|
|
67
|
+
if (r.from) return `${fmt(r.from)} – …`;
|
|
68
|
+
return placeholder;
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
const hasRange = Boolean(range?.from);
|
|
70
|
-
const label = formatRange(range
|
|
72
|
+
const label = formatRange(range);
|
|
73
|
+
|
|
74
|
+
// Below `sm`, two side-by-side months push the popover off-screen (#reviewed).
|
|
75
|
+
const isNarrow = useIsMobile(NARROW_BREAKPOINT);
|
|
76
|
+
const monthsToShow = isNarrow ? 1 : numberOfMonths;
|
|
71
77
|
|
|
72
78
|
return (
|
|
73
79
|
<Popover>
|
|
@@ -76,8 +82,9 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
76
82
|
ref={ref}
|
|
77
83
|
variant="outline"
|
|
78
84
|
aria-haspopup="dialog"
|
|
85
|
+
{...props}
|
|
79
86
|
className={cn(
|
|
80
|
-
"w-
|
|
87
|
+
"w-full justify-start gap-2 text-start font-normal",
|
|
81
88
|
!hasRange && "text-muted-foreground",
|
|
82
89
|
className,
|
|
83
90
|
)}
|
|
@@ -86,10 +93,14 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
86
93
|
{label}
|
|
87
94
|
</Button>
|
|
88
95
|
</PopoverTrigger>
|
|
89
|
-
<PopoverContent
|
|
96
|
+
<PopoverContent
|
|
97
|
+
className="w-auto max-w-[calc(100vw-1rem)] p-0"
|
|
98
|
+
align="start"
|
|
99
|
+
collisionPadding={8}
|
|
100
|
+
>
|
|
90
101
|
{presets && presets.length > 0 ? (
|
|
91
102
|
<div className="flex">
|
|
92
|
-
<div className="flex flex-col gap-1 border-
|
|
103
|
+
<div className="flex flex-col gap-1 border-e border-border p-2">
|
|
93
104
|
{presets.map((preset) => (
|
|
94
105
|
<Button
|
|
95
106
|
key={preset.label}
|
|
@@ -104,7 +115,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
104
115
|
</div>
|
|
105
116
|
<Calendar
|
|
106
117
|
mode="range"
|
|
107
|
-
numberOfMonths={
|
|
118
|
+
numberOfMonths={monthsToShow}
|
|
108
119
|
selected={range}
|
|
109
120
|
onSelect={setRange}
|
|
110
121
|
autoFocus
|
|
@@ -113,7 +124,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
113
124
|
) : (
|
|
114
125
|
<Calendar
|
|
115
126
|
mode="range"
|
|
116
|
-
numberOfMonths={
|
|
127
|
+
numberOfMonths={monthsToShow}
|
|
117
128
|
selected={range}
|
|
118
129
|
onSelect={setRange}
|
|
119
130
|
autoFocus
|
|
@@ -3,7 +3,7 @@ import { createContext, forwardRef, use, type HTMLAttributes } from "react";
|
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
5
|
|
|
6
|
-
const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-
|
|
6
|
+
const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-body", {
|
|
7
7
|
variants: {
|
|
8
8
|
columns: {
|
|
9
9
|
1: "grid-cols-1",
|
|
@@ -47,11 +47,16 @@ describe("DialogContent size variants", () => {
|
|
|
47
47
|
expect(content.className).toContain("max-w-3xl");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it("size='full' renders max-w-[95vw] and h-[
|
|
50
|
+
it("size='full' renders max-w-[95vw] and h-[90dvh] — dvh, not vh (mobile browser chrome)", () => {
|
|
51
51
|
render(<Wrapper size="full" />);
|
|
52
52
|
const content = screen.getByRole("dialog");
|
|
53
53
|
expect(content.className).toContain("max-w-[95vw]");
|
|
54
|
-
expect(content.className).toContain("h-[
|
|
54
|
+
expect(content.className).toContain("h-[90dvh]");
|
|
55
|
+
expect(content.className).toContain("max-h-[90dvh]");
|
|
56
|
+
// `vh` on mobile Safari/Chrome is measured against the viewport with the
|
|
57
|
+
// browser chrome collapsed, so a plain `vh` value overflows past what is
|
|
58
|
+
// actually visible while the address bar is shown.
|
|
59
|
+
expect(content.className).not.toMatch(/(?<!d)vh\]/);
|
|
55
60
|
});
|
|
56
61
|
});
|
|
57
62
|
|
|
@@ -106,7 +111,7 @@ describe("DialogContent height/scroll regime (#341)", () => {
|
|
|
106
111
|
// later variant utility, so `full` still lets its body own the scroll.
|
|
107
112
|
expect(content.className).toContain("overflow-hidden");
|
|
108
113
|
expect(content.className).not.toContain("overflow-y-auto");
|
|
109
|
-
expect(content.className).toContain("max-h-[
|
|
114
|
+
expect(content.className).toContain("max-h-[90dvh]");
|
|
110
115
|
expect(content.className).not.toContain("max-h-[calc(100dvh-2rem)]");
|
|
111
116
|
});
|
|
112
117
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
forwardRef,
|
|
3
5
|
useCallback,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
12
14
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
15
|
import { cn } from "../../lib/cn";
|
|
16
|
+
import { useLocale } from "../locale-provider";
|
|
14
17
|
import { Heading, Text } from "../typography";
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -55,7 +58,11 @@ export const dialogContentVariants = cva(
|
|
|
55
58
|
sm: "max-w-sm",
|
|
56
59
|
lg: "max-w-lg",
|
|
57
60
|
xl: "max-w-3xl",
|
|
58
|
-
|
|
61
|
+
// `dvh` (dynamic viewport height), not `vh`: on mobile Safari/Chrome
|
|
62
|
+
// `vh` is measured against the LARGEST viewport (browser chrome
|
|
63
|
+
// collapsed), so `size="full"` overflows past the visible viewport
|
|
64
|
+
// while the address bar is showing.
|
|
65
|
+
full: "max-w-[95vw] h-[90dvh] max-h-[90dvh] overflow-hidden",
|
|
59
66
|
},
|
|
60
67
|
},
|
|
61
68
|
defaultVariants: { size: "lg" },
|
|
@@ -76,7 +83,7 @@ export const DialogOverlay = forwardRef<
|
|
|
76
83
|
ref={ref}
|
|
77
84
|
data-slot="dialog-overlay"
|
|
78
85
|
className={cn(
|
|
79
|
-
"fixed inset-0 z-50 bg-
|
|
86
|
+
"fixed inset-0 z-50 bg-overlay backdrop-blur-overlay",
|
|
80
87
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
81
88
|
className,
|
|
82
89
|
)}
|
|
@@ -90,6 +97,7 @@ export const DialogContent = forwardRef<
|
|
|
90
97
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
91
98
|
VariantProps<typeof dialogContentVariants>
|
|
92
99
|
>(function DialogContent({ className, children, size, onOpenAutoFocus, ...props }, ref) {
|
|
100
|
+
const { t } = useLocale();
|
|
93
101
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
94
102
|
const setRefs = useCallback(
|
|
95
103
|
(node: HTMLDivElement | null) => {
|
|
@@ -149,8 +157,8 @@ export const DialogContent = forwardRef<
|
|
|
149
157
|
{children}
|
|
150
158
|
<DialogPrimitive.Close
|
|
151
159
|
data-slot="dialog-close"
|
|
152
|
-
className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus
|
|
153
|
-
aria-label="
|
|
160
|
+
className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-ring"
|
|
161
|
+
aria-label={t("close")}
|
|
154
162
|
>
|
|
155
163
|
<svg
|
|
156
164
|
width="16"
|
|
@@ -213,7 +221,7 @@ export const DialogDescription = forwardRef<
|
|
|
213
221
|
<DialogPrimitive.Description
|
|
214
222
|
ref={ref}
|
|
215
223
|
data-slot="dialog-description"
|
|
216
|
-
className={cn("text-
|
|
224
|
+
className={cn("text-body text-muted-foreground", className)}
|
|
217
225
|
{...props}
|
|
218
226
|
/>
|
|
219
227
|
);
|
|
@@ -275,7 +283,7 @@ export const DialogBody = forwardRef<HTMLDivElement, DialogBodyProps>(function D
|
|
|
275
283
|
// focusable content.
|
|
276
284
|
tabIndex={tabIndex ?? 0}
|
|
277
285
|
className={cn(
|
|
278
|
-
"-m-1 min-h-0 overflow-y-auto overscroll-contain p-1 scroll-p-1 focus-
|
|
286
|
+
"-m-1 min-h-0 overflow-y-auto overscroll-contain p-1 scroll-p-1 focus-ring",
|
|
279
287
|
className,
|
|
280
288
|
)}
|
|
281
289
|
{...props}
|
|
@@ -25,7 +25,8 @@ const DrawerOverlay = forwardRef<
|
|
|
25
25
|
return (
|
|
26
26
|
<DrawerPrimitive.Overlay
|
|
27
27
|
ref={ref}
|
|
28
|
-
|
|
28
|
+
data-slot="drawer-overlay"
|
|
29
|
+
className={cn("fixed inset-0 z-50 bg-overlay", className)}
|
|
29
30
|
{...props}
|
|
30
31
|
/>
|
|
31
32
|
);
|
|
@@ -40,13 +41,14 @@ export const DrawerContent = forwardRef<
|
|
|
40
41
|
<DrawerOverlay />
|
|
41
42
|
<DrawerPrimitive.Content
|
|
42
43
|
ref={ref}
|
|
44
|
+
data-slot="drawer-content"
|
|
43
45
|
className={cn(
|
|
44
46
|
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col overscroll-contain rounded-t-xl border bg-card text-card-foreground",
|
|
45
47
|
className,
|
|
46
48
|
)}
|
|
47
49
|
{...props}
|
|
48
50
|
>
|
|
49
|
-
<div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
51
|
+
<div data-slot="drawer-handle" className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
50
52
|
{children}
|
|
51
53
|
</DrawerPrimitive.Content>
|
|
52
54
|
</DrawerPortal>
|
|
@@ -54,19 +56,33 @@ export const DrawerContent = forwardRef<
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
export function DrawerHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
57
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="drawer-header"
|
|
62
|
+
className={cn("grid gap-1.5 p-4 text-center sm:text-start", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
58
66
|
}
|
|
59
67
|
export function DrawerFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
60
|
-
return
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-slot="drawer-footer"
|
|
71
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
61
75
|
}
|
|
62
76
|
export const DrawerTitle = forwardRef<
|
|
63
77
|
ElementRef<typeof DrawerPrimitive.Title>,
|
|
64
78
|
ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
65
79
|
>(function DrawerTitle({ className, ...props }, ref) {
|
|
66
80
|
return (
|
|
81
|
+
// `text-title`, matching `DialogTitle` (#286) — was `text-lg font-semibold`.
|
|
67
82
|
<DrawerPrimitive.Title
|
|
68
83
|
ref={ref}
|
|
69
|
-
|
|
84
|
+
data-slot="drawer-title"
|
|
85
|
+
className={cn("text-title leading-none", className)}
|
|
70
86
|
{...props}
|
|
71
87
|
/>
|
|
72
88
|
);
|
|
@@ -78,7 +94,8 @@ export const DrawerDescription = forwardRef<
|
|
|
78
94
|
return (
|
|
79
95
|
<DrawerPrimitive.Description
|
|
80
96
|
ref={ref}
|
|
81
|
-
|
|
97
|
+
data-slot="drawer-description"
|
|
98
|
+
className={cn("text-body text-muted-foreground", className)}
|
|
82
99
|
{...props}
|
|
83
100
|
/>
|
|
84
101
|
);
|
|
@@ -147,7 +147,7 @@ export const SubmenuRadioAndCheckbox: Story = {
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
150
|
+
* Cross-theme sweep (a durable, `test-storybook`-run artifact rather than a
|
|
151
151
|
* one-off manual `preview.tsx` toggle). `<ThemeProvider>` (not a plain
|
|
152
152
|
* `data-theme` wrapper `<div>`) because it writes the attribute onto the
|
|
153
153
|
* DOCUMENT ROOT — this menu's Radix `Portal` mounts its content on
|