@object-ui/components 3.3.0 → 3.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 (321) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +21 -1
  3. package/dist/index.css +6339 -2
  4. package/dist/index.js +17600 -17481
  5. package/dist/index.umd.cjs +36 -36
  6. package/dist/packages/components/src/custom/empty.d.ts +12 -1
  7. package/dist/packages/components/src/renderers/action/action-bar.d.ts +12 -1
  8. package/dist/packages/components/src/ui/chart.d.ts +10 -29
  9. package/package.json +65 -44
  10. package/.turbo/turbo-build.log +0 -84
  11. package/README_SHADCN_SYNC.md +0 -281
  12. package/TESTING.md +0 -335
  13. package/docs/FilterBuilder.md +0 -268
  14. package/metadata/Chart.component.yml +0 -30
  15. package/metadata/FilterBuilder.component.yml +0 -39
  16. package/metadata/GridLayout.component.yml +0 -27
  17. package/metadata/Menu.component.yml +0 -31
  18. package/metadata/ObjectForm.component.yml +0 -34
  19. package/metadata/ObjectGrid.component.yml +0 -72
  20. package/metadata/Page.component.yml +0 -24
  21. package/postcss.config.js +0 -14
  22. package/shadcn-components.json +0 -440
  23. package/src/SchemaRenderer.tsx +0 -28
  24. package/src/__tests__/PageRendererRegions.test.tsx +0 -668
  25. package/src/__tests__/README.md +0 -124
  26. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +0 -811
  27. package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +0 -327
  28. package/src/__tests__/accessibility.test.tsx +0 -137
  29. package/src/__tests__/action-bar.test.tsx +0 -206
  30. package/src/__tests__/api-consistency.test.tsx +0 -596
  31. package/src/__tests__/basic-renderers.test.tsx +0 -255
  32. package/src/__tests__/color-contrast.test.tsx +0 -212
  33. package/src/__tests__/complex-disclosure-renderers.test.tsx +0 -302
  34. package/src/__tests__/compliance.test.tsx +0 -72
  35. package/src/__tests__/config-field-renderer.test.tsx +0 -307
  36. package/src/__tests__/config-panel-renderer.test.tsx +0 -580
  37. package/src/__tests__/config-primitives.test.tsx +0 -106
  38. package/src/__tests__/edge-cases.test.tsx +0 -285
  39. package/src/__tests__/feedback-overlay-renderers.test.tsx +0 -349
  40. package/src/__tests__/filter-builder.test.tsx +0 -409
  41. package/src/__tests__/form-renderers.test.tsx +0 -364
  42. package/src/__tests__/layout-data-renderers.test.tsx +0 -340
  43. package/src/__tests__/mobile-accessibility.test.tsx +0 -120
  44. package/src/__tests__/navigation-overlay.test.tsx +0 -370
  45. package/src/__tests__/snapshot-critical.test.tsx +0 -317
  46. package/src/__tests__/snapshot.test.tsx +0 -205
  47. package/src/__tests__/test-utils.tsx +0 -190
  48. package/src/__tests__/use-config-draft.test.tsx +0 -295
  49. package/src/__tests__/view-compliance.test.tsx +0 -153
  50. package/src/__tests__/wcag-audit.test.tsx +0 -493
  51. package/src/custom/action-param-dialog.tsx +0 -264
  52. package/src/custom/button-group.tsx +0 -91
  53. package/src/custom/combobox.tsx +0 -104
  54. package/src/custom/config-field-renderer.tsx +0 -276
  55. package/src/custom/config-panel-renderer.tsx +0 -306
  56. package/src/custom/config-row.tsx +0 -50
  57. package/src/custom/date-picker.tsx +0 -61
  58. package/src/custom/empty.tsx +0 -112
  59. package/src/custom/field.tsx +0 -81
  60. package/src/custom/filter-builder.tsx +0 -418
  61. package/src/custom/index.ts +0 -21
  62. package/src/custom/input-group.tsx +0 -53
  63. package/src/custom/item.tsx +0 -201
  64. package/src/custom/kbd.tsx +0 -36
  65. package/src/custom/mobile-dialog-content.tsx +0 -67
  66. package/src/custom/native-select.tsx +0 -33
  67. package/src/custom/navigation-overlay.tsx +0 -334
  68. package/src/custom/section-header.tsx +0 -68
  69. package/src/custom/sort-builder.tsx +0 -129
  70. package/src/custom/spinner.tsx +0 -26
  71. package/src/custom/view-skeleton.tsx +0 -243
  72. package/src/custom/view-states.tsx +0 -153
  73. package/src/debug/DebugPanel.tsx +0 -313
  74. package/src/debug/__tests__/DebugPanel.test.tsx +0 -134
  75. package/src/debug/index.ts +0 -10
  76. package/src/hooks/use-config-draft.ts +0 -127
  77. package/src/hooks/use-mobile.tsx +0 -27
  78. package/src/index.css +0 -245
  79. package/src/index.ts +0 -47
  80. package/src/lib/use-sync-external-store-shim.ts +0 -10
  81. package/src/lib/use-sync-external-store-with-selector-shim.ts +0 -90
  82. package/src/lib/utils.tsx +0 -35
  83. package/src/new-components.test.ts +0 -73
  84. package/src/renderers/action/action-bar.tsx +0 -221
  85. package/src/renderers/action/action-button.tsx +0 -158
  86. package/src/renderers/action/action-group.tsx +0 -270
  87. package/src/renderers/action/action-icon.tsx +0 -150
  88. package/src/renderers/action/action-menu.tsx +0 -203
  89. package/src/renderers/action/index.ts +0 -19
  90. package/src/renderers/action/resolve-icon.ts +0 -35
  91. package/src/renderers/basic/button-group.tsx +0 -79
  92. package/src/renderers/basic/div.tsx +0 -60
  93. package/src/renderers/basic/html.tsx +0 -43
  94. package/src/renderers/basic/icon.tsx +0 -89
  95. package/src/renderers/basic/image.tsx +0 -49
  96. package/src/renderers/basic/index.ts +0 -18
  97. package/src/renderers/basic/navigation-menu.tsx +0 -81
  98. package/src/renderers/basic/pagination.tsx +0 -109
  99. package/src/renderers/basic/separator.tsx +0 -57
  100. package/src/renderers/basic/span.tsx +0 -63
  101. package/src/renderers/basic/text.tsx +0 -52
  102. package/src/renderers/complex/README-KANBAN.md +0 -208
  103. package/src/renderers/complex/TIMELINE.md +0 -353
  104. package/src/renderers/complex/__tests__/data-table-airtable-ux.test.tsx +0 -239
  105. package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +0 -275
  106. package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +0 -120
  107. package/src/renderers/complex/__tests__/data-table-editing.test.tsx +0 -221
  108. package/src/renderers/complex/__tests__/data-table.test.ts +0 -76
  109. package/src/renderers/complex/carousel.tsx +0 -69
  110. package/src/renderers/complex/data-table.tsx +0 -1243
  111. package/src/renderers/complex/filter-builder.tsx +0 -77
  112. package/src/renderers/complex/index.ts +0 -16
  113. package/src/renderers/complex/resizable.tsx +0 -66
  114. package/src/renderers/complex/scroll-area.tsx +0 -58
  115. package/src/renderers/complex/table.tsx +0 -95
  116. package/src/renderers/data-display/alert.tsx +0 -46
  117. package/src/renderers/data-display/avatar.tsx +0 -38
  118. package/src/renderers/data-display/badge.tsx +0 -55
  119. package/src/renderers/data-display/breadcrumb.tsx +0 -61
  120. package/src/renderers/data-display/index.ts +0 -18
  121. package/src/renderers/data-display/kbd.tsx +0 -50
  122. package/src/renderers/data-display/list.tsx +0 -75
  123. package/src/renderers/data-display/statistic.tsx +0 -95
  124. package/src/renderers/data-display/table.tsx +0 -78
  125. package/src/renderers/data-display/tree-view.tsx +0 -176
  126. package/src/renderers/disclosure/accordion.tsx +0 -69
  127. package/src/renderers/disclosure/collapsible.tsx +0 -53
  128. package/src/renderers/disclosure/index.ts +0 -11
  129. package/src/renderers/disclosure/toggle-group.tsx +0 -79
  130. package/src/renderers/feedback/empty.tsx +0 -49
  131. package/src/renderers/feedback/index.ts +0 -16
  132. package/src/renderers/feedback/loading.tsx +0 -78
  133. package/src/renderers/feedback/progress.tsx +0 -29
  134. package/src/renderers/feedback/skeleton.tsx +0 -31
  135. package/src/renderers/feedback/sonner.tsx +0 -56
  136. package/src/renderers/feedback/spinner.tsx +0 -55
  137. package/src/renderers/feedback/toast.tsx +0 -59
  138. package/src/renderers/feedback/toaster.tsx +0 -23
  139. package/src/renderers/form/button.tsx +0 -103
  140. package/src/renderers/form/calendar.tsx +0 -34
  141. package/src/renderers/form/checkbox.tsx +0 -71
  142. package/src/renderers/form/combobox.tsx +0 -48
  143. package/src/renderers/form/command.tsx +0 -58
  144. package/src/renderers/form/date-picker.tsx +0 -84
  145. package/src/renderers/form/file-upload.tsx +0 -184
  146. package/src/renderers/form/form.tsx +0 -540
  147. package/src/renderers/form/index.ts +0 -26
  148. package/src/renderers/form/input-otp.tsx +0 -51
  149. package/src/renderers/form/input.tsx +0 -121
  150. package/src/renderers/form/label.tsx +0 -45
  151. package/src/renderers/form/radio-group.tsx +0 -63
  152. package/src/renderers/form/select.tsx +0 -94
  153. package/src/renderers/form/slider.tsx +0 -61
  154. package/src/renderers/form/switch.tsx +0 -48
  155. package/src/renderers/form/textarea.tsx +0 -76
  156. package/src/renderers/form/toggle.tsx +0 -42
  157. package/src/renderers/index.ts +0 -18
  158. package/src/renderers/layout/aspect-ratio.tsx +0 -51
  159. package/src/renderers/layout/card.tsx +0 -85
  160. package/src/renderers/layout/container.tsx +0 -122
  161. package/src/renderers/layout/flex.tsx +0 -132
  162. package/src/renderers/layout/grid.tsx +0 -178
  163. package/src/renderers/layout/index.ts +0 -19
  164. package/src/renderers/layout/page.tsx +0 -466
  165. package/src/renderers/layout/semantic.tsx +0 -48
  166. package/src/renderers/layout/stack.tsx +0 -132
  167. package/src/renderers/layout/tabs.tsx +0 -97
  168. package/src/renderers/navigation/header-bar.tsx +0 -118
  169. package/src/renderers/navigation/index.ts +0 -10
  170. package/src/renderers/navigation/sidebar.tsx +0 -208
  171. package/src/renderers/overlay/alert-dialog.tsx +0 -72
  172. package/src/renderers/overlay/context-menu.tsx +0 -100
  173. package/src/renderers/overlay/dialog.tsx +0 -77
  174. package/src/renderers/overlay/drawer.tsx +0 -77
  175. package/src/renderers/overlay/dropdown-menu.tsx +0 -99
  176. package/src/renderers/overlay/hover-card.tsx +0 -55
  177. package/src/renderers/overlay/index.ts +0 -18
  178. package/src/renderers/overlay/menubar.tsx +0 -76
  179. package/src/renderers/overlay/popover.tsx +0 -56
  180. package/src/renderers/overlay/sheet.tsx +0 -77
  181. package/src/renderers/overlay/tooltip.tsx +0 -67
  182. package/src/renderers/placeholders.tsx +0 -107
  183. package/src/stories/CRMApp.stories.tsx +0 -706
  184. package/src/stories/ConfigPanel.stories.tsx +0 -232
  185. package/src/stories/Guide.mdx +0 -55
  186. package/src/stories/MockedData.stories.tsx +0 -121
  187. package/src/stories/assets/accessibility.png +0 -0
  188. package/src/stories/assets/accessibility.svg +0 -1
  189. package/src/stories/assets/addon-library.png +0 -0
  190. package/src/stories/assets/assets.png +0 -0
  191. package/src/stories/assets/avif-test-image.avif +0 -0
  192. package/src/stories/assets/context.png +0 -0
  193. package/src/stories/assets/discord.svg +0 -1
  194. package/src/stories/assets/docs.png +0 -0
  195. package/src/stories/assets/figma-plugin.png +0 -0
  196. package/src/stories/assets/github.svg +0 -1
  197. package/src/stories/assets/share.png +0 -0
  198. package/src/stories/assets/styling.png +0 -0
  199. package/src/stories/assets/testing.png +0 -0
  200. package/src/stories/assets/theming.png +0 -0
  201. package/src/stories/assets/tutorials.svg +0 -1
  202. package/src/stories/assets/youtube.svg +0 -1
  203. package/src/stories/button.css +0 -30
  204. package/src/stories/header.css +0 -32
  205. package/src/stories/page.css +0 -68
  206. package/src/stories-json/Accessibility.mdx +0 -297
  207. package/src/stories-json/EdgeCases.stories.tsx +0 -160
  208. package/src/stories-json/GettingStarted.mdx +0 -89
  209. package/src/stories-json/Introduction.mdx +0 -127
  210. package/src/stories-json/accordion.stories.tsx +0 -43
  211. package/src/stories-json/aggrid.stories.tsx +0 -103
  212. package/src/stories-json/alert.stories.tsx +0 -39
  213. package/src/stories-json/aspect-ratio.stories.tsx +0 -34
  214. package/src/stories-json/avatar.stories.tsx +0 -38
  215. package/src/stories-json/badge.stories.tsx +0 -53
  216. package/src/stories-json/breadcrumb.stories.tsx +0 -30
  217. package/src/stories-json/button-group.stories.tsx +0 -43
  218. package/src/stories-json/button.stories.tsx +0 -73
  219. package/src/stories-json/calendar.stories.tsx +0 -85
  220. package/src/stories-json/card.stories.tsx +0 -48
  221. package/src/stories-json/carousel.stories.tsx +0 -33
  222. package/src/stories-json/charts.stories.tsx +0 -195
  223. package/src/stories-json/chatbot.stories.tsx +0 -349
  224. package/src/stories-json/code-editor.stories.tsx +0 -92
  225. package/src/stories-json/collapsible.stories.tsx +0 -40
  226. package/src/stories-json/controls.stories.tsx +0 -36
  227. package/src/stories-json/crm-live-data.stories.tsx +0 -154
  228. package/src/stories-json/dashboard.stories.tsx +0 -318
  229. package/src/stories-json/data-table.stories.tsx +0 -136
  230. package/src/stories-json/data_display_extras.stories.tsx +0 -102
  231. package/src/stories-json/date-picker.stories.tsx +0 -28
  232. package/src/stories-json/detail-view.stories.tsx +0 -258
  233. package/src/stories-json/dialog.stories.tsx +0 -43
  234. package/src/stories-json/feedback_extras.stories.tsx +0 -40
  235. package/src/stories-json/feedback_others.stories.tsx +0 -46
  236. package/src/stories-json/form-variants.stories.tsx +0 -210
  237. package/src/stories-json/form_advanced.stories.tsx +0 -117
  238. package/src/stories-json/form_extras.stories.tsx +0 -123
  239. package/src/stories-json/grid.stories.tsx +0 -56
  240. package/src/stories-json/icon.stories.tsx +0 -36
  241. package/src/stories-json/input.stories.tsx +0 -52
  242. package/src/stories-json/kanban.stories.tsx +0 -295
  243. package/src/stories-json/layout_extended.stories.tsx +0 -76
  244. package/src/stories-json/layout_flex.stories.tsx +0 -107
  245. package/src/stories-json/list-view.stories.tsx +0 -97
  246. package/src/stories-json/markdown.stories.tsx +0 -129
  247. package/src/stories-json/menus.stories.tsx +0 -63
  248. package/src/stories-json/metric-card.stories.tsx +0 -143
  249. package/src/stories-json/navigation-menu.stories.tsx +0 -37
  250. package/src/stories-json/object-aggrid-advanced.stories.tsx +0 -389
  251. package/src/stories-json/object-aggrid.stories.tsx +0 -252
  252. package/src/stories-json/object-form.stories.tsx +0 -130
  253. package/src/stories-json/object-gantt.stories.tsx +0 -114
  254. package/src/stories-json/object-grid.stories.tsx +0 -315
  255. package/src/stories-json/object-map.stories.tsx +0 -116
  256. package/src/stories-json/object-view.stories.tsx +0 -118
  257. package/src/stories-json/overlay_extras.stories.tsx +0 -113
  258. package/src/stories-json/overlay_others.stories.tsx +0 -76
  259. package/src/stories-json/page.stories.tsx +0 -55
  260. package/src/stories-json/reports.stories.tsx +0 -163
  261. package/src/stories-json/resizable.stories.tsx +0 -44
  262. package/src/stories-json/select.stories.tsx +0 -34
  263. package/src/stories-json/separator.stories.tsx +0 -41
  264. package/src/stories-json/sidebar.stories.tsx +0 -147
  265. package/src/stories-json/statistic.stories.tsx +0 -44
  266. package/src/stories-json/tabs.stories.tsx +0 -51
  267. package/src/stories-json/timeline.stories.tsx +0 -188
  268. package/src/stories-json/typography.stories.tsx +0 -45
  269. package/src/types/config-panel.ts +0 -101
  270. package/src/ui/accordion.tsx +0 -66
  271. package/src/ui/alert-dialog.tsx +0 -149
  272. package/src/ui/alert.tsx +0 -67
  273. package/src/ui/aspect-ratio.tsx +0 -15
  274. package/src/ui/avatar.tsx +0 -58
  275. package/src/ui/badge.tsx +0 -44
  276. package/src/ui/breadcrumb.tsx +0 -123
  277. package/src/ui/button.tsx +0 -64
  278. package/src/ui/calendar.tsx +0 -221
  279. package/src/ui/card.tsx +0 -87
  280. package/src/ui/carousel.tsx +0 -270
  281. package/src/ui/chart.tsx +0 -377
  282. package/src/ui/checkbox.tsx +0 -38
  283. package/src/ui/collapsible.tsx +0 -19
  284. package/src/ui/command.tsx +0 -161
  285. package/src/ui/context-menu.tsx +0 -208
  286. package/src/ui/dialog.tsx +0 -130
  287. package/src/ui/drawer.tsx +0 -126
  288. package/src/ui/dropdown-menu.tsx +0 -208
  289. package/src/ui/form.tsx +0 -186
  290. package/src/ui/hover-card.tsx +0 -37
  291. package/src/ui/index.ts +0 -56
  292. package/src/ui/input-otp.tsx +0 -79
  293. package/src/ui/input.tsx +0 -30
  294. package/src/ui/label.tsx +0 -34
  295. package/src/ui/menubar.tsx +0 -264
  296. package/src/ui/navigation-menu.tsx +0 -136
  297. package/src/ui/pagination.tsx +0 -125
  298. package/src/ui/popover.tsx +0 -39
  299. package/src/ui/progress.tsx +0 -36
  300. package/src/ui/radio-group.tsx +0 -52
  301. package/src/ui/resizable.tsx +0 -53
  302. package/src/ui/scroll-area.tsx +0 -56
  303. package/src/ui/select.tsx +0 -168
  304. package/src/ui/separator.tsx +0 -39
  305. package/src/ui/sheet.tsx +0 -150
  306. package/src/ui/sidebar.tsx +0 -781
  307. package/src/ui/skeleton.tsx +0 -23
  308. package/src/ui/slider.tsx +0 -39
  309. package/src/ui/sonner.tsx +0 -53
  310. package/src/ui/switch.tsx +0 -37
  311. package/src/ui/table.tsx +0 -125
  312. package/src/ui/tabs.tsx +0 -63
  313. package/src/ui/textarea.tsx +0 -30
  314. package/src/ui/toast.tsx +0 -137
  315. package/src/ui/toggle-group.tsx +0 -69
  316. package/src/ui/toggle.tsx +0 -53
  317. package/src/ui/tooltip.tsx +0 -38
  318. package/src/ui/typography.tsx +0 -85
  319. package/tsconfig.json +0 -19
  320. package/vite.config.ts +0 -71
  321. package/vitest.config.ts +0 -5
@@ -1,596 +0,0 @@
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
- * P3.1 Component Quality Audit - API Consistency
11
- *
12
- * Comprehensive audit verifying all exported UI components follow
13
- * consistent patterns:
14
- * - data-slot attribute for custom components
15
- * - className prop acceptance and forwarding via cn()
16
- * - React.forwardRef for primitive components
17
- * - displayName set on forwardRef components
18
- * - Consistent prop naming (variant/size, not variants/sizes)
19
- * - All exported types are defined
20
- * - Source-level pattern scanning for cn() usage
21
- */
22
-
23
- import { describe, it, expect } from 'vitest';
24
- import React from 'react';
25
- import { render, screen } from '@testing-library/react';
26
- import '@testing-library/jest-dom';
27
- import * as fs from 'node:fs';
28
- import * as path from 'node:path';
29
-
30
- import {
31
- Badge,
32
- Button,
33
- Card,
34
- CardHeader,
35
- CardTitle,
36
- CardDescription,
37
- CardContent,
38
- CardFooter,
39
- Input,
40
- Label,
41
- Separator,
42
- Skeleton,
43
- Progress,
44
- Alert,
45
- AlertTitle,
46
- AlertDescription,
47
- Textarea,
48
- } from '../ui';
49
-
50
- import {
51
- Kbd,
52
- KbdGroup,
53
- Empty,
54
- EmptyHeader,
55
- EmptyTitle,
56
- EmptyDescription,
57
- EmptyContent,
58
- EmptyMedia,
59
- ButtonGroup,
60
- Item,
61
- ItemGroup,
62
- ItemContent,
63
- ItemTitle,
64
- ItemDescription as ItemDesc,
65
- ItemActions,
66
- ItemMedia,
67
- Spinner,
68
- DataLoadingState,
69
- DataEmptyState,
70
- DataErrorState,
71
- } from '../custom';
72
-
73
- import { cn } from '../lib/utils';
74
-
75
- // ---------------------------------------------------------------------------
76
- // Helpers
77
- // ---------------------------------------------------------------------------
78
-
79
- const UI_DIR = path.resolve(__dirname, '..', 'ui');
80
- const CUSTOM_DIR = path.resolve(__dirname, '..', 'custom');
81
-
82
- /** Read a source file from either ui/ or custom/ directory. */
83
- function readSource(dir: string, filename: string): string {
84
- return fs.readFileSync(path.join(dir, filename), 'utf-8');
85
- }
86
-
87
- /** List .tsx component files in a directory (excluding index.ts). */
88
- function listComponentFiles(dir: string): string[] {
89
- return fs
90
- .readdirSync(dir)
91
- .filter((f) => f.endsWith('.tsx') && f !== 'index.tsx');
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // 1. data-slot attribute pattern (custom components)
96
- // ---------------------------------------------------------------------------
97
- describe('P3.1 API Consistency Audit', () => {
98
- describe('data-slot attribute on custom components', () => {
99
- const customComponentsWithSlot = [
100
- { name: 'Kbd', Component: Kbd, slot: 'kbd', children: 'K' },
101
- { name: 'KbdGroup', Component: KbdGroup, slot: 'kbd-group', children: 'G' },
102
- { name: 'Empty', Component: Empty, slot: 'empty', children: 'E' },
103
- { name: 'EmptyHeader', Component: EmptyHeader, slot: 'empty-header', children: 'H' },
104
- { name: 'EmptyTitle', Component: EmptyTitle, slot: 'empty-title', children: 'T' },
105
- { name: 'EmptyDescription', Component: EmptyDescription, slot: 'empty-description', children: 'D' },
106
- { name: 'EmptyContent', Component: EmptyContent, slot: 'empty-content', children: 'C' },
107
- { name: 'ButtonGroup', Component: ButtonGroup, slot: 'button-group', children: 'B' },
108
- { name: 'Item', Component: Item, slot: 'item', children: 'I' },
109
- { name: 'ItemGroup', Component: ItemGroup, slot: 'item-group', children: 'G' },
110
- { name: 'ItemContent', Component: ItemContent, slot: 'item-content', children: 'C' },
111
- { name: 'ItemTitle', Component: ItemTitle, slot: 'item-title', children: 'T' },
112
- { name: 'ItemDesc', Component: ItemDesc, slot: 'item-description', children: 'D' },
113
- { name: 'ItemActions', Component: ItemActions, slot: 'item-actions', children: 'A' },
114
- { name: 'DataLoadingState', Component: DataLoadingState, slot: 'data-loading-state', children: undefined },
115
- { name: 'DataEmptyState', Component: DataEmptyState, slot: 'data-empty-state', children: undefined },
116
- { name: 'DataErrorState', Component: DataErrorState, slot: 'data-error-state', children: undefined },
117
- ];
118
-
119
- it.each(customComponentsWithSlot)(
120
- '$name renders data-slot="$slot"',
121
- ({ Component, slot, children }) => {
122
- const { container } = render(
123
- <Component>{children}</Component>
124
- );
125
- const el = container.querySelector(`[data-slot="${slot}"]`);
126
- expect(el).toBeInTheDocument();
127
- }
128
- );
129
- });
130
-
131
- // ---------------------------------------------------------------------------
132
- // 2. className prop acceptance and forwarding
133
- // ---------------------------------------------------------------------------
134
- describe('className prop forwarding', () => {
135
- const CUSTOM_CLASS = 'oui-test-custom-class';
136
-
137
- const classNameTests = [
138
- { name: 'Badge', render: () => render(<Badge className={CUSTOM_CLASS}>tag</Badge>) },
139
- { name: 'Button', render: () => render(<Button className={CUSTOM_CLASS}>btn</Button>) },
140
- { name: 'Card', render: () => render(<Card className={CUSTOM_CLASS}>card</Card>) },
141
- { name: 'CardHeader', render: () => render(<CardHeader className={CUSTOM_CLASS} />) },
142
- { name: 'CardTitle', render: () => render(<CardTitle className={CUSTOM_CLASS}>T</CardTitle>) },
143
- { name: 'CardDescription', render: () => render(<CardDescription className={CUSTOM_CLASS}>D</CardDescription>) },
144
- { name: 'CardContent', render: () => render(<CardContent className={CUSTOM_CLASS} />) },
145
- { name: 'CardFooter', render: () => render(<CardFooter className={CUSTOM_CLASS} />) },
146
- { name: 'Input', render: () => render(<Input className={CUSTOM_CLASS} data-testid="inp" />) },
147
- { name: 'Textarea', render: () => render(<Textarea className={CUSTOM_CLASS} data-testid="ta" />) },
148
- { name: 'Label', render: () => render(<Label className={CUSTOM_CLASS}>L</Label>) },
149
- { name: 'Separator', render: () => render(<Separator className={CUSTOM_CLASS} />) },
150
- { name: 'Skeleton', render: () => render(<Skeleton className={CUSTOM_CLASS} />) },
151
- { name: 'Alert', render: () => render(<Alert className={CUSTOM_CLASS}>A</Alert>) },
152
- { name: 'AlertTitle', render: () => render(<AlertTitle className={CUSTOM_CLASS}>T</AlertTitle>) },
153
- { name: 'AlertDescription', render: () => render(<AlertDescription className={CUSTOM_CLASS}>D</AlertDescription>) },
154
- { name: 'Kbd', render: () => render(<Kbd className={CUSTOM_CLASS}>K</Kbd>) },
155
- { name: 'Empty', render: () => render(<Empty className={CUSTOM_CLASS}>E</Empty>) },
156
- { name: 'ButtonGroup', render: () => render(<ButtonGroup className={CUSTOM_CLASS}>B</ButtonGroup>) },
157
- { name: 'Item', render: () => render(<Item className={CUSTOM_CLASS}>I</Item>) },
158
- { name: 'Spinner', render: () => render(<Spinner className={CUSTOM_CLASS} />) },
159
- ];
160
-
161
- it.each(classNameTests)(
162
- '$name accepts and forwards className',
163
- ({ render: doRender }) => {
164
- const { container } = doRender();
165
- const el = container.querySelector(`.${CUSTOM_CLASS}`);
166
- expect(el).toBeInTheDocument();
167
- }
168
- );
169
- });
170
-
171
- // ---------------------------------------------------------------------------
172
- // 3. Source files use cn() utility for class merging
173
- // ---------------------------------------------------------------------------
174
- describe('cn() utility usage in source files', () => {
175
- const uiFiles = listComponentFiles(UI_DIR);
176
- const customFiles = listComponentFiles(CUSTOM_DIR);
177
-
178
- // Representative UI files that must import cn
179
- const representativeUiFiles = [
180
- 'button.tsx',
181
- 'card.tsx',
182
- 'input.tsx',
183
- 'badge.tsx',
184
- 'alert.tsx',
185
- 'separator.tsx',
186
- 'label.tsx',
187
- 'textarea.tsx',
188
- 'progress.tsx',
189
- 'typography.tsx',
190
- ].filter((f) => uiFiles.includes(f));
191
-
192
- it.each(representativeUiFiles)(
193
- 'ui/%s imports and uses cn()',
194
- (file) => {
195
- const src = readSource(UI_DIR, file);
196
- expect(src).toMatch(/import\s*\{[^}]*\bcn\b[^}]*\}\s*from/);
197
- expect(src).toContain('cn(');
198
- }
199
- );
200
-
201
- const representativeCustomFiles = [
202
- 'empty.tsx',
203
- 'kbd.tsx',
204
- 'button-group.tsx',
205
- 'item.tsx',
206
- 'spinner.tsx',
207
- 'view-states.tsx',
208
- ].filter((f) => customFiles.includes(f));
209
-
210
- it.each(representativeCustomFiles)(
211
- 'custom/%s imports and uses cn()',
212
- (file) => {
213
- const src = readSource(CUSTOM_DIR, file);
214
- expect(src).toMatch(/import\s*\{[^}]*\bcn\b[^}]*\}\s*from/);
215
- expect(src).toContain('cn(');
216
- }
217
- );
218
- });
219
-
220
- // ---------------------------------------------------------------------------
221
- // 4. React.forwardRef on primitive UI components
222
- // ---------------------------------------------------------------------------
223
- describe('React.forwardRef on primitive components', () => {
224
- const forwardRefFiles = [
225
- 'button.tsx',
226
- 'card.tsx',
227
- 'input.tsx',
228
- 'textarea.tsx',
229
- 'label.tsx',
230
- 'separator.tsx',
231
- 'progress.tsx',
232
- 'alert.tsx',
233
- ];
234
-
235
- it.each(forwardRefFiles)(
236
- 'ui/%s uses React.forwardRef',
237
- (file) => {
238
- const src = readSource(UI_DIR, file);
239
- expect(src).toContain('forwardRef');
240
- }
241
- );
242
-
243
- // Runtime verification: refs actually resolve
244
- const refTests = [
245
- { name: 'Button', ref: React.createRef<HTMLButtonElement>(), el: () => <Button ref={React.createRef<HTMLButtonElement>()}>B</Button>, instanceOf: HTMLButtonElement },
246
- { name: 'Input', ref: React.createRef<HTMLInputElement>(), el: () => <Input ref={React.createRef<HTMLInputElement>()} />, instanceOf: HTMLInputElement },
247
- { name: 'Card', ref: React.createRef<HTMLDivElement>(), el: () => <Card ref={React.createRef<HTMLDivElement>()}>C</Card>, instanceOf: HTMLDivElement },
248
- { name: 'Textarea', ref: React.createRef<HTMLTextAreaElement>(), el: () => <Textarea ref={React.createRef<HTMLTextAreaElement>()} />, instanceOf: HTMLTextAreaElement },
249
- { name: 'Alert', ref: React.createRef<HTMLDivElement>(), el: () => <Alert ref={React.createRef<HTMLDivElement>()}>A</Alert>, instanceOf: HTMLDivElement },
250
- ];
251
-
252
- it.each(refTests)(
253
- '$name forwards ref to correct DOM element',
254
- ({ name, instanceOf }) => {
255
- const ref = React.createRef<any>();
256
- const components: Record<string, JSX.Element> = {
257
- Button: <Button ref={ref}>B</Button>,
258
- Input: <Input ref={ref} />,
259
- Card: <Card ref={ref}>C</Card>,
260
- Textarea: <Textarea ref={ref} />,
261
- Alert: <Alert ref={ref}>A</Alert>,
262
- };
263
- render(components[name]);
264
- expect(ref.current).toBeInstanceOf(instanceOf);
265
- }
266
- );
267
- });
268
-
269
- // ---------------------------------------------------------------------------
270
- // 5. displayName set on forwardRef components
271
- // ---------------------------------------------------------------------------
272
- describe('displayName on forwardRef components', () => {
273
- const filesWithForwardRef = [
274
- 'button.tsx',
275
- 'card.tsx',
276
- 'input.tsx',
277
- 'textarea.tsx',
278
- 'label.tsx',
279
- 'separator.tsx',
280
- 'progress.tsx',
281
- 'alert.tsx',
282
- 'typography.tsx',
283
- ];
284
-
285
- it.each(filesWithForwardRef)(
286
- 'ui/%s sets displayName on every forwardRef component',
287
- (file) => {
288
- const src = readSource(UI_DIR, file);
289
- const forwardRefCount = (src.match(/forwardRef/g) || []).length;
290
- const displayNameCount = (src.match(/\.displayName\s*=/g) || []).length;
291
- // Each forwardRef call should have a corresponding displayName assignment
292
- expect(displayNameCount).toBeGreaterThanOrEqual(forwardRefCount);
293
- }
294
- );
295
-
296
- // Runtime check: exported components have displayName
297
- const namedComponents = [
298
- { name: 'Button', component: Button },
299
- { name: 'Input', component: Input },
300
- { name: 'Card', component: Card },
301
- { name: 'CardHeader', component: CardHeader },
302
- { name: 'CardTitle', component: CardTitle },
303
- { name: 'CardDescription', component: CardDescription },
304
- { name: 'CardContent', component: CardContent },
305
- { name: 'CardFooter', component: CardFooter },
306
- { name: 'Textarea', component: Textarea },
307
- { name: 'Label', component: Label },
308
- { name: 'Alert', component: Alert },
309
- ];
310
-
311
- it.each(namedComponents)(
312
- '$name has a displayName set',
313
- ({ component }) => {
314
- // forwardRef components expose displayName on the component object
315
- expect((component as any).displayName).toBeTruthy();
316
- }
317
- );
318
- });
319
-
320
- // ---------------------------------------------------------------------------
321
- // 6. Prop naming conventions: variant/size (singular), not variants/sizes
322
- // ---------------------------------------------------------------------------
323
- describe('prop naming conventions', () => {
324
- const allSourceFiles = [
325
- ...listComponentFiles(UI_DIR).map((f) => ({ dir: UI_DIR, file: f })),
326
- ...listComponentFiles(CUSTOM_DIR).map((f) => ({ dir: CUSTOM_DIR, file: f })),
327
- ];
328
-
329
- it('no component source uses plural "variants:" as a CVA key (should be "variant:")', () => {
330
- for (const { dir, file } of allSourceFiles) {
331
- const src = readSource(dir, file);
332
- // Detect if "variants" appears as a destructured prop in function args.
333
- // CVA config legitimately uses `variants: { variant: ... }` at the top
334
- // level — we only flag files that destructure `variants` from component
335
- // props, which would indicate an incorrect plural prop name.
336
- const destructuresVariantsProp = /\{\s*(?:.*,\s*)?variants\s*[,}]/.test(src);
337
- const hasCvaVariantsBlock = /variants\s*:\s*\{/.test(src);
338
- const hasPluralPropDestructure = destructuresVariantsProp && !hasCvaVariantsBlock;
339
- expect(hasPluralPropDestructure).toBe(false);
340
- }
341
- });
342
-
343
- it('no component source uses plural "sizes" as a prop name', () => {
344
- for (const { dir, file } of allSourceFiles) {
345
- const src = readSource(dir, file);
346
- // "sizes" should not appear as a destructured prop
347
- expect(src).not.toMatch(/\(\s*\{[^}]*\bsizes\b/);
348
- }
349
- });
350
-
351
- // CVA definitions should use "variant" and "size" (singular) as variant keys
352
- it('CVA variant keys use singular "variant" not "variants"', () => {
353
- const cvaFiles = allSourceFiles.filter(({ dir, file }) =>
354
- readSource(dir, file).includes('cva(')
355
- );
356
- for (const { dir, file } of cvaFiles) {
357
- const src = readSource(dir, file);
358
- // Inside the CVA config, look for `variants: { variant:` pattern
359
- // This confirms variant keys are singular inside the CVA variants object
360
- if (src.includes('variant:')) {
361
- expect(src).toMatch(/variants\s*:\s*\{[^}]*\bvariant\b\s*:/s);
362
- }
363
- }
364
- });
365
- });
366
-
367
- // ---------------------------------------------------------------------------
368
- // 7. Exported types and values are defined
369
- // ---------------------------------------------------------------------------
370
- describe('exported types and values are defined', () => {
371
- it('cn utility is exported and functional', () => {
372
- expect(typeof cn).toBe('function');
373
- expect(cn('a', 'b')).toBe('a b');
374
- // Tailwind merge deduplication
375
- expect(cn('p-4', 'p-2')).toBe('p-2');
376
- });
377
-
378
- it('all UI components are exported as functions or objects', () => {
379
- const uiComponents = [
380
- Badge, Button, Card, CardHeader, CardTitle, CardDescription,
381
- CardContent, CardFooter, Input, Label, Separator, Skeleton,
382
- Progress, Alert, AlertTitle, AlertDescription, Textarea,
383
- ];
384
- for (const comp of uiComponents) {
385
- expect(typeof comp).toMatch(/^(function|object)$/);
386
- }
387
- });
388
-
389
- it('all custom components are exported as functions', () => {
390
- const customComponents = [
391
- Kbd, KbdGroup, Empty, EmptyHeader, EmptyTitle, EmptyDescription,
392
- EmptyContent, EmptyMedia, ButtonGroup, Item, ItemGroup,
393
- ItemContent, ItemTitle, ItemDesc, ItemActions, ItemMedia,
394
- Spinner, DataLoadingState, DataEmptyState, DataErrorState,
395
- ];
396
- for (const comp of customComponents) {
397
- expect(typeof comp).toBe('function');
398
- }
399
- });
400
-
401
- it('ui/index.ts re-exports all component files', () => {
402
- const indexSrc = fs.readFileSync(path.join(UI_DIR, 'index.ts'), 'utf-8');
403
- const uiFiles = listComponentFiles(UI_DIR);
404
- // toast.tsx is an internal module re-exported through sonner.tsx, not
405
- // listed in ui/index.ts directly. Exclude it from the re-export check.
406
- const internalModules = new Set(['toast']);
407
- const missingExports: string[] = [];
408
- for (const file of uiFiles) {
409
- const moduleName = file.replace('.tsx', '');
410
- if (internalModules.has(moduleName)) continue;
411
- if (!indexSrc.includes(`'./${moduleName}'`) && !indexSrc.includes(`"./${moduleName}"`)) {
412
- missingExports.push(moduleName);
413
- }
414
- }
415
- expect(missingExports).toEqual([]);
416
- });
417
- });
418
-
419
- // ---------------------------------------------------------------------------
420
- // Variant support (runtime)
421
- // ---------------------------------------------------------------------------
422
- describe('variant support', () => {
423
- it('Badge renders default variant without explicit prop', () => {
424
- const { container } = render(<Badge>tag</Badge>);
425
- expect(container.firstElementChild!.className).toContain('bg-primary');
426
- });
427
-
428
- it('Badge renders destructive variant', () => {
429
- const { container } = render(<Badge variant="destructive">err</Badge>);
430
- expect(container.firstElementChild!.className).toContain('bg-destructive');
431
- });
432
-
433
- it('Badge renders outline variant', () => {
434
- const { container } = render(<Badge variant="outline">out</Badge>);
435
- expect(container.firstElementChild!.className).toContain('text-foreground');
436
- });
437
-
438
- it('Button renders default variant without explicit prop', () => {
439
- render(<Button>Click</Button>);
440
- expect(screen.getByRole('button').className).toContain('bg-primary');
441
- });
442
-
443
- it('Button renders destructive variant', () => {
444
- render(<Button variant="destructive">Del</Button>);
445
- expect(screen.getByRole('button').className).toContain('bg-destructive');
446
- });
447
-
448
- it('Button renders ghost variant', () => {
449
- render(<Button variant="ghost">G</Button>);
450
- expect(screen.getByRole('button').className).toContain('hover:bg-accent');
451
- });
452
-
453
- it('Button renders outline variant', () => {
454
- render(<Button variant="outline">O</Button>);
455
- expect(screen.getByRole('button').className).toContain('border');
456
- });
457
-
458
- it('Alert renders default variant', () => {
459
- render(<Alert>info</Alert>);
460
- expect(screen.getByRole('alert').className).toContain('bg-background');
461
- });
462
-
463
- it('Alert renders destructive variant', () => {
464
- render(<Alert variant="destructive">err</Alert>);
465
- expect(screen.getByRole('alert').className).toContain('border-destructive');
466
- });
467
- });
468
-
469
- // ---------------------------------------------------------------------------
470
- // Button size variants
471
- // ---------------------------------------------------------------------------
472
- describe('Button size variants', () => {
473
- it('renders default size', () => {
474
- render(<Button>D</Button>);
475
- expect(screen.getByRole('button').className).toContain('h-10');
476
- });
477
-
478
- it('renders sm size', () => {
479
- render(<Button size="sm">S</Button>);
480
- expect(screen.getByRole('button').className).toContain('h-9');
481
- });
482
-
483
- it('renders lg size', () => {
484
- render(<Button size="lg">L</Button>);
485
- expect(screen.getByRole('button').className).toContain('h-11');
486
- });
487
-
488
- it('renders icon size', () => {
489
- render(<Button size="icon">I</Button>);
490
- expect(screen.getByRole('button').className).toContain('w-10');
491
- });
492
- });
493
-
494
- // ---------------------------------------------------------------------------
495
- // HTML attribute pass-through
496
- // ---------------------------------------------------------------------------
497
- describe('HTML attribute pass-through', () => {
498
- it('Button supports disabled attribute', () => {
499
- render(<Button disabled>Dis</Button>);
500
- expect(screen.getByRole('button')).toBeDisabled();
501
- });
502
-
503
- it('Button supports type attribute', () => {
504
- render(<Button type="submit">Sub</Button>);
505
- expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
506
- });
507
-
508
- it('Input supports placeholder', () => {
509
- render(<Input placeholder="Enter..." data-testid="inp" />);
510
- expect(screen.getByTestId('inp')).toHaveAttribute('placeholder', 'Enter...');
511
- });
512
-
513
- it('Input supports disabled', () => {
514
- render(<Input disabled data-testid="inp" />);
515
- expect(screen.getByTestId('inp')).toBeDisabled();
516
- });
517
-
518
- it('Badge supports data-* attributes', () => {
519
- const { container } = render(<Badge data-testid="b1">tag</Badge>);
520
- expect(container.querySelector('[data-testid="b1"]')).toBeInTheDocument();
521
- });
522
- });
523
-
524
- // ---------------------------------------------------------------------------
525
- // Card composition pattern
526
- // ---------------------------------------------------------------------------
527
- describe('Card composition pattern', () => {
528
- it('renders full Card composition', () => {
529
- const { container } = render(
530
- <Card>
531
- <CardHeader>
532
- <CardTitle>Title</CardTitle>
533
- <CardDescription>Description</CardDescription>
534
- </CardHeader>
535
- <CardContent>Body</CardContent>
536
- <CardFooter>Footer</CardFooter>
537
- </Card>
538
- );
539
- expect(container.textContent).toContain('Title');
540
- expect(container.textContent).toContain('Description');
541
- expect(container.textContent).toContain('Body');
542
- expect(container.textContent).toContain('Footer');
543
- });
544
- });
545
-
546
- // ---------------------------------------------------------------------------
547
- // Alert composition pattern
548
- // ---------------------------------------------------------------------------
549
- describe('Alert composition pattern', () => {
550
- it('renders Alert with title and description', () => {
551
- render(
552
- <Alert>
553
- <AlertTitle>Heads up!</AlertTitle>
554
- <AlertDescription>You can add components.</AlertDescription>
555
- </Alert>
556
- );
557
- expect(screen.getByRole('alert')).toBeInTheDocument();
558
- expect(screen.getByText('Heads up!')).toBeInTheDocument();
559
- expect(screen.getByText('You can add components.')).toBeInTheDocument();
560
- });
561
- });
562
-
563
- // ---------------------------------------------------------------------------
564
- // Consistent defaults
565
- // ---------------------------------------------------------------------------
566
- describe('consistent defaults', () => {
567
- it('Separator defaults to horizontal orientation', () => {
568
- const { container } = render(<Separator />);
569
- const sep = container.firstElementChild!;
570
- expect(sep.getAttribute('data-orientation')).toBe('horizontal');
571
- });
572
-
573
- it('Separator can be vertical', () => {
574
- const { container } = render(<Separator orientation="vertical" />);
575
- const sep = container.firstElementChild!;
576
- expect(sep.getAttribute('data-orientation')).toBe('vertical');
577
- });
578
-
579
- it('Progress renders with zero value by default', () => {
580
- render(<Progress data-testid="prog" />);
581
- const prog = screen.getByTestId('prog');
582
- expect(prog).toBeInTheDocument();
583
- });
584
-
585
- it('Progress accepts value prop', () => {
586
- render(<Progress value={50} data-testid="prog" />);
587
- expect(screen.getByTestId('prog')).toBeInTheDocument();
588
- });
589
-
590
- it('Skeleton renders as a div', () => {
591
- const { container } = render(<Skeleton />);
592
- expect(container.firstElementChild!.tagName).toBe('DIV');
593
- expect(container.firstElementChild!.className).toContain('animate-pulse');
594
- });
595
- });
596
- });