@elabs-ai/components-ui 4.0.0 → 4.1.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-JKPPTL63.js +48 -0
- package/dist/chunk-JKPPTL63.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 +2485 -167
- package/dist/index.js +10716 -2706
- package/dist/index.js.map +1 -1
- package/package.json +21 -7
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +4 -1
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.tsx +35 -2
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +1 -1
- package/src/components/attribution-panel/attributions.generated.ts +81 -77
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.tsx +1 -1
- package/src/components/bento-grid/bento-grid.test.tsx +1 -1
- package/src/components/bento-grid/bento-grid.tsx +1 -1
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +7 -2
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +1 -1
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.tsx +1 -1
- 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 +185 -4
- package/src/components/checkbox/checkbox.tsx +1 -1
- package/src/components/color-picker/color-picker.tsx +6 -5
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +73 -0
- package/src/components/command/command.tsx +30 -7
- 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-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.tsx +1 -1
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- 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 +340 -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 +48 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +7 -2
- package/src/components/file-upload/file-upload.tsx +15 -4
- package/src/components/form/form.tsx +5 -2
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +2 -2
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +2 -2
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +13 -4
- package/src/components/input-otp/input-otp.tsx +5 -1
- 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/link-preview/link-preview.tsx +1 -1
- package/src/components/locale-provider/index.ts +6 -1
- 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 +501 -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.tsx +8 -38
- package/src/components/mention-input/mention-value.ts +8 -2
- 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 +1 -1
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.tsx +57 -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 +189 -12
- 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 +90 -0
- package/src/components/pagination/pagination.tsx +9 -5
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +1 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.tsx +1 -1
- 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 +46 -3
- 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.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +900 -0
- package/src/components/schema-form/schema-form.tsx +1545 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +12 -1
- package/src/components/select/select.tsx +6 -2
- package/src/components/sheet/sheet.tsx +1 -1
- 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 +273 -0
- package/src/components/side-dock/side-dock.tsx +502 -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 +36 -1
- package/src/components/sidebar/sidebar.tsx +245 -33
- 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/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 +96 -17
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +1 -1
- package/src/components/table/table.stories.tsx +62 -0
- package/src/components/table/table.test.tsx +142 -1
- package/src/components/table/table.tsx +60 -1
- package/src/components/tabs/tabs.tsx +8 -2
- package/src/components/tag-input/tag-input.tsx +3 -2
- package/src/components/team-switcher/team-switcher.tsx +34 -5
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.tsx +6 -2
- 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 +11 -1
- package/src/components/toggle/toggle.tsx +1 -1
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.tsx +14 -9
- package/src/components/typography/prose.tsx +1 -1
- 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.test.tsx +1 -1
- package/src/components/wizard/wizard.tsx +1 -1
- 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 +75 -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/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 +92 -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/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-mobile.ts +12 -6
- package/src/motion.stories.tsx +6 -0
- package/src/templates-object-detail-hub.stories.tsx +2 -2
- package/src/templates-screen-states.stories.tsx +5 -5
- package/src/templates-settings.stories.tsx +1 -1
- package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
- package/src/blocks/sidebar-02/logo.tsx +0 -18
- package/src/blocks/sidebar-02/nav-main.tsx +0 -13
- package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
- package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
- package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
- package/src/blocks/sidebar-04/mail-context.tsx +0 -29
- package/src/blocks/sidebar-04/nav-user.tsx +0 -6
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
- package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
- package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { FileText, History, MessageSquare } from "lucide-react";
|
|
3
|
+
import { expect, screen, waitFor } from "storybook/test";
|
|
4
|
+
|
|
5
|
+
import { ContextRail, type ContextRailSection } from "./context-rail";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Layout/ContextRail",
|
|
9
|
+
component: ContextRail,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "fullscreen",
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"A right-hand rail whose collapsed state is a 48px icon strip that doubles as its own section switcher — click a switcher entry to make it active, click the ACTIVE entry again to collapse/expand. Below `overlayBreakpoint` (default 768) it renders the strip plus a `Sheet` overlay for the expanded body instead of a fixed-width panel. See `docs/ADR/0035-context-rail-and-side-dock.md`.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} satisfies Meta<typeof ContextRail>;
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const sections: ContextRailSection[] = [
|
|
25
|
+
{
|
|
26
|
+
id: "sources",
|
|
27
|
+
label: "Sources",
|
|
28
|
+
icon: <FileText />,
|
|
29
|
+
count: 4,
|
|
30
|
+
content: (
|
|
31
|
+
<ul className="flex flex-col gap-2 text-body">
|
|
32
|
+
<li className="rounded-md border border-border p-2">quarterly-report.pdf — p.12</li>
|
|
33
|
+
<li className="rounded-md border border-border p-2">design-notes.md — §3</li>
|
|
34
|
+
<li className="rounded-md border border-border p-2">onboarding-flow.fig</li>
|
|
35
|
+
<li className="rounded-md border border-border p-2">support-thread-4821</li>
|
|
36
|
+
</ul>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "comments",
|
|
41
|
+
label: "Comments",
|
|
42
|
+
icon: <MessageSquare />,
|
|
43
|
+
count: 2,
|
|
44
|
+
content: (
|
|
45
|
+
<ul className="flex flex-col gap-2 text-body">
|
|
46
|
+
<li className="rounded-md border border-border p-2">
|
|
47
|
+
“Can we tighten this copy?” — Priya
|
|
48
|
+
</li>
|
|
49
|
+
<li className="rounded-md border border-border p-2">
|
|
50
|
+
“Looks good, shipping.” — Sam
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "activity",
|
|
57
|
+
label: "Activity",
|
|
58
|
+
icon: <History />,
|
|
59
|
+
content: (
|
|
60
|
+
<ul className="flex flex-col gap-2 text-body text-muted-foreground">
|
|
61
|
+
<li>Created 2 hours ago</li>
|
|
62
|
+
<li>Edited by Priya, 40 minutes ago</li>
|
|
63
|
+
<li>Reviewed by Sam, 5 minutes ago</li>
|
|
64
|
+
</ul>
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Expanded (`open`, default), three believable sections, the first active. */
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="flex h-[560px] w-full">
|
|
73
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
74
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" />
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
play: async ({ canvas }) => {
|
|
78
|
+
await expect(canvas.getByRole("button", { name: "Sources 4 items" })).toBeInTheDocument();
|
|
79
|
+
await expect(canvas.getByRole("button", { name: "Comments 2 items" })).toBeInTheDocument();
|
|
80
|
+
await expect(canvas.getByRole("button", { name: "Activity" })).toBeInTheDocument();
|
|
81
|
+
await expect(canvas.getByText("Sources")).toBeInTheDocument();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** `defaultOpen={false}` — the collapsed 48px icon strip, still fully operable as a switcher (uncontrolled, so the click-to-expand in the play function actually moves state). */
|
|
86
|
+
export const Collapsed: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="flex h-[560px] w-full">
|
|
89
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
90
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" defaultOpen={false} />
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
play: async ({ canvas, userEvent }) => {
|
|
94
|
+
const sourcesEntry = await canvas.findByRole("button", { name: "Sources 4 items" });
|
|
95
|
+
await expect(sourcesEntry).toBeInTheDocument();
|
|
96
|
+
// Content stays mounted while collapsed (only the ACTIVE section's
|
|
97
|
+
// content is ever mounted at all — collapsing hides it visually via
|
|
98
|
+
// CSS, it doesn't unmount it) — so assert visibility, not presence.
|
|
99
|
+
await expect(canvas.getByText("quarterly-report.pdf — p.12")).not.toBeVisible();
|
|
100
|
+
|
|
101
|
+
// Clicking the ACTIVE entry while collapsed expands the rail again.
|
|
102
|
+
await userEvent.click(sourcesEntry);
|
|
103
|
+
await expect(canvas.getByText("quarterly-report.pdf — p.12")).toBeVisible();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** No sections — the default localized empty state. */
|
|
108
|
+
export const Empty: Story = {
|
|
109
|
+
render: () => (
|
|
110
|
+
<div className="flex h-[560px] w-full">
|
|
111
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
112
|
+
<ContextRail sections={[]} />
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
play: async ({ canvas }) => {
|
|
116
|
+
await expect(await canvas.findByText("No sections")).toBeInTheDocument();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Below `overlayBreakpoint` the rail mounts its own strip plus a `Sheet`
|
|
122
|
+
* instead of `Sidebar`. A viewport-size story parameter cannot force this
|
|
123
|
+
* branch under a headless run (the composed story's `window.innerWidth`
|
|
124
|
+
* doesn't track the addon's viewport there), so this story forces it
|
|
125
|
+
* deterministically with a very high `overlayBreakpoint` instead — reliable
|
|
126
|
+
* both interactively and under `vitest --project storybook`.
|
|
127
|
+
*
|
|
128
|
+
* Known constraint (task-11f-brief.md Finding 5): this story's own fixed
|
|
129
|
+
* `h-[560px]` host is shorter than the browser viewport, so the expanded
|
|
130
|
+
* body's `Sheet` (always viewport-height) visibly outruns the persistent
|
|
131
|
+
* strip below it — see the seam this reproduces in `ContextRail`'s own doc
|
|
132
|
+
* comment.
|
|
133
|
+
*/
|
|
134
|
+
export const Narrow: Story = {
|
|
135
|
+
// The sheet's real entrance/exit transition is load-bearing here: the
|
|
136
|
+
// occlusion hit-test is taken against the SETTLED panel, and the final
|
|
137
|
+
// assertion reads the panel's `data-state="closed"` while its exit animation
|
|
138
|
+
// is still running (Radix unmounts only afterwards). The suite otherwise runs
|
|
139
|
+
// under `prefers-reduced-motion: reduce` (`apps/docs/vitest.config.ts`, #125),
|
|
140
|
+
// which removes that exit window entirely — so this story opts back into full
|
|
141
|
+
// motion rather than the lock being rewritten around a missing animation.
|
|
142
|
+
globals: { motionPref: "full" },
|
|
143
|
+
render: () => (
|
|
144
|
+
<div className="flex h-[560px] w-full">
|
|
145
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
146
|
+
<ContextRail sections={sections} defaultActiveSectionId="sources" overlayBreakpoint={2000} />
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
150
|
+
await expect(canvas.getByRole("button", { name: "Sources 4 items" })).toBeInTheDocument();
|
|
151
|
+
await expect(canvas.getByRole("button", { name: "Comments 2 items" })).toBeInTheDocument();
|
|
152
|
+
await expect(canvasElement.querySelector('[data-slot="sidebar"]')).toBeNull();
|
|
153
|
+
await expect(canvasElement.querySelector('[data-slot="context-rail"]')).not.toBeNull();
|
|
154
|
+
|
|
155
|
+
// `ContextRail` has no `open`/`defaultOpen` here, so `SidebarProvider`'s
|
|
156
|
+
// own default (`true`) drives the Sheet `ContextRailNarrow` wires to that
|
|
157
|
+
// same `open` state — the panel renders OPEN at the same time as the
|
|
158
|
+
// persistent 48px strip, which is exactly the configuration the
|
|
159
|
+
// panel-occludes-the-strip regression (fix round 1, task-9b-fix-1.md)
|
|
160
|
+
// needs to reproduce. `userEvent.click` only checks `pointer-events:
|
|
161
|
+
// none` — it does NOT hit-test — so an occluded-but-visible button would
|
|
162
|
+
// still report a passing click. Assert the browser's REAL hit-test
|
|
163
|
+
// (`document.elementFromPoint`) instead, for an INACTIVE entry and for
|
|
164
|
+
// the ACTIVE/toggle entry — the reviewer's raw-coordinate repro landed
|
|
165
|
+
// on the sheet's own close button from exactly the active entry's
|
|
166
|
+
// position, so both are asserted, not just the easy one.
|
|
167
|
+
async function assertHitTestable(name: string) {
|
|
168
|
+
const button = canvas.getByRole("button", { name });
|
|
169
|
+
// The sheet's own entrance/exit transition (`duration-slow`, 380ms —
|
|
170
|
+
// `themes.css`) animates the panel in via `slide-in-from-right`. The
|
|
171
|
+
// panel is `position: fixed`, so mid-transition it sits translated by
|
|
172
|
+
// roughly its own width — a hit-test taken the instant after the click
|
|
173
|
+
// that opened it would see that IN-FLIGHT position (measured: right
|
|
174
|
+
// edge past the viewport, not its resting inset-by-the-strip-width
|
|
175
|
+
// position) and fail for a reason that has nothing to do with the
|
|
176
|
+
// Blocker this lock exists to catch. Wait for the REAL, settled
|
|
177
|
+
// hit-test to succeed instead of sleeping a fixed amount — this reads
|
|
178
|
+
// as fast as the animation actually is, and still fails for real
|
|
179
|
+
// (after timing out) if the panel keeps covering the strip once
|
|
180
|
+
// settled, which is the only case task-9b-fix-1.md's regression is.
|
|
181
|
+
await waitFor(
|
|
182
|
+
() => {
|
|
183
|
+
const rect = button.getBoundingClientRect();
|
|
184
|
+
if (rect.width === 0) {
|
|
185
|
+
throw new Error(`"${name}" has not been laid out yet (zero width)`);
|
|
186
|
+
}
|
|
187
|
+
const x = rect.left + rect.width / 2;
|
|
188
|
+
const y = rect.top + rect.height / 2;
|
|
189
|
+
const hit = document.elementFromPoint(x, y);
|
|
190
|
+
const hitDescription =
|
|
191
|
+
hit == null
|
|
192
|
+
? "nothing"
|
|
193
|
+
: `<${hit.tagName.toLowerCase()}${
|
|
194
|
+
hit.getAttribute("data-slot")
|
|
195
|
+
? ` data-slot="${hit.getAttribute("data-slot")}"`
|
|
196
|
+
: ""
|
|
197
|
+
}>`;
|
|
198
|
+
expect(
|
|
199
|
+
hit != null && button.contains(hit),
|
|
200
|
+
`expected the centre of "${name}" (${x}, ${y}) to hit-test to that button, but it ` +
|
|
201
|
+
`hit ${hitDescription} instead — the sheet panel is likely covering the strip`,
|
|
202
|
+
).toBe(true);
|
|
203
|
+
},
|
|
204
|
+
{ timeout: 1000 },
|
|
205
|
+
);
|
|
206
|
+
return button;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The switched-to section's content renders inside `SheetContent`, which
|
|
210
|
+
// Radix portals to `document.body` — a real DOM sibling of
|
|
211
|
+
// `canvasElement`, not a descendant of it — so it must be queried via
|
|
212
|
+
// `screen` (bound to `document.body`), not `canvas` (scoped to
|
|
213
|
+
// `canvasElement`).
|
|
214
|
+
|
|
215
|
+
// Inactive entry: clicking it changes the active section.
|
|
216
|
+
const commentsEntry = await assertHitTestable("Comments 2 items");
|
|
217
|
+
await userEvent.click(commentsEntry);
|
|
218
|
+
await expect(screen.getByText("“Looks good, shipping.” — Sam")).toBeVisible();
|
|
219
|
+
|
|
220
|
+
// Active/toggle entry: clicking it collapses the rail (closes the Sheet)
|
|
221
|
+
// rather than changing the section. Radix flips the panel's own
|
|
222
|
+
// `data-state` synchronously with the click (the exit ANIMATION, and the
|
|
223
|
+
// eventual unmount, follow after) — asserting that attribute is the
|
|
224
|
+
// immediate, non-flaky signal that the toggle actually fired, rather
|
|
225
|
+
// than racing the animation to observe visibility.
|
|
226
|
+
const commentsToggleEntry = await assertHitTestable("Comments 2 items");
|
|
227
|
+
await userEvent.click(commentsToggleEntry);
|
|
228
|
+
await expect(screen.getByRole("dialog", { hidden: true })).toHaveAttribute(
|
|
229
|
+
"data-state",
|
|
230
|
+
"closed",
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Eight sections in the default 20rem rail — the count at which the header's
|
|
237
|
+
* two occupants genuinely compete for the band. `sections` is caller-supplied
|
|
238
|
+
* and unbounded, and the switcher is a fixed-height row of 32px buttons, so
|
|
239
|
+
* left uncapped it reserves its full width before the heading gets any: the
|
|
240
|
+
* active section's label was measured down to roughly 20px of usable width
|
|
241
|
+
* (its own padding included), which is no label at all. The cap keeps the
|
|
242
|
+
* heading's half of the band and moves the overflow into the switcher, which
|
|
243
|
+
* scrolls.
|
|
244
|
+
*/
|
|
245
|
+
export const ManySections: Story = {
|
|
246
|
+
render: () => (
|
|
247
|
+
<div className="flex h-[560px] w-full">
|
|
248
|
+
<div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
|
|
249
|
+
<ContextRail
|
|
250
|
+
sections={[
|
|
251
|
+
...sections,
|
|
252
|
+
{ id: "files", label: "Files", icon: <FileText />, content: <p>Files</p> },
|
|
253
|
+
{ id: "notes", label: "Notes", icon: <MessageSquare />, content: <p>Notes</p> },
|
|
254
|
+
{ id: "audit", label: "Audit", icon: <History />, content: <p>Audit</p> },
|
|
255
|
+
{ id: "links", label: "Links", icon: <FileText />, content: <p>Links</p> },
|
|
256
|
+
{ id: "tasks", label: "Tasks", icon: <MessageSquare />, content: <p>Tasks</p> },
|
|
257
|
+
]}
|
|
258
|
+
defaultActiveSectionId="sources"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
),
|
|
262
|
+
play: async ({ canvasElement }) => {
|
|
263
|
+
const header = canvasElement.querySelector<HTMLElement>('[data-slot="context-rail-header"]');
|
|
264
|
+
const heading = canvasElement.querySelector<HTMLElement>('[data-slot="context-rail-heading"]');
|
|
265
|
+
const switcher = canvasElement.querySelector<HTMLElement>(
|
|
266
|
+
'[data-slot="context-rail-switcher"]',
|
|
267
|
+
);
|
|
268
|
+
await expect(header).not.toBeNull();
|
|
269
|
+
await expect(heading).not.toBeNull();
|
|
270
|
+
await expect(switcher).not.toBeNull();
|
|
271
|
+
|
|
272
|
+
const headerWidth = header!.getBoundingClientRect().width;
|
|
273
|
+
const switcherWidth = switcher!.getBoundingClientRect().width;
|
|
274
|
+
const headingWidth = heading!.getBoundingClientRect().width;
|
|
275
|
+
|
|
276
|
+
// The switcher never takes more than half the band…
|
|
277
|
+
await expect(switcherWidth).toBeLessThanOrEqual(headerWidth / 2 + 0.5);
|
|
278
|
+
// …so the heading keeps a width a label can actually render in. The
|
|
279
|
+
// regression this locks measured ~20px here.
|
|
280
|
+
await expect(headingWidth).toBeGreaterThan(80);
|
|
281
|
+
// And the header itself never outgrows the rail (the overflow lives
|
|
282
|
+
// inside the switcher, which scrolls).
|
|
283
|
+
await expect(switcher!.scrollWidth).toBeGreaterThan(switcher!.clientWidth);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
4
|
+
import { FileText, Quote } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { ContextRail, type ContextRailSection } from "./context-rail";
|
|
7
|
+
|
|
8
|
+
const sections: ContextRailSection[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "sources",
|
|
11
|
+
label: "Sources",
|
|
12
|
+
icon: <FileText />,
|
|
13
|
+
count: 3,
|
|
14
|
+
content: <p>Sources body</p>,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "quotes",
|
|
18
|
+
label: "Quotes",
|
|
19
|
+
icon: <Quote />,
|
|
20
|
+
content: <p>Quotes body</p>,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const ORIGINAL_INNER_WIDTH = window.innerWidth;
|
|
25
|
+
|
|
26
|
+
function setViewportWidth(width: number) {
|
|
27
|
+
Object.defineProperty(window, "innerWidth", {
|
|
28
|
+
writable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
value: width,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("ContextRail", () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
setViewportWidth(ORIGINAL_INNER_WIDTH);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("1. is reachable by exact accessible name when collapsed", () => {
|
|
40
|
+
render(<ContextRail sections={sections} open={false} />);
|
|
41
|
+
expect(screen.getByRole("button", { name: "Sources 3 items" })).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByRole("button", { name: "Quotes" })).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("2. mounts only the active section's content", () => {
|
|
46
|
+
render(<ContextRail sections={sections} activeSectionId="sources" />);
|
|
47
|
+
expect(screen.getByText("Sources body")).toBeInTheDocument();
|
|
48
|
+
expect(screen.queryByText("Quotes body")).not.toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("3. calls onActiveSectionChange with the clicked section's id", () => {
|
|
52
|
+
const onActiveSectionChange = vi.fn();
|
|
53
|
+
render(
|
|
54
|
+
<ContextRail
|
|
55
|
+
sections={sections}
|
|
56
|
+
activeSectionId="sources"
|
|
57
|
+
onActiveSectionChange={onActiveSectionChange}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
fireEvent.click(screen.getByRole("button", { name: "Quotes" }));
|
|
61
|
+
expect(onActiveSectionChange).toHaveBeenCalledWith("quotes");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("4. defaults to sections[0] uncontrolled, honours defaultActiveSectionId, and updates on click with no controlled prop", () => {
|
|
65
|
+
const { unmount } = render(<ContextRail sections={sections} />);
|
|
66
|
+
expect(screen.getByText("Sources body")).toBeInTheDocument();
|
|
67
|
+
unmount();
|
|
68
|
+
|
|
69
|
+
render(<ContextRail sections={sections} defaultActiveSectionId="quotes" />);
|
|
70
|
+
expect(screen.getByText("Quotes body")).toBeInTheDocument();
|
|
71
|
+
fireEvent.click(screen.getByRole("button", { name: "Sources 3 items" }));
|
|
72
|
+
expect(screen.getByText("Sources body")).toBeInTheDocument();
|
|
73
|
+
expect(screen.queryByText("Quotes body")).not.toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("5. gives the active entry aria-current=true", () => {
|
|
77
|
+
render(<ContextRail sections={sections} activeSectionId="quotes" />);
|
|
78
|
+
expect(screen.getByRole("button", { name: "Quotes" })).toHaveAttribute("aria-current", "true");
|
|
79
|
+
expect(screen.getByRole("button", { name: "Sources 3 items" })).not.toHaveAttribute(
|
|
80
|
+
"aria-current",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Finding 1 (task-11f-brief.md, BLOCKER): `isActive` was computed but never
|
|
85
|
+
// passed to `SidebarMenuButton`, so `data-active` defaulted to `false` on
|
|
86
|
+
// every entry and the ENTIRE active-state stylesheet (fill, label weight,
|
|
87
|
+
// icon colour, accent bar — all gated on `data-[active=true]:…` in
|
|
88
|
+
// `sidebarMenuButtonVariants`) never applied. Assert BOTH halves: the
|
|
89
|
+
// active entry carries `data-active="true"` AND an inactive one does not —
|
|
90
|
+
// asserting only the first half would pass against code that marks every
|
|
91
|
+
// entry active.
|
|
92
|
+
it("5b. gives the active entry data-active=true and an inactive one data-active=false", () => {
|
|
93
|
+
render(<ContextRail sections={sections} activeSectionId="quotes" />);
|
|
94
|
+
expect(screen.getByRole("button", { name: "Quotes" })).toHaveAttribute("data-active", "true");
|
|
95
|
+
expect(screen.getByRole("button", { name: "Sources 3 items" })).toHaveAttribute(
|
|
96
|
+
"data-active",
|
|
97
|
+
"false",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("6. clicking the active entry toggles open; clicking an inactive entry opens and reports the new section", () => {
|
|
102
|
+
const onOpenChange = vi.fn();
|
|
103
|
+
const { rerender } = render(
|
|
104
|
+
<ContextRail
|
|
105
|
+
sections={sections}
|
|
106
|
+
activeSectionId="sources"
|
|
107
|
+
open={true}
|
|
108
|
+
onOpenChange={onOpenChange}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
fireEvent.click(screen.getByRole("button", { name: "Sources 3 items" }));
|
|
112
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
113
|
+
|
|
114
|
+
onOpenChange.mockClear();
|
|
115
|
+
const onActiveSectionChange = vi.fn();
|
|
116
|
+
rerender(
|
|
117
|
+
<ContextRail
|
|
118
|
+
sections={sections}
|
|
119
|
+
activeSectionId="sources"
|
|
120
|
+
open={false}
|
|
121
|
+
onOpenChange={onOpenChange}
|
|
122
|
+
onActiveSectionChange={onActiveSectionChange}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
fireEvent.click(screen.getByRole("button", { name: "Quotes" }));
|
|
126
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
127
|
+
expect(onActiveSectionChange).toHaveBeenCalledWith("quotes");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("7. disabled sections render a disabled button and do not fire onActiveSectionChange", () => {
|
|
131
|
+
const onActiveSectionChange = vi.fn();
|
|
132
|
+
const disabledSections: ContextRailSection[] = [
|
|
133
|
+
sections[0]!,
|
|
134
|
+
{ ...sections[1]!, disabled: true },
|
|
135
|
+
];
|
|
136
|
+
render(
|
|
137
|
+
<ContextRail
|
|
138
|
+
sections={disabledSections}
|
|
139
|
+
activeSectionId="sources"
|
|
140
|
+
onActiveSectionChange={onActiveSectionChange}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
const quotesButton = screen.getByRole("button", { name: "Quotes" });
|
|
144
|
+
expect(quotesButton).toBeDisabled();
|
|
145
|
+
fireEvent.click(quotesButton);
|
|
146
|
+
expect(onActiveSectionChange).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("8. renders the empty slot for an empty sections array, localized by default, overridable via `empty`", () => {
|
|
150
|
+
const { container, rerender } = render(<ContextRail sections={[]} />);
|
|
151
|
+
const emptySlot = container.querySelector('[data-slot="context-rail-empty"]');
|
|
152
|
+
expect(emptySlot).not.toBeNull();
|
|
153
|
+
expect(within(emptySlot as HTMLElement).getByText("No sections")).toBeInTheDocument();
|
|
154
|
+
|
|
155
|
+
const custom: ReactNode = <p>Nothing to show here</p>;
|
|
156
|
+
rerender(<ContextRail sections={[]} empty={custom} />);
|
|
157
|
+
const customEmptySlot = container.querySelector('[data-slot="context-rail-empty"]');
|
|
158
|
+
expect(customEmptySlot).not.toBeNull();
|
|
159
|
+
expect(screen.getByText("Nothing to show here")).toBeInTheDocument();
|
|
160
|
+
expect(screen.queryByText("No sections")).not.toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("9. publishes `width` as --sidebar-width on the provider element, defaulting to 20rem", () => {
|
|
164
|
+
const { container, rerender } = render(<ContextRail sections={sections} />);
|
|
165
|
+
const provider = container.firstElementChild as HTMLElement;
|
|
166
|
+
expect(provider.getAttribute("style") ?? "").toContain("--sidebar-width: 20rem");
|
|
167
|
+
|
|
168
|
+
rerender(<ContextRail sections={sections} width="24rem" />);
|
|
169
|
+
const providerAfter = container.firstElementChild as HTMLElement;
|
|
170
|
+
expect(providerAfter.getAttribute("style") ?? "").toContain("--sidebar-width: 24rem");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("10. warns exactly once when overlayBreakpoint < 768, and not when >= 768", () => {
|
|
174
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
175
|
+
const { unmount } = render(<ContextRail sections={sections} overlayBreakpoint={500} />);
|
|
176
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
177
|
+
unmount();
|
|
178
|
+
warnSpy.mockClear();
|
|
179
|
+
|
|
180
|
+
render(<ContextRail sections={sections} overlayBreakpoint={768} />);
|
|
181
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
182
|
+
warnSpy.mockRestore();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("11. below overlayBreakpoint renders the narrow strip (no [data-slot=sidebar]) with the same switcher names", () => {
|
|
186
|
+
setViewportWidth(500);
|
|
187
|
+
const { container } = render(<ContextRail sections={sections} />);
|
|
188
|
+
expect(container.querySelector('[data-slot="sidebar"]')).toBeNull();
|
|
189
|
+
expect(container.querySelector('[data-slot="context-rail"]')).not.toBeNull();
|
|
190
|
+
expect(screen.getByRole("button", { name: "Sources 3 items" })).toBeInTheDocument();
|
|
191
|
+
expect(screen.getByRole("button", { name: "Quotes" })).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("12. renders all seven data-slot values on an open, non-empty rail", () => {
|
|
195
|
+
const { container } = render(
|
|
196
|
+
<ContextRail sections={sections} open={true} activeSectionId="sources" />,
|
|
197
|
+
);
|
|
198
|
+
for (const slot of [
|
|
199
|
+
"context-rail",
|
|
200
|
+
"context-rail-switcher",
|
|
201
|
+
"context-rail-switcher-item",
|
|
202
|
+
"context-rail-count",
|
|
203
|
+
"context-rail-heading",
|
|
204
|
+
"context-rail-body",
|
|
205
|
+
]) {
|
|
206
|
+
expect(
|
|
207
|
+
container.querySelector(`[data-slot="${slot}"]`),
|
|
208
|
+
`expected [data-slot="${slot}"] to be present`,
|
|
209
|
+
).not.toBeNull();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// #382 — `variant` used to be a derived (`Omit<ComponentProps<typeof
|
|
214
|
+
// Sidebar>, …>`) member of `ContextRailProps`, honoured only in the wide
|
|
215
|
+
// (`Sidebar`) branch and silently inert below `overlayBreakpoint` (the
|
|
216
|
+
// narrow branch has no `Sidebar` to forward it to). Fixed by declaring the
|
|
217
|
+
// props surface instead of deriving it — `variant` is no longer a public
|
|
218
|
+
// prop at all. This is a compile-time lock: it fails the moment someone
|
|
219
|
+
// re-derives `ContextRailProps` from `Sidebar`, which is the only way the
|
|
220
|
+
// prop returns.
|
|
221
|
+
it("13. (type-level) does not accept a `variant` prop", () => {
|
|
222
|
+
function typeOnly() {
|
|
223
|
+
// @ts-expect-error — `variant` was removed from `ContextRailProps`; a
|
|
224
|
+
// consumer can no longer pass it, and it can no longer be silently
|
|
225
|
+
// inert in the narrow branch.
|
|
226
|
+
return <ContextRail sections={[]} variant="floating" />;
|
|
227
|
+
}
|
|
228
|
+
expect(typeof typeOnly).toBe("function");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Companion to #13: the type was re-derived from `ComponentProps<"div">`
|
|
232
|
+
// and stopped omitting `children` alongside `onSelect`, unintentionally
|
|
233
|
+
// making `children` a public prop again even though both `ContextRailWide`
|
|
234
|
+
// and `ContextRailNarrow` supply their own JSX children after spreading
|
|
235
|
+
// `props` — a caller's `children` type-checks and is then silently
|
|
236
|
+
// discarded at render, the same advertised-but-inert failure #382 fixed
|
|
237
|
+
// for `variant`. Compile-time lock: fails the moment `children` is
|
|
238
|
+
// dropped from the `Omit` again.
|
|
239
|
+
it("15. (type-level) does not accept a `children` prop", () => {
|
|
240
|
+
function typeOnly() {
|
|
241
|
+
// @ts-expect-error — `children` is omitted from `ContextRailProps`;
|
|
242
|
+
// both render branches discard it after spreading `props`, so it
|
|
243
|
+
// must not be publicly accepted.
|
|
244
|
+
return <ContextRail sections={[]}>discarded</ContextRail>;
|
|
245
|
+
}
|
|
246
|
+
expect(typeof typeOnly).toBe("function");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("14. the wide branch renders data-variant=sidebar on the sidebar root; the narrow branch renders no data-variant at all", () => {
|
|
250
|
+
const wide = render(<ContextRail sections={sections} open={true} activeSectionId="sources" />);
|
|
251
|
+
const sidebarRoot = wide.container.querySelector('[data-slot="sidebar"]');
|
|
252
|
+
expect(sidebarRoot).not.toBeNull();
|
|
253
|
+
expect(sidebarRoot).toHaveAttribute("data-variant", "sidebar");
|
|
254
|
+
wide.unmount();
|
|
255
|
+
|
|
256
|
+
setViewportWidth(500);
|
|
257
|
+
const narrow = render(
|
|
258
|
+
<ContextRail sections={sections} open={true} activeSectionId="sources" />,
|
|
259
|
+
);
|
|
260
|
+
expect(narrow.container.querySelector("[data-variant]")).toBeNull();
|
|
261
|
+
narrow.unmount();
|
|
262
|
+
});
|
|
263
|
+
});
|