@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,194 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ type CardSize = "default" | "compact";
7
+ /** Semantic 3px left-edge accent stripe. */
8
+ type CardAccent = "primary" | "success" | "warning" | "info" | "attention" | "destructive";
9
+ /** Surface fill — plain card, muted band, borderless outline, or emphasized featured ring. */
10
+ type CardVariant = "default" | "muted" | "outline" | "featured";
11
+ /** Padding density — base 16px · tight 12px · cozy 20px. */
12
+ type CardDensity = "tight" | "cozy";
13
+
14
+ const cardVariants = cva("group/card border", {
15
+ variants: {
16
+ size: {
17
+ default: "",
18
+ compact: "",
19
+ },
20
+ },
21
+ defaultVariants: { size: "default" },
22
+ });
23
+
24
+ export type CardProps = React.HTMLAttributes<HTMLDivElement> &
25
+ VariantProps<typeof cardVariants> & {
26
+ size?: CardSize;
27
+ accent?: CardAccent;
28
+ variant?: CardVariant;
29
+ density?: CardDensity;
30
+ };
31
+
32
+ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
33
+ ({ className, size = "default", accent, variant, density, ...props }, ref) => (
34
+ <div
35
+ ref={ref}
36
+ className={cn(cardVariants({ size }), className)}
37
+ data-slot="card"
38
+ data-size={size === "compact" ? "compact" : undefined}
39
+ data-accent={accent}
40
+ data-variant={variant && variant !== "default" ? variant : undefined}
41
+ data-density={density}
42
+ {...props}
43
+ />
44
+ ),
45
+ );
46
+ Card.displayName = "Card";
47
+
48
+ /** Full-bleed cover media — first child; header below uses section top (φ⁰), not shell. */
49
+ export type CardCoverProps = React.HTMLAttributes<HTMLDivElement>;
50
+
51
+ export const CardCover = React.forwardRef<HTMLDivElement, CardCoverProps>(
52
+ ({ className, ...props }, ref) => (
53
+ <div ref={ref} data-slot="card-cover" className={cn("ui-card-cover", className)} {...props} />
54
+ ),
55
+ );
56
+ CardCover.displayName = "CardCover";
57
+
58
+ export type CardHeaderProps = React.HTMLAttributes<HTMLDivElement> & {
59
+ /** Muted background + border-bottom — section band (mirror footer `separated`). */
60
+ banded?: boolean;
61
+ };
62
+
63
+ export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
64
+ ({ className, banded, ...props }, ref) => (
65
+ <div
66
+ ref={ref}
67
+ data-slot="card-header"
68
+ data-banded={banded ? "" : undefined}
69
+ className={cn(banded && "ui-card-header--banded", className)}
70
+ {...props}
71
+ />
72
+ ),
73
+ );
74
+ CardHeader.displayName = "CardHeader";
75
+
76
+ export const CardTitle = React.forwardRef<
77
+ HTMLHeadingElement,
78
+ React.HTMLAttributes<HTMLHeadingElement>
79
+ >(({ className, children, ...props }, ref) => (
80
+ <h3 ref={ref} data-slot="card-title" className={className} {...props}>
81
+ {children}
82
+ </h3>
83
+ ));
84
+ CardTitle.displayName = "CardTitle";
85
+
86
+ export const CardDescription = React.forwardRef<
87
+ HTMLParagraphElement,
88
+ React.HTMLAttributes<HTMLParagraphElement>
89
+ >(({ className, ...props }, ref) => (
90
+ <p ref={ref} data-slot="card-description" className={className} {...props} />
91
+ ));
92
+ CardDescription.displayName = "CardDescription";
93
+
94
+ export type CardContentProps = React.HTMLAttributes<HTMLDivElement> & {
95
+ /** Edge-to-edge body (tables, tabs list). Horizontal padding removed. */
96
+ flush?: boolean;
97
+ /** No gap after header — pair with tabs / flush toolbar. */
98
+ tight?: boolean;
99
+ /** No header above — top padding matches card shell. */
100
+ solo?: boolean;
101
+ };
102
+
103
+ export const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
104
+ ({ className, flush, tight, solo, ...props }, ref) => (
105
+ <div
106
+ ref={ref}
107
+ data-slot="card-content"
108
+ data-flush={flush ? "" : undefined}
109
+ data-tight={tight ? "" : undefined}
110
+ data-solo={solo ? "" : undefined}
111
+ className={className}
112
+ {...props}
113
+ />
114
+ ),
115
+ );
116
+ CardContent.displayName = "CardContent";
117
+
118
+ export type CardFooterProps = React.HTMLAttributes<HTMLDivElement> & {
119
+ /** Top border + symmetric action band — form Save/Cancel, table summary. */
120
+ separated?: boolean;
121
+ /** Full-bleed footer (Ant Design `actions` bar). */
122
+ flush?: boolean;
123
+ };
124
+
125
+ export const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
126
+ ({ className, separated, flush, ...props }, ref) => (
127
+ <div
128
+ ref={ref}
129
+ data-slot="card-footer"
130
+ data-separated={separated ? "" : undefined}
131
+ data-flush={flush ? "" : undefined}
132
+ className={className}
133
+ {...props}
134
+ />
135
+ ),
136
+ );
137
+ CardFooter.displayName = "CardFooter";
138
+
139
+ export type CardStatProps = React.HTMLAttributes<HTMLDivElement> &
140
+ VariantProps<typeof cardVariants> & {
141
+ label: React.ReactNode;
142
+ value: React.ReactNode;
143
+ hint?: React.ReactNode;
144
+ /** Optional compact trend text beside the value. Avoid badge-like deltas. */
145
+ delta?: React.ReactNode;
146
+ /** KPI layout: stacked = design default, inline = label left / value right. */
147
+ layout?: "stacked" | "inline";
148
+ /** Align the metric group. */
149
+ align?: "start" | "end";
150
+ };
151
+
152
+ /** KPI / stat tile — token-driven layout aligned to dashboard KPI cards. */
153
+ export function CardStat({
154
+ label,
155
+ value,
156
+ hint,
157
+ delta,
158
+ layout = "stacked",
159
+ align = "start",
160
+ className,
161
+ size = "compact",
162
+ ...props
163
+ }: CardStatProps) {
164
+ return (
165
+ <Card
166
+ size={size ?? "compact"}
167
+ className={className}
168
+ data-stat-card=""
169
+ data-stat-layout={layout}
170
+ data-stat-align={align}
171
+ {...props}
172
+ >
173
+ <div data-slot="card-stat-body">
174
+ <div data-slot="card-stat-label">{label}</div>
175
+ {hint && layout === "inline" ? <div data-slot="card-stat-hint">{hint}</div> : null}
176
+ </div>
177
+ <div>
178
+ <div data-slot="card-stat-value-row">
179
+ <span data-slot="card-stat-value">{value}</span>
180
+ {delta ? <span data-slot="card-stat-delta">{delta}</span> : null}
181
+ </div>
182
+ {hint && layout !== "inline" ? <div data-slot="card-stat-hint">{hint}</div> : null}
183
+ </div>
184
+ </Card>
185
+ );
186
+ }
187
+
188
+ /** Header actions slot — pair with `CardHeader className="flex flex-row …"`. */
189
+ export const CardAction = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
190
+ ({ className, ...props }, ref) => (
191
+ <div ref={ref} data-slot="card-action" className={className} {...props} />
192
+ ),
193
+ );
194
+ CardAction.displayName = "CardAction";
@@ -0,0 +1,28 @@
1
+ import type { ComponentType, SVGProps } from "react";
2
+ import { Hash, ShoppingBag, Truck } from "lucide-react";
3
+
4
+ export type CodeBadgeKind = "internal" | "seller" | "yamato";
5
+
6
+ export type CodeBadgeProps = {
7
+ kind: CodeBadgeKind;
8
+ value: string;
9
+ };
10
+
11
+ const codeBadgeConfig = {
12
+ internal: { label: "INT", icon: Hash },
13
+ seller: { label: "SLR", icon: ShoppingBag },
14
+ yamato: { label: "YMT", icon: Truck },
15
+ } satisfies Record<CodeBadgeKind, { label: string; icon: ComponentType<SVGProps<SVGSVGElement>> }>;
16
+
17
+ export function CodeBadge({ kind, value }: CodeBadgeProps) {
18
+ const config = codeBadgeConfig[kind] ?? codeBadgeConfig.internal;
19
+ const Icon = config.icon;
20
+
21
+ return (
22
+ <span className="ui-code-badge" data-kind={kind}>
23
+ <span className="ui-code-badge-label">{config.label}</span>
24
+ <Icon aria-hidden="true" />
25
+ <span className="ui-code-badge-value">{value}</span>
26
+ </span>
27
+ );
28
+ }
@@ -0,0 +1,5 @@
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2
+
3
+ export const Collapsible = CollapsiblePrimitive.Root;
4
+ export const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
5
+ export const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
@@ -0,0 +1,476 @@
1
+ // DataTable — compound component for admin lists.
2
+ //
3
+ // Encapsulates: sticky header, density toggle, per-row click navigation, bulk
4
+ // selection, empty/loading states, cursor pagination. Use this everywhere
5
+ // instead of raw <Table> markup.
6
+ //
7
+ // Compound API (drop these as children of <DataTable>):
8
+ // <DataTable.Toolbar> — left-aligned status / right-aligned controls
9
+ // <DataTable.SelectAll> — header checkbox bound to selection state
10
+ // <DataTable.BulkActions> — only rendered when count > 0; sits in the toolbar
11
+ // <DataTable.DensityToggle> — compact ↔ comfortable
12
+ // <DataTable.Content> — the actual table body (auto-included when omitted)
13
+ // <DataTable.Pagination> — cursor pagination footer
14
+ import * as React from "react";
15
+ import { ArrowDown, ArrowUp, ChevronsUpDown, Layers, Layers2, MoreHorizontal } from "lucide-react";
16
+
17
+ import { useTranslation } from "../../i18n/use-translation";
18
+ import { Inline } from "../layout/inline";
19
+ import { Button } from "../general/button";
20
+ import {
21
+ Table,
22
+ TableBody,
23
+ TableCell,
24
+ TableHead,
25
+ TableHeader,
26
+ TableRow,
27
+ } from "../data-display/table";
28
+ import { cn } from "../../lib/utils";
29
+ import { densityClass } from "../../lib/variants";
30
+ import {
31
+ controlIconSmClass,
32
+ tableCellPaddingClass,
33
+ tableRowHeightClass,
34
+ } from "../../lib/control-styles";
35
+ import type { ColumnDefProp, TableDensityProp, SortStateProp } from "../../props/vocabulary";
36
+
37
+ export type Density = TableDensityProp;
38
+ export type ColumnDef<T> = ColumnDefProp<T>;
39
+
40
+ interface DataTableContextValue<T = unknown> {
41
+ data: T[];
42
+ columns: ColumnDef<T>[];
43
+ density: Density;
44
+ setDensity: (d: Density) => void;
45
+ selected: Set<string>;
46
+ toggleSelect: (id: string) => void;
47
+ toggleSelectAll: () => void;
48
+ allSelected: boolean;
49
+ someSelected: boolean;
50
+ selectable: boolean;
51
+ getRowId: (row: T) => string;
52
+ onRowClick?: (row: T) => void;
53
+ sort?: SortStateProp;
54
+ onSortChange?: (sort: SortStateProp | undefined) => void;
55
+ }
56
+
57
+ const DataTableContext = React.createContext<DataTableContextValue | null>(null);
58
+
59
+ function useDataTableContext<T>() {
60
+ const ctx = React.useContext(DataTableContext);
61
+ if (!ctx) throw new Error("DataTable subcomponents must be used inside <DataTable>");
62
+ return ctx as unknown as DataTableContextValue<T>;
63
+ }
64
+
65
+ function useOptionalDataTableContext<T>() {
66
+ return React.useContext(DataTableContext) as unknown as DataTableContextValue<T> | null;
67
+ }
68
+
69
+ interface DataTableProps<T> {
70
+ data: T[];
71
+ columns: ColumnDef<T>[];
72
+ /** Required when `selectable` is true. Default: assume row.id (typed as any). */
73
+ getRowId?: (row: T) => string;
74
+ selectable?: boolean;
75
+ selected?: Set<string>;
76
+ onSelectChange?: (next: Set<string>) => void;
77
+ onRowClick?: (row: T) => void;
78
+ density?: Density;
79
+ onDensityChange?: (d: Density) => void;
80
+ sort?: SortStateProp;
81
+ onSortChange?: (sort: SortStateProp | undefined) => void;
82
+ className?: string;
83
+ children?: React.ReactNode;
84
+ }
85
+
86
+ const noopGetRowId = <T,>(row: T): string => {
87
+ const id = (row as { id?: unknown }).id;
88
+ if (typeof id === "string") return id;
89
+ if (typeof id === "number") return String(id);
90
+ return "";
91
+ };
92
+
93
+ export function DataTable<T>({
94
+ data,
95
+ columns,
96
+ getRowId = noopGetRowId,
97
+ selectable = false,
98
+ selected: controlledSelected,
99
+ onSelectChange,
100
+ onRowClick,
101
+ density: controlledDensity,
102
+ onDensityChange,
103
+ sort,
104
+ onSortChange,
105
+ className,
106
+ children,
107
+ }: DataTableProps<T>) {
108
+ const [internalDensity, setInternalDensity] = React.useState<Density>("compact");
109
+ const density = controlledDensity ?? internalDensity;
110
+ const setDensity = (d: Density) => {
111
+ setInternalDensity(d);
112
+ onDensityChange?.(d);
113
+ };
114
+
115
+ const [internalSelected, setInternalSelected] = React.useState<Set<string>>(new Set());
116
+ const selected = controlledSelected ?? internalSelected;
117
+ const setSelected = (next: Set<string>) => {
118
+ setInternalSelected(next);
119
+ onSelectChange?.(next);
120
+ };
121
+
122
+ const toggleSelect = (id: string) => {
123
+ const next = new Set(selected);
124
+ if (next.has(id)) next.delete(id);
125
+ else next.add(id);
126
+ setSelected(next);
127
+ };
128
+ const allSelected = data.length > 0 && data.every((r) => selected.has(getRowId(r)));
129
+ const someSelected = !allSelected && data.some((r) => selected.has(getRowId(r)));
130
+ const toggleSelectAll = () => {
131
+ if (allSelected) setSelected(new Set());
132
+ else setSelected(new Set(data.map(getRowId)));
133
+ };
134
+
135
+ const ctx: DataTableContextValue<T> = {
136
+ data,
137
+ columns,
138
+ density,
139
+ setDensity,
140
+ selected,
141
+ toggleSelect,
142
+ toggleSelectAll,
143
+ allSelected,
144
+ someSelected,
145
+ selectable,
146
+ getRowId,
147
+ onRowClick,
148
+ sort,
149
+ onSortChange,
150
+ };
151
+
152
+ // Determine if children include a Content slot — if not, render default.
153
+ const hasContent = React.Children.toArray(children).some(
154
+ (c) =>
155
+ React.isValidElement(c) &&
156
+ (c.type as { displayName?: string }).displayName === "DataTable.Content",
157
+ );
158
+
159
+ return (
160
+ <DataTableContext.Provider value={ctx as DataTableContextValue}>
161
+ <div
162
+ className={cn(
163
+ "ui-data-table-root",
164
+ densityClass[density === "compact" ? "compact" : "comfortable"],
165
+ className,
166
+ )}
167
+ >
168
+ {children}
169
+ {!hasContent && <DataTable.Content />}
170
+ </div>
171
+ </DataTableContext.Provider>
172
+ );
173
+ }
174
+
175
+ // ── Toolbar ────────────────────────────────────────────────────────────
176
+
177
+ DataTable.Toolbar = function DataTableToolbar({
178
+ children,
179
+ className,
180
+ }: {
181
+ children?: React.ReactNode;
182
+ className?: string;
183
+ }) {
184
+ return <div className={cn("ui-data-table-toolbar", className)}>{children}</div>;
185
+ };
186
+ (DataTable.Toolbar as React.FC).displayName = "DataTable.Toolbar";
187
+
188
+ // ── SelectAll header checkbox ──────────────────────────────────────────
189
+
190
+ DataTable.SelectAll = function DataTableSelectAll() {
191
+ const { allSelected, someSelected, toggleSelectAll, selectable } = useDataTableContext();
192
+ const { t } = useTranslation();
193
+ if (!selectable) return null;
194
+ return (
195
+ <input
196
+ type="checkbox"
197
+ checked={allSelected}
198
+ ref={(el) => {
199
+ if (el) el.indeterminate = someSelected;
200
+ }}
201
+ onChange={toggleSelectAll}
202
+ aria-label={t("dataTable.selectAll")}
203
+ />
204
+ );
205
+ };
206
+ (DataTable.SelectAll as React.FC).displayName = "DataTable.SelectAll";
207
+
208
+ // ── BulkActions — visible when selection > 0 ───────────────────────────
209
+
210
+ interface BulkActionsProps {
211
+ count?: number;
212
+ children?: React.ReactNode;
213
+ className?: string;
214
+ }
215
+
216
+ DataTable.BulkActions = function DataTableBulkActions({
217
+ count,
218
+ children,
219
+ className,
220
+ }: BulkActionsProps) {
221
+ const ctx = useOptionalDataTableContext();
222
+ const { t } = useTranslation();
223
+ const c = count ?? ctx?.selected.size ?? 0;
224
+ if (c === 0) return null;
225
+ return (
226
+ <div
227
+ role="region"
228
+ aria-label={t("dataTable.bulkActions")}
229
+ className={cn("ui-data-table-bulk", className)}
230
+ >
231
+ <span className="text-muted-foreground">
232
+ <strong className="text-foreground">{t("common.selectedCount", { count: c })}</strong>
233
+ </span>
234
+ <div className="ui-data-table-bulk-actions">{children}</div>
235
+ </div>
236
+ );
237
+ };
238
+ (DataTable.BulkActions as React.FC).displayName = "DataTable.BulkActions";
239
+
240
+ // ── Density toggle ────────────────────────────────────────────────────
241
+
242
+ DataTable.DensityToggle = function DataTableDensityToggle() {
243
+ const { density, setDensity } = useDataTableContext();
244
+ const { t } = useTranslation();
245
+ const next: Density = density === "compact" ? "comfortable" : "compact";
246
+ const Icon = density === "compact" ? Layers : Layers2;
247
+ const nextLabel =
248
+ next === "compact" ? t("dataTable.densityCompact") : t("dataTable.densityComfortable");
249
+ return (
250
+ <Button
251
+ variant="ghost"
252
+ size="sm"
253
+ onClick={() => {
254
+ setDensity(next);
255
+ }}
256
+ aria-label={t("dataTable.densitySwitch", { density: nextLabel })}
257
+ >
258
+ <Inline gap="xs">
259
+ <Icon className="size-4" aria-hidden="true" />
260
+ {density === "compact" ? t("dataTable.densityCompact") : t("dataTable.densityComfortable")}
261
+ </Inline>
262
+ </Button>
263
+ );
264
+ };
265
+ (DataTable.DensityToggle as React.FC).displayName = "DataTable.DensityToggle";
266
+
267
+ // ── Content (the actual table) ─────────────────────────────────────────
268
+
269
+ DataTable.Content = function DataTableContent() {
270
+ const {
271
+ data,
272
+ columns,
273
+ density: _density,
274
+ selectable,
275
+ selected,
276
+ toggleSelect,
277
+ getRowId,
278
+ onRowClick,
279
+ sort,
280
+ onSortChange,
281
+ } = useDataTableContext();
282
+ const { t } = useTranslation();
283
+
284
+ const rowPadding = tableRowHeightClass;
285
+ const cellPadding = tableCellPaddingClass;
286
+
287
+ const onHeaderClick = (col: ColumnDef<unknown>) => {
288
+ if (!col.sortable || !onSortChange) return;
289
+ if (sort?.key !== col.key) {
290
+ onSortChange({ key: col.key, direction: "asc" });
291
+ } else if (sort.direction === "asc") {
292
+ onSortChange({ key: col.key, direction: "desc" });
293
+ } else {
294
+ onSortChange(undefined);
295
+ }
296
+ };
297
+
298
+ return (
299
+ <div className="ui-data-table-scroll">
300
+ <div className="ui-data-table-surface min-w-[640px] sm:min-w-0">
301
+ <Table>
302
+ <TableHeader className="bg-secondary sticky top-0 z-10">
303
+ <TableRow>
304
+ {selectable && (
305
+ <TableHead className="w-10">
306
+ <DataTable.SelectAll />
307
+ </TableHead>
308
+ )}
309
+ {columns.map((col) => (
310
+ <TableHead
311
+ key={col.key}
312
+ className={cn(
313
+ col.width,
314
+ col.align === "right" && "text-right",
315
+ col.align === "center" && "text-center",
316
+ col.sortable && onSortChange && "cursor-pointer select-none",
317
+ )}
318
+ onClick={() => {
319
+ onHeaderClick(col);
320
+ }}
321
+ >
322
+ <span className="ui-data-table-sort-label">
323
+ {col.header}
324
+ {col.sortable &&
325
+ onSortChange &&
326
+ (sort?.key === col.key ? (
327
+ sort.direction === "asc" ? (
328
+ <ArrowUp className="size-3" aria-hidden="true" />
329
+ ) : (
330
+ <ArrowDown className="size-3" aria-hidden="true" />
331
+ )
332
+ ) : (
333
+ <ChevronsUpDown
334
+ className="text-muted-foreground size-3"
335
+ aria-hidden="true"
336
+ />
337
+ ))}
338
+ </span>
339
+ </TableHead>
340
+ ))}
341
+ </TableRow>
342
+ </TableHeader>
343
+ <TableBody>
344
+ {data.map((row) => {
345
+ const id = getRowId(row);
346
+ const isSelected = selected.has(id);
347
+ return (
348
+ <TableRow
349
+ key={id}
350
+ data-state={isSelected ? "selected" : undefined}
351
+ onClick={(e) => {
352
+ // Don't trigger row click if user clicked on an interactive child.
353
+ const target = e.target as HTMLElement;
354
+ if (target.closest("button, a, input, select, textarea, [role=menuitem]"))
355
+ return;
356
+ onRowClick?.(row);
357
+ }}
358
+ className={cn(
359
+ rowPadding,
360
+ onRowClick && "hover:bg-muted/50 cursor-pointer",
361
+ isSelected && "bg-muted/30",
362
+ )}
363
+ >
364
+ {selectable && (
365
+ <TableCell className={cellPadding}>
366
+ <input
367
+ type="checkbox"
368
+ checked={isSelected}
369
+ onChange={() => {
370
+ toggleSelect(id);
371
+ }}
372
+ aria-label={t("dataTable.selectRow", { id })}
373
+ onClick={(e) => {
374
+ e.stopPropagation();
375
+ }}
376
+ />
377
+ </TableCell>
378
+ )}
379
+ {columns.map((col) => (
380
+ <TableCell
381
+ key={col.key}
382
+ className={cn(
383
+ cellPadding,
384
+ col.width,
385
+ col.align === "right" && "text-right",
386
+ col.align === "center" && "text-center",
387
+ )}
388
+ >
389
+ {col.render
390
+ ? col.render(row)
391
+ : (() => {
392
+ const v = (row as Record<string, unknown>)[col.key];
393
+ if (v == null) return "—";
394
+ if (typeof v === "string" || typeof v === "number") return String(v);
395
+ return "—";
396
+ })()}
397
+ </TableCell>
398
+ ))}
399
+ </TableRow>
400
+ );
401
+ })}
402
+ </TableBody>
403
+ </Table>
404
+ </div>
405
+ </div>
406
+ );
407
+ };
408
+ (DataTable.Content as React.FC).displayName = "DataTable.Content";
409
+
410
+ // ── Pagination ─────────────────────────────────────────────────────────
411
+
412
+ interface PaginationProps {
413
+ cursor?: string;
414
+ hasMore: boolean;
415
+ onChange: (cursor: string | undefined) => void;
416
+ className?: string;
417
+ }
418
+
419
+ DataTable.Pagination = function DataTablePagination({
420
+ cursor,
421
+ hasMore,
422
+ onChange,
423
+ className,
424
+ }: PaginationProps) {
425
+ const { t } = useTranslation();
426
+ return (
427
+ <div className={cn("ui-data-table-pagination", className)}>
428
+ <Button
429
+ variant="outline"
430
+ size="sm"
431
+ disabled={!cursor}
432
+ onClick={() => {
433
+ onChange(undefined);
434
+ }}
435
+ >
436
+ {t("common.first")}
437
+ </Button>
438
+ <Button
439
+ variant="outline"
440
+ size="sm"
441
+ disabled={!hasMore}
442
+ onClick={() => {
443
+ onChange(cursor);
444
+ }}
445
+ >
446
+ {t("common.next")}
447
+ </Button>
448
+ </div>
449
+ );
450
+ };
451
+ (DataTable.Pagination as React.FC).displayName = "DataTable.Pagination";
452
+
453
+ // ── More-actions dropdown trigger (kebab) ──────────────────────────────
454
+
455
+ interface RowActionsProps {
456
+ ariaLabel?: string;
457
+ children: React.ReactNode;
458
+ }
459
+
460
+ /** Kebab menu trigger for per-row actions. Wrap children in a DropdownMenu in
461
+ * the consumer — this is just the trigger button shape. */
462
+ DataTable.RowActions = function DataTableRowActions({ ariaLabel, children }: RowActionsProps) {
463
+ const { t } = useTranslation();
464
+ return (
465
+ <Button
466
+ variant="ghost"
467
+ size="icon"
468
+ aria-label={ariaLabel ?? t("dataTable.rowActions")}
469
+ className={controlIconSmClass}
470
+ >
471
+ <MoreHorizontal className="size-4" aria-hidden="true" />
472
+ {children}
473
+ </Button>
474
+ );
475
+ };
476
+ (DataTable.RowActions as React.FC).displayName = "DataTable.RowActions";