@xenide-io/the-old-ui-theme 0.2.9 → 0.3.1

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 (133) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-5HSOBJ4F.mjs +5968 -0
  3. package/dist/index-BgG8Homu.d.mts +1461 -0
  4. package/dist/index-BgG8Homu.d.ts +1461 -0
  5. package/dist/index.d.mts +206 -100
  6. package/dist/index.d.ts +206 -100
  7. package/dist/index.js +4275 -1455
  8. package/dist/index.mjs +689 -735
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +3 -2
  12. package/dist/ui.d.ts +3 -2
  13. package/dist/ui.js +3449 -701
  14. package/dist/ui.mjs +106 -3
  15. package/package.json +20 -16
  16. package/src/app/globals.css +1953 -266
  17. package/src/components/charts/index.ts +9 -8
  18. package/src/components/charts/quill/BarChart.tsx +85 -0
  19. package/src/components/charts/quill/FunnelChart.tsx +49 -0
  20. package/src/components/charts/quill/InsightShell.tsx +27 -0
  21. package/src/components/charts/quill/MetricCard.tsx +44 -0
  22. package/src/components/charts/quill/PieChart.tsx +81 -0
  23. package/src/components/charts/quill/Sparkline.tsx +57 -0
  24. package/src/components/charts/quill/TimeSeriesLineChart.tsx +62 -0
  25. package/src/components/charts/quill/index.ts +9 -0
  26. package/src/components/icons/IconBase.tsx +20 -12
  27. package/src/components/icons/createIconBase.tsx +12 -7
  28. package/src/components/icons/icons.tsx +210 -18
  29. package/src/components/icons/index.ts +3 -0
  30. package/src/components/icons/lemon-brand-icons.tsx +16 -0
  31. package/src/components/icons/lemon-category-icons.tsx +72 -0
  32. package/src/components/icons/lemon-icons.tsx +57 -0
  33. package/src/components/sections/AlertShowcase.tsx +2 -1
  34. package/src/components/sections/BadgeShowcase.tsx +1 -1
  35. package/src/components/sections/ButtonShowcase.tsx +6 -10
  36. package/src/components/sections/CardShowcase.tsx +22 -6
  37. package/src/components/sections/DropdownShowcase.tsx +4 -6
  38. package/src/components/sections/FilterChipsShowcase.tsx +74 -26
  39. package/src/components/sections/FormShowcase.tsx +2 -1
  40. package/src/components/sections/IconShowcase.tsx +195 -212
  41. package/src/components/sections/KbdShowcase.tsx +47 -21
  42. package/src/components/sections/ModalShowcase.tsx +7 -6
  43. package/src/components/sections/NewComponentsShowcase.tsx +1052 -0
  44. package/src/components/sections/ProductLayoutsShowcase.tsx +78 -0
  45. package/src/components/sections/ProgressShowcase.tsx +50 -53
  46. package/src/components/sections/QuillChartShowcase.tsx +92 -0
  47. package/src/components/sections/QuillDashboardShowcase.tsx +149 -0
  48. package/src/components/sections/SidebarShowcase.tsx +151 -0
  49. package/src/components/sections/SwapShowcase.tsx +13 -5
  50. package/src/components/sections/TableShowcase.tsx +2 -0
  51. package/src/components/sections/TooltipShowcase.tsx +19 -25
  52. package/src/components/ui/Alert.tsx +28 -19
  53. package/src/components/ui/AlertDialog.tsx +62 -0
  54. package/src/components/ui/AppLayout.tsx +27 -0
  55. package/src/components/ui/AuthLayout.tsx +58 -0
  56. package/src/components/ui/Autocomplete.tsx +106 -0
  57. package/src/components/ui/Badge.tsx +10 -5
  58. package/src/components/ui/Banner.tsx +63 -0
  59. package/src/components/ui/Button.tsx +22 -12
  60. package/src/components/ui/ButtonChrome.tsx +1 -1
  61. package/src/components/ui/ButtonGroup.tsx +16 -0
  62. package/src/components/ui/Calendar.tsx +6 -7
  63. package/src/components/ui/Card.tsx +21 -11
  64. package/src/components/ui/Chip.tsx +46 -0
  65. package/src/components/ui/Collapsible.tsx +46 -0
  66. package/src/components/ui/CollapsibleSection.tsx +11 -11
  67. package/src/components/ui/Combobox.tsx +119 -0
  68. package/src/components/ui/ContextMenu.tsx +81 -0
  69. package/src/components/ui/DataTable.tsx +133 -0
  70. package/src/components/ui/DatePicker.tsx +96 -0
  71. package/src/components/ui/Dialog.tsx +75 -0
  72. package/src/components/ui/Divider.tsx +39 -0
  73. package/src/components/ui/Dot.tsx +32 -0
  74. package/src/components/ui/DropdownMenu.tsx +189 -65
  75. package/src/components/ui/FilterBar.tsx +165 -0
  76. package/src/components/ui/FilterChips.tsx +65 -35
  77. package/src/components/ui/FilterControls.tsx +30 -0
  78. package/src/components/ui/FormField.tsx +69 -0
  79. package/src/components/ui/Input.tsx +166 -140
  80. package/src/components/ui/InputGroup.tsx +16 -0
  81. package/src/components/ui/Kbd.tsx +88 -13
  82. package/src/components/ui/Lettermark.tsx +47 -0
  83. package/src/components/ui/Link.tsx +32 -0
  84. package/src/components/ui/Loader.tsx +96 -0
  85. package/src/components/ui/LoadingBar.tsx +25 -0
  86. package/src/components/ui/Menubar.tsx +66 -0
  87. package/src/components/ui/Metric.tsx +34 -0
  88. package/src/components/ui/Modal.tsx +66 -36
  89. package/src/components/ui/NumberField.tsx +76 -0
  90. package/src/components/ui/Popover.tsx +73 -0
  91. package/src/components/ui/ProgressCircle.tsx +63 -0
  92. package/src/components/ui/Resizable.tsx +71 -0
  93. package/src/components/ui/Row.tsx +51 -0
  94. package/src/components/ui/ScrollArea.tsx +54 -0
  95. package/src/components/ui/SettingsLayout.tsx +94 -0
  96. package/src/components/ui/ShowcaseWrapper.tsx +4 -16
  97. package/src/components/ui/Sidebar.tsx +77 -0
  98. package/src/components/ui/Snack.tsx +48 -0
  99. package/src/components/ui/Spinner.tsx +30 -0
  100. package/src/components/ui/Splotch.tsx +33 -0
  101. package/src/components/ui/Stepper.tsx +5 -6
  102. package/src/components/ui/Table.tsx +2 -0
  103. package/src/components/ui/Tag.tsx +65 -0
  104. package/src/components/ui/Toggle.tsx +62 -0
  105. package/src/components/ui/Tooltip.test.tsx +58 -0
  106. package/src/components/ui/Tooltip.tsx +211 -0
  107. package/src/components/ui/Widget.tsx +31 -0
  108. package/src/components/ui/first-slice.test.tsx +98 -0
  109. package/src/components/ui/index.ts +335 -22
  110. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  111. package/src/components/ui/kbd-icons.test.tsx +90 -0
  112. package/src/components/ui/product-patterns.test.tsx +77 -0
  113. package/src/styles/themes.css +411 -1
  114. package/tailwind-preset.ts +7 -0
  115. package/dist/chunk-SHF57J7U.mjs +0 -2910
  116. package/dist/index-CmS6hcUk.d.mts +0 -753
  117. package/dist/index-CmS6hcUk.d.ts +0 -753
  118. package/src/components/charts/ph-insight-charts.tsx +0 -162
  119. package/src/components/dashboard/DashboardFiltersBar.tsx +0 -19
  120. package/src/components/dashboard/DashboardGrid.tsx +0 -23
  121. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +0 -37
  122. package/src/components/dashboard/DashboardKpiCard.tsx +0 -25
  123. package/src/components/dashboard/DashboardToolbar.tsx +0 -23
  124. package/src/components/dashboard/DashboardWell.tsx +0 -10
  125. package/src/components/dashboard/InsightMiniCard.tsx +0 -26
  126. package/src/components/dashboard/InsightShell.tsx +0 -37
  127. package/src/components/dashboard/InsightShellHeader.tsx +0 -22
  128. package/src/components/dashboard/MiniTrendBars.tsx +0 -51
  129. package/src/components/dashboard/index.ts +0 -31
  130. package/src/components/dashboard/types.ts +0 -9
  131. package/src/components/sections/ChartShowcase.tsx +0 -190
  132. package/src/components/sections/DashboardShowcase.tsx +0 -191
  133. package/src/components/sections/StatShowcase.tsx +0 -129
@@ -0,0 +1,65 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type TagType = "default" | "primary" | "success" | "warning" | "danger" | "info" | "muted" | "highlight";
5
+
6
+ export interface TagProps extends HTMLAttributes<HTMLDivElement> {
7
+ type?: TagType;
8
+ size?: "sm" | "md";
9
+ icon?: ReactNode;
10
+ closable?: boolean;
11
+ onClose?: () => void;
12
+ children: ReactNode;
13
+ }
14
+
15
+ const typeClass: Record<TagType, string> = {
16
+ default: "ph-tag-default",
17
+ primary: "ph-tag-primary",
18
+ success: "ph-tag-success",
19
+ warning: "ph-tag-warning",
20
+ danger: "ph-tag-danger",
21
+ info: "ph-tag-info",
22
+ muted: "ph-tag-muted",
23
+ highlight: "ph-tag-highlight",
24
+ };
25
+
26
+ export const Tag = forwardRef<HTMLDivElement, TagProps>(function Tag({
27
+ type = "default",
28
+ size = "md",
29
+ icon,
30
+ closable = false,
31
+ onClose,
32
+ className,
33
+ children,
34
+ ...props
35
+ }, ref) {
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ className={cn(
40
+ "ph-tag",
41
+ typeClass[type],
42
+ size === "sm" && "ph-tag-sm",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {icon && <span className="ph-tag__icon">{icon}</span>}
48
+ <span className="ph-tag__text">{children}</span>
49
+ {closable && (
50
+ <button
51
+ type="button"
52
+ onClick={(e) => { e.stopPropagation(); onClose?.(); }}
53
+ className="ph-tag__close"
54
+ aria-label="Remove"
55
+ >
56
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
57
+ <path d="M3 3l6 6M9 3l-6 6" />
58
+ </svg>
59
+ </button>
60
+ )}
61
+ </div>
62
+ );
63
+ });
64
+
65
+ Tag.displayName = "Tag";
@@ -0,0 +1,62 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface ToggleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
5
+ pressed?: boolean;
6
+ onPressedChange?: (pressed: boolean) => void;
7
+ icon?: ReactNode;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(function ToggleButton({
12
+ pressed = false,
13
+ onPressedChange,
14
+ icon,
15
+ children,
16
+ className,
17
+ ...props
18
+ }, ref) {
19
+ return (
20
+ <button
21
+ ref={ref}
22
+ type="button"
23
+ aria-pressed={pressed}
24
+ onClick={() => onPressedChange?.(!pressed)}
25
+ className={cn(
26
+ "ph-toggle-btn",
27
+ pressed && "ph-toggle-btn--pressed",
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ {icon && <span className="ph-toggle-btn__icon">{icon}</span>}
33
+ {children}
34
+ </button>
35
+ );
36
+ });
37
+
38
+ ToggleButton.displayName = "ToggleButton";
39
+
40
+ export interface ToggleGroupProps {
41
+ type?: "single" | "multiple";
42
+ value?: string | string[];
43
+ onValueChange?: (value: string | string[]) => void;
44
+ children: ReactNode;
45
+ className?: string;
46
+ }
47
+
48
+ export function ToggleGroup({
49
+ type = "single",
50
+ value,
51
+ onValueChange,
52
+ children,
53
+ className,
54
+ }: ToggleGroupProps) {
55
+ return (
56
+ <div className={cn("ph-toggle-group", className)} role="group">
57
+ {children}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ ToggleGroup.displayName = "ToggleGroup";
@@ -0,0 +1,58 @@
1
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { Tooltip, TooltipProvider } from "@/components/ui/Tooltip";
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe("Tooltip", () => {
9
+ it("opens when its trigger is hovered", async () => {
10
+ const user = userEvent.setup();
11
+ render(
12
+ <Tooltip content="Hover details" delayDuration={0}>
13
+ <button type="button">Hover target</button>
14
+ </Tooltip>,
15
+ );
16
+
17
+ await user.hover(screen.getByRole("button", { name: "Hover target" }));
18
+
19
+ expect(await screen.findByRole("tooltip")).toHaveTextContent("Hover details");
20
+ });
21
+
22
+ it("opens on keyboard focus and exposes its description to assistive technology", async () => {
23
+ const user = userEvent.setup();
24
+ render(
25
+ <TooltipProvider delayDuration={0}>
26
+ <Tooltip content="Keyboard details" side="bottom">
27
+ <button type="button">Focus target</button>
28
+ </Tooltip>
29
+ </TooltipProvider>,
30
+ );
31
+
32
+ await user.tab();
33
+
34
+ const trigger = screen.getByRole("button", { name: "Focus target" });
35
+ const tooltip = await screen.findByRole("tooltip");
36
+ expect(trigger).toHaveFocus();
37
+ expect(tooltip).toHaveTextContent("Keyboard details");
38
+ expect(trigger).toHaveAttribute("aria-describedby", tooltip.id);
39
+ });
40
+
41
+ it("dismisses an open tooltip with Escape without moving focus", async () => {
42
+ const user = userEvent.setup();
43
+ render(
44
+ <Tooltip content="Dismissible details">
45
+ <button type="button">Dismiss target</button>
46
+ </Tooltip>,
47
+ );
48
+
49
+ await user.tab();
50
+ const trigger = screen.getByRole("button", { name: "Dismiss target" });
51
+ expect(await screen.findByRole("tooltip")).toBeInTheDocument();
52
+
53
+ await user.keyboard("{Escape}");
54
+
55
+ await waitFor(() => expect(screen.queryByRole("tooltip")).not.toBeInTheDocument());
56
+ expect(trigger).toHaveFocus();
57
+ });
58
+ });
@@ -0,0 +1,211 @@
1
+ "use client";
2
+
3
+ import {
4
+ cloneElement,
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useRef,
10
+ useState,
11
+ type HTMLAttributes,
12
+ type ReactElement,
13
+ type ReactNode,
14
+ } from "react";
15
+ import { createPortal } from "react-dom";
16
+ import { cn } from "@/lib/cn";
17
+
18
+ export type TooltipSide = "top" | "right" | "bottom" | "left";
19
+ export type TooltipAlign = "start" | "center" | "end";
20
+
21
+ export interface TooltipProviderProps {
22
+ children: ReactNode;
23
+ delayDuration?: number;
24
+ }
25
+
26
+ export interface TooltipProps {
27
+ content: ReactNode;
28
+ children: ReactElement<HTMLAttributes<HTMLElement>>;
29
+ side?: TooltipSide;
30
+ align?: TooltipAlign;
31
+ sideOffset?: number;
32
+ collisionPadding?: number;
33
+ className?: string;
34
+ open?: boolean;
35
+ defaultOpen?: boolean;
36
+ onOpenChange?: (open: boolean) => void;
37
+ delayDuration?: number;
38
+ disableHoverableContent?: boolean;
39
+ }
40
+
41
+ const TooltipDelayContext = createContext(0);
42
+
43
+ export function TooltipProvider({
44
+ children,
45
+ delayDuration = 0,
46
+ }: TooltipProviderProps) {
47
+ return (
48
+ <TooltipDelayContext.Provider value={delayDuration}>
49
+ {children}
50
+ </TooltipDelayContext.Provider>
51
+ );
52
+ }
53
+
54
+ function tooltipPosition(
55
+ trigger: DOMRect,
56
+ side: TooltipSide,
57
+ align: TooltipAlign,
58
+ offset: number,
59
+ ) {
60
+ const horizontal =
61
+ align === "start"
62
+ ? trigger.left
63
+ : align === "end"
64
+ ? trigger.right
65
+ : trigger.left + trigger.width / 2;
66
+ const vertical =
67
+ align === "start"
68
+ ? trigger.top
69
+ : align === "end"
70
+ ? trigger.bottom
71
+ : trigger.top + trigger.height / 2;
72
+ if (side === "top")
73
+ return {
74
+ left: horizontal,
75
+ top: trigger.top - offset,
76
+ transform: "translate(-50%, -100%)",
77
+ };
78
+ if (side === "bottom")
79
+ return {
80
+ left: horizontal,
81
+ top: trigger.bottom + offset,
82
+ transform: "translate(-50%, 0)",
83
+ };
84
+ if (side === "left")
85
+ return {
86
+ left: trigger.left - offset,
87
+ top: vertical,
88
+ transform: "translate(-100%, -50%)",
89
+ };
90
+ return {
91
+ left: trigger.right + offset,
92
+ top: vertical,
93
+ transform: "translate(0, -50%)",
94
+ };
95
+ }
96
+
97
+ export function Tooltip({
98
+ content,
99
+ children,
100
+ side = "top",
101
+ align = "center",
102
+ sideOffset = 7,
103
+ className,
104
+ open,
105
+ defaultOpen = false,
106
+ onOpenChange,
107
+ delayDuration,
108
+ }: TooltipProps) {
109
+ const providerDelay = useContext(TooltipDelayContext);
110
+ const generatedId = useId().replace(/:/g, "");
111
+ const triggerRef = useRef<HTMLSpanElement>(null);
112
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
113
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
114
+ const [position, setPosition] = useState<ReturnType<
115
+ typeof tooltipPosition
116
+ > | null>(null);
117
+ const isOpen = open ?? internalOpen;
118
+ const triggerId = children.props.id;
119
+ const tooltipId = triggerId
120
+ ? `${triggerId}-tooltip`
121
+ : `ph-tooltip-${generatedId}`;
122
+
123
+ function setOpen(next: boolean) {
124
+ if (open === undefined) setInternalOpen(next);
125
+ onOpenChange?.(next);
126
+ }
127
+
128
+ function clearTimer() {
129
+ if (timerRef.current) clearTimeout(timerRef.current);
130
+ timerRef.current = null;
131
+ }
132
+
133
+ function updatePosition() {
134
+ if (!triggerRef.current) return;
135
+ setPosition(
136
+ tooltipPosition(
137
+ triggerRef.current.getBoundingClientRect(),
138
+ side,
139
+ align,
140
+ sideOffset,
141
+ ),
142
+ );
143
+ }
144
+
145
+ function show(immediate: boolean) {
146
+ clearTimer();
147
+ updatePosition();
148
+ const delay = immediate ? 0 : (delayDuration ?? providerDelay);
149
+ if (!delay) setOpen(true);
150
+ else timerRef.current = setTimeout(() => setOpen(true), delay);
151
+ }
152
+
153
+ function hide() {
154
+ clearTimer();
155
+ setOpen(false);
156
+ }
157
+
158
+ useEffect(() => {
159
+ if (!isOpen) return;
160
+ const reposition = () => updatePosition();
161
+ window.addEventListener("resize", reposition);
162
+ window.addEventListener("scroll", reposition, true);
163
+ return () => {
164
+ window.removeEventListener("resize", reposition);
165
+ window.removeEventListener("scroll", reposition, true);
166
+ };
167
+ });
168
+
169
+ useEffect(() => () => clearTimer(), []);
170
+
171
+ const describedBy = [children.props["aria-describedby"], tooltipId]
172
+ .filter(Boolean)
173
+ .join(" ");
174
+ const trigger = cloneElement(children, { "aria-describedby": describedBy });
175
+
176
+ return (
177
+ <>
178
+ <span
179
+ ref={triggerRef}
180
+ className="inline-flex"
181
+ onMouseEnter={() => show(false)}
182
+ onMouseLeave={hide}
183
+ onFocusCapture={() => show(true)}
184
+ onBlurCapture={hide}
185
+ onKeyDown={(event) => {
186
+ if (event.key === "Escape") hide();
187
+ }}
188
+ >
189
+ {trigger}
190
+ </span>
191
+ {isOpen && position && typeof document !== "undefined"
192
+ ? createPortal(
193
+ <span
194
+ style={{ position: "fixed", ...position }}
195
+ className="pointer-events-none z-[200]"
196
+ >
197
+ <span
198
+ id={tooltipId}
199
+ role="tooltip"
200
+ data-side={side}
201
+ className={cn("ph-tooltip-content", className)}
202
+ >
203
+ {content}
204
+ </span>
205
+ </span>,
206
+ document.body,
207
+ )
208
+ : null}
209
+ </>
210
+ );
211
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface WidgetProps {
5
+ title: ReactNode;
6
+ onClose?: () => void;
7
+ actions?: ReactNode;
8
+ children?: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function Widget({ title, onClose, actions, children, className }: WidgetProps) {
13
+ return (
14
+ <div className={cn("ph-widget", className)}>
15
+ <div className="ph-widget__header">
16
+ <span className="ph-widget__title">{title}</span>
17
+ {actions}
18
+ {onClose && (
19
+ <button type="button" onClick={onClose} className="ph-widget__close" aria-label="Close">
20
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
21
+ <path d="M3 3l8 8M11 3l-8 8" />
22
+ </svg>
23
+ </button>
24
+ )}
25
+ </div>
26
+ <div className="ph-widget__content">{children}</div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ Widget.displayName = "Widget";
@@ -0,0 +1,98 @@
1
+ import { createRef, useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Alert } from "@/components/ui/Alert";
6
+ import { Badge } from "@/components/ui/Badge";
7
+ import { Button } from "@/components/ui/Button";
8
+ import { Card } from "@/components/ui/Card";
9
+ import { Input } from "@/components/ui/Input";
10
+ import { Modal } from "@/components/ui/Modal";
11
+
12
+ describe("first-slice components", () => {
13
+ it("forwards a button ref and exposes loading state", () => {
14
+ const ref = createRef<HTMLButtonElement>();
15
+ render(<Button ref={ref} loading>Save changes</Button>);
16
+
17
+ expect(ref.current).toBe(screen.getByRole("button", { name: "Save changes" }));
18
+ expect(ref.current).toBeDisabled();
19
+ expect(ref.current).toHaveAttribute("aria-busy", "true");
20
+ });
21
+
22
+ it("uses the same tactile chrome for every filled variant", () => {
23
+ const { rerender } = render(<Button variant="signal">Launch</Button>);
24
+ expect(screen.getByRole("button", { name: "Launch" })).toHaveClass("ph-btn-raised");
25
+ expect(screen.getByRole("button", { name: "Launch" }).querySelector(".ph-btn-chrome")).toBeInTheDocument();
26
+
27
+ rerender(<Button variant="danger">Delete</Button>);
28
+ expect(screen.getByRole("button", { name: "Delete" }).querySelector(".ph-btn-chrome")).toBeInTheDocument();
29
+ });
30
+
31
+ it("associates input help and error messages", () => {
32
+ const { rerender } = render(
33
+ <Input label="Email" helperText="Use your work address" />,
34
+ );
35
+
36
+ expect(screen.getByLabelText("Email")).toHaveAccessibleDescription("Use your work address");
37
+
38
+ rerender(<Input label="Email" error="Enter a valid email address" />);
39
+ const input = screen.getByLabelText("Email");
40
+ expect(input).toHaveAttribute("aria-invalid", "true");
41
+ expect(input).toHaveAccessibleErrorMessage("Enter a valid email address");
42
+ });
43
+
44
+ it("keeps cards static unless explicitly interactive", () => {
45
+ const { rerender } = render(<Card title="Static card" />);
46
+ expect(screen.getByText("Static card").closest(".ph-card")).not.toHaveClass("ph-card-interactive");
47
+
48
+ rerender(<Card title="Interactive card" interactive />);
49
+ expect(screen.getByText("Interactive card").closest(".ph-card")).toHaveClass("ph-card-interactive");
50
+ });
51
+
52
+ it("forwards badge attributes and keeps static alerts out of live regions", () => {
53
+ render(
54
+ <>
55
+ <Badge data-testid="badge">Live</Badge>
56
+ <Alert title="Saved" />
57
+ <Alert title="Background sync complete" live="polite" />
58
+ </>,
59
+ );
60
+
61
+ expect(screen.getByTestId("badge")).toHaveTextContent("Live");
62
+ expect(screen.getByText("Saved").closest(".ph-alert")).not.toHaveAttribute("role");
63
+ expect(screen.getByRole("status")).toHaveTextContent("Background sync complete");
64
+ });
65
+
66
+ it("closes the modal with Escape and restores trigger focus", async () => {
67
+ const user = userEvent.setup();
68
+ const onClose = vi.fn();
69
+
70
+ function Fixture() {
71
+ const [open, setOpen] = useState(false);
72
+
73
+ return (
74
+ <>
75
+ <button type="button" onClick={() => setOpen(true)}>Open settings</button>
76
+ <Modal
77
+ open={open}
78
+ onClose={() => {
79
+ onClose();
80
+ setOpen(false);
81
+ }}
82
+ title="Workspace settings"
83
+ >
84
+ <button type="button">Save settings</button>
85
+ </Modal>
86
+ </>
87
+ );
88
+ }
89
+
90
+ render(<Fixture />);
91
+ const trigger = screen.getByRole("button", { name: "Open settings" });
92
+ await user.click(trigger);
93
+ expect(screen.getByRole("dialog", { name: "Workspace settings" })).toBeInTheDocument();
94
+ await user.keyboard("{Escape}");
95
+ expect(onClose).toHaveBeenCalledTimes(1);
96
+ expect(trigger).toHaveFocus();
97
+ });
98
+ });