@hilum/ui 0.1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ CardFooter,
10
+ CardMedia,
11
+ } from "../card";
12
+ import { Avatar, AvatarFallback, AvatarImage, AvatarWithStatus } from "../avatar";
13
+ import { AvatarStack } from "../avatar-stack";
14
+ import { StatCard } from "../stat-card";
15
+ import { Notification } from "../notification";
16
+ import { MediaObject } from "../media-object";
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Card */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ describe("Card", () => {
23
+ it("renders children", () => {
24
+ render(<Card>Content</Card>);
25
+ expect(screen.getByText("Content")).toBeInTheDocument();
26
+ });
27
+
28
+ it("carries data-slot attribute", () => {
29
+ const { container } = render(<Card>Content</Card>);
30
+ expect(container.firstChild).toHaveAttribute("data-slot", "card");
31
+ });
32
+
33
+ it("renders all variants without error", () => {
34
+ const variants = ["default", "muted", "ghost"] as const;
35
+ for (const variant of variants) {
36
+ const { unmount } = render(<Card variant={variant}>Card</Card>);
37
+ expect(screen.getByText("Card")).toBeInTheDocument();
38
+ unmount();
39
+ }
40
+ });
41
+
42
+ it("composes sub-components correctly", () => {
43
+ render(
44
+ <Card>
45
+ <CardHeader>
46
+ <CardTitle>Title</CardTitle>
47
+ <CardDescription>Description</CardDescription>
48
+ </CardHeader>
49
+ <CardContent>Body</CardContent>
50
+ <CardFooter>Footer</CardFooter>
51
+ </Card>,
52
+ );
53
+ expect(screen.getByText("Title")).toBeInTheDocument();
54
+ expect(screen.getByText("Description")).toBeInTheDocument();
55
+ expect(screen.getByText("Body")).toBeInTheDocument();
56
+ expect(screen.getByText("Footer")).toBeInTheDocument();
57
+ });
58
+
59
+ it("renders CardMedia", () => {
60
+ const { container } = render(<CardMedia data-testid="media" />);
61
+ expect(container.querySelector('[data-slot="card-media"]')).toBeInTheDocument();
62
+ });
63
+ });
64
+
65
+ /* ------------------------------------------------------------------ */
66
+ /* Avatar */
67
+ /* ------------------------------------------------------------------ */
68
+
69
+ describe("Avatar", () => {
70
+ it("renders with fallback text", () => {
71
+ render(
72
+ <Avatar>
73
+ <AvatarFallback>JD</AvatarFallback>
74
+ </Avatar>,
75
+ );
76
+ expect(screen.getByText("JD")).toBeInTheDocument();
77
+ });
78
+
79
+ it("renders all sizes without error", () => {
80
+ const sizes = ["xs", "sm", "md", "lg", "xl"] as const;
81
+ for (const size of sizes) {
82
+ const { unmount } = render(
83
+ <Avatar size={size}>
84
+ <AvatarFallback>X</AvatarFallback>
85
+ </Avatar>,
86
+ );
87
+ expect(screen.getByText("X")).toBeInTheDocument();
88
+ unmount();
89
+ }
90
+ });
91
+
92
+ it("renders AvatarImage with alt text", () => {
93
+ render(
94
+ <Avatar>
95
+ <AvatarImage src="https://example.com/avatar.jpg" alt="User avatar" />
96
+ <AvatarFallback>JD</AvatarFallback>
97
+ </Avatar>,
98
+ );
99
+ // AvatarFallback shows when image fails — just check it's in the DOM
100
+ expect(screen.getByText("JD")).toBeInTheDocument();
101
+ });
102
+ });
103
+
104
+ describe("AvatarWithStatus", () => {
105
+ it("renders children", () => {
106
+ render(
107
+ <AvatarWithStatus>
108
+ <AvatarFallback>AB</AvatarFallback>
109
+ </AvatarWithStatus>,
110
+ );
111
+ expect(screen.getByText("AB")).toBeInTheDocument();
112
+ });
113
+
114
+ it("renders status indicator when status is provided", () => {
115
+ const statuses = ["online", "offline", "busy", "away"] as const;
116
+ for (const status of statuses) {
117
+ const { unmount } = render(
118
+ <AvatarWithStatus status={status}>
119
+ <AvatarFallback>X</AvatarFallback>
120
+ </AvatarWithStatus>,
121
+ );
122
+ expect(screen.getByText("X")).toBeInTheDocument();
123
+ unmount();
124
+ }
125
+ });
126
+ });
127
+
128
+ /* ------------------------------------------------------------------ */
129
+ /* AvatarStack */
130
+ /* ------------------------------------------------------------------ */
131
+
132
+ describe("AvatarStack", () => {
133
+ const avatars = [{ name: "Alice" }, { name: "Bob" }, { name: "Carol" }];
134
+
135
+ it("renders all avatars by default", () => {
136
+ render(<AvatarStack avatars={avatars} />);
137
+ expect(screen.getByTitle("Alice")).toBeInTheDocument();
138
+ expect(screen.getByTitle("Bob")).toBeInTheDocument();
139
+ expect(screen.getByTitle("Carol")).toBeInTheDocument();
140
+ });
141
+
142
+ it("shows overflow count when max is set", () => {
143
+ render(<AvatarStack avatars={avatars} max={2} />);
144
+ expect(screen.getByText("+1")).toBeInTheDocument();
145
+ });
146
+
147
+ it("renders all sizes without error", () => {
148
+ const sizes = ["sm", "md", "lg"] as const;
149
+ for (const size of sizes) {
150
+ const { unmount } = render(<AvatarStack avatars={[{ name: "A" }]} size={size} />);
151
+ expect(screen.getByTitle("A")).toBeInTheDocument();
152
+ unmount();
153
+ }
154
+ });
155
+ });
156
+
157
+ /* ------------------------------------------------------------------ */
158
+ /* StatCard */
159
+ /* ------------------------------------------------------------------ */
160
+
161
+ describe("StatCard", () => {
162
+ it("renders label and value", () => {
163
+ render(<StatCard label="Total Users" value="1,234" />);
164
+ expect(screen.getByText("Total Users")).toBeInTheDocument();
165
+ expect(screen.getByText("1,234")).toBeInTheDocument();
166
+ });
167
+
168
+ it("renders trend when provided", () => {
169
+ render(<StatCard label="Revenue" value="$10k" trend={{ value: "+12%", direction: "up" }} />);
170
+ expect(screen.getByText("+12%")).toBeInTheDocument();
171
+ });
172
+
173
+ it("renders all trend directions without error", () => {
174
+ const directions = ["up", "down", "neutral"] as const;
175
+ for (const direction of directions) {
176
+ const { unmount } = render(
177
+ <StatCard label="L" value="V" trend={{ value: "5%", direction }} />,
178
+ );
179
+ expect(screen.getByText("5%")).toBeInTheDocument();
180
+ unmount();
181
+ }
182
+ });
183
+ });
184
+
185
+ /* ------------------------------------------------------------------ */
186
+ /* Notification */
187
+ /* ------------------------------------------------------------------ */
188
+
189
+ describe("Notification", () => {
190
+ it("renders title", () => {
191
+ render(<Notification title="File uploaded" />);
192
+ expect(screen.getByText("File uploaded")).toBeInTheDocument();
193
+ });
194
+
195
+ it("renders description when provided", () => {
196
+ render(<Notification title="Done" description="Your export is ready." />);
197
+ expect(screen.getByText("Your export is ready.")).toBeInTheDocument();
198
+ });
199
+
200
+ it("renders close button when onClose is provided", () => {
201
+ render(<Notification title="Alert" onClose={() => {}} />);
202
+ expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument();
203
+ });
204
+
205
+ it("renders actions when provided", () => {
206
+ render(
207
+ <Notification
208
+ title="Update available"
209
+ actions={[{ label: "Install" }, { label: "Dismiss", variant: "ghost" }]}
210
+ />,
211
+ );
212
+ expect(screen.getByText("Install")).toBeInTheDocument();
213
+ expect(screen.getByText("Dismiss")).toBeInTheDocument();
214
+ });
215
+
216
+ it("renders all variants without error", () => {
217
+ const variants = ["default", "success", "error", "warning", "info"] as const;
218
+ for (const variant of variants) {
219
+ const { unmount } = render(<Notification title="T" variant={variant} />);
220
+ expect(screen.getByText("T")).toBeInTheDocument();
221
+ unmount();
222
+ }
223
+ });
224
+ });
225
+
226
+ /* ------------------------------------------------------------------ */
227
+ /* MediaObject */
228
+ /* ------------------------------------------------------------------ */
229
+
230
+ describe("MediaObject", () => {
231
+ it("renders media and content", () => {
232
+ render(
233
+ <MediaObject media={<img alt="thumb" src="" />}>
234
+ <p>Content text</p>
235
+ </MediaObject>,
236
+ );
237
+ expect(screen.getByAltText("thumb")).toBeInTheDocument();
238
+ expect(screen.getByText("Content text")).toBeInTheDocument();
239
+ });
240
+
241
+ it('renders mediaPosition="right" without error', () => {
242
+ render(
243
+ <MediaObject media={<span>icon</span>} mediaPosition="right">
244
+ Content
245
+ </MediaObject>,
246
+ );
247
+ expect(screen.getByText("Content")).toBeInTheDocument();
248
+ });
249
+
250
+ it("renders all gap options without error", () => {
251
+ const gaps = ["sm", "md", "lg"] as const;
252
+ for (const gap of gaps) {
253
+ const { unmount } = render(
254
+ <MediaObject media={<span />} gap={gap}>
255
+ Body
256
+ </MediaObject>,
257
+ );
258
+ expect(screen.getByText("Body")).toBeInTheDocument();
259
+ unmount();
260
+ }
261
+ });
262
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Alert, AlertTitle, AlertDescription } from "../alert";
4
+ import { Progress } from "../progress";
5
+ import { Steps } from "../steps";
6
+ import { EmptyState } from "../empty-state";
7
+ import type { Step } from "../steps";
8
+
9
+ /* ------------------------------------------------------------------ */
10
+ /* Alert */
11
+ /* ------------------------------------------------------------------ */
12
+
13
+ describe("Alert", () => {
14
+ it('renders with role="alert"', () => {
15
+ render(<Alert>Message</Alert>);
16
+ expect(screen.getByRole("alert")).toBeInTheDocument();
17
+ });
18
+
19
+ it("renders sub-components together", () => {
20
+ render(
21
+ <Alert>
22
+ <AlertTitle>Warning</AlertTitle>
23
+ <AlertDescription>Something went wrong.</AlertDescription>
24
+ </Alert>,
25
+ );
26
+ expect(screen.getByText("Warning")).toBeInTheDocument();
27
+ expect(screen.getByText("Something went wrong.")).toBeInTheDocument();
28
+ });
29
+
30
+ it("renders all variants without error", () => {
31
+ const variants = ["default", "info", "success", "warning", "destructive"] as const;
32
+ for (const variant of variants) {
33
+ const { unmount } = render(<Alert variant={variant}>Alert</Alert>);
34
+ expect(screen.getByRole("alert")).toBeInTheDocument();
35
+ unmount();
36
+ }
37
+ });
38
+ });
39
+
40
+ /* ------------------------------------------------------------------ */
41
+ /* Progress */
42
+ /* ------------------------------------------------------------------ */
43
+
44
+ describe("Progress", () => {
45
+ it("renders with a progressbar role", () => {
46
+ render(<Progress value={50} />);
47
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
48
+ });
49
+
50
+ it("reflects the correct value", () => {
51
+ render(<Progress value={75} />);
52
+ expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "75");
53
+ });
54
+
55
+ it("renders at 0%", () => {
56
+ render(<Progress value={0} />);
57
+ expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "0");
58
+ });
59
+
60
+ it("renders at 100%", () => {
61
+ render(<Progress value={100} />);
62
+ expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "100");
63
+ });
64
+ });
65
+
66
+ /* ------------------------------------------------------------------ */
67
+ /* Steps */
68
+ /* ------------------------------------------------------------------ */
69
+
70
+ const STEPS: Step[] = [
71
+ { name: "Cart", status: "complete" },
72
+ { name: "Shipping", status: "current" },
73
+ { name: "Payment", status: "upcoming" },
74
+ ];
75
+
76
+ describe("Steps", () => {
77
+ it("renders all step names in circles variant (default)", () => {
78
+ render(<Steps steps={STEPS} />);
79
+ expect(screen.getByText("Cart")).toBeInTheDocument();
80
+ expect(screen.getByText("Shipping")).toBeInTheDocument();
81
+ expect(screen.getByText("Payment")).toBeInTheDocument();
82
+ });
83
+
84
+ it("renders bullets variant without error", () => {
85
+ render(<Steps steps={STEPS} variant="bullets" />);
86
+ expect(screen.getByRole("navigation", { name: /progress/i })).toBeInTheDocument();
87
+ });
88
+
89
+ it("renders progress variant without error", () => {
90
+ render(<Steps steps={STEPS} variant="progress" />);
91
+ expect(screen.getByText("Cart")).toBeInTheDocument();
92
+ });
93
+
94
+ it("bullets variant shows step count", () => {
95
+ render(<Steps steps={STEPS} variant="bullets" />);
96
+ expect(screen.getByText(/Step 2 of 3/i)).toBeInTheDocument();
97
+ });
98
+
99
+ it("renders with descriptions in circles variant", () => {
100
+ const stepsWithDesc: Step[] = [
101
+ { name: "Step 1", description: "First step", status: "complete" },
102
+ { name: "Step 2", status: "current" },
103
+ ];
104
+ render(<Steps steps={stepsWithDesc} />);
105
+ expect(screen.getByText("First step")).toBeInTheDocument();
106
+ });
107
+ });
108
+
109
+ /* ------------------------------------------------------------------ */
110
+ /* EmptyState */
111
+ /* ------------------------------------------------------------------ */
112
+
113
+ describe("EmptyState", () => {
114
+ it("renders the title", () => {
115
+ render(<EmptyState title="No results found" />);
116
+ expect(screen.getByText("No results found")).toBeInTheDocument();
117
+ });
118
+
119
+ it("renders description when provided", () => {
120
+ render(<EmptyState title="Empty" description="Try a different search." />);
121
+ expect(screen.getByText("Try a different search.")).toBeInTheDocument();
122
+ });
123
+
124
+ it("renders action button with label", () => {
125
+ render(<EmptyState title="Empty" action={{ label: "Add item" }} />);
126
+ expect(screen.getByRole("button", { name: "Add item" })).toBeInTheDocument();
127
+ });
128
+
129
+ it("renders action as anchor when href is provided", () => {
130
+ render(<EmptyState title="Empty" action={{ label: "Create", href: "/new" }} />);
131
+ expect(screen.getByRole("link", { name: "Create" })).toHaveAttribute("href", "/new");
132
+ });
133
+
134
+ it("renders icon when provided", () => {
135
+ render(<EmptyState title="Empty" icon={<span data-testid="icon">📭</span>} />);
136
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
137
+ });
138
+ });
@@ -0,0 +1,297 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { userEvent } from "@testing-library/user-event";
4
+ import { Checkbox } from "../checkbox";
5
+ import { RadioGroup, RadioGroupItem } from "../radio-group";
6
+ import { Switch } from "../switch";
7
+ import { Slider } from "../slider";
8
+ import { InputNumber } from "../input-number";
9
+ import { InputGroup } from "../input-group";
10
+ import { Field } from "../field";
11
+ import { NativeSelect, NativeSelectOption } from "../native-select";
12
+ import { RadioCards } from "../radio-card";
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Checkbox */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ describe("Checkbox", () => {
19
+ it("renders a checkbox button", () => {
20
+ render(<Checkbox />);
21
+ expect(screen.getByRole("checkbox")).toBeInTheDocument();
22
+ });
23
+
24
+ it("is unchecked by default", () => {
25
+ render(<Checkbox />);
26
+ expect(screen.getByRole("checkbox")).not.toBeChecked();
27
+ });
28
+
29
+ it("is disabled when disabled prop is set", () => {
30
+ render(<Checkbox disabled />);
31
+ expect(screen.getByRole("checkbox")).toBeDisabled();
32
+ });
33
+ });
34
+
35
+ /* ------------------------------------------------------------------ */
36
+ /* RadioGroup */
37
+ /* ------------------------------------------------------------------ */
38
+
39
+ describe("RadioGroup", () => {
40
+ it("renders radio buttons", () => {
41
+ render(
42
+ <RadioGroup defaultValue="a">
43
+ <RadioGroupItem value="a" id="a" />
44
+ <RadioGroupItem value="b" id="b" />
45
+ </RadioGroup>,
46
+ );
47
+ expect(screen.getAllByRole("radio")).toHaveLength(2);
48
+ });
49
+
50
+ it("marks default value as checked", () => {
51
+ render(
52
+ <RadioGroup defaultValue="first">
53
+ <RadioGroupItem value="first" id="first" />
54
+ <RadioGroupItem value="second" id="second" />
55
+ </RadioGroup>,
56
+ );
57
+ const radios = screen.getAllByRole("radio");
58
+ expect(radios[0]).toBeChecked();
59
+ expect(radios[1]).not.toBeChecked();
60
+ });
61
+ });
62
+
63
+ /* ------------------------------------------------------------------ */
64
+ /* Switch */
65
+ /* ------------------------------------------------------------------ */
66
+
67
+ describe("Switch", () => {
68
+ it("renders a switch button", () => {
69
+ render(<Switch />);
70
+ expect(screen.getByRole("switch")).toBeInTheDocument();
71
+ });
72
+
73
+ it("is off by default", () => {
74
+ render(<Switch />);
75
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
76
+ });
77
+
78
+ it("toggles on click", async () => {
79
+ const user = userEvent.setup();
80
+ render(<Switch />);
81
+ const sw = screen.getByRole("switch");
82
+ await user.click(sw);
83
+ expect(sw).toHaveAttribute("data-state", "checked");
84
+ });
85
+ });
86
+
87
+ /* ------------------------------------------------------------------ */
88
+ /* Slider */
89
+ /* ------------------------------------------------------------------ */
90
+
91
+ describe("Slider", () => {
92
+ it("renders a slider", () => {
93
+ render(<Slider defaultValue={[50]} />);
94
+ expect(screen.getByRole("slider")).toBeInTheDocument();
95
+ });
96
+
97
+ it("shows the provided value", () => {
98
+ render(<Slider defaultValue={[25]} min={0} max={100} />);
99
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "25");
100
+ });
101
+ });
102
+
103
+ /* ------------------------------------------------------------------ */
104
+ /* InputNumber */
105
+ /* ------------------------------------------------------------------ */
106
+
107
+ describe("InputNumber", () => {
108
+ it("renders an input with the given value", () => {
109
+ render(<InputNumber value={42} onChange={() => {}} />);
110
+ expect(screen.getByRole("textbox")).toHaveValue("42");
111
+ });
112
+
113
+ it("displays unit suffix", () => {
114
+ render(<InputNumber value={100} onChange={() => {}} unit="px" />);
115
+ expect(screen.getByText("px")).toBeInTheDocument();
116
+ });
117
+
118
+ it("calls onChange with incremented value when increment button clicked", async () => {
119
+ const user = userEvent.setup();
120
+ const onChange = vi.fn();
121
+ render(<InputNumber value={10} onChange={onChange} step={1} />);
122
+ await user.click(screen.getByRole("button", { name: /increment/i }));
123
+ expect(onChange).toHaveBeenCalledWith(11);
124
+ });
125
+
126
+ it("calls onChange with decremented value when decrement button clicked", async () => {
127
+ const user = userEvent.setup();
128
+ const onChange = vi.fn();
129
+ render(<InputNumber value={10} onChange={onChange} step={1} />);
130
+ await user.click(screen.getByRole("button", { name: /decrement/i }));
131
+ expect(onChange).toHaveBeenCalledWith(9);
132
+ });
133
+
134
+ it("clamps to max on increment", async () => {
135
+ const user = userEvent.setup();
136
+ const onChange = vi.fn();
137
+ render(<InputNumber value={10} onChange={onChange} max={10} />);
138
+ await user.click(screen.getByRole("button", { name: /increment/i }));
139
+ expect(onChange).toHaveBeenCalledWith(10);
140
+ });
141
+
142
+ it("hides steppers when hideSteppers is true", () => {
143
+ render(<InputNumber value={5} onChange={() => {}} hideSteppers />);
144
+ expect(screen.queryByRole("button", { name: /increment/i })).not.toBeInTheDocument();
145
+ });
146
+
147
+ it("renders with precision", () => {
148
+ render(<InputNumber value={3.14} onChange={() => {}} precision={2} />);
149
+ expect(screen.getByRole("textbox")).toHaveValue("3.14");
150
+ });
151
+ });
152
+
153
+ /* ------------------------------------------------------------------ */
154
+ /* InputGroup */
155
+ /* ------------------------------------------------------------------ */
156
+
157
+ describe("InputGroup", () => {
158
+ it("renders a plain input when no addons", () => {
159
+ render(<InputGroup placeholder="Enter text" />);
160
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
161
+ });
162
+
163
+ it("renders leading addon text", () => {
164
+ render(<InputGroup leadingAddon="https://" placeholder="domain.com" />);
165
+ expect(screen.getByText("https://")).toBeInTheDocument();
166
+ });
167
+
168
+ it("renders trailing addon text", () => {
169
+ render(<InputGroup trailingAddon=".com" placeholder="domain" />);
170
+ expect(screen.getByText(".com")).toBeInTheDocument();
171
+ });
172
+
173
+ it("renders error state", () => {
174
+ const { container } = render(<InputGroup error />);
175
+ // Error icon is rendered inside
176
+ expect(container.querySelector("input")).toBeInTheDocument();
177
+ });
178
+ });
179
+
180
+ /* ------------------------------------------------------------------ */
181
+ /* Field */
182
+ /* ------------------------------------------------------------------ */
183
+
184
+ describe("Field", () => {
185
+ it("renders label and children", () => {
186
+ render(
187
+ <Field label="Email" htmlFor="email">
188
+ <input id="email" type="email" />
189
+ </Field>,
190
+ );
191
+ expect(screen.getByLabelText("Email")).toBeInTheDocument();
192
+ });
193
+
194
+ it("renders hint text", () => {
195
+ render(
196
+ <Field label="Username" hint="Must be unique">
197
+ <input />
198
+ </Field>,
199
+ );
200
+ expect(screen.getByText("Must be unique")).toBeInTheDocument();
201
+ });
202
+
203
+ it("renders error over hint when both provided", () => {
204
+ render(
205
+ <Field label="Field" hint="Hint" error="This is required">
206
+ <input />
207
+ </Field>,
208
+ );
209
+ expect(screen.getByText("This is required")).toBeInTheDocument();
210
+ expect(screen.queryByText("Hint")).not.toBeInTheDocument();
211
+ });
212
+
213
+ it("renders required asterisk", () => {
214
+ const { container } = render(
215
+ <Field label="Name" required>
216
+ <input />
217
+ </Field>,
218
+ );
219
+ expect(container.querySelector("span")).toHaveTextContent("*");
220
+ });
221
+
222
+ it("renders cornerHint", () => {
223
+ render(
224
+ <Field label="Password" cornerHint="Optional">
225
+ <input />
226
+ </Field>,
227
+ );
228
+ expect(screen.getByText("Optional")).toBeInTheDocument();
229
+ });
230
+ });
231
+
232
+ /* ------------------------------------------------------------------ */
233
+ /* NativeSelect */
234
+ /* ------------------------------------------------------------------ */
235
+
236
+ describe("NativeSelect", () => {
237
+ it("renders a combobox", () => {
238
+ render(
239
+ <NativeSelect>
240
+ <NativeSelectOption value="a">Option A</NativeSelectOption>
241
+ <NativeSelectOption value="b">Option B</NativeSelectOption>
242
+ </NativeSelect>,
243
+ );
244
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
245
+ expect(screen.getByText("Option A")).toBeInTheDocument();
246
+ expect(screen.getByText("Option B")).toBeInTheDocument();
247
+ });
248
+
249
+ it("is disabled when disabled prop is set", () => {
250
+ render(
251
+ <NativeSelect disabled>
252
+ <NativeSelectOption value="x">X</NativeSelectOption>
253
+ </NativeSelect>,
254
+ );
255
+ expect(screen.getByRole("combobox")).toBeDisabled();
256
+ });
257
+ });
258
+
259
+ /* ------------------------------------------------------------------ */
260
+ /* RadioCards */
261
+ /* ------------------------------------------------------------------ */
262
+
263
+ describe("RadioCards", () => {
264
+ const options = [
265
+ { value: "plan-a", label: "Basic" },
266
+ { value: "plan-b", label: "Pro" },
267
+ { value: "plan-c", label: "Enterprise" },
268
+ ];
269
+
270
+ it("renders all option labels", () => {
271
+ render(<RadioCards options={options} />);
272
+ expect(screen.getByText("Basic")).toBeInTheDocument();
273
+ expect(screen.getByText("Pro")).toBeInTheDocument();
274
+ expect(screen.getByText("Enterprise")).toBeInTheDocument();
275
+ });
276
+
277
+ it("marks the selected option as checked", () => {
278
+ render(<RadioCards options={options} value="plan-b" />);
279
+ const proBtn = screen.getByRole("radio", { name: "Pro" });
280
+ expect(proBtn).toHaveAttribute("aria-checked", "true");
281
+ });
282
+
283
+ it("calls onValueChange when an option is clicked", () => {
284
+ const onValueChange = vi.fn();
285
+ render(<RadioCards options={options} onValueChange={onValueChange} />);
286
+ fireEvent.click(screen.getByRole("radio", { name: "Enterprise" }));
287
+ expect(onValueChange).toHaveBeenCalledWith("plan-c");
288
+ });
289
+
290
+ it("does not call onValueChange for disabled options", () => {
291
+ const onValueChange = vi.fn();
292
+ const opts = [{ value: "x", label: "Disabled", disabled: true }];
293
+ render(<RadioCards options={opts} onValueChange={onValueChange} />);
294
+ fireEvent.click(screen.getByRole("radio", { name: "Disabled" }));
295
+ expect(onValueChange).not.toHaveBeenCalled();
296
+ });
297
+ });