@godxjp/ui 5.0.2 → 6.0.0

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 (298) hide show
  1. package/README.md +101 -142
  2. package/package.json +124 -128
  3. package/scripts/ui-audit.mjs +179 -0
  4. package/src/app/__tests__/app-provider.test.tsx +232 -0
  5. package/src/app/__tests__/date-format-labels.test.ts +36 -0
  6. package/src/app/__tests__/date-formats.test.ts +44 -0
  7. package/src/app/__tests__/timezones.test.ts +65 -0
  8. package/src/app/app-provider.tsx +227 -0
  9. package/src/app/date-format-labels.ts +21 -0
  10. package/src/app/date-formats.ts +30 -0
  11. package/src/app/index.ts +40 -0
  12. package/src/app/locales.ts +32 -0
  13. package/src/app/request-headers.ts +31 -0
  14. package/src/app/storage.ts +44 -0
  15. package/src/app/time-format-labels.ts +19 -0
  16. package/src/app/time-formats.ts +15 -0
  17. package/src/app/timezones.ts +208 -0
  18. package/src/app/types.ts +39 -0
  19. package/src/app/use-formatting.ts +47 -0
  20. package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
  21. package/src/components/__tests__/docs-parity.test.ts +41 -0
  22. package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
  23. package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
  24. package/src/components/admin/index.ts +76 -0
  25. package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
  26. package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
  27. package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
  28. package/src/components/data-display/__tests__/popover.test.tsx +92 -0
  29. package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
  30. package/src/components/data-display/badge.tsx +27 -0
  31. package/src/components/data-display/card.tsx +194 -0
  32. package/src/components/data-display/code-badge.tsx +28 -0
  33. package/src/components/data-display/collapsible.tsx +5 -0
  34. package/src/components/data-display/data-table.tsx +476 -0
  35. package/src/components/data-display/empty-state.tsx +22 -0
  36. package/src/components/data-display/index.ts +41 -0
  37. package/src/components/data-display/key-value-grid.tsx +46 -0
  38. package/src/components/data-display/popover.tsx +62 -0
  39. package/src/components/data-display/progress-meter.tsx +20 -0
  40. package/src/components/data-display/scan-panel.tsx +16 -0
  41. package/src/components/data-display/scroll-area.tsx +42 -0
  42. package/src/components/data-display/status-badge.tsx +83 -0
  43. package/src/components/data-display/table.tsx +59 -0
  44. package/src/components/data-display/timeline.tsx +42 -0
  45. package/src/components/data-display/tree-list.tsx +42 -0
  46. package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
  47. package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
  48. package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
  49. package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
  50. package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
  51. package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
  52. package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
  53. package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
  54. package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
  55. package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
  56. package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
  57. package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
  58. package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
  59. package/src/components/data-entry/autocomplete.tsx +91 -0
  60. package/src/components/data-entry/calendar.tsx +90 -0
  61. package/src/components/data-entry/cascader.tsx +305 -0
  62. package/src/components/data-entry/checkbox-group.tsx +90 -0
  63. package/src/components/data-entry/checkbox.tsx +30 -0
  64. package/src/components/data-entry/choice-field.tsx +27 -0
  65. package/src/components/data-entry/choice-option.ts +20 -0
  66. package/src/components/data-entry/color-picker.tsx +75 -0
  67. package/src/components/data-entry/command.tsx +56 -0
  68. package/src/components/data-entry/country-select.tsx +88 -0
  69. package/src/components/data-entry/date-picker.tsx +69 -0
  70. package/src/components/data-entry/date-range-picker.tsx +75 -0
  71. package/src/components/data-entry/form-field.tsx +59 -0
  72. package/src/components/data-entry/index.ts +62 -0
  73. package/src/components/data-entry/input.tsx +26 -0
  74. package/src/components/data-entry/label.tsx +25 -0
  75. package/src/components/data-entry/radio.tsx +109 -0
  76. package/src/components/data-entry/search-input.tsx +103 -0
  77. package/src/components/data-entry/select.tsx +149 -0
  78. package/src/components/data-entry/slider.tsx +38 -0
  79. package/src/components/data-entry/switch-field.tsx +91 -0
  80. package/src/components/data-entry/switch.tsx +24 -0
  81. package/src/components/data-entry/textarea.tsx +12 -0
  82. package/src/components/data-entry/time-picker.tsx +214 -0
  83. package/src/components/data-entry/transfer.tsx +231 -0
  84. package/src/components/data-entry/tree-select-strategy.ts +6 -0
  85. package/src/components/data-entry/tree-select.tsx +279 -0
  86. package/src/components/data-entry/tree-utils.ts +221 -0
  87. package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
  88. package/src/components/data-entry/upload-types.ts +86 -0
  89. package/src/components/data-entry/upload.tsx +498 -0
  90. package/src/components/data-entry/use-upload-draft.ts +93 -0
  91. package/src/components/feedback/__tests__/alert.test.tsx +127 -0
  92. package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
  93. package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
  94. package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
  95. package/src/components/feedback/__tests__/toast.test.tsx +52 -0
  96. package/src/components/feedback/alert.tsx +167 -0
  97. package/src/components/feedback/dialog.tsx +325 -0
  98. package/src/components/feedback/index.ts +53 -0
  99. package/src/components/feedback/sheet.tsx +130 -0
  100. package/src/components/feedback/skeleton.tsx +95 -0
  101. package/src/components/feedback/sonner.tsx +54 -0
  102. package/src/components/feedback/toaster.tsx +1 -0
  103. package/src/components/feedback/use-toast.ts +62 -0
  104. package/src/components/general/__tests__/button.test.tsx +71 -0
  105. package/src/components/general/button.tsx +61 -0
  106. package/src/components/general/index.ts +2 -0
  107. package/src/components/layout/__tests__/page-container.test.tsx +69 -0
  108. package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
  109. package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
  110. package/src/components/layout/app-shell.tsx +42 -0
  111. package/src/components/layout/breadcrumb.tsx +35 -0
  112. package/src/components/layout/index.ts +31 -0
  113. package/src/components/layout/inline.tsx +13 -0
  114. package/src/components/layout/menu.tsx +34 -0
  115. package/src/components/layout/mobile-frame.tsx +57 -0
  116. package/src/components/layout/page-container.tsx +81 -0
  117. package/src/components/layout/page-inset.tsx +16 -0
  118. package/src/components/layout/responsive-grid.tsx +14 -0
  119. package/src/components/layout/shell-app.tsx +30 -0
  120. package/src/components/layout/sidebar.tsx +98 -0
  121. package/src/components/layout/split-pane.tsx +16 -0
  122. package/src/components/layout/stack.tsx +13 -0
  123. package/src/components/layout/topbar.tsx +108 -0
  124. package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
  125. package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
  126. package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
  127. package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
  128. package/src/components/navigation/date-format-picker.tsx +55 -0
  129. package/src/components/navigation/dropdown-menu.tsx +190 -0
  130. package/src/components/navigation/filter-bar.tsx +38 -0
  131. package/src/components/navigation/index.ts +28 -0
  132. package/src/components/navigation/locale-picker.tsx +49 -0
  133. package/src/components/navigation/page-header.tsx +50 -0
  134. package/src/components/navigation/pagination-utils.ts +35 -0
  135. package/src/components/navigation/pagination.tsx +168 -0
  136. package/src/components/navigation/steps.tsx +163 -0
  137. package/src/components/navigation/tabs-items.tsx +69 -0
  138. package/src/components/navigation/tabs.tsx +67 -0
  139. package/src/components/navigation/time-format-picker.tsx +55 -0
  140. package/src/components/navigation/timezone-picker.tsx +63 -0
  141. package/src/components/query/__tests__/data-state.test.tsx +214 -0
  142. package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
  143. package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
  144. package/src/components/query/data-state.tsx +58 -0
  145. package/src/components/query/index.ts +10 -0
  146. package/src/components/query/infinite-query-state.tsx +99 -0
  147. package/src/components/query/mutation-feedback.tsx +31 -0
  148. package/src/components/query/prefetch-link.tsx +45 -0
  149. package/src/components/query/query-refetch-button.tsx +41 -0
  150. package/src/components/ui/alert-dialog.tsx +1 -0
  151. package/src/components/ui/alert.tsx +1 -0
  152. package/src/components/ui/autocomplete.tsx +1 -0
  153. package/src/components/ui/badge.tsx +1 -0
  154. package/src/components/ui/button.tsx +1 -0
  155. package/src/components/ui/calendar.tsx +1 -0
  156. package/src/components/ui/card.tsx +1 -0
  157. package/src/components/ui/checkbox.tsx +1 -0
  158. package/src/components/ui/color-picker.tsx +1 -0
  159. package/src/components/ui/command.tsx +1 -0
  160. package/src/components/ui/date-picker.tsx +1 -0
  161. package/src/components/ui/date-range-picker.tsx +1 -0
  162. package/src/components/ui/dialog.tsx +1 -0
  163. package/src/components/ui/dropdown-menu.tsx +1 -0
  164. package/src/components/ui/index.tsx +31 -0
  165. package/src/components/ui/input.tsx +1 -0
  166. package/src/components/ui/label.tsx +1 -0
  167. package/src/components/ui/pagination.tsx +1 -0
  168. package/src/components/ui/popover.tsx +1 -0
  169. package/src/components/ui/radio.tsx +1 -0
  170. package/src/components/ui/scroll-area.tsx +1 -0
  171. package/src/components/ui/select.tsx +1 -0
  172. package/src/components/ui/sheet.tsx +1 -0
  173. package/src/components/ui/slider.tsx +1 -0
  174. package/src/components/ui/sonner.tsx +1 -0
  175. package/src/components/ui/switch.tsx +1 -0
  176. package/src/components/ui/table.tsx +1 -0
  177. package/src/components/ui/tabs-items.tsx +1 -0
  178. package/src/components/ui/tabs.tsx +1 -0
  179. package/src/components/ui/textarea.tsx +1 -0
  180. package/src/components/ui/time-picker.tsx +1 -0
  181. package/src/components/ui/upload.tsx +1 -0
  182. package/src/form/__tests__/use-zod-form.test.tsx +97 -0
  183. package/src/form/form-field-control.tsx +44 -0
  184. package/src/form/form-root.tsx +29 -0
  185. package/src/form/index.ts +7 -0
  186. package/src/form/use-zod-form.ts +29 -0
  187. package/src/i18n/__tests__/translate.test.ts +23 -0
  188. package/src/i18n/index.ts +9 -0
  189. package/src/i18n/messages/en.json +171 -0
  190. package/src/i18n/messages/ja.json +171 -0
  191. package/src/i18n/messages/vi.json +171 -0
  192. package/src/i18n/translate.ts +74 -0
  193. package/src/i18n/use-translation.ts +53 -0
  194. package/src/index.ts +3 -0
  195. package/src/lib/__tests__/control-styles.test.ts +78 -0
  196. package/src/lib/__tests__/datetime.test.ts +77 -0
  197. package/src/lib/__tests__/format-date.test.ts +97 -0
  198. package/src/lib/__tests__/format.test.ts +62 -0
  199. package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
  200. package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
  201. package/src/lib/__tests__/token-governance.test.ts +191 -0
  202. package/src/lib/__tests__/variants.test.ts +18 -0
  203. package/src/lib/control-styles.ts +33 -0
  204. package/src/lib/datetime/detect.ts +25 -0
  205. package/src/lib/datetime/format-date.ts +100 -0
  206. package/src/lib/datetime/format.ts +140 -0
  207. package/src/lib/datetime/index.ts +25 -0
  208. package/src/lib/datetime/parse.ts +51 -0
  209. package/src/lib/datetime/sync.ts +48 -0
  210. package/src/lib/format.ts +114 -0
  211. package/src/lib/hooks.ts +54 -0
  212. package/src/lib/utils.ts +6 -0
  213. package/src/lib/variants.ts +40 -0
  214. package/src/props/components/app.prop.ts +99 -0
  215. package/src/props/components/data-display.prop.ts +73 -0
  216. package/src/props/components/data-entry.prop.ts +334 -0
  217. package/src/props/components/feedback.prop.ts +80 -0
  218. package/src/props/components/form.prop.ts +46 -0
  219. package/src/props/components/general.prop.ts +18 -0
  220. package/src/props/components/index.ts +99 -0
  221. package/src/props/components/layout.prop.ts +130 -0
  222. package/src/props/components/navigation.prop.ts +88 -0
  223. package/src/props/components/query.prop.ts +94 -0
  224. package/src/props/index.ts +17 -0
  225. package/src/props/registry.ts +603 -0
  226. package/src/props/vocabulary/content.prop.ts +35 -0
  227. package/src/props/vocabulary/data.prop.ts +46 -0
  228. package/src/props/vocabulary/index.ts +73 -0
  229. package/src/props/vocabulary/interaction.prop.ts +42 -0
  230. package/src/props/vocabulary/layout.prop.ts +25 -0
  231. package/src/props/vocabulary/navigation.prop.ts +19 -0
  232. package/src/props/vocabulary/shared.prop.ts +59 -0
  233. package/src/styles/alert-layout.css +191 -0
  234. package/src/styles/badge-layout.css +22 -0
  235. package/src/styles/card-layout.css +373 -0
  236. package/src/styles/control.css +504 -0
  237. package/src/styles/data-display-layout.css +246 -0
  238. package/src/styles/density.css +43 -0
  239. package/src/styles/dialog-layout.css +84 -0
  240. package/src/styles/index.css +105 -0
  241. package/src/styles/layout.css +479 -0
  242. package/src/styles/shell-layout.css +604 -0
  243. package/src/styles/table-layout.css +109 -0
  244. package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
  245. package/src/test/jest-dom.d.ts +4 -0
  246. package/src/test/render-loop-guard.tsx +50 -0
  247. package/src/test/render.tsx +29 -0
  248. package/src/test/theme-globals.test.ts +77 -0
  249. package/src/test/theme-globals.ts +134 -0
  250. package/src/test/theme-test-utils.tsx +67 -0
  251. package/src/theme/example.service.css +37 -0
  252. package/src/tokens/base.css +13 -0
  253. package/src/tokens/foundation.css +151 -0
  254. package/src/tokens/primitives/badge.css +13 -0
  255. package/src/tokens/primitives/card.css +29 -0
  256. package/src/tokens/primitives/control.css +55 -0
  257. package/src/tokens/primitives/feedback.css +17 -0
  258. package/src/tokens/primitives/layout.css +20 -0
  259. package/src/tokens/primitives/navigation.css +13 -0
  260. package/src/tokens/primitives/table.css +10 -0
  261. package/BRAND.md +0 -296
  262. package/CHANGELOG.md +0 -668
  263. package/config/eslint.js +0 -54
  264. package/config/prettier.cjs +0 -20
  265. package/config/tsconfig.base.json +0 -22
  266. package/config/vitest.base.ts +0 -26
  267. package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
  268. package/dist/Table.types-BbsxoIYE.d.ts +0 -352
  269. package/dist/color-DO0qqUAb.d.ts +0 -38
  270. package/dist/components/composites.d.ts +0 -963
  271. package/dist/components/composites.js +0 -7343
  272. package/dist/components/composites.js.map +0 -1
  273. package/dist/components/primitives.d.ts +0 -2744
  274. package/dist/components/primitives.js +0 -7356
  275. package/dist/components/primitives.js.map +0 -1
  276. package/dist/components/shell.d.ts +0 -182
  277. package/dist/components/shell.js +0 -774
  278. package/dist/components/shell.js.map +0 -1
  279. package/dist/hooks.d.ts +0 -100
  280. package/dist/hooks.js +0 -558
  281. package/dist/hooks.js.map +0 -1
  282. package/dist/i18n.d.ts +0 -61
  283. package/dist/i18n.js +0 -860
  284. package/dist/i18n.js.map +0 -1
  285. package/dist/index.d.ts +0 -33
  286. package/dist/index.js +0 -13062
  287. package/dist/index.js.map +0 -1
  288. package/dist/padding-DY0JV5Ja.d.ts +0 -16
  289. package/dist/preferences.d.ts +0 -132
  290. package/dist/preferences.js +0 -262
  291. package/dist/preferences.js.map +0 -1
  292. package/dist/props.d.ts +0 -86
  293. package/dist/props.js +0 -16
  294. package/dist/props.js.map +0 -1
  295. package/dist/size-CQwNvOWd.d.ts +0 -19
  296. package/dist/types-LTj-2bl-.d.ts +0 -30
  297. package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
  298. package/src/tokens/tailwind.css +0 -158
@@ -0,0 +1,95 @@
1
+ // Skeleton family — shaped placeholders for loading states. Always pick the
2
+ // shape closest to the final layout; spinner-overlay is forbidden.
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { tableCellPaddingClass, tableRowHeightClass } from "../../lib/control-styles";
7
+
8
+ function SkeletonBlock({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
9
+ return (
10
+ <div
11
+ aria-busy="true"
12
+ aria-live="polite"
13
+ className={cn("ui-skeleton-block", className)}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ interface SkeletonRowsProps {
20
+ rows?: number;
21
+ columns?: number;
22
+ className?: string;
23
+ }
24
+
25
+ /** Skeleton for a flat list of rows (use inside a Card or section). */
26
+ export function SkeletonRows({ rows = 6, columns = 4, className }: SkeletonRowsProps) {
27
+ return (
28
+ <div className={cn("ui-skeleton-rows", className)} aria-busy="true">
29
+ {Array.from({ length: rows }).map((_, i) => (
30
+ <div key={i} className="ui-skeleton-row">
31
+ {Array.from({ length: columns }).map((_, j) => (
32
+ <SkeletonBlock
33
+ key={j}
34
+ className={cn("h-4", j === 0 ? "w-1/4" : j === columns - 1 ? "w-1/6" : "flex-1")}
35
+ />
36
+ ))}
37
+ </div>
38
+ ))}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ /** Skeleton matching the DataTable layout — header row + N body rows. */
44
+ export function SkeletonTable({ rows = 8, columns = 5 }: SkeletonRowsProps) {
45
+ return (
46
+ <div className="ui-skeleton-table" aria-busy="true">
47
+ <div className={cn("ui-skeleton-table-head", tableCellPaddingClass, tableRowHeightClass)}>
48
+ {Array.from({ length: columns }).map((_, j) => (
49
+ <SkeletonBlock key={j} className={cn("h-3", j === 0 ? "w-1/5" : "flex-1")} />
50
+ ))}
51
+ </div>
52
+ <div className="ui-skeleton-table-body">
53
+ {Array.from({ length: rows }).map((_, i) => (
54
+ <div
55
+ key={i}
56
+ className={cn("ui-skeleton-table-row", tableCellPaddingClass, tableRowHeightClass)}
57
+ >
58
+ {Array.from({ length: columns }).map((_, j) => (
59
+ <SkeletonBlock key={j} className={cn("h-4", j === 0 ? "w-1/5" : "flex-1")} />
60
+ ))}
61
+ </div>
62
+ ))}
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ /** Skeleton matching a Card detail layout — title + 6 metadata rows. */
69
+ export function SkeletonDetail() {
70
+ return (
71
+ <div className="ui-skeleton-detail ui-skeleton-detail-stack" aria-busy="true">
72
+ <SkeletonBlock className="h-7 w-1/3" />
73
+ <SkeletonBlock className="h-4 w-1/2" />
74
+ <div className="ui-skeleton-detail-box ui-skeleton-detail-stack">
75
+ {Array.from({ length: 6 }).map((_, i) => (
76
+ <div key={i} className="ui-skeleton-detail-stack">
77
+ <SkeletonBlock className="h-3 w-24" />
78
+ <SkeletonBlock className="h-4 w-full max-w-md" />
79
+ </div>
80
+ ))}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ /** Skeleton matching a stat card / dashboard tile. */
87
+ export function SkeletonCard() {
88
+ return (
89
+ <div className="ui-skeleton-card" aria-busy="true">
90
+ <SkeletonBlock className="h-3 w-24" />
91
+ <SkeletonBlock className="h-[length:var(--control-height)] w-32" />
92
+ <SkeletonBlock className="h-3 w-20" />
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,54 @@
1
+ // shadcn/ui Sonner — recommended toast (replaces deprecated Radix Toast).
2
+ // @see https://ui.shadcn.com/docs/components/sonner
3
+ import * as React from "react";
4
+ import { CheckCircle2, Info, Loader2, OctagonX, TriangleAlert } from "lucide-react";
5
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
6
+
7
+ function useDocumentTheme(): ToasterProps["theme"] {
8
+ return React.useSyncExternalStore(
9
+ (onStoreChange) => {
10
+ if (typeof document === "undefined") return () => undefined;
11
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
12
+ mq.addEventListener("change", onStoreChange);
13
+ const obs = new MutationObserver(onStoreChange);
14
+ obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
15
+ return () => {
16
+ mq.removeEventListener("change", onStoreChange);
17
+ obs.disconnect();
18
+ };
19
+ },
20
+ () => (document.documentElement.classList.contains("dark") ? "dark" : "light"),
21
+ () => "light",
22
+ );
23
+ }
24
+
25
+ function Toaster({ ...props }: ToasterProps) {
26
+ const theme = useDocumentTheme();
27
+
28
+ return (
29
+ <Sonner
30
+ theme={theme}
31
+ className="toaster group"
32
+ icons={{
33
+ success: <CheckCircle2 className="size-4" aria-hidden="true" />,
34
+ info: <Info className="size-4" aria-hidden="true" />,
35
+ warning: <TriangleAlert className="size-4" aria-hidden="true" />,
36
+ error: <OctagonX className="size-4" aria-hidden="true" />,
37
+ loading: <Loader2 className="size-4 animate-spin" aria-hidden="true" />,
38
+ }}
39
+ style={
40
+ {
41
+ "--normal-bg": "var(--popover)",
42
+ "--normal-text": "var(--popover-foreground)",
43
+ "--normal-border": "var(--border)",
44
+ "--border-radius": "var(--radius)",
45
+ } as React.CSSProperties
46
+ }
47
+ position="bottom-right"
48
+ mobileOffset={{ bottom: "16px", right: "16px" }}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export { Toaster };
@@ -0,0 +1 @@
1
+ export { Toaster } from "./sonner";
@@ -0,0 +1,62 @@
1
+ // Toast API — Sonner (shadcn recommended).
2
+ //
3
+ // `toast("msg")` / `toast.success("msg")` — Sonner native API (preferred).
4
+ // `toast({ title, description, variant })` — legacy admin compat.
5
+ import type * as React from "react";
6
+ import { toast as sonnerToast, type ExternalToast } from "sonner";
7
+
8
+ export type { ExternalToast } from "sonner";
9
+ export { sonnerToast };
10
+
11
+ export type LegacyToastOptions = ExternalToast & {
12
+ title?: React.ReactNode;
13
+ description?: React.ReactNode;
14
+ variant?: "default" | "destructive" | "success";
15
+ };
16
+
17
+ function nodeText(value: React.ReactNode): string {
18
+ if (value == null) return "";
19
+ if (typeof value === "string") return value;
20
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
21
+ return "";
22
+ }
23
+
24
+ function legacyToast(options: LegacyToastOptions) {
25
+ const { title, description, variant, ...rest } = options;
26
+ const titleText = nodeText(title);
27
+ const descText = nodeText(description);
28
+ const message = titleText || descText;
29
+ const desc = titleText && descText ? descText : undefined;
30
+ const sonnerOptions: ExternalToast = { ...rest, description: desc };
31
+
32
+ switch (variant) {
33
+ case "destructive":
34
+ return sonnerToast.error(message, sonnerOptions);
35
+ case "success":
36
+ return sonnerToast.success(message, sonnerOptions);
37
+ default:
38
+ return sonnerToast(message, sonnerOptions);
39
+ }
40
+ }
41
+
42
+ type ToastFn = typeof sonnerToast &
43
+ ((options: LegacyToastOptions) => ReturnType<typeof sonnerToast>);
44
+
45
+ /** Sonner toast + legacy `{ title, variant }` object form. */
46
+ const toast = Object.assign((messageOrOptions: string | LegacyToastOptions) => {
47
+ if (typeof messageOrOptions === "string") {
48
+ return sonnerToast(messageOrOptions);
49
+ }
50
+ return legacyToast(messageOrOptions);
51
+ }, sonnerToast) as ToastFn;
52
+
53
+ /** Legacy hook — prefer `toast` import directly; kept for existing admin pages. */
54
+ function useToast() {
55
+ return {
56
+ toast: (options: LegacyToastOptions) => legacyToast(options),
57
+ dismiss: sonnerToast.dismiss,
58
+ toasts: [] as const,
59
+ };
60
+ }
61
+
62
+ export { toast, legacyToast, useToast };
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Button } from "../button";
4
+
5
+ describe("Button", () => {
6
+ it("renders children and default type=submit implicit button", () => {
7
+ renderWithUi(<Button>Click me</Button>);
8
+ const btn = screen.getByRole("button", { name: "Click me" });
9
+ expect(btn).toBeInTheDocument();
10
+ expect(btn.tagName).toBe("BUTTON");
11
+ });
12
+
13
+ it("calls onClick when clicked", async () => {
14
+ const user = userEvent.setup();
15
+ const onClick = vi.fn();
16
+ renderWithUi(<Button onClick={onClick}>Go</Button>);
17
+ await user.click(screen.getByRole("button", { name: "Go" }));
18
+ expect(onClick).toHaveBeenCalledOnce();
19
+ });
20
+
21
+ it("does not fire onClick when disabled", async () => {
22
+ const user = userEvent.setup();
23
+ const onClick = vi.fn();
24
+ renderWithUi(
25
+ <Button disabled onClick={onClick}>
26
+ Blocked
27
+ </Button>,
28
+ );
29
+ await user.click(screen.getByRole("button", { name: "Blocked" }));
30
+ expect(onClick).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it.each(["destructive", "outline", "secondary", "ghost", "link"] as const)(
34
+ "renders variant=%s",
35
+ (variant) => {
36
+ renderWithUi(<Button variant={variant}>V</Button>);
37
+ expect(screen.getByRole("button", { name: "V" })).toBeInTheDocument();
38
+ },
39
+ );
40
+
41
+ it.each(["xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg"] as const)(
42
+ "renders size=%s",
43
+ (size) => {
44
+ renderWithUi(<Button size={size}>S</Button>);
45
+ expect(screen.getByRole("button", { name: "S" })).toBeInTheDocument();
46
+ },
47
+ );
48
+
49
+ it("exposes shadcn data-slot and state attributes", () => {
50
+ renderWithUi(
51
+ <Button variant="outline" size="sm" aria-invalid>
52
+ State
53
+ </Button>,
54
+ );
55
+ const btn = screen.getByRole("button", { name: "State" });
56
+ expect(btn).toHaveAttribute("data-slot", "button");
57
+ expect(btn).toHaveAttribute("data-variant", "outline");
58
+ expect(btn).toHaveAttribute("data-size", "sm");
59
+ expect(btn).toHaveClass("aria-invalid:border-destructive");
60
+ });
61
+
62
+ it("default size applies ui-button size token binding", () => {
63
+ renderWithUi(<Button>Density</Button>);
64
+ expect(screen.getByRole("button", { name: "Density" })).toHaveClass("ui-button--default-size");
65
+ });
66
+
67
+ it("default variant uses semantic button token class", () => {
68
+ renderWithUi(<Button>Primary</Button>);
69
+ expect(screen.getByRole("button", { name: "Primary" })).toHaveClass("ui-button--default");
70
+ });
71
+ });
@@ -0,0 +1,61 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva } from "class-variance-authority";
4
+ import { cn } from "../../lib/utils";
5
+ import type { ButtonProp } from "../../props/components/general.prop";
6
+
7
+ const buttonVariants = cva("ui-button", {
8
+ variants: {
9
+ variant: {
10
+ default: "ui-button--default bg-primary text-primary-foreground hover:bg-primary/90",
11
+ destructive:
12
+ "ui-button--destructive bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20",
13
+ outline:
14
+ "ui-button--outline border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
15
+ secondary:
16
+ "ui-button--secondary bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
+ ghost: "ui-button--ghost hover:bg-accent hover:text-accent-foreground",
18
+ link: "ui-button--link text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "ui-button--default-size py-2 has-[>svg]:px-3",
22
+ xs: "h-[calc(var(--control-height)-0.75rem)] gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
23
+ sm: "ui-button--sm gap-1.5 rounded-md has-[>svg]:px-2.5",
24
+ lg: "ui-button--lg rounded-md has-[>svg]:px-4",
25
+ icon: "ui-button--icon",
26
+ "icon-xs":
27
+ "size-[calc(var(--control-height)-0.75rem)] rounded-md [&_svg:not([class*='size-'])]:size-3",
28
+ "icon-sm": "size-[calc(var(--control-height)-0.5rem)]",
29
+ "icon-lg": "size-[calc(var(--control-height)+0.25rem)]",
30
+ },
31
+ },
32
+ defaultVariants: { variant: "default", size: "default" },
33
+ });
34
+
35
+ export type { ButtonProp, ButtonProp as ButtonProps } from "../../props/components/general.prop";
36
+
37
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProp>(
38
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
39
+ const Comp = asChild ? Slot : "button";
40
+ return (
41
+ <Comp
42
+ data-slot="button"
43
+ data-variant={variant ?? "default"}
44
+ data-size={size ?? "default"}
45
+ className={cn(
46
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none",
47
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
48
+ "disabled:pointer-events-none disabled:opacity-50",
49
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
50
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
51
+ buttonVariants({ variant, size, className }),
52
+ )}
53
+ ref={ref}
54
+ {...props}
55
+ />
56
+ );
57
+ },
58
+ );
59
+ Button.displayName = "Button";
60
+
61
+ export { buttonVariants };
@@ -0,0 +1,2 @@
1
+ export { Button, buttonVariants } from "./button";
2
+ export type { ButtonProps } from "./button";
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen } from "@/test/render";
3
+ import { PageContainer } from "../page-container";
4
+ import { Button } from "../../general/button";
5
+
6
+ describe("PageContainer", () => {
7
+ it("renders title as h1", () => {
8
+ renderWithUi(<PageContainer title="Customers" />);
9
+ expect(screen.getByRole("heading", { level: 1, name: "Customers" })).toBeInTheDocument();
10
+ });
11
+
12
+ it("renders subtitle when provided", () => {
13
+ renderWithUi(<PageContainer title="Customers" subtitle="CRM list" />);
14
+ expect(screen.getByText("CRM list")).toHaveClass("ui-page-subtitle");
15
+ });
16
+
17
+ it("renders extra slot in header row", () => {
18
+ renderWithUi(<PageContainer title="Customers" extra={<Button>Create</Button>} />);
19
+ expect(screen.getByRole("button", { name: "Create" })).toBeInTheDocument();
20
+ });
21
+
22
+ it("renders footer slot", () => {
23
+ renderWithUi(<PageContainer title="Edit" footer={<Button>Save</Button>} />);
24
+ expect(screen.getByRole("contentinfo")).toContainElement(
25
+ screen.getByRole("button", { name: "Save" }),
26
+ );
27
+ });
28
+
29
+ it("renders breadcrumb trail with links", () => {
30
+ renderWithUi(
31
+ <PageContainer
32
+ title="Detail"
33
+ breadcrumb={[
34
+ { label: "CRM", to: "/crm" },
35
+ { label: "Customers", to: "/crm/customers" },
36
+ { label: "Detail" },
37
+ ]}
38
+ />,
39
+ );
40
+ const nav = screen.getByRole("navigation", { name: "Breadcrumb" });
41
+ expect(nav).toBeInTheDocument();
42
+ expect(screen.getByRole("link", { name: "CRM" })).toHaveAttribute("href", "/crm");
43
+ expect(nav).toHaveTextContent("Detail");
44
+ });
45
+
46
+ it("applies density class on root", () => {
47
+ const { container } = renderWithUi(<PageContainer title="Compact" density="compact" />);
48
+ expect(container.firstChild).toHaveClass("ui-density-compact");
49
+ });
50
+
51
+ it("applies variant modifier class", () => {
52
+ const { container } = renderWithUi(<PageContainer title="List" variant="flush" />);
53
+ expect(container.firstChild).toHaveClass("ui-page-container--flush");
54
+ });
55
+
56
+ it("applies sticky footer modifier when enabled", () => {
57
+ const { container } = renderWithUi(<PageContainer title="Form" stickyFooter />);
58
+ expect(container.firstChild).toHaveClass("ui-page-container--sticky-footer");
59
+ });
60
+
61
+ it("renders children in page body", () => {
62
+ renderWithUi(
63
+ <PageContainer title="Page">
64
+ <p>Body content</p>
65
+ </PageContainer>,
66
+ );
67
+ expect(screen.getByText("Body content")).toBeInTheDocument();
68
+ });
69
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen } from "@/test/render";
3
+ import { PageInset } from "../page-inset";
4
+
5
+ describe("PageInset", () => {
6
+ it("renders children with inset class", () => {
7
+ renderWithUi(
8
+ <PageInset>
9
+ <p>Filter zone</p>
10
+ </PageInset>,
11
+ );
12
+ expect(screen.getByText("Filter zone").parentElement).toHaveClass("ui-page-inset");
13
+ });
14
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi } from "@/test/render";
3
+ import { Stack } from "../stack";
4
+ import { Inline } from "../inline";
5
+
6
+ describe("Stack", () => {
7
+ it.each([
8
+ ["xs", "ui-stack-xs"],
9
+ ["sm", "ui-stack-sm"],
10
+ ["md", "ui-stack-md"],
11
+ ["lg", "ui-stack-lg"],
12
+ ["xl", "ui-stack-xl"],
13
+ ] as const)("applies gap=%s → %s", (gap, cls) => {
14
+ const { container } = renderWithUi(
15
+ <Stack gap={gap}>
16
+ <span>a</span>
17
+ <span>b</span>
18
+ </Stack>,
19
+ );
20
+ expect(container.firstChild).toHaveClass(cls);
21
+ });
22
+ });
23
+
24
+ describe("Inline", () => {
25
+ it.each([
26
+ ["xs", "ui-inline-xs"],
27
+ ["sm", "ui-inline-sm"],
28
+ ["md", "ui-inline-md"],
29
+ ["lg", "ui-inline-lg"],
30
+ ] as const)("applies gap=%s → %s", (gap, cls) => {
31
+ const { container } = renderWithUi(
32
+ <Inline gap={gap}>
33
+ <span>a</span>
34
+ <span>b</span>
35
+ </Inline>,
36
+ );
37
+ expect(container.firstChild).toHaveClass(cls);
38
+ });
39
+ });
@@ -0,0 +1,42 @@
1
+ import type { AppShellProp } from "../../props/components/layout.prop";
2
+
3
+ export type {
4
+ AppShellProp,
5
+ AppShellProp as AppShellProps,
6
+ } from "../../props/components/layout.prop";
7
+
8
+ export function AppShell({
9
+ sidebar,
10
+ topbar,
11
+ topbarLeft,
12
+ topbarRight,
13
+ logo,
14
+ breadcrumb,
15
+ footer,
16
+ children,
17
+ sidebarCollapsed = false,
18
+ }: AppShellProp) {
19
+ const resolvedTopbar =
20
+ topbar !== undefined ? (
21
+ topbar
22
+ ) : (
23
+ <div className="app-topbar-rail">
24
+ {logo !== undefined && <div className="app-topbar-logo">{logo}</div>}
25
+ {topbarLeft !== undefined && <div className="app-topbar-left">{topbarLeft}</div>}
26
+ <div className="app-topbar-spacer" />
27
+ {topbarRight !== undefined && <div className="app-topbar-right">{topbarRight}</div>}
28
+ </div>
29
+ );
30
+
31
+ return (
32
+ <div className="app-root" data-collapsed={sidebarCollapsed ? "true" : undefined}>
33
+ <aside className="app-sidebar">{sidebar}</aside>
34
+ <header className="app-topbar">{resolvedTopbar}</header>
35
+ <main className="app-main">
36
+ {breadcrumb !== undefined && <div className="app-breadcrumb">{breadcrumb}</div>}
37
+ {children}
38
+ </main>
39
+ {footer !== undefined && <footer className="app-footer">{footer}</footer>}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,35 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { Link } from "react-router-dom";
3
+
4
+ import type { BreadcrumbProp } from "../../props/vocabulary/navigation.prop";
5
+
6
+ export type BreadcrumbProps = {
7
+ items: BreadcrumbProp;
8
+ };
9
+
10
+ export function Breadcrumb({ items }: BreadcrumbProps) {
11
+ return (
12
+ <nav aria-label="Breadcrumb" className="ui-breadcrumb">
13
+ <ol className="ui-breadcrumb-list">
14
+ {items.map((item, index) => {
15
+ const isLast = index === items.length - 1;
16
+
17
+ return (
18
+ <li key={item.to ?? index} className="ui-breadcrumb-item">
19
+ {item.to && !isLast ? (
20
+ <Link to={item.to} className="ui-breadcrumb-link">
21
+ {item.label}
22
+ </Link>
23
+ ) : (
24
+ <span className="ui-breadcrumb-current" aria-current={isLast ? "page" : undefined}>
25
+ {item.label}
26
+ </span>
27
+ )}
28
+ {!isLast ? <ChevronRight aria-hidden="true" /> : null}
29
+ </li>
30
+ );
31
+ })}
32
+ </ol>
33
+ </nav>
34
+ );
35
+ }
@@ -0,0 +1,31 @@
1
+ export { PageContainer } from "./page-container";
2
+ export type {
3
+ PageContainerProp,
4
+ PageContainerProps,
5
+ BreadcrumbItem,
6
+ BreadcrumbItemProp,
7
+ } from "./page-container";
8
+ export { PageInset } from "./page-inset";
9
+ export type { PageInsetProp, PageInsetProps } from "./page-inset";
10
+ export { Stack } from "./stack";
11
+ export type { StackProp, StackProps } from "./stack";
12
+ export { Inline } from "./inline";
13
+ export type { InlineProp, InlineProps } from "./inline";
14
+ export { AppShell } from "./app-shell";
15
+ export type { AppShellProps } from "./app-shell";
16
+ export { ShellApp } from "./shell-app";
17
+ export type { ShellAppProps } from "./shell-app";
18
+ export { Menu } from "./menu";
19
+ export type { MenuItem, MenuProps, MenuSection } from "./menu";
20
+ export { Breadcrumb } from "./breadcrumb";
21
+ export type { BreadcrumbProps } from "./breadcrumb";
22
+ export { Sidebar } from "./sidebar";
23
+ export type { SidebarItem, SidebarProduct, SidebarProps, SidebarSection } from "./sidebar";
24
+ export { Topbar } from "./topbar";
25
+ export type { TopbarProduct, TopbarProject, TopbarProps } from "./topbar";
26
+ export { ResponsiveGrid } from "./responsive-grid";
27
+ export type { ResponsiveGridProps } from "./responsive-grid";
28
+ export { SplitPane } from "./split-pane";
29
+ export type { SplitPaneProps } from "./split-pane";
30
+ export { MobileFrame } from "./mobile-frame";
31
+ export type { MobileFrameNavItem, MobileFrameProps } from "./mobile-frame";
@@ -0,0 +1,13 @@
1
+ import { cn } from "../../lib/utils";
2
+ import { inlineGapClass } from "../../lib/variants";
3
+ import type { InlineProp } from "../../props/components/layout.prop";
4
+
5
+ export type { InlineProp, InlineProp as InlineProps } from "../../props/components/layout.prop";
6
+
7
+ export function Inline({ gap = "sm", className, children, ...props }: InlineProp) {
8
+ return (
9
+ <div className={cn(inlineGapClass[gap], className)} {...props}>
10
+ {children}
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ import type { SidebarItem, SidebarSection } from "./sidebar";
2
+ import { Sidebar } from "./sidebar";
3
+
4
+ export type MenuItem = SidebarItem & {
5
+ active?: boolean;
6
+ };
7
+
8
+ export type MenuSection = {
9
+ label?: string;
10
+ items: MenuItem[];
11
+ };
12
+
13
+ export type MenuProps = {
14
+ items: MenuSection[];
15
+ };
16
+
17
+ export function Menu({ items }: MenuProps) {
18
+ const sections: SidebarSection[] = items.map((section) => ({
19
+ label: section.label,
20
+ items: section.items.map(({ active: _active, ...item }) => item),
21
+ }));
22
+ const activeId =
23
+ items.flatMap((section) => section.items).find((item) => item.active)?.id ??
24
+ items[0]?.items[0]?.id ??
25
+ "";
26
+
27
+ return (
28
+ <Sidebar
29
+ activeId={activeId}
30
+ sections={sections}
31
+ product={{ name: "Acme Console", role: "Workspace", color: "hsl(var(--attention))" }}
32
+ />
33
+ );
34
+ }