@object-ui/components 2.0.0 → 3.0.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 (34) hide show
  1. package/.turbo/turbo-build.log +12 -12
  2. package/CHANGELOG.md +28 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.js +19610 -19344
  5. package/dist/index.umd.cjs +29 -29
  6. package/dist/src/custom/index.d.ts +2 -0
  7. package/dist/src/custom/view-skeleton.d.ts +37 -0
  8. package/dist/src/custom/view-states.d.ts +33 -0
  9. package/package.json +17 -17
  10. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
  11. package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
  12. package/src/__tests__/accessibility.test.tsx +137 -0
  13. package/src/__tests__/api-consistency.test.tsx +596 -0
  14. package/src/__tests__/color-contrast.test.tsx +212 -0
  15. package/src/__tests__/edge-cases.test.tsx +285 -0
  16. package/src/__tests__/snapshot-critical.test.tsx +317 -0
  17. package/src/__tests__/snapshot.test.tsx +205 -0
  18. package/src/__tests__/wcag-audit.test.tsx +493 -0
  19. package/src/custom/index.ts +2 -0
  20. package/src/custom/view-skeleton.tsx +243 -0
  21. package/src/custom/view-states.tsx +153 -0
  22. package/src/renderers/complex/data-table.tsx +28 -13
  23. package/src/renderers/complex/resizable.tsx +20 -17
  24. package/src/renderers/data-display/list.tsx +1 -1
  25. package/src/renderers/data-display/table.tsx +1 -1
  26. package/src/renderers/data-display/tree-view.tsx +2 -1
  27. package/src/renderers/form/form.tsx +10 -6
  28. package/src/renderers/layout/aspect-ratio.tsx +1 -1
  29. package/src/stories-json/Accessibility.mdx +297 -0
  30. package/src/stories-json/EdgeCases.stories.tsx +160 -0
  31. package/src/stories-json/GettingStarted.mdx +89 -0
  32. package/src/stories-json/Introduction.mdx +127 -0
  33. package/src/ui/slider.tsx +6 -2
  34. package/src/stories/Introduction.mdx +0 -61
@@ -0,0 +1,327 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Badge snapshots > renders variant="default" 1`] = `
4
+ <div
5
+ class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80"
6
+ >
7
+ Label
8
+ </div>
9
+ `;
10
+
11
+ exports[`Badge snapshots > renders variant="destructive" 1`] = `
12
+ <div
13
+ class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80"
14
+ >
15
+ Label
16
+ </div>
17
+ `;
18
+
19
+ exports[`Badge snapshots > renders variant="outline" 1`] = `
20
+ <div
21
+ class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground"
22
+ >
23
+ Label
24
+ </div>
25
+ `;
26
+
27
+ exports[`Badge snapshots > renders variant="secondary" 1`] = `
28
+ <div
29
+ class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80"
30
+ >
31
+ Label
32
+ </div>
33
+ `;
34
+
35
+ exports[`Badge snapshots > renders with custom className 1`] = `
36
+ <div
37
+ class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80 custom-class"
38
+ >
39
+ Custom
40
+ </div>
41
+ `;
42
+
43
+ exports[`Button snapshots > renders disabled state 1`] = `
44
+ <button
45
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
46
+ disabled=""
47
+ >
48
+ Disabled
49
+ </button>
50
+ `;
51
+
52
+ exports[`Button snapshots > renders size="icon" 1`] = `
53
+ <button
54
+ aria-label="Icon button"
55
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 w-10"
56
+ >
57
+
58
+ </button>
59
+ `;
60
+
61
+ exports[`Button snapshots > renders size="lg" 1`] = `
62
+ <button
63
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md px-8"
64
+ >
65
+ Large
66
+ </button>
67
+ `;
68
+
69
+ exports[`Button snapshots > renders size="sm" 1`] = `
70
+ <button
71
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md px-3"
72
+ >
73
+ Small
74
+ </button>
75
+ `;
76
+
77
+ exports[`Button snapshots > renders variant="default" 1`] = `
78
+ <button
79
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
80
+ >
81
+ Click me
82
+ </button>
83
+ `;
84
+
85
+ exports[`Button snapshots > renders variant="destructive" 1`] = `
86
+ <button
87
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2"
88
+ >
89
+ Click me
90
+ </button>
91
+ `;
92
+
93
+ exports[`Button snapshots > renders variant="ghost" 1`] = `
94
+ <button
95
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
96
+ >
97
+ Click me
98
+ </button>
99
+ `;
100
+
101
+ exports[`Button snapshots > renders variant="link" 1`] = `
102
+ <button
103
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-primary underline-offset-4 hover:underline h-10 px-4 py-2"
104
+ >
105
+ Click me
106
+ </button>
107
+ `;
108
+
109
+ exports[`Button snapshots > renders variant="outline" 1`] = `
110
+ <button
111
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
112
+ >
113
+ Click me
114
+ </button>
115
+ `;
116
+
117
+ exports[`Button snapshots > renders variant="secondary" 1`] = `
118
+ <button
119
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-10 px-4 py-2"
120
+ >
121
+ Click me
122
+ </button>
123
+ `;
124
+
125
+ exports[`Card snapshots > renders content-only card 1`] = `
126
+ <div
127
+ class="rounded-lg border bg-card text-card-foreground shadow-sm"
128
+ >
129
+ <div
130
+ class="p-6 pt-0"
131
+ >
132
+ Just content
133
+ </div>
134
+ </div>
135
+ `;
136
+
137
+ exports[`Card snapshots > renders header-only card 1`] = `
138
+ <div
139
+ class="rounded-lg border bg-card text-card-foreground shadow-sm"
140
+ >
141
+ <div
142
+ class="flex flex-col space-y-1.5 p-6"
143
+ >
144
+ <div
145
+ class="text-2xl font-semibold leading-none tracking-tight"
146
+ >
147
+ Minimal Card
148
+ </div>
149
+ </div>
150
+ </div>
151
+ `;
152
+
153
+ exports[`Card snapshots > renders with custom className 1`] = `
154
+ <div
155
+ class="rounded-lg border bg-card text-card-foreground w-96 shadow-lg"
156
+ >
157
+ <div
158
+ class="p-6 pt-0"
159
+ >
160
+ Styled card
161
+ </div>
162
+ </div>
163
+ `;
164
+
165
+ exports[`Card snapshots > renders with header, content, and footer 1`] = `
166
+ <div
167
+ class="rounded-lg border bg-card text-card-foreground shadow-sm"
168
+ >
169
+ <div
170
+ class="flex flex-col space-y-1.5 p-6"
171
+ >
172
+ <div
173
+ class="text-2xl font-semibold leading-none tracking-tight"
174
+ >
175
+ Card Title
176
+ </div>
177
+ <div
178
+ class="text-sm text-muted-foreground"
179
+ >
180
+ A brief description
181
+ </div>
182
+ </div>
183
+ <div
184
+ class="p-6 pt-0"
185
+ >
186
+ <p>
187
+ Card body content
188
+ </p>
189
+ </div>
190
+ <div
191
+ class="flex items-center p-6 pt-0"
192
+ >
193
+ <button
194
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
195
+ >
196
+ Action
197
+ </button>
198
+ </div>
199
+ </div>
200
+ `;
201
+
202
+ exports[`Empty state snapshots > renders empty media with default variant 1`] = `
203
+ <div
204
+ class="flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-transparent"
205
+ data-slot="empty-icon"
206
+ data-variant="default"
207
+ >
208
+ <span>
209
+ 🔍
210
+ </span>
211
+ </div>
212
+ `;
213
+
214
+ exports[`Empty state snapshots > renders full empty state with media, title, description, and content 1`] = `
215
+ <div
216
+ class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12"
217
+ data-slot="empty"
218
+ >
219
+ <div
220
+ class="flex max-w-sm flex-col items-center gap-2 text-center"
221
+ data-slot="empty-header"
222
+ >
223
+ <div
224
+ class="mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6"
225
+ data-slot="empty-icon"
226
+ data-variant="icon"
227
+ >
228
+ <span>
229
+ 📭
230
+ </span>
231
+ </div>
232
+ <div
233
+ class="text-lg font-medium tracking-tight"
234
+ data-slot="empty-title"
235
+ >
236
+ No results found
237
+ </div>
238
+ <div
239
+ class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4"
240
+ data-slot="empty-description"
241
+ >
242
+ Try adjusting your search or filter criteria.
243
+ </div>
244
+ </div>
245
+ <div
246
+ class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance"
247
+ data-slot="empty-content"
248
+ >
249
+ <button
250
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
251
+ >
252
+ Clear filters
253
+ </button>
254
+ </div>
255
+ </div>
256
+ `;
257
+
258
+ exports[`Empty state snapshots > renders minimal empty state with title only 1`] = `
259
+ <div
260
+ class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12"
261
+ data-slot="empty"
262
+ >
263
+ <div
264
+ class="flex max-w-sm flex-col items-center gap-2 text-center"
265
+ data-slot="empty-header"
266
+ >
267
+ <div
268
+ class="text-lg font-medium tracking-tight"
269
+ data-slot="empty-title"
270
+ >
271
+ Nothing here yet
272
+ </div>
273
+ </div>
274
+ </div>
275
+ `;
276
+
277
+ exports[`Spinner snapshots > renders default spinner 1`] = `
278
+ <div
279
+ aria-label="Loading"
280
+ class="flex items-center justify-center"
281
+ role="status"
282
+ >
283
+ <svg
284
+ aria-hidden="true"
285
+ class="lucide lucide-loader-circle animate-spin text-muted-foreground w-full h-full min-w-4 min-h-4"
286
+ fill="none"
287
+ height="24"
288
+ stroke="currentColor"
289
+ stroke-linecap="round"
290
+ stroke-linejoin="round"
291
+ stroke-width="2"
292
+ viewBox="0 0 24 24"
293
+ width="24"
294
+ xmlns="http://www.w3.org/2000/svg"
295
+ >
296
+ <path
297
+ d="M21 12a9 9 0 1 1-6.219-8.56"
298
+ />
299
+ </svg>
300
+ </div>
301
+ `;
302
+
303
+ exports[`Spinner snapshots > renders with custom className 1`] = `
304
+ <div
305
+ aria-label="Loading"
306
+ class="flex items-center justify-center h-8 w-8"
307
+ role="status"
308
+ >
309
+ <svg
310
+ aria-hidden="true"
311
+ class="lucide lucide-loader-circle animate-spin text-muted-foreground w-full h-full min-w-4 min-h-4"
312
+ fill="none"
313
+ height="24"
314
+ stroke="currentColor"
315
+ stroke-linecap="round"
316
+ stroke-linejoin="round"
317
+ stroke-width="2"
318
+ viewBox="0 0 24 24"
319
+ width="24"
320
+ xmlns="http://www.w3.org/2000/svg"
321
+ >
322
+ <path
323
+ d="M21 12a9 9 0 1 1-6.219-8.56"
324
+ />
325
+ </svg>
326
+ </div>
327
+ `;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Axe-core accessibility test suite for ObjectUI Shadcn components.
11
+ *
12
+ * Tests core UI primitives against WCAG 2.1 AA standards using axe-core.
13
+ * Part of Q1 2026 roadmap §1.4 Test Coverage Improvement.
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { render } from '@testing-library/react';
18
+ import { axe } from 'vitest-axe';
19
+ import React from 'react';
20
+ import {
21
+ Button,
22
+ Badge,
23
+ Card,
24
+ CardContent,
25
+ CardHeader,
26
+ CardTitle,
27
+ CardDescription,
28
+ Input,
29
+ Label,
30
+ Skeleton,
31
+ } from '@object-ui/components';
32
+
33
+ /**
34
+ * Helper to assert no axe violations.
35
+ * Works with vitest-axe's AxeResults format.
36
+ */
37
+ async function expectNoViolations(container: HTMLElement) {
38
+ const results = await axe(container);
39
+ const violations = results.violations || [];
40
+ if (violations.length > 0) {
41
+ const messages = violations.map(
42
+ (v: any) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} instance(s))`
43
+ );
44
+ throw new Error(`Expected no accessibility violations, but found ${violations.length}:\n${messages.join('\n')}`);
45
+ }
46
+ }
47
+
48
+ describe('Accessibility — axe-core (WCAG 2.1 AA)', () => {
49
+ it('Button should have no a11y violations', async () => {
50
+ const { container } = render(<Button>Click me</Button>);
51
+ await expectNoViolations(container);
52
+ });
53
+
54
+ it('Icon-only Button with aria-label should have no violations', async () => {
55
+ const { container } = render(
56
+ <Button aria-label="Close" size="icon">
57
+
58
+ </Button>
59
+ );
60
+ await expectNoViolations(container);
61
+ });
62
+
63
+ it('Badge should have no a11y violations', async () => {
64
+ const { container } = render(<Badge>Status</Badge>);
65
+ await expectNoViolations(container);
66
+ });
67
+
68
+ it('Card with proper heading hierarchy should have no violations', async () => {
69
+ const { container } = render(
70
+ <Card>
71
+ <CardHeader>
72
+ <CardTitle>Card Title</CardTitle>
73
+ <CardDescription>Card description text</CardDescription>
74
+ </CardHeader>
75
+ <CardContent>
76
+ <p>Card content</p>
77
+ </CardContent>
78
+ </Card>
79
+ );
80
+ await expectNoViolations(container);
81
+ });
82
+
83
+ it('Input with associated Label should have no violations', async () => {
84
+ const { container } = render(
85
+ <div>
86
+ <Label htmlFor="email">Email</Label>
87
+ <Input id="email" type="email" placeholder="Enter email" />
88
+ </div>
89
+ );
90
+ await expectNoViolations(container);
91
+ });
92
+
93
+ it('Input with aria-label should have no violations', async () => {
94
+ const { container } = render(
95
+ <Input aria-label="Search" type="search" placeholder="Search..." />
96
+ );
97
+ await expectNoViolations(container);
98
+ });
99
+
100
+ it('Skeleton should have no a11y violations', async () => {
101
+ const { container } = render(
102
+ <div role="status" aria-label="Loading">
103
+ <Skeleton className="h-10 w-full" />
104
+ <Skeleton className="h-4 w-3/4" />
105
+ </div>
106
+ );
107
+ await expectNoViolations(container);
108
+ });
109
+
110
+ it('Multiple Buttons in a group should have no violations', async () => {
111
+ const { container } = render(
112
+ <div role="group" aria-label="Actions">
113
+ <Button variant="default">Save</Button>
114
+ <Button variant="outline">Cancel</Button>
115
+ <Button variant="destructive">Delete</Button>
116
+ </div>
117
+ );
118
+ await expectNoViolations(container);
119
+ });
120
+
121
+ it('Form with proper labels should have no violations', async () => {
122
+ const { container } = render(
123
+ <form aria-label="Login form">
124
+ <div>
125
+ <Label htmlFor="username">Username</Label>
126
+ <Input id="username" type="text" />
127
+ </div>
128
+ <div>
129
+ <Label htmlFor="password">Password</Label>
130
+ <Input id="password" type="password" />
131
+ </div>
132
+ <Button type="submit">Sign In</Button>
133
+ </form>
134
+ );
135
+ await expectNoViolations(container);
136
+ });
137
+ });