@tangle-network/ui 11.51.7 → 11.51.9

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -0
  3. package/dist/chat/chat-message.js +2 -7
  4. package/dist/chat/user-message.js +3 -5
  5. package/dist/files/file-card.js +1 -1
  6. package/dist/files/file-tree.d.ts +0 -6
  7. package/dist/files/file-tree.js +2 -6
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/primitives/hover-card.d.ts +22 -13
  11. package/dist/primitives/hover-card.js +149 -40
  12. package/dist/primitives/popover.d.ts +3 -1
  13. package/dist/primitives/popover.js +15 -12
  14. package/dist/primitives/upload-progress.d.ts +0 -6
  15. package/dist/primitives/upload-progress.js +2 -6
  16. package/dist/run/run-group.js +2 -2
  17. package/dist/run/run-item-primitives.js +2 -2
  18. package/dist/run/run-row-shell.js +2 -2
  19. package/dist/utils/format.d.ts +14 -12
  20. package/dist/utils/format.js +50 -30
  21. package/dist/utils.d.ts +2 -2
  22. package/dist/utils.js +2 -2
  23. package/package.json +11 -2
  24. package/src/chat/chat-message.tsx +2 -7
  25. package/src/chat/user-message.tsx +3 -5
  26. package/src/editor/source-markdown-editor.test.tsx +7 -7
  27. package/src/files/file-tree.tsx +2 -7
  28. package/src/primitives/hover-card.tsx +64 -74
  29. package/src/primitives/popover.tsx +3 -3
  30. package/src/primitives/upload-progress.tsx +2 -7
  31. package/src/utils/format.ts +50 -33
  32. package/src/utils/index.ts +1 -1
  33. package/src/editor/document-editor-pane.test.tsx +0 -60
  34. package/src/editor/editor-lazy.test.tsx +0 -102
  35. package/src/editor/editor-peers.test.ts +0 -250
  36. package/src/editor/editor-provider.test.tsx +0 -226
  37. package/src/editor/markdown-document-editor.test.tsx +0 -126
  38. package/src/editor/tiptap-editor.test.tsx +0 -228
  39. package/src/files/file-artifact-pane.test.tsx +0 -301
  40. package/src/files/file-card.test.tsx +0 -62
  41. package/src/files/file-list.test.tsx +0 -81
  42. package/src/files/pdf-loader.test.ts +0 -130
  43. package/src/files/pdf-viewer.test.tsx +0 -245
  44. package/src/files/rich-file-tree.test.tsx +0 -90
  45. package/src/markdown/api-presentation.test.tsx +0 -63
  46. package/src/markdown/code-block.test.tsx +0 -201
  47. package/src/markdown/markdown-table.test.tsx +0 -76
  48. package/src/markdown/markdown.test.tsx +0 -166
  49. package/src/openui/schema.test.tsx +0 -158
  50. package/src/primitives/button.test.tsx +0 -230
  51. package/src/primitives/card.test.tsx +0 -71
  52. package/src/primitives/chip.test.tsx +0 -200
  53. package/src/primitives/date-picker.test.tsx +0 -146
  54. package/src/primitives/dialog.test.tsx +0 -75
  55. package/src/primitives/input.test.tsx +0 -161
  56. package/src/primitives/interaction-states.test.tsx +0 -287
  57. package/src/primitives/pagination-alert.test.tsx +0 -180
  58. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
  59. package/src/primitives/radio-group.test.tsx +0 -26
  60. package/src/primitives/segmented-control.test.tsx +0 -451
  61. package/src/primitives/table.test.tsx +0 -72
  62. package/src/primitives/theme-toggle.test.tsx +0 -168
  63. package/src/primitives/toast.test.tsx +0 -130
  64. package/src/primitives/wizard.test.tsx +0 -106
  65. package/src/run/expandable-content.test.tsx +0 -52
  66. package/src/utils/format.test.ts +0 -58
@@ -1,75 +0,0 @@
1
- import { createRef, type ComponentProps } from "react";
2
- import { render, screen, waitFor } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "./dialog";
6
-
7
- function Example(props: ComponentProps<typeof DialogContent>) {
8
- return (
9
- <Dialog defaultOpen>
10
- <DialogContent {...props}>
11
- <DialogTitle>Confirm operation</DialogTitle>
12
- <DialogDescription>Choose whether to continue.</DialogDescription>
13
- <DialogClose asChild><button type="button">Cancel operation</button></DialogClose>
14
- </DialogContent>
15
- </Dialog>
16
- );
17
- }
18
-
19
- describe("DialogContent close-button visibility", () => {
20
- it("retains the default accessible close button and its dismissal behavior", async () => {
21
- const user = userEvent.setup();
22
- render(<Example />);
23
- expect(screen.getByRole("dialog", { name: "Confirm operation" })).toBeInTheDocument();
24
- await user.click(screen.getByRole("button", { name: "Close", exact: true }));
25
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
26
- });
27
-
28
- it.each(["default", "sandbox"] as const)("hides only the built-in button for the %s variant", async (variant) => {
29
- const user = userEvent.setup();
30
- render(<Example hideCloseButton variant={variant} />);
31
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
32
- expect(screen.getByRole("dialog")).not.toHaveAttribute("hideCloseButton");
33
- expect(screen.getByRole("dialog")).toHaveAccessibleDescription("Choose whether to continue.");
34
- await user.click(screen.getByRole("button", { name: "Cancel operation" }));
35
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
36
- });
37
-
38
- it("restores the same default button when visibility changes", () => {
39
- const { rerender } = render(<Example hideCloseButton />);
40
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
41
- rerender(<Example hideCloseButton={false} />);
42
- expect(screen.getByRole("button", { name: "Close", exact: true })).toBeInTheDocument();
43
- rerender(<Example hideCloseButton />);
44
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
45
- });
46
-
47
- it("does not silently disable Escape when the close button is hidden", async () => {
48
- const user = userEvent.setup();
49
- render(<Example hideCloseButton />);
50
- await user.keyboard("{Escape}");
51
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
52
- });
53
-
54
- it("preserves the caller's Escape guard and an explicit keyboard-operable exit", async () => {
55
- const user = userEvent.setup();
56
- const guard = vi.fn((event: KeyboardEvent) => event.preventDefault());
57
- render(<Example hideCloseButton onEscapeKeyDown={guard} />);
58
- await user.keyboard("{Escape}");
59
- expect(guard).toHaveBeenCalledOnce();
60
- expect(screen.getByRole("dialog")).toBeInTheDocument();
61
- screen.getByRole("button", { name: "Cancel operation" }).focus();
62
- await user.keyboard("{Enter}");
63
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
64
- });
65
-
66
- it("forwards content refs, native attributes and class overrides", () => {
67
- const ref = createRef<HTMLDivElement>();
68
- render(<Example ref={ref} hideCloseButton id="operation-dialog" data-example="retained" className="max-w-xl" />);
69
- const dialog = screen.getByRole("dialog");
70
- expect(ref.current).toBe(dialog);
71
- expect(dialog).toHaveAttribute("id", "operation-dialog");
72
- expect(dialog).toHaveAttribute("data-example", "retained");
73
- expect(dialog).toHaveClass("max-w-xl");
74
- });
75
- });
@@ -1,161 +0,0 @@
1
- import * as React from "react";
2
- import { act, cleanup, render, screen, within } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { hydrateRoot, type Root } from "react-dom/client";
5
- import { renderToString } from "react-dom/server";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { Input, Textarea } from "./input";
8
- import { Label } from "./label";
9
-
10
- afterEach(cleanup);
11
-
12
- describe.each([
13
- ["Input", Input],
14
- ["Textarea", Textarea],
15
- ] as const)("%s relationships", (_name, Field) => {
16
- it("gives repeated labels distinct, stable IDs and focuses the correct field", async () => {
17
- const user = userEvent.setup();
18
- const { rerender } = render(<><Field label="Name" /><Field label="Name" /></>);
19
- const fields = screen.getAllByRole("textbox");
20
- const ids = fields.map((field) => field.id);
21
- expect(ids.every(Boolean)).toBe(true);
22
- expect(new Set(ids).size).toBe(2);
23
- expect(screen.getAllByLabelText("Name")).toEqual(fields);
24
- for (const [index, label] of screen.getAllByText("Name", { selector: "label" }).entries()) {
25
- expect(label).toHaveAttribute("for", ids[index]);
26
- await user.click(label);
27
- expect(fields[index]).toHaveFocus();
28
- }
29
- rerender(<><Field label="Renamed" /><Field label="Renamed" /></>);
30
- expect(screen.getAllByRole("textbox").map((field) => field.id)).toEqual(ids);
31
- });
32
-
33
- it("preserves an explicit ID and merges caller, hint and error descriptions without duplicates", () => {
34
- render(<>
35
- <p id="external">External instructions.</p>
36
- <Field
37
- id="profile"
38
- label="Name"
39
- hint="Use your full name."
40
- error="Name is required."
41
- aria-describedby={" external external\tprofile-hint "}
42
- aria-invalid={false}
43
- />
44
- </>);
45
- const field = screen.getByRole("textbox", { name: "Name" });
46
- expect(field).toHaveAttribute("id", "profile");
47
- expect(field).toHaveAttribute("aria-describedby", "external profile-hint profile-error");
48
- expect(screen.getByText("Use your full name.")).toHaveAttribute("id", "profile-hint");
49
- expect(screen.getByText("Name is required.")).toHaveAttribute("id", "profile-error");
50
- expect(field).toHaveAccessibleDescription("External instructions. Use your full name. Name is required.");
51
- expect(field).toHaveAttribute("aria-invalid", "true");
52
- });
53
-
54
- it("associates descriptions without a convenience label and removes stale relationships", () => {
55
- const { rerender } = render(<Field aria-label="Name" hint="A hint" error="An error" />);
56
- const field = screen.getByRole("textbox", { name: "Name" });
57
- const id = field.id;
58
- expect(id).not.toBe("");
59
- expect(field).toHaveAccessibleDescription("A hint An error");
60
- rerender(<Field aria-label="Name" hint="A hint" />);
61
- expect(field.id).toBe(id);
62
- expect(field).toHaveAttribute("aria-describedby", `${id}-hint`);
63
- expect(field).not.toHaveAttribute("aria-invalid");
64
- expect(document.getElementById(`${id}-error`)).toBeNull();
65
- rerender(<Field aria-label="Name" />);
66
- const bareField = screen.getByRole("textbox", { name: "Name" });
67
- expect(bareField.id).toBe(id);
68
- expect(bareField).not.toHaveAttribute("aria-describedby");
69
- expect(document.getElementById(`${id}-hint`)).toBeNull();
70
- });
71
-
72
- it("preserves the bare-element API and caller invalid states", () => {
73
- const { container, rerender } = render(
74
- <Field aria-label="Name" aria-describedby="external" aria-invalid="spelling" className="caller-class" />,
75
- );
76
- const field = screen.getByRole("textbox");
77
- expect(container.firstElementChild).toBe(field);
78
- expect(container.childElementCount).toBe(1);
79
- expect(field).toHaveAttribute("aria-describedby", "external");
80
- expect(field).toHaveAttribute("aria-invalid", "spelling");
81
- expect(field).toHaveClass("caller-class");
82
- rerender(<Field aria-label="Name" aria-invalid={false} />);
83
- expect(field).toHaveAttribute("aria-invalid", "false");
84
- });
85
-
86
- it("uses the existing Label treatment", () => {
87
- render(<><Label className="block">Reference</Label><Field label="Name" /></>);
88
- expect(screen.getByText("Name", { selector: "label" }).className)
89
- .toBe(screen.getByText("Reference").className);
90
- });
91
- });
92
-
93
- it("keeps Input and Textarea IDs distinct even with the same label", () => {
94
- render(<><Input label="Notes" /><Textarea label="Notes" /></>);
95
- expect(new Set(screen.getAllByRole("textbox").map((field) => field.id)).size).toBe(2);
96
- });
97
-
98
- it("exposes Input's error variant without requiring error text", () => {
99
- render(<Input aria-label="Name" variant="error" />);
100
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
101
- });
102
-
103
- it("forwards native field refs, values and change handlers", async () => {
104
- const user = userEvent.setup();
105
- const inputRef = React.createRef<HTMLInputElement>();
106
- const textareaRef = React.createRef<HTMLTextAreaElement>();
107
- const inputChange = vi.fn();
108
- const textareaChange = vi.fn();
109
- const { unmount } = render(<>
110
- <Input ref={inputRef} label="Name" defaultValue="A" onChange={inputChange} />
111
- <Textarea ref={textareaRef} label="Notes" defaultValue="B" onChange={textareaChange} />
112
- </>);
113
- expect(inputRef.current).toBe(screen.getByRole("textbox", { name: "Name" }));
114
- expect(textareaRef.current).toBe(screen.getByRole("textbox", { name: "Notes" }));
115
- await user.type(inputRef.current!, "lice");
116
- await user.type(textareaRef.current!, "ody");
117
- expect(inputRef.current).toHaveValue("Alice");
118
- expect(textareaRef.current).toHaveValue("Body");
119
- expect(inputChange).toHaveBeenCalled();
120
- expect(textareaChange).toHaveBeenCalled();
121
- unmount();
122
- expect(inputRef.current).toBeNull();
123
- expect(textareaRef.current).toBeNull();
124
- });
125
-
126
- it("hydrates SSR IDs and all description relationships without mismatch", async () => {
127
- const ui = <React.StrictMode>
128
- <p id="external">External instructions.</p>
129
- <Input label="Name" hint="First hint" />
130
- <Input label="Name" error="Required" />
131
- <Textarea label="Name" hint="Notes hint" error="Too short" aria-describedby="external" />
132
- <Input id="explicit" label="Explicit" hint="Explicit hint" />
133
- </React.StrictMode>;
134
- const container = document.createElement("div");
135
- container.innerHTML = renderToString(ui, { identifierPrefix: "fields-" });
136
- document.body.appendChild(container);
137
- const serverHTML = container.innerHTML;
138
- const serverIds = within(container).getAllByRole("textbox").map((field) => field.id);
139
- const recoverableError = vi.fn();
140
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
141
- let root: Root | undefined;
142
- try {
143
- await act(async () => {
144
- root = hydrateRoot(container, ui, { identifierPrefix: "fields-", onRecoverableError: recoverableError });
145
- });
146
- expect(new Set(serverIds).size).toBe(4);
147
- expect(within(container).getAllByRole("textbox").map((field) => field.id)).toEqual(serverIds);
148
- expect(container.innerHTML).toBe(serverHTML);
149
- for (const field of within(container).getAllByRole("textbox")) {
150
- for (const id of field.getAttribute("aria-describedby")!.split(" ")) {
151
- expect(document.getElementById(id)).not.toBeNull();
152
- }
153
- }
154
- expect(recoverableError).not.toHaveBeenCalled();
155
- expect(consoleError).not.toHaveBeenCalled();
156
- } finally {
157
- await act(async () => { root?.unmount(); });
158
- consoleError.mockRestore();
159
- container.remove();
160
- }
161
- });
@@ -1,287 +0,0 @@
1
- import * as React from "react";
2
- import { cleanup, render, screen } from "@testing-library/react";
3
- import { compile } from "tailwindcss";
4
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
5
- import { focusFieldWithin } from "../lib/focus";
6
- import {
7
- ixControl,
8
- ixControlAfter,
9
- ixControlSoft,
10
- ixField,
11
- ixRow,
12
- ixSelected,
13
- ixTabUnderline,
14
- } from "../lib/interaction";
15
- import { Button } from "./button";
16
- import { Card, CardDescription } from "./card";
17
- import {
18
- DropdownMenu,
19
- DropdownMenuCheckboxItem,
20
- DropdownMenuContent,
21
- DropdownMenuItem,
22
- DropdownMenuRadioGroup,
23
- DropdownMenuRadioItem,
24
- DropdownMenuSub,
25
- DropdownMenuSubTrigger,
26
- DropdownMenuTrigger,
27
- } from "./dropdown-menu";
28
- import { Input, Textarea } from "./input";
29
- import { SegmentedControl } from "./segmented-control";
30
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
31
- import { Tabs, TabsList, TabsTrigger } from "./tabs";
32
-
33
- /**
34
- * Every primitive draws its interaction states from one helper in
35
- * lib/interaction.ts (ROOT-SPEC section b). This is the cheap static guard:
36
- * each primitive's rendered className carries every class of its helper, so a
37
- * later edit or a tailwind-merge conflict cannot silently drop a state. The
38
- * second suite compiles each helper with Tailwind to prove the classes emit the
39
- * declarations and selectors the states need.
40
- */
41
- afterEach(cleanup);
42
- beforeAll(() => {
43
- // jsdom lacks the layout APIs Radix Select and Menu call while opening.
44
- Element.prototype.scrollIntoView ??= vi.fn();
45
- Element.prototype.hasPointerCapture ??= vi.fn(() => false);
46
- Element.prototype.releasePointerCapture ??= vi.fn();
47
- });
48
-
49
- const tokens = (helper: string) => helper.split(/\s+/).filter(Boolean);
50
- const classes = (el: Element) => new Set(el.className.split(/\s+/));
51
-
52
- /** Every class of every helper is on the element. */
53
- function expectHelpers(el: Element, ...helpers: string[]) {
54
- const have = classes(el);
55
- for (const helper of helpers) {
56
- for (const token of tokens(helper)) expect(have, `${el.tagName} is missing ${token}`).toContain(token);
57
- }
58
- }
59
-
60
- /** The glow only strong controls get. */
61
- const HOVER_GLOW = "hover:shadow-[var(--ix-hover)]";
62
- /** focusRing, which every control keeps exactly. */
63
- const FOCUS_RING = [
64
- "focus-visible:outline-1",
65
- "focus-visible:outline-[var(--focus-border)]",
66
- "focus-visible:ring-4",
67
- "focus-visible:ring-[var(--focus-halo)]",
68
- ];
69
- /** focusField's `:focus` part, which every field keeps exactly. */
70
- const FOCUS_FIELD = ["focus:outline-hidden", "focus:border-[var(--focus-border)]", "focus:ring-3", "focus:ring-[var(--focus-halo)]"];
71
-
72
- describe("each primitive renders its interaction helper", () => {
73
- it.each(["default", "destructive", "secondary", "outline", "sandbox"] as const)(
74
- "Button %s takes ixControl and keeps the focus ring",
75
- (variant) => {
76
- render(<Button variant={variant}>Go</Button>);
77
- const button = screen.getByRole("button");
78
- expectHelpers(button, ixControl);
79
- for (const token of FOCUS_RING) expect(classes(button)).toContain(token);
80
- },
81
- );
82
-
83
- it.each(["ghost", "link", "bare"] as const)("Button %s takes ixControlSoft and never the hover glow", (variant) => {
84
- render(<Button variant={variant}>Go</Button>);
85
- const button = screen.getByRole("button");
86
- expectHelpers(button, ixControlSoft);
87
- expect(classes(button)).not.toContain(HOVER_GLOW);
88
- expect(classes(button)).not.toContain("hover:bg-muted");
89
- for (const token of FOCUS_RING) expect(classes(button)).toContain(token);
90
- });
91
-
92
- it.each(["outline", "ghost", "bare"] as const)("a %s toggle Button (aria-pressed) adds ixControl and ixSelected", (variant) => {
93
- render(<Button variant={variant} aria-pressed>Bold</Button>);
94
- expectHelpers(screen.getByRole("button"), ixControl, ixSelected);
95
- });
96
-
97
- it("Input and Textarea take ixField with focusField's focus border and halo", () => {
98
- render(<><Input aria-label="Name" /><Textarea aria-label="Notes" /></>);
99
- for (const field of [screen.getByLabelText("Name"), screen.getByLabelText("Notes")]) {
100
- expectHelpers(field, ixField);
101
- for (const token of FOCUS_FIELD) expect(classes(field)).toContain(token);
102
- expect(classes(field)).not.toContain("border-border");
103
- expect(classes(field)).not.toContain("hover:border-[var(--border-strong)]");
104
- }
105
- });
106
-
107
- it("focusFieldWithin is a field: ixField plus the has-focus ring, without the neutral hover", () => {
108
- expect(tokens(focusFieldWithin)).toEqual(expect.arrayContaining(tokens(ixField)));
109
- expect(focusFieldWithin).toContain("has-[");
110
- expect(focusFieldWithin).toContain(":focus]:ring-[var(--focus-halo)]");
111
- expect(tokens(focusFieldWithin)).not.toContain("hover:border-[var(--border-strong)]");
112
- expect(tokens(focusFieldWithin)).not.toContain("border-border");
113
- });
114
-
115
- it("SelectTrigger is a field that keeps its well and an after-click ring while open", () => {
116
- render(<Select><SelectTrigger aria-label="Status"><SelectValue /></SelectTrigger></Select>);
117
- const trigger = screen.getByRole("combobox");
118
- expectHelpers(trigger, ixField, ixControlAfter);
119
- for (const token of FOCUS_FIELD) expect(classes(trigger)).toContain(token);
120
- expect(classes(trigger)).toContain("data-[state=open]:shadow-[var(--ix-after)]");
121
- expect(classes(trigger)).toContain("data-[state=open]:border-[var(--ix-hover-border)]");
122
- // The open state must not repaint the well.
123
- expect([...classes(trigger)].filter((c) => c.startsWith("data-[state=open]:bg-"))).toEqual([]);
124
- });
125
-
126
- it("SelectContent paints the popover level and SelectItem takes ixRow", async () => {
127
- render(
128
- <Select open value="a">
129
- <SelectTrigger aria-label="Status"><SelectValue /></SelectTrigger>
130
- <SelectContent><SelectItem value="a">Active</SelectItem><SelectItem value="b">Paused</SelectItem></SelectContent>
131
- </Select>,
132
- );
133
- const listbox = await screen.findByRole("listbox");
134
- const content = listbox.closest("[data-radix-select-viewport]")?.parentElement ?? listbox;
135
- expect(classes(content)).toContain("bg-popover");
136
- expect(classes(content)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
137
- expect(classes(content)).not.toContain("bg-card");
138
- for (const option of screen.getAllByRole("option")) {
139
- expectHelpers(option, ixRow);
140
- expect(classes(option)).not.toContain("focus:bg-[var(--accent-surface-strong)]");
141
- }
142
- });
143
-
144
- it("DropdownMenu content paints the popover level and every item kind takes ixRow", async () => {
145
- render(
146
- <DropdownMenu open>
147
- <DropdownMenuTrigger>Menu</DropdownMenuTrigger>
148
- <DropdownMenuContent>
149
- <DropdownMenuItem>Rename</DropdownMenuItem>
150
- <DropdownMenuCheckboxItem checked>Pinned</DropdownMenuCheckboxItem>
151
- <DropdownMenuRadioGroup value="a"><DropdownMenuRadioItem value="a">Newest</DropdownMenuRadioItem></DropdownMenuRadioGroup>
152
- <DropdownMenuSub><DropdownMenuSubTrigger>Move to</DropdownMenuSubTrigger></DropdownMenuSub>
153
- </DropdownMenuContent>
154
- </DropdownMenu>,
155
- );
156
- const menu = await screen.findByRole("menu");
157
- expect(classes(menu)).toContain("bg-popover");
158
- expect(classes(menu)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
159
- expect(classes(menu)).not.toContain("bg-card");
160
- const items = [
161
- screen.getByRole("menuitem", { name: "Rename" }),
162
- screen.getByRole("menuitemcheckbox", { name: "Pinned" }),
163
- screen.getByRole("menuitemradio", { name: "Newest" }),
164
- screen.getByRole("menuitem", { name: "Move to" }),
165
- ];
166
- for (const item of items) {
167
- expectHelpers(item, ixRow);
168
- expect(classes(item)).not.toContain("focus:bg-muted/50");
169
- }
170
- });
171
-
172
- it("Card is a ladder level with a shadow; hover and interactive cards take ixControl", () => {
173
- render(
174
- <>
175
- <Card data-testid="plain" />
176
- <Card data-testid="hover" hover />
177
- <Card data-testid="interactive" interactive />
178
- <CardDescription>Guests, staff and vendors message your business on WhatsApp.</CardDescription>
179
- </>,
180
- );
181
- const plain = screen.getByTestId("plain");
182
- expect(plain).toHaveAttribute("data-slot", "card");
183
- expect(classes(plain)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
184
- expect(classes(plain)).not.toContain(HOVER_GLOW);
185
- for (const id of ["hover", "interactive"]) {
186
- const card = screen.getByTestId(id);
187
- expectHelpers(card, ixControl);
188
- expect(card).not.toHaveAttribute("interactive");
189
- expect(classes(card)).not.toContain("hover:border-[var(--border-strong)]");
190
- }
191
- expect(classes(screen.getByText(/Guests, staff/))).toContain("line-clamp-2");
192
- });
193
-
194
- it("SegmentedControl row: a quiet well track, soft items, a raised thumb on aria-checked and no ring", () => {
195
- const options = [{ value: "a", label: "A–Z" }, { value: "b", label: "Recent" }];
196
- render(<SegmentedControl aria-label="Sort" value="a" onValueChange={() => {}} options={options} />);
197
- const track = screen.getByRole("radiogroup");
198
- expect(classes(track)).toContain("bg-[var(--well)]");
199
- expect(classes(track)).toContain("border-border");
200
- expect(classes(track)).not.toContain("bg-card");
201
- for (const radio of screen.getAllByRole("radio")) {
202
- expectHelpers(radio, ixControlSoft);
203
- expect(classes(radio)).toContain("aria-checked:bg-card");
204
- expect([...classes(radio)].some((token) => token.includes("--ix-sel-ring"))).toBe(false);
205
- for (const token of FOCUS_RING) expect(classes(radio)).toContain(token);
206
- expect(classes(radio)).not.toContain("bg-[var(--accent-surface)]");
207
- }
208
- });
209
-
210
- it("SegmentedControl tabs: the underline helper and a transparent border slot", () => {
211
- const options = [{ value: "a", label: "Team" }, { value: "b", label: "Roles" }];
212
- render(<SegmentedControl aria-label="Section" variant="tabs" value="a" onValueChange={() => {}} options={options} />);
213
- for (const radio of screen.getAllByRole("radio")) {
214
- expectHelpers(radio, ixTabUnderline);
215
- expect(classes(radio)).toContain("border-transparent");
216
- expect(classes(radio)).not.toContain("border-[var(--accent-text)]");
217
- }
218
- });
219
-
220
- it.each([
221
- ["default", [ixControlSoft, ixSelected]],
222
- ["pills", [ixControlSoft, ixSelected]],
223
- ["underline", [ixTabUnderline]],
224
- ] as const)("TabsTrigger %s takes its helper", (variant, helpers) => {
225
- render(
226
- <Tabs defaultValue="a">
227
- <TabsList variant={variant}>
228
- <TabsTrigger variant={variant} value="a">Overview</TabsTrigger>
229
- <TabsTrigger variant={variant} value="b">Activity</TabsTrigger>
230
- </TabsList>
231
- </Tabs>,
232
- );
233
- for (const tab of screen.getAllByRole("tab")) {
234
- expectHelpers(tab, ...helpers);
235
- for (const token of FOCUS_RING) expect(classes(tab)).toContain(token);
236
- expect([...classes(tab)]).not.toEqual(expect.arrayContaining(["data-[state=active]:bg-background"]));
237
- expect(classes(tab)).not.toContain("data-[state=active]:border-primary");
238
- }
239
- });
240
- });
241
-
242
- describe("Tailwind compiles every interaction class to the state it names", () => {
243
- /** A fresh compiler per class, so each class's CSS is read on its own. */
244
- async function cssFor(candidate: string) {
245
- const compiler = await compile("@tailwind utilities;");
246
- return compiler.build([candidate]);
247
- }
248
-
249
- const all = [...new Set([ixControl, ixControlAfter, ixControlSoft, ixField, ixSelected, ixTabUnderline, ixRow].flatMap(tokens))];
250
-
251
- it.each(all)("%s emits a rule", async (candidate) => {
252
- expect(await cssFor(candidate)).toMatch(/\{[^}]*:[^}]*\}/);
253
- });
254
-
255
- it("composes the hover and press glows with the focus ring instead of replacing it", async () => {
256
- for (const [candidate, value] of [
257
- ["hover:shadow-[var(--ix-hover)]", "var(--ix-hover)"],
258
- ["active:shadow-[var(--ix-press)]", "var(--ix-press)"],
259
- ]) {
260
- const css = await cssFor(candidate);
261
- expect(css).toContain(`--tw-shadow: ${value}`);
262
- expect(css).toContain("var(--tw-ring-shadow), var(--tw-shadow)");
263
- }
264
- });
265
-
266
- it("keys the after-click ring on mouse focus only", async () => {
267
- const css = await cssFor("focus:not-focus-visible:shadow-[var(--ix-after)]");
268
- expect(css).toContain(":focus:not(:focus-visible)");
269
- expect(css).toContain("--tw-shadow: var(--ix-after)");
270
- });
271
-
272
- it("reads the field fill and border from the surface context", async () => {
273
- expect(await cssFor("bg-[var(--field-surface,var(--well))]")).toContain("background-color: var(--field-surface,var(--well))");
274
- expect(await cssFor("border-[var(--field-border)]")).toContain("border-color: var(--field-border)");
275
- expect(await cssFor("hover:border-[var(--ix-hover-border)]")).toContain("border-color: var(--ix-hover-border)");
276
- });
277
-
278
- it("draws selection on aria-pressed, aria-checked, aria-current, data-state on and active", async () => {
279
- const css = await cssFor("aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]");
280
- expect(css).toContain('[aria-current="page"]');
281
- expect(css).toContain("inset 0 0 0 1px");
282
- expect(await cssFor("aria-pressed:bg-[var(--ix-sel-bg)]")).toContain('[aria-pressed="true"]');
283
- expect(await cssFor("aria-checked:bg-[var(--ix-sel-bg)]")).toContain('[aria-checked="true"]');
284
- expect(await cssFor("data-[state=active]:bg-[var(--ix-sel-bg)]")).toContain('[data-state="active"]');
285
- expect(await cssFor("data-[highlighted]:bg-[var(--ix-sel-bg)]")).toContain("[data-highlighted]");
286
- });
287
- });