@craftzbay/ui 0.9.0 → 0.11.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 (208) hide show
  1. package/README.md +82 -10
  2. package/dist-lib/__tests__/helpers/color.d.ts +9 -0
  3. package/dist-lib/__tests__/helpers/theme.d.ts +18 -0
  4. package/dist-lib/components/ui/Accordion.js +71 -0
  5. package/dist-lib/components/ui/Accordion.js.map +1 -0
  6. package/dist-lib/components/ui/Alert.d.ts +17 -1
  7. package/dist-lib/components/ui/Alert.js +79 -0
  8. package/dist-lib/components/ui/Alert.js.map +1 -0
  9. package/dist-lib/components/ui/Avatar.d.ts +3 -3
  10. package/dist-lib/components/ui/Avatar.js +94 -0
  11. package/dist-lib/components/ui/Avatar.js.map +1 -0
  12. package/dist-lib/components/ui/Badge.d.ts +4 -2
  13. package/dist-lib/components/ui/Badge.js +76 -0
  14. package/dist-lib/components/ui/Badge.js.map +1 -0
  15. package/dist-lib/components/ui/Breadcrumbs.d.ts +2 -0
  16. package/dist-lib/components/ui/Breadcrumbs.js +52 -0
  17. package/dist-lib/components/ui/Breadcrumbs.js.map +1 -0
  18. package/dist-lib/components/ui/Button.d.ts +6 -4
  19. package/dist-lib/components/ui/Button.js +131 -0
  20. package/dist-lib/components/ui/Button.js.map +1 -0
  21. package/dist-lib/components/ui/Calendar.d.ts +7 -1
  22. package/dist-lib/components/ui/Calendar.js +139 -0
  23. package/dist-lib/components/ui/Calendar.js.map +1 -0
  24. package/dist-lib/components/ui/Card.d.ts +3 -1
  25. package/dist-lib/components/ui/Card.js +97 -0
  26. package/dist-lib/components/ui/Card.js.map +1 -0
  27. package/dist-lib/components/ui/Carousel.d.ts +11 -7
  28. package/dist-lib/components/ui/Carousel.js +174 -0
  29. package/dist-lib/components/ui/Carousel.js.map +1 -0
  30. package/dist-lib/components/ui/Chart.d.ts +68 -7
  31. package/dist-lib/components/ui/Chart.js +487 -0
  32. package/dist-lib/components/ui/Chart.js.map +1 -0
  33. package/dist-lib/components/ui/Checkbox.js +70 -0
  34. package/dist-lib/components/ui/Checkbox.js.map +1 -0
  35. package/dist-lib/components/ui/Combobox.d.ts +9 -0
  36. package/dist-lib/components/ui/Combobox.js +195 -0
  37. package/dist-lib/components/ui/Combobox.js.map +1 -0
  38. package/dist-lib/components/ui/CommandPalette.d.ts +9 -6
  39. package/dist-lib/components/ui/CommandPalette.js +179 -0
  40. package/dist-lib/components/ui/CommandPalette.js.map +1 -0
  41. package/dist-lib/components/ui/ContextMenu.d.ts +7 -2
  42. package/dist-lib/components/ui/ContextMenu.js +165 -0
  43. package/dist-lib/components/ui/ContextMenu.js.map +1 -0
  44. package/dist-lib/components/ui/DataGrid.d.ts +17 -4
  45. package/dist-lib/components/ui/DataGrid.js +99 -0
  46. package/dist-lib/components/ui/DataGrid.js.map +1 -0
  47. package/dist-lib/components/ui/DatePicker.d.ts +16 -13
  48. package/dist-lib/components/ui/DatePicker.js +193 -0
  49. package/dist-lib/components/ui/DatePicker.js.map +1 -0
  50. package/dist-lib/components/ui/DesignSystemProvider.d.ts +102 -41
  51. package/dist-lib/components/ui/DesignSystemProvider.js +135 -0
  52. package/dist-lib/components/ui/DesignSystemProvider.js.map +1 -0
  53. package/dist-lib/components/ui/Dialog.d.ts +19 -4
  54. package/dist-lib/components/ui/Dialog.js +165 -0
  55. package/dist-lib/components/ui/Dialog.js.map +1 -0
  56. package/dist-lib/components/ui/Drawer.d.ts +23 -2
  57. package/dist-lib/components/ui/Drawer.js +132 -0
  58. package/dist-lib/components/ui/Drawer.js.map +1 -0
  59. package/dist-lib/components/ui/DropdownMenu.js +169 -0
  60. package/dist-lib/components/ui/DropdownMenu.js.map +1 -0
  61. package/dist-lib/components/ui/EmptyState.d.ts +2 -0
  62. package/dist-lib/components/ui/EmptyState.js +46 -0
  63. package/dist-lib/components/ui/EmptyState.js.map +1 -0
  64. package/dist-lib/components/ui/ErrorState.d.ts +13 -2
  65. package/dist-lib/components/ui/ErrorState.js +69 -0
  66. package/dist-lib/components/ui/ErrorState.js.map +1 -0
  67. package/dist-lib/components/ui/FileUpload.d.ts +3 -0
  68. package/dist-lib/components/ui/FileUpload.js +111 -0
  69. package/dist-lib/components/ui/FileUpload.js.map +1 -0
  70. package/dist-lib/components/ui/Form.d.ts +16 -2
  71. package/dist-lib/components/ui/Form.js +131 -0
  72. package/dist-lib/components/ui/Form.js.map +1 -0
  73. package/dist-lib/components/ui/Icon.d.ts +1 -1
  74. package/dist-lib/components/ui/Icon.js +32 -0
  75. package/dist-lib/components/ui/Icon.js.map +1 -0
  76. package/dist-lib/components/ui/IconButton.d.ts +3 -3
  77. package/dist-lib/components/ui/IconButton.js +64 -0
  78. package/dist-lib/components/ui/IconButton.js.map +1 -0
  79. package/dist-lib/components/ui/Input.d.ts +9 -4
  80. package/dist-lib/components/ui/Input.js +149 -0
  81. package/dist-lib/components/ui/Input.js.map +1 -0
  82. package/dist-lib/components/ui/Kbd.js +24 -0
  83. package/dist-lib/components/ui/Kbd.js.map +1 -0
  84. package/dist-lib/components/ui/MultiSelect.d.ts +3 -1
  85. package/dist-lib/components/ui/MultiSelect.js +210 -0
  86. package/dist-lib/components/ui/MultiSelect.js.map +1 -0
  87. package/dist-lib/components/ui/Pagination.d.ts +23 -0
  88. package/dist-lib/components/ui/Pagination.js +153 -0
  89. package/dist-lib/components/ui/Pagination.js.map +1 -0
  90. package/dist-lib/components/ui/Popover.js +35 -0
  91. package/dist-lib/components/ui/Popover.js.map +1 -0
  92. package/dist-lib/components/ui/Progress.d.ts +3 -3
  93. package/dist-lib/components/ui/Progress.js +100 -0
  94. package/dist-lib/components/ui/Progress.js.map +1 -0
  95. package/dist-lib/components/ui/RadioGroup.d.ts +1 -1
  96. package/dist-lib/components/ui/RadioGroup.js +81 -0
  97. package/dist-lib/components/ui/RadioGroup.js.map +1 -0
  98. package/dist-lib/components/ui/RelativeTime.d.ts +19 -0
  99. package/dist-lib/components/ui/RelativeTime.js +31 -0
  100. package/dist-lib/components/ui/RelativeTime.js.map +1 -0
  101. package/dist-lib/components/ui/ScrollArea.d.ts +9 -1
  102. package/dist-lib/components/ui/ScrollArea.js +60 -0
  103. package/dist-lib/components/ui/ScrollArea.js.map +1 -0
  104. package/dist-lib/components/ui/Select.d.ts +6 -1
  105. package/dist-lib/components/ui/Select.js +139 -0
  106. package/dist-lib/components/ui/Select.js.map +1 -0
  107. package/dist-lib/components/ui/Separator.js +26 -0
  108. package/dist-lib/components/ui/Separator.js.map +1 -0
  109. package/dist-lib/components/ui/Sheet.d.ts +10 -1
  110. package/dist-lib/components/ui/Sheet.js +136 -0
  111. package/dist-lib/components/ui/Sheet.js.map +1 -0
  112. package/dist-lib/components/ui/Sidebar.d.ts +29 -14
  113. package/dist-lib/components/ui/Sidebar.js +212 -0
  114. package/dist-lib/components/ui/Sidebar.js.map +1 -0
  115. package/dist-lib/components/ui/Skeleton.d.ts +13 -2
  116. package/dist-lib/components/ui/Skeleton.js +29 -0
  117. package/dist-lib/components/ui/Skeleton.js.map +1 -0
  118. package/dist-lib/components/ui/Slider.js +64 -0
  119. package/dist-lib/components/ui/Slider.js.map +1 -0
  120. package/dist-lib/components/ui/Snackbar.d.ts +1 -1
  121. package/dist-lib/components/ui/Snackbar.js +57 -0
  122. package/dist-lib/components/ui/Snackbar.js.map +1 -0
  123. package/dist-lib/components/ui/Spinner.js +54 -0
  124. package/dist-lib/components/ui/Spinner.js.map +1 -0
  125. package/dist-lib/components/ui/Stepper.d.ts +6 -0
  126. package/dist-lib/components/ui/Stepper.js +111 -0
  127. package/dist-lib/components/ui/Stepper.js.map +1 -0
  128. package/dist-lib/components/ui/Switch.js +80 -0
  129. package/dist-lib/components/ui/Switch.js.map +1 -0
  130. package/dist-lib/components/ui/Table.d.ts +41 -5
  131. package/dist-lib/components/ui/Table.js +179 -0
  132. package/dist-lib/components/ui/Table.js.map +1 -0
  133. package/dist-lib/components/ui/Tabs.d.ts +1 -1
  134. package/dist-lib/components/ui/Tabs.js +91 -0
  135. package/dist-lib/components/ui/Tabs.js.map +1 -0
  136. package/dist-lib/components/ui/TagInput.d.ts +17 -2
  137. package/dist-lib/components/ui/TagInput.js +111 -0
  138. package/dist-lib/components/ui/TagInput.js.map +1 -0
  139. package/dist-lib/components/ui/Textarea.js +88 -0
  140. package/dist-lib/components/ui/Textarea.js.map +1 -0
  141. package/dist-lib/components/ui/Timeline.d.ts +16 -6
  142. package/dist-lib/components/ui/Timeline.js +56 -0
  143. package/dist-lib/components/ui/Timeline.js.map +1 -0
  144. package/dist-lib/components/ui/Toast.d.ts +1 -1
  145. package/dist-lib/components/ui/Toast.js +158 -0
  146. package/dist-lib/components/ui/Toast.js.map +1 -0
  147. package/dist-lib/components/ui/Tooltip.d.ts +4 -1
  148. package/dist-lib/components/ui/Tooltip.js +60 -0
  149. package/dist-lib/components/ui/Tooltip.js.map +1 -0
  150. package/dist-lib/components/ui/TopNav.d.ts +7 -3
  151. package/dist-lib/components/ui/TopNav.js +62 -0
  152. package/dist-lib/components/ui/TopNav.js.map +1 -0
  153. package/dist-lib/components/ui/Tree.d.ts +34 -7
  154. package/dist-lib/components/ui/Tree.js +170 -0
  155. package/dist-lib/components/ui/Tree.js.map +1 -0
  156. package/dist-lib/hooks/use-debounce.d.ts +19 -0
  157. package/dist-lib/hooks/use-debounce.js +36 -0
  158. package/dist-lib/hooks/use-debounce.js.map +1 -0
  159. package/dist-lib/hooks/use-delayed-loading.d.ts +21 -0
  160. package/dist-lib/hooks/use-delayed-loading.js +20 -0
  161. package/dist-lib/hooks/use-delayed-loading.js.map +1 -0
  162. package/dist-lib/hooks/use-field-ids.d.ts +30 -0
  163. package/dist-lib/hooks/use-field-ids.js +18 -0
  164. package/dist-lib/hooks/use-field-ids.js.map +1 -0
  165. package/dist-lib/hooks/use-media-query.d.ts +3 -1
  166. package/dist-lib/hooks/use-media-query.js +22 -0
  167. package/dist-lib/hooks/use-media-query.js.map +1 -0
  168. package/dist-lib/hooks/use-modifier-key.d.ts +13 -0
  169. package/dist-lib/hooks/use-modifier-key.js +19 -0
  170. package/dist-lib/hooks/use-modifier-key.js.map +1 -0
  171. package/dist-lib/hooks/use-strings.d.ts +5 -0
  172. package/dist-lib/hooks/use-strings.js +12 -0
  173. package/dist-lib/hooks/use-strings.js.map +1 -0
  174. package/dist-lib/hooks/use-toast.d.ts +7 -2
  175. package/dist-lib/hooks/use-toast.js +67 -0
  176. package/dist-lib/hooks/use-toast.js.map +1 -0
  177. package/dist-lib/icon.d.ts +8 -0
  178. package/dist-lib/icon.js +7 -0
  179. package/dist-lib/icon.js.map +1 -0
  180. package/dist-lib/icons/index.d.ts +80 -88
  181. package/dist-lib/icons/index.js +163 -0
  182. package/dist-lib/icons/index.js.map +1 -0
  183. package/dist-lib/illustrations/index.d.ts +1 -0
  184. package/dist-lib/illustrations/index.js +114 -0
  185. package/dist-lib/illustrations/index.js.map +1 -0
  186. package/dist-lib/index.d.ts +68 -59
  187. package/dist-lib/index.js +288 -4530
  188. package/dist-lib/index.js.map +1 -1
  189. package/dist-lib/lib/format.d.ts +65 -0
  190. package/dist-lib/lib/format.js +95 -0
  191. package/dist-lib/lib/format.js.map +1 -0
  192. package/dist-lib/lib/return-focus.d.ts +11 -0
  193. package/dist-lib/lib/return-focus.js +12 -0
  194. package/dist-lib/lib/return-focus.js.map +1 -0
  195. package/dist-lib/lib/strings.d.ts +190 -0
  196. package/dist-lib/lib/strings.js +141 -0
  197. package/dist-lib/lib/strings.js.map +1 -0
  198. package/dist-lib/lib/strings.mn.d.ts +3 -0
  199. package/dist-lib/lib/strings.mn.js +115 -0
  200. package/dist-lib/lib/strings.mn.js.map +1 -0
  201. package/dist-lib/lib/utils.d.ts +0 -1
  202. package/dist-lib/lib/utils.js +10 -0
  203. package/dist-lib/lib/utils.js.map +1 -0
  204. package/dist-lib/styles.css +1 -1
  205. package/dist-lib/theme.css +840 -0
  206. package/package.json +38 -8
  207. package/dist-lib/index.cjs +0 -8
  208. package/dist-lib/index.cjs.map +0 -1
package/README.md CHANGED
@@ -15,19 +15,81 @@ primitives — Button through DataGrid — plus composed patterns
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- pnpm add @craftzbay/ui
18
+ pnpm add @craftzbay/ui # peers: react ^18 || ^19, react-dom ^18 || ^19
19
19
  ```
20
20
 
21
+ **1. Fonts** — Geist + Geist Mono are referenced by the tokens but not bundled (Inter is only a fallback in the stack). Google Fonts serves the `cyrillic-ext` subset automatically; self-hosting (≤4 woff2, weights 400/500/600) is equally fine. Add to `<head>`:
22
+
23
+ ```html
24
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
25
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
26
+ <link
27
+ href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap"
28
+ rel="stylesheet"
29
+ />
30
+ ```
31
+
32
+ **2. CSS** — pick one:
33
+
34
+ ```css
35
+ /* a) No Tailwind in your app: one precompiled sheet (tokens + base + every utility the components use) */
36
+ @import '@craftzbay/ui/styles.css';
37
+
38
+ /* b) Your app already uses Tailwind v4: share the tokens and let Tailwind compile the library's classes */
39
+ @import 'tailwindcss';
40
+ @import 'tw-animate-css'; /* required on this path — overlays use animate-in / animate-out */
41
+ @import '@craftzbay/ui/theme.css';
42
+ @source "../node_modules/@craftzbay/ui/dist-lib";
43
+ ```
44
+
45
+ Path (b) needs `tw-animate-css` installed (`pnpm add -D tw-animate-css`; it is an optional peer). Path (a) already bundles it.
46
+
47
+ **3. Dark mode** — toggle the `dark` class on `<html>`; every token flips and `color-scheme` follows. Set it from a blocking script before first paint to avoid a flash (three states: light / dark / system). `data-theme` is not used.
48
+
49
+ **4. Providers** — `<Toaster />` once near the root if you use `toast()`; wrap the app in `<TooltipProvider>` if you use `Tooltip`. Nothing else is required.
50
+
21
51
  ```tsx
22
- import { Button, Dialog, Toast } from '@craftzbay/ui';
23
- import '@craftzbay/ui/styles.css';
52
+ import { Button, Toaster, TooltipProvider, toast } from '@craftzbay/ui';
24
53
 
25
54
  export function App() {
26
- return <Button variant="primary">Save</Button>;
55
+ return (
56
+ <TooltipProvider>
57
+ <Button onClick={() => toast({ title: 'Saved' })}>Save</Button>
58
+ <Toaster />
59
+ </TooltipProvider>
60
+ );
27
61
  }
28
62
  ```
29
63
 
30
- Peer dependencies: `react@>=18`, `react-dom@>=18`.
64
+ **Next.js App Router** — since 0.10 every built module carries a `'use client'` banner, so `import { Button } from '@craftzbay/ui'` works directly inside Server Components without a local re-export file. Put the CSS import and fonts in `app/layout.tsx`. Note: the pure helpers (`formatDate`, `formatNumber`, `formatMNT`, `mnStrings`, `defaultStrings`) are also client-marked today — they still run fine in Server Components (the directive only affects the boundary), but they are not tree-shaken into a server-only chunk.
65
+
66
+ **Name-addressed icons** — `Icons.*` (curated, tree-shaken) ships in the main entry. The lazy `<Icon name="…">` + `iconNames` list lives in a separate entry so its ~1500-icon import map never enters your bundle unless asked for:
67
+
68
+ ```tsx
69
+ import { Icon } from '@craftzbay/ui/icon';
70
+ <Icon name="calendar" className="size-4" />;
71
+ ```
72
+
73
+ The package is ESM-only, ships one module per component (`sideEffects` limited to CSS), so `import { Button }` pulls in about 8 KB gzipped — no calendar, drawer, or form dependencies.
74
+
75
+ ## Localisation (Mongolian built in)
76
+
77
+ Every built-in string (close/dismiss labels, placeholders, "No results.", pagination summary, error-state copy…) is typed in `UiStrings` and read through `useStrings()`. Defaults are English; a full Mongolian set ships as `mnStrings`.
78
+
79
+ ```tsx
80
+ import { DesignSystemProvider, mnStrings } from '@craftzbay/ui';
81
+ <DesignSystemProvider strings={mnStrings}>…</DesignSystemProvider>; // whole library in Mongolian
82
+ <DesignSystemProvider strings={{ dialog: { close: 'Schließen' } }}>…</DesignSystemProvider>; // partial override, deep-merged over defaults
83
+ ```
84
+
85
+ Precedence is per-component props (`placeholder`, `labels`, `aria-label`) → nearest provider → `defaultStrings`. Templates use `{name}` placeholders, e.g. `pagination.showing: '{from}–{to} / {total}'`.
86
+
87
+ ## Design rules
88
+
89
+ This library implements the [craftzbay design-research](https://github.com/craftzbay/design-research) guidelines
90
+ ([rendered site](https://craftzbay.github.io/design-research/)) — colour, type, spacing, components, accessibility, tokens.
91
+ design-research is the source of truth (canonical numbers in [`14-defaults.md`](https://github.com/craftzbay/design-research/blob/main/14-defaults.md));
92
+ [`docs/PHILOSOPHY.md`](./docs/PHILOSOPHY.md) is the library-specific summary.
31
93
 
32
94
  ## Local development
33
95
 
@@ -46,8 +108,8 @@ Inside `packages/ui` itself, `pnpm build` produces the distributable bundle and
46
108
 
47
109
  ## Tech stack
48
110
 
49
- - **Tailwind CSS v4** — tokens defined in `@theme` in `src/styles/globals.css`
50
- - **React 18** + **TypeScript 5.7**
111
+ - **Tailwind CSS v4** — tokens defined in `@theme` in `src/styles/theme.css` (`globals.css` = tailwind + tw-animate-css + theme)
112
+ - **React 18 / 19** + **TypeScript 5.7**
51
113
  - **Radix UI** primitives for accessibility-correct overlays
52
114
  - **class-variance-authority** + `cn()` (`clsx` + `tailwind-merge`)
53
115
  - **Lucide** icons (16 / 20px, 1.5 stroke)
@@ -58,12 +120,14 @@ Inside `packages/ui` itself, `pnpm build` produces the distributable bundle and
58
120
  ```
59
121
  src/
60
122
  ├── styles/
61
- └── globals.css # @theme tokens, semantic vars, base layer
123
+ ├── theme.css # @theme tokens, semantic vars, dark variant, base layer
124
+ │ └── globals.css # tailwindcss + tw-animate-css + theme.css
62
125
  ├── lib/
63
126
  │ ├── utils.ts # cn() + uid()
64
- └── cva.ts
127
+ ├── format.ts # formatDate / formatNumber / formatMNT
128
+ │ └── strings.ts # UiStrings, defaultStrings (+ strings.mn.ts)
65
129
  ├── components/
66
- │ ├── ui/ # primitives — 40 components
130
+ │ ├── ui/ # primitives — 52 components
67
131
  │ └── patterns/ # composed layouts
68
132
  ├── hooks/
69
133
  │ ├── use-toast.ts
@@ -78,6 +142,7 @@ docs/
78
142
  ## Component index
79
143
 
80
144
  ### Inputs
145
+
81
146
  - [Input](./src/components/ui/Input.tsx) — text / email / password / number / search with prefix, suffix, error
82
147
  - [Textarea](./src/components/ui/Textarea.tsx) — auto-resize multi-line input
83
148
  - [Select](./src/components/ui/Select.tsx) — single-choice menu (Radix)
@@ -91,11 +156,13 @@ docs/
91
156
  - [Form primitives](./src/components/ui/Form.tsx) — react-hook-form bindings
92
157
 
93
158
  ### Buttons
159
+
94
160
  - [Button](./src/components/ui/Button.tsx) — primary · secondary · outline · ghost · destructive · link
95
161
  - [IconButton](./src/components/ui/IconButton.tsx) — square icon-only variant
96
162
  - [Pagination](./src/components/ui/Pagination.tsx) — numbered + jumps + page-size
97
163
 
98
164
  ### Feedback
165
+
99
166
  - [Alert](./src/components/ui/Alert.tsx) — inline banner, dismissible
100
167
  - [Toast](./src/components/ui/Toast.tsx) + `useToast` hook
101
168
  - [Spinner](./src/components/ui/Spinner.tsx) — accent / neutral / on-accent tones
@@ -105,6 +172,7 @@ docs/
105
172
  - [ErrorState](./src/components/ui/ErrorState.tsx) — 404 / 500 / generic
106
173
 
107
174
  ### Navigation
175
+
108
176
  - [TopNav](./src/components/ui/TopNav.tsx) + `TopNavLink`
109
177
  - [Sidebar](./src/components/ui/Sidebar.tsx) + `SidebarSection` + `SidebarItem` + `SidebarGroup`
110
178
  - [Breadcrumbs](./src/components/ui/Breadcrumbs.tsx) — with overflow ellipsis
@@ -112,12 +180,14 @@ docs/
112
180
  - [Stepper](./src/components/ui/Stepper.tsx) — horizontal + vertical
113
181
 
114
182
  ### Layout
183
+
115
184
  - [Card](./src/components/ui/Card.tsx) + `CardHeader/Title/Description/Content/Footer`
116
185
  - [Separator](./src/components/ui/Separator.tsx)
117
186
  - [ScrollArea](./src/components/ui/ScrollArea.tsx) — Radix-backed styled scroll
118
187
  - [Accordion](./src/components/ui/Accordion.tsx) — single + multiple
119
188
 
120
189
  ### Overlays
190
+
121
191
  - [Dialog](./src/components/ui/Dialog.tsx) + `ConfirmationDialog`
122
192
  - [Sheet](./src/components/ui/Sheet.tsx) — left / right / top / bottom
123
193
  - [Popover](./src/components/ui/Popover.tsx)
@@ -127,12 +197,14 @@ docs/
127
197
  - [CommandPalette](./src/components/ui/CommandPalette.tsx) — ⌘K palette
128
198
 
129
199
  ### Data display
200
+
130
201
  - [Table](./src/components/ui/Table.tsx) + `TableSortHeader`
131
202
  - [DataGrid](./src/components/ui/DataGrid.tsx) — column visibility, filter, sortable
132
203
  - [Badge](./src/components/ui/Badge.tsx) — subtle + outline, 6 tones
133
204
  - [Avatar](./src/components/ui/Avatar.tsx) + `AvatarGroup`
134
205
 
135
206
  ### Typography
207
+
136
208
  - [Kbd](./src/components/ui/Kbd.tsx) — keyboard shortcut indicator
137
209
 
138
210
  ## Blocks (page templates)
@@ -0,0 +1,9 @@
1
+ /** Test-only colour maths: CSS `hsl()` / `oklch()` / hex → sRGB, WCAG 2.x contrast. */
2
+ export type RGB = [number, number, number];
3
+ export declare function parseColor(input: string): RGB;
4
+ export declare function hslToRgb(h: number, s: number, l: number): RGB;
5
+ /** OKLCH → OKLab → linear sRGB → gamma sRGB (clipped to gamut). */
6
+ export declare function oklchToRgb(L: number, C: number, hDeg: number): RGB;
7
+ export declare function luminance([r, g, b]: RGB): number;
8
+ export declare function contrast(a: RGB, b: RGB): number;
9
+ export declare function toHex([r, g, b]: RGB): string;
@@ -0,0 +1,18 @@
1
+ export declare const THEME_PATH: any;
2
+ export type TokenMap = Record<string, string>;
3
+ export interface ParsedTheme {
4
+ /** `:root { … }` semantic block. */
5
+ light: TokenMap;
6
+ /** `.dark { … }` semantic block. */
7
+ dark: TokenMap;
8
+ /** `@theme { … }` raw primitives (fonts, text scale, radius, shadow, z, motion, palette). */
9
+ raw: TokenMap;
10
+ /** `[data-accent='x']` presets: light (`:root[...]`) and dark (`.dark[...]`) overrides. */
11
+ accents: Record<string, {
12
+ shared: TokenMap;
13
+ light: TokenMap;
14
+ dark: TokenMap;
15
+ }>;
16
+ }
17
+ export declare function parseDeclarations(body: string): TokenMap;
18
+ export declare function parseTheme(css?: any): ParsedTheme;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { jsx as t, jsxs as d } from "react/jsx-runtime";
3
+ import { forwardRef as a } from "react";
4
+ import * as n from "@radix-ui/react-accordion";
5
+ import { ChevronDown as m } from "../../icons/index.js";
6
+ import { cn as i } from "../../lib/utils.js";
7
+ const v = n.Root, f = a(function({ className: o, ...e }, r) {
8
+ return /* @__PURE__ */ t(
9
+ n.Item,
10
+ {
11
+ ref: r,
12
+ className: i("border-border border-b", o),
13
+ ...e
14
+ }
15
+ );
16
+ });
17
+ f.displayName = "AccordionItem";
18
+ const l = a(function({ className: o, children: e, ...r }, c) {
19
+ return /* @__PURE__ */ t(n.Header, { className: "flex", children: /* @__PURE__ */ d(
20
+ n.Trigger,
21
+ {
22
+ ref: c,
23
+ className: i(
24
+ "text-foreground flex flex-1 items-center justify-between gap-2 py-4 text-left text-sm font-medium",
25
+ "transition-colors duration-[var(--duration-fast)] outline-none",
26
+ "hover:text-accent",
27
+ "focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2",
28
+ "[&[data-state=open]>svg]:rotate-180",
29
+ o
30
+ ),
31
+ ...r,
32
+ children: [
33
+ e,
34
+ /* @__PURE__ */ t(
35
+ m,
36
+ {
37
+ className: "text-foreground-subtle size-4 shrink-0 transition-transform duration-[var(--duration-base)]",
38
+ "aria-hidden": !0
39
+ }
40
+ )
41
+ ]
42
+ }
43
+ ) });
44
+ });
45
+ l.displayName = "AccordionTrigger";
46
+ const u = a(function({ className: o, children: e, ...r }, c) {
47
+ return /* @__PURE__ */ t(
48
+ n.Content,
49
+ {
50
+ ref: c,
51
+ className: i(
52
+ "text-foreground-muted overflow-hidden text-sm",
53
+ // Deliberate motion exception: height animates (layout property). Radix only
54
+ // keeps Content mounted through CSS *animations*, so a grid-rows transition
55
+ // would cut the close. The panel is short and nothing below it reflows
56
+ // except by design; `prefers-reduced-motion` disables it globally.
57
+ "data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
58
+ ),
59
+ ...r,
60
+ children: /* @__PURE__ */ t("div", { className: i("pt-0 pb-4", o), children: e })
61
+ }
62
+ );
63
+ });
64
+ u.displayName = "AccordionContent";
65
+ export {
66
+ v as Accordion,
67
+ u as AccordionContent,
68
+ f as AccordionItem,
69
+ l as AccordionTrigger
70
+ };
71
+ //# sourceMappingURL=Accordion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Accordion.js","sources":["../../../src/components/ui/Accordion.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react';\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { ChevronDown } from '@/icons';\nimport { cn } from '@/lib/utils';\n\n/**\n * Accessible disclosure list. Supports single (`type=\"single\"`) and multiple\n * (`type=\"multiple\"`) open behaviour — both come from Radix.\n *\n * @example FAQ\n * <Accordion type=\"single\" collapsible>\n * <AccordionItem value=\"q1\">\n * <AccordionTrigger>Can I cancel anytime?</AccordionTrigger>\n * <AccordionContent>Yes — usage stops at the end of the billing period.</AccordionContent>\n * </AccordionItem>\n * …\n * </Accordion>\n *\n * @do Use for FAQs, settings groups, and progressive-disclosure forms.\n * @dont Nest accordions inside accordions — the focus order becomes opaque.\n */\nexport const Accordion = AccordionPrimitive.Root;\n\nexport const AccordionItem = forwardRef<\n ElementRef<typeof AccordionPrimitive.Item>,\n ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(function AccordionItem({ className, ...props }, ref) {\n return (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn('border-border border-b', className)}\n {...props}\n />\n );\n});\nAccordionItem.displayName = 'AccordionItem';\n\nexport const AccordionTrigger = forwardRef<\n ElementRef<typeof AccordionPrimitive.Trigger>,\n ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(function AccordionTrigger({ className, children, ...props }, ref) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n 'text-foreground flex flex-1 items-center justify-between gap-2 py-4 text-left text-sm font-medium',\n 'transition-colors duration-[var(--duration-fast)] outline-none',\n 'hover:text-accent',\n 'focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2',\n '[&[data-state=open]>svg]:rotate-180',\n className,\n )}\n {...props}\n >\n {children}\n <ChevronDown\n className=\"text-foreground-subtle size-4 shrink-0 transition-transform duration-[var(--duration-base)]\"\n aria-hidden\n />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n});\nAccordionTrigger.displayName = 'AccordionTrigger';\n\nexport const AccordionContent = forwardRef<\n ElementRef<typeof AccordionPrimitive.Content>,\n ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(function AccordionContent({ className, children, ...props }, ref) {\n return (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(\n 'text-foreground-muted overflow-hidden text-sm',\n // Deliberate motion exception: height animates (layout property). Radix only\n // keeps Content mounted through CSS *animations*, so a grid-rows transition\n // would cut the close. The panel is short and nothing below it reflows\n // except by design; `prefers-reduced-motion` disables it globally.\n 'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',\n )}\n {...props}\n >\n <div className={cn('pt-0 pb-4', className)}>{children}</div>\n </AccordionPrimitive.Content>\n );\n});\nAccordionContent.displayName = 'AccordionContent';\n"],"names":["Accordion","AccordionPrimitive","AccordionItem","forwardRef","className","props","ref","jsx","cn","AccordionTrigger","children","jsxs","ChevronDown","AccordionContent"],"mappings":";;;;;;AAuBO,MAAMA,IAAYC,EAAmB,MAE/BC,IAAgBC,EAG3B,SAAuB,EAAE,WAAAC,GAAW,GAAGC,EAAA,GAASC,GAAK;AACrD,SACE,gBAAAC;AAAA,IAACN,EAAmB;AAAA,IAAnB;AAAA,MACC,KAAAK;AAAA,MACA,WAAWE,EAAG,0BAA0BJ,CAAS;AAAA,MAChD,GAAGC;AAAA,IAAA;AAAA,EAAA;AAGV,CAAC;AACDH,EAAc,cAAc;AAErB,MAAMO,IAAmBN,EAG9B,SAA0B,EAAE,WAAAC,GAAW,UAAAM,GAAU,GAAGL,EAAA,GAASC,GAAK;AAClE,SACE,gBAAAC,EAACN,EAAmB,QAAnB,EAA0B,WAAU,QACnC,UAAA,gBAAAU;AAAA,IAACV,EAAmB;AAAA,IAAnB;AAAA,MACC,KAAAK;AAAA,MACA,WAAWE;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACAJ;AAAA,MAAA;AAAA,MAED,GAAGC;AAAA,MAEH,UAAA;AAAA,QAAAK;AAAA,QACD,gBAAAH;AAAA,UAACK;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAW;AAAA,UAAA;AAAA,QAAA;AAAA,MACb;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ,CAAC;AACDH,EAAiB,cAAc;AAExB,MAAMI,IAAmBV,EAG9B,SAA0B,EAAE,WAAAC,GAAW,UAAAM,GAAU,GAAGL,EAAA,GAASC,GAAK;AAClE,SACE,gBAAAC;AAAA,IAACN,EAAmB;AAAA,IAAnB;AAAA,MACC,KAAAK;AAAA,MACA,WAAWE;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA,MAAA;AAAA,MAED,GAAGH;AAAA,MAEJ,4BAAC,OAAA,EAAI,WAAWG,EAAG,aAAaJ,CAAS,GAAI,UAAAM,EAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAG5D,CAAC;AACDG,EAAiB,cAAc;"}
@@ -1,5 +1,5 @@
1
1
  import { HTMLAttributes, ReactNode } from 'react';
2
- import { VariantProps } from '../../lib/cva';
2
+ import { VariantProps } from '../../lib/cva.js';
3
3
  declare const alert: (props?: ({
4
4
  variant?: "default" | "success" | "warning" | "danger" | "info" | null | undefined;
5
5
  } & import('class-variance-authority/types').ClassProp) | undefined) => string;
@@ -10,8 +10,24 @@ export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'
10
10
  dismissible?: boolean;
11
11
  /** Called when the user dismisses. */
12
12
  onDismiss?: () => void;
13
+ /**
14
+ * Controlled visibility. Omit for the default uncontrolled behaviour
15
+ * (visible until dismissed).
16
+ */
17
+ open?: boolean;
18
+ /** Called with the next visibility when the user dismisses. */
19
+ onOpenChange?: (open: boolean) => void;
13
20
  /** Override the variant's default icon. Pass `false` to suppress the icon. */
14
21
  icon?: ReactNode | false;
22
+ /**
23
+ * Announce the alert to screen readers when it appears: `role="alert"` for
24
+ * `danger`, `role="status"` otherwise. Off by default — content that is on
25
+ * the page at load should not be announced as a live update.
26
+ * @default false
27
+ */
28
+ live?: boolean;
29
+ /** Heading level of `title`. Default 3 — fits under a page h1 / section h2. */
30
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
15
31
  }
16
32
  /**
17
33
  * Inline banner for static page-level or section-level state. For transient
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsxs as l, jsx as e } from "react/jsx-runtime";
3
+ import { forwardRef as k, useState as w, createElement as z } from "react";
4
+ import { X as A, XCircle as j, AlertTriangle as y, CheckCircle2 as I, Info as S } from "../../icons/index.js";
5
+ import { cn as o } from "../../lib/utils.js";
6
+ import { useStrings as V } from "../../hooks/use-strings.js";
7
+ import { cva as X } from "class-variance-authority";
8
+ const E = X(["relative flex w-full gap-3 rounded-lg border p-4", "text-sm"], {
9
+ variants: {
10
+ variant: {
11
+ default: "border-border bg-background-subtle text-foreground",
12
+ info: "border-info-border-soft bg-info-soft text-info-text",
13
+ success: "border-success-border-soft bg-success-soft text-success-text",
14
+ warning: "border-warning-border-soft bg-warning-soft text-warning-text",
15
+ danger: "border-danger-border-soft bg-danger-soft text-danger-text"
16
+ }
17
+ },
18
+ defaultVariants: { variant: "default" }
19
+ }), F = {
20
+ default: null,
21
+ info: /* @__PURE__ */ e(S, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": !0 }),
22
+ success: /* @__PURE__ */ e(I, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": !0 }),
23
+ warning: /* @__PURE__ */ e(y, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": !0 }),
24
+ danger: /* @__PURE__ */ e(j, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": !0 })
25
+ }, R = k(function({
26
+ className: d,
27
+ variant: r = "default",
28
+ title: i,
29
+ dismissible: c,
30
+ onDismiss: t,
31
+ open: n,
32
+ onOpenChange: s,
33
+ icon: a,
34
+ live: f = !1,
35
+ headingLevel: u = 3,
36
+ children: m,
37
+ ...g
38
+ }, b) {
39
+ const x = V(), [h, v] = w(!0);
40
+ if (!(n ?? h)) return null;
41
+ const p = () => {
42
+ n === void 0 && v(!1), s == null || s(!1), t == null || t();
43
+ }, N = a === !1 ? null : a ?? F[r ?? "default"];
44
+ return /* @__PURE__ */ l(
45
+ "div",
46
+ {
47
+ ref: b,
48
+ role: f ? r === "danger" ? "alert" : "status" : void 0,
49
+ className: o(E({ variant: r }), d),
50
+ ...g,
51
+ children: [
52
+ N,
53
+ /* @__PURE__ */ l("div", { className: "min-w-0 flex-1", children: [
54
+ i && z(`h${u}`, { className: "mb-1 leading-tight font-medium" }, i),
55
+ /* @__PURE__ */ e("div", { className: "text-sm leading-relaxed [&_p]:leading-relaxed", children: m })
56
+ ] }),
57
+ c && /* @__PURE__ */ e(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ "aria-label": x.alert.dismiss,
62
+ onClick: p,
63
+ className: o(
64
+ "inline-flex size-6 shrink-0 items-center justify-center rounded-md",
65
+ "outline-none hover:bg-current/10",
66
+ "focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2"
67
+ ),
68
+ children: /* @__PURE__ */ e(A, { className: "size-3.5", "aria-hidden": !0 })
69
+ }
70
+ )
71
+ ]
72
+ }
73
+ );
74
+ });
75
+ R.displayName = "Alert";
76
+ export {
77
+ R as Alert
78
+ };
79
+ //# sourceMappingURL=Alert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Alert.js","sources":["../../../src/components/ui/Alert.tsx"],"sourcesContent":["'use client';\n\nimport { createElement, forwardRef, useState, type HTMLAttributes, type ReactNode } from 'react';\nimport { AlertTriangle, CheckCircle2, Info, X, XCircle } from '@/icons';\nimport { cn } from '@/lib/utils';\nimport { useStrings } from '@/hooks/use-strings';\nimport { cva, type VariantProps } from '@/lib/cva';\n\nconst alert = cva(['relative flex w-full gap-3 rounded-lg border p-4', 'text-sm'], {\n variants: {\n variant: {\n default: 'border-border bg-background-subtle text-foreground',\n info: 'border-info-border-soft bg-info-soft text-info-text',\n success: 'border-success-border-soft bg-success-soft text-success-text',\n warning: 'border-warning-border-soft bg-warning-soft text-warning-text',\n danger: 'border-danger-border-soft bg-danger-soft text-danger-text',\n },\n },\n defaultVariants: { variant: 'default' },\n});\n\nconst iconForVariant = {\n default: null,\n info: <Info className=\"mt-0.5 size-4 shrink-0\" aria-hidden />,\n success: <CheckCircle2 className=\"mt-0.5 size-4 shrink-0\" aria-hidden />,\n warning: <AlertTriangle className=\"mt-0.5 size-4 shrink-0\" aria-hidden />,\n danger: <XCircle className=\"mt-0.5 size-4 shrink-0\" aria-hidden />,\n};\n\nexport interface AlertProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>, VariantProps<typeof alert> {\n /** Heading. Renders bold above the description. */\n title?: ReactNode;\n /** Show a dismiss (×) button. */\n dismissible?: boolean;\n /** Called when the user dismisses. */\n onDismiss?: () => void;\n /**\n * Controlled visibility. Omit for the default uncontrolled behaviour\n * (visible until dismissed).\n */\n open?: boolean;\n /** Called with the next visibility when the user dismisses. */\n onOpenChange?: (open: boolean) => void;\n /** Override the variant's default icon. Pass `false` to suppress the icon. */\n icon?: ReactNode | false;\n /**\n * Announce the alert to screen readers when it appears: `role=\"alert\"` for\n * `danger`, `role=\"status\"` otherwise. Off by default — content that is on\n * the page at load should not be announced as a live update.\n * @default false\n */\n live?: boolean;\n /** Heading level of `title`. Default 3 — fits under a page h1 / section h2. */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n}\n\n/**\n * Inline banner for static page-level or section-level state. For transient\n * notifications use `Toast` instead.\n *\n * @example Inline error\n * <Alert variant=\"danger\" title=\"Payment failed\">\n * Your card was declined. Update your billing info to retry.\n * </Alert>\n *\n * @example Dismissible info\n * <Alert variant=\"info\" dismissible onDismiss={dismiss}>\n * We're improving search — give us feedback at #search-feedback.\n * </Alert>\n *\n * @do Use the closest semantic variant — `info` for neutral facts,\n * `warning` for \"watch out\", `danger` for \"this is broken\".\n * @dont Use Alert for one-time confirmations — that's Toast.\n */\nexport const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(\n {\n className,\n variant = 'default',\n title,\n dismissible,\n onDismiss,\n open: openProp,\n onOpenChange,\n icon,\n live = false,\n headingLevel = 3,\n children,\n ...props\n },\n ref,\n) {\n const strings = useStrings();\n const [internalOpen, setInternalOpen] = useState(true);\n const open = openProp ?? internalOpen;\n if (!open) return null;\n\n const handleDismiss = () => {\n if (openProp === undefined) setInternalOpen(false);\n onOpenChange?.(false);\n onDismiss?.();\n };\n\n const renderedIcon = icon === false ? null : (icon ?? iconForVariant[variant ?? 'default']);\n\n return (\n <div\n ref={ref}\n // Only errors interrupt the screen reader; everything else is polite.\n role={live ? (variant === 'danger' ? 'alert' : 'status') : undefined}\n className={cn(alert({ variant }), className)}\n {...props}\n >\n {renderedIcon}\n <div className=\"min-w-0 flex-1\">\n {title &&\n createElement(`h${headingLevel}`, { className: 'mb-1 leading-tight font-medium' }, title)}\n <div className=\"text-sm leading-relaxed [&_p]:leading-relaxed\">{children}</div>\n </div>\n {dismissible && (\n <button\n type=\"button\"\n aria-label={strings.alert.dismiss}\n onClick={handleDismiss}\n className={cn(\n 'inline-flex size-6 shrink-0 items-center justify-center rounded-md',\n 'outline-none hover:bg-current/10',\n 'focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2',\n )}\n >\n <X className=\"size-3.5\" aria-hidden />\n </button>\n )}\n </div>\n );\n});\nAlert.displayName = 'Alert';\n"],"names":["alert","cva","iconForVariant","jsx","Info","CheckCircle2","AlertTriangle","XCircle","Alert","forwardRef","className","variant","title","dismissible","onDismiss","openProp","onOpenChange","icon","live","headingLevel","children","props","ref","strings","useStrings","internalOpen","setInternalOpen","useState","handleDismiss","renderedIcon","jsxs","cn","createElement","X"],"mappings":";;;;;;;AAQA,MAAMA,IAAQC,EAAI,CAAC,oDAAoD,SAAS,GAAG;AAAA,EACjF,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,IAAA;AAAA,EACV;AAAA,EAEF,iBAAiB,EAAE,SAAS,UAAA;AAC9B,CAAC,GAEKC,IAAiB;AAAA,EACrB,SAAS;AAAA,EACT,MAAM,gBAAAC,EAACC,GAAA,EAAK,WAAU,0BAAyB,eAAW,IAAC;AAAA,EAC3D,SAAS,gBAAAD,EAACE,GAAA,EAAa,WAAU,0BAAyB,eAAW,IAAC;AAAA,EACtE,SAAS,gBAAAF,EAACG,GAAA,EAAc,WAAU,0BAAyB,eAAW,IAAC;AAAA,EACvE,QAAQ,gBAAAH,EAACI,GAAA,EAAQ,WAAU,0BAAyB,eAAW,GAAA,CAAC;AAClE,GAgDaC,IAAQC,EAAuC,SAC1D;AAAA,EACE,WAAAC;AAAA,EACA,SAAAC,IAAU;AAAA,EACV,OAAAC;AAAA,EACA,aAAAC;AAAA,EACA,WAAAC;AAAA,EACA,MAAMC;AAAA,EACN,cAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC,IAAO;AAAA,EACP,cAAAC,IAAe;AAAA,EACf,UAAAC;AAAA,EACA,GAAGC;AACL,GACAC,GACA;AACA,QAAMC,IAAUC,EAAA,GACV,CAACC,GAAcC,CAAe,IAAIC,EAAS,EAAI;AAErD,MAAI,EADSZ,KAAYU,GACd,QAAO;AAElB,QAAMG,IAAgB,MAAM;AAC1B,IAAIb,MAAa,UAAWW,EAAgB,EAAK,GACjDV,KAAA,QAAAA,EAAe,KACfF,KAAA,QAAAA;AAAA,EACF,GAEMe,IAAeZ,MAAS,KAAQ,OAAQA,KAAQf,EAAeS,KAAW,SAAS;AAEzF,SACE,gBAAAmB;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAR;AAAA,MAEA,MAAMJ,IAAQP,MAAY,WAAW,UAAU,WAAY;AAAA,MAC3D,WAAWoB,EAAG/B,EAAM,EAAE,SAAAW,EAAA,CAAS,GAAGD,CAAS;AAAA,MAC1C,GAAGW;AAAA,MAEH,UAAA;AAAA,QAAAQ;AAAA,QACD,gBAAAC,EAAC,OAAA,EAAI,WAAU,kBACZ,UAAA;AAAA,UAAAlB,KACCoB,EAAc,IAAIb,CAAY,IAAI,EAAE,WAAW,iCAAA,GAAoCP,CAAK;AAAA,UAC1F,gBAAAT,EAAC,OAAA,EAAI,WAAU,iDAAiD,UAAAiB,EAAA,CAAS;AAAA,QAAA,GAC3E;AAAA,QACCP,KACC,gBAAAV;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAYoB,EAAQ,MAAM;AAAA,YAC1B,SAASK;AAAA,YACT,WAAWG;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA,gBAAA5B,EAAC8B,GAAA,EAAE,WAAU,YAAW,eAAW,GAAA,CAAC;AAAA,UAAA;AAAA,QAAA;AAAA,MACtC;AAAA,IAAA;AAAA,EAAA;AAIR,CAAC;AACDzB,EAAM,cAAc;"}
@@ -1,8 +1,8 @@
1
1
  import { ComponentPropsWithoutRef, HTMLAttributes, ReactNode } from 'react';
2
- import { VariantProps } from '../../lib/cva';
2
+ import { VariantProps } from '../../lib/cva.js';
3
3
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
4
4
  declare const sizeMap: {
5
- readonly xs: "size-5 text-[10px]";
5
+ readonly xs: "size-5 text-xs";
6
6
  readonly sm: "size-6 text-xs";
7
7
  readonly md: "size-8 text-xs";
8
8
  readonly lg: "size-10 text-sm";
@@ -40,7 +40,7 @@ export declare const Avatar: import('react').ForwardRefExoticComponent<AvatarPro
40
40
  export interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {
41
41
  /** Maximum visible avatars before showing a "+N" overflow chip. */
42
42
  max?: number;
43
- /** Avatar size applied to children + overflow. */
43
+ /** Avatar size applied to children (unless a child sets its own) + overflow. */
44
44
  size?: keyof typeof sizeMap;
45
45
  /** Children — Avatars. */
46
46
  children: ReactNode;
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsxs as o, jsx as i } from "react/jsx-runtime";
3
+ import { forwardRef as b, Children as N, isValidElement as h, cloneElement as y } from "react";
4
+ import * as g from "@radix-ui/react-avatar";
5
+ import { cn as l } from "../../lib/utils.js";
6
+ import { useStrings as p } from "../../hooks/use-strings.js";
7
+ import { formatString as v } from "../../lib/strings.js";
8
+ import { cva as A } from "class-variance-authority";
9
+ const x = {
10
+ xs: "size-5 text-xs",
11
+ sm: "size-6 text-xs",
12
+ md: "size-8 text-xs",
13
+ lg: "size-10 text-sm",
14
+ xl: "size-12 text-base"
15
+ }, j = A("relative inline-flex shrink-0", {
16
+ variants: { size: x },
17
+ defaultVariants: { size: "md" }
18
+ }), w = {
19
+ online: "bg-success",
20
+ busy: "bg-danger",
21
+ away: "bg-warning",
22
+ offline: "bg-foreground-subtle"
23
+ }, G = b(
24
+ function({ className: m, size: t, src: e, alt: c, fallback: u, status: r, ...d }, a) {
25
+ const s = p();
26
+ return /* @__PURE__ */ o("span", { className: l(j({ size: t }), m), children: [
27
+ /* @__PURE__ */ o(
28
+ g.Root,
29
+ {
30
+ ref: a,
31
+ className: "bg-background-muted text-foreground-muted block size-full overflow-hidden rounded-full",
32
+ ...d,
33
+ children: [
34
+ e && /* @__PURE__ */ i(
35
+ g.Image,
36
+ {
37
+ src: e,
38
+ alt: c ?? "",
39
+ className: "aspect-square size-full object-cover"
40
+ }
41
+ ),
42
+ /* @__PURE__ */ i(
43
+ g.Fallback,
44
+ {
45
+ delayMs: e ? 200 : 0,
46
+ className: "bg-background-muted flex size-full items-center justify-center font-medium uppercase",
47
+ children: u ?? "?"
48
+ }
49
+ )
50
+ ]
51
+ }
52
+ ),
53
+ r && /* @__PURE__ */ i(
54
+ "span",
55
+ {
56
+ role: "img",
57
+ "aria-label": v(s.avatar.status, { status: r }),
58
+ className: l(
59
+ "ring-background absolute right-0 bottom-0 block size-[28%] rounded-full ring-2",
60
+ w[r]
61
+ )
62
+ }
63
+ )
64
+ ] });
65
+ }
66
+ );
67
+ G.displayName = "Avatar";
68
+ const C = b(function({ max: m = 4, size: t = "md", className: e, children: c, ...u }, r) {
69
+ const d = p(), a = N.toArray(c), s = a.slice(0, m), f = a.length - s.length;
70
+ return /* @__PURE__ */ o("div", { ref: r, className: l("flex items-center -space-x-2", e), ...u, children: [
71
+ s.map((n, k) => /* @__PURE__ */ i("div", { className: "ring-background rounded-full ring-2", children: h(n) && n.props.size === void 0 ? y(n, { size: t }) : n }, k)),
72
+ f > 0 && /* @__PURE__ */ o(
73
+ "div",
74
+ {
75
+ className: l(
76
+ "bg-background-muted text-foreground-muted ring-background inline-flex items-center justify-center rounded-full ring-2",
77
+ x[t],
78
+ "font-medium"
79
+ ),
80
+ "aria-label": v(d.avatar.more, { n: f }),
81
+ children: [
82
+ "+",
83
+ f
84
+ ]
85
+ }
86
+ )
87
+ ] });
88
+ });
89
+ C.displayName = "AvatarGroup";
90
+ export {
91
+ G as Avatar,
92
+ C as AvatarGroup
93
+ };
94
+ //# sourceMappingURL=Avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Avatar.js","sources":["../../../src/components/ui/Avatar.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n type ComponentPropsWithoutRef,\n type ElementRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\nimport { cn } from '@/lib/utils';\nimport { useStrings } from '@/hooks/use-strings';\nimport { formatString } from '@/lib/strings';\nimport { cva, type VariantProps } from '@/lib/cva';\n\nconst sizeMap = {\n xs: 'size-5 text-xs',\n sm: 'size-6 text-xs',\n md: 'size-8 text-xs',\n lg: 'size-10 text-sm',\n xl: 'size-12 text-base',\n} as const;\n\n// The wrapper sets size — Root sits inside and clips image/fallback only.\nconst avatarWrapper = cva('relative inline-flex shrink-0', {\n variants: { size: sizeMap },\n defaultVariants: { size: 'md' },\n});\n\nexport interface AvatarProps\n extends\n ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n VariantProps<typeof avatarWrapper> {\n /** Image URL. If absent or fails to load, fallback initials are shown. */\n src?: string;\n /** Alt text for the image; falls back to fallback when there's no `src`. */\n alt?: string;\n /** Initials shown when image is missing or loading. Pass at most 2 characters. */\n fallback?: string;\n /** Status dot rendered on the bottom-right. */\n status?: 'online' | 'busy' | 'away' | 'offline';\n}\n\nconst statusColour: Record<NonNullable<AvatarProps['status']>, string> = {\n online: 'bg-success',\n busy: 'bg-danger',\n away: 'bg-warning',\n offline: 'bg-foreground-subtle',\n};\n\n/**\n * Avatar with image, initials fallback, and optional status dot.\n *\n * @example\n * <Avatar src={user.photo} alt={user.name} fallback={getInitials(user.name)} status=\"online\" />\n *\n * @example AvatarGroup\n * <AvatarGroup max={3}>\n * {users.map(u => <Avatar key={u.id} src={u.photo} fallback={getInitials(u.name)} />)}\n * </AvatarGroup>\n *\n * @do Provide `alt` for image avatars and meaningful initials for the\n * fallback so screen readers announce something useful.\n * @dont Use the status dot without a tooltip explaining what it means.\n */\nexport const Avatar = forwardRef<ElementRef<typeof AvatarPrimitive.Root>, AvatarProps>(\n function Avatar({ className, size, src, alt, fallback, status, ...props }, ref) {\n const strings = useStrings();\n return (\n <span className={cn(avatarWrapper({ size }), className)}>\n <AvatarPrimitive.Root\n ref={ref}\n className=\"bg-background-muted text-foreground-muted block size-full overflow-hidden rounded-full\"\n {...props}\n >\n {src && (\n <AvatarPrimitive.Image\n src={src}\n alt={alt ?? ''}\n className=\"aspect-square size-full object-cover\"\n />\n )}\n <AvatarPrimitive.Fallback\n delayMs={src ? 200 : 0}\n className=\"bg-background-muted flex size-full items-center justify-center font-medium uppercase\"\n >\n {fallback ?? '?'}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n {status && (\n <span\n role=\"img\"\n aria-label={formatString(strings.avatar.status, { status })}\n className={cn(\n 'ring-background absolute right-0 bottom-0 block size-[28%] rounded-full ring-2',\n statusColour[status],\n )}\n />\n )}\n </span>\n );\n },\n);\nAvatar.displayName = 'Avatar';\n\nexport interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {\n /** Maximum visible avatars before showing a \"+N\" overflow chip. */\n max?: number;\n /** Avatar size applied to children (unless a child sets its own) + overflow. */\n size?: keyof typeof sizeMap;\n /** Children — Avatars. */\n children: ReactNode;\n}\n\n/**\n * Overlapping avatars with overflow indicator. Pass plain `<Avatar>` children.\n */\nexport const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(function AvatarGroup(\n { max = 4, size = 'md', className, children, ...props },\n ref,\n) {\n const strings = useStrings();\n const items = Children.toArray(children);\n const visible = items.slice(0, max);\n const overflow = items.length - visible.length;\n\n return (\n <div ref={ref} className={cn('flex items-center -space-x-2', className)} {...props}>\n {visible.map((child, i) => (\n <div key={i} className=\"ring-background rounded-full ring-2\">\n {isValidElement<AvatarProps>(child) && child.props.size === undefined\n ? cloneElement(child, { size })\n : child}\n </div>\n ))}\n {overflow > 0 && (\n <div\n className={cn(\n 'bg-background-muted text-foreground-muted ring-background inline-flex items-center justify-center rounded-full ring-2',\n sizeMap[size],\n 'font-medium',\n )}\n aria-label={formatString(strings.avatar.more, { n: overflow })}\n >\n +{overflow}\n </div>\n )}\n </div>\n );\n});\nAvatarGroup.displayName = 'AvatarGroup';\n"],"names":["sizeMap","avatarWrapper","cva","statusColour","Avatar","forwardRef","className","size","src","alt","fallback","status","props","ref","strings","useStrings","jsxs","cn","AvatarPrimitive","jsx","formatString","AvatarGroup","max","children","items","Children","visible","overflow","child","i","isValidElement","cloneElement"],"mappings":";;;;;;;;AAkBA,MAAMA,IAAU;AAAA,EACd,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN,GAGMC,IAAgBC,EAAI,iCAAiC;AAAA,EACzD,UAAU,EAAE,MAAMF,EAAA;AAAA,EAClB,iBAAiB,EAAE,MAAM,KAAA;AAC3B,CAAC,GAgBKG,IAAmE;AAAA,EACvE,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AACX,GAiBaC,IAASC;AAAA,EACpB,SAAgB,EAAE,WAAAC,GAAW,MAAAC,GAAM,KAAAC,GAAK,KAAAC,GAAK,UAAAC,GAAU,QAAAC,GAAQ,GAAGC,EAAA,GAASC,GAAK;AAC9E,UAAMC,IAAUC,EAAA;AAChB,WACE,gBAAAC,EAAC,QAAA,EAAK,WAAWC,EAAGhB,EAAc,EAAE,MAAAM,EAAA,CAAM,GAAGD,CAAS,GACpD,UAAA;AAAA,MAAA,gBAAAU;AAAA,QAACE,EAAgB;AAAA,QAAhB;AAAA,UACC,KAAAL;AAAA,UACA,WAAU;AAAA,UACT,GAAGD;AAAA,UAEH,UAAA;AAAA,YAAAJ,KACC,gBAAAW;AAAA,cAACD,EAAgB;AAAA,cAAhB;AAAA,gBACC,KAAAV;AAAA,gBACA,KAAKC,KAAO;AAAA,gBACZ,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAGd,gBAAAU;AAAA,cAACD,EAAgB;AAAA,cAAhB;AAAA,gBACC,SAASV,IAAM,MAAM;AAAA,gBACrB,WAAU;AAAA,gBAET,UAAAE,KAAY;AAAA,cAAA;AAAA,YAAA;AAAA,UACf;AAAA,QAAA;AAAA,MAAA;AAAA,MAEDC,KACC,gBAAAQ;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAYC,EAAaN,EAAQ,OAAO,QAAQ,EAAE,QAAAH,GAAQ;AAAA,UAC1D,WAAWM;AAAA,YACT;AAAA,YACAd,EAAaQ,CAAM;AAAA,UAAA;AAAA,QACrB;AAAA,MAAA;AAAA,IACF,GAEJ;AAAA,EAEJ;AACF;AACAP,EAAO,cAAc;AAcd,MAAMiB,IAAchB,EAA6C,SACtE,EAAE,KAAAiB,IAAM,GAAG,MAAAf,IAAO,MAAM,WAAAD,GAAW,UAAAiB,GAAU,GAAGX,EAAA,GAChDC,GACA;AACA,QAAMC,IAAUC,EAAA,GACVS,IAAQC,EAAS,QAAQF,CAAQ,GACjCG,IAAUF,EAAM,MAAM,GAAGF,CAAG,GAC5BK,IAAWH,EAAM,SAASE,EAAQ;AAExC,SACE,gBAAAV,EAAC,SAAI,KAAAH,GAAU,WAAWI,EAAG,gCAAgCX,CAAS,GAAI,GAAGM,GAC1E,UAAA;AAAA,IAAAc,EAAQ,IAAI,CAACE,GAAOC,wBAClB,OAAA,EAAY,WAAU,uCACpB,UAAAC,EAA4BF,CAAK,KAAKA,EAAM,MAAM,SAAS,SACxDG,EAAaH,GAAO,EAAE,MAAArB,GAAM,IAC5BqB,EAAA,GAHIC,CAIV,CACD;AAAA,IACAF,IAAW,KACV,gBAAAX;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWC;AAAA,UACT;AAAA,UACAjB,EAAQO,CAAI;AAAA,UACZ;AAAA,QAAA;AAAA,QAEF,cAAYa,EAAaN,EAAQ,OAAO,MAAM,EAAE,GAAGa,GAAU;AAAA,QAC9D,UAAA;AAAA,UAAA;AAAA,UACGA;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EACJ,GAEJ;AAEJ,CAAC;AACDN,EAAY,cAAc;"}
@@ -1,5 +1,5 @@
1
- import { HTMLAttributes } from 'react';
2
- import { VariantProps } from '../../lib/cva';
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ import { VariantProps } from '../../lib/cva.js';
3
3
  declare const badge: (props?: ({
4
4
  variant?: "subtle" | "outline" | null | undefined;
5
5
  tone?: "success" | "warning" | "danger" | "info" | "neutral" | "accent" | null | undefined;
@@ -7,6 +7,8 @@ declare const badge: (props?: ({
7
7
  export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badge> {
8
8
  /** Show a leading status dot in the same tone. */
9
9
  dot?: boolean;
10
+ /** Leading icon (decorative — sized 12px, `aria-hidden`). Overrides `dot`. */
11
+ icon?: ReactNode;
10
12
  }
11
13
  /**
12
14
  * Status pill. Use `subtle` for high-volume contexts (table cells, lists);
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsxs as u, jsx as n } from "react/jsx-runtime";
3
+ import { forwardRef as d } from "react";
4
+ import { cn as a } from "../../lib/utils.js";
5
+ import { cva as g } from "class-variance-authority";
6
+ const b = g(
7
+ [
8
+ "inline-flex items-center gap-1 rounded-full px-2 py-0.5",
9
+ "text-xs font-medium whitespace-nowrap"
10
+ ],
11
+ {
12
+ variants: {
13
+ variant: {
14
+ subtle: "",
15
+ outline: "border bg-transparent"
16
+ },
17
+ tone: {
18
+ neutral: "",
19
+ accent: "",
20
+ success: "",
21
+ warning: "",
22
+ danger: "",
23
+ info: ""
24
+ }
25
+ },
26
+ compoundVariants: [
27
+ // Subtle — coloured fill + matching text.
28
+ { variant: "subtle", tone: "neutral", class: "bg-background-muted text-foreground-muted" },
29
+ { variant: "subtle", tone: "accent", class: "bg-accent-soft text-on-accent-soft" },
30
+ { variant: "subtle", tone: "success", class: "bg-success-soft text-success-text" },
31
+ { variant: "subtle", tone: "warning", class: "bg-warning-soft text-warning-text" },
32
+ { variant: "subtle", tone: "danger", class: "bg-danger-soft text-danger-text" },
33
+ { variant: "subtle", tone: "info", class: "bg-info-soft text-info-text" },
34
+ // Outline — coloured border + text, transparent fill.
35
+ { variant: "outline", tone: "neutral", class: "border-border text-foreground-muted" },
36
+ { variant: "outline", tone: "accent", class: "border-accent text-accent" },
37
+ {
38
+ variant: "outline",
39
+ tone: "success",
40
+ class: "border-success-border-soft text-success-text"
41
+ },
42
+ {
43
+ variant: "outline",
44
+ tone: "warning",
45
+ class: "border-warning-border-soft text-warning-text"
46
+ },
47
+ { variant: "outline", tone: "danger", class: "border-danger-border-soft text-danger-text" },
48
+ { variant: "outline", tone: "info", class: "border-info-border-soft text-info-text" }
49
+ ],
50
+ defaultVariants: { variant: "subtle", tone: "neutral" }
51
+ }
52
+ ), f = {
53
+ neutral: "bg-foreground-subtle",
54
+ accent: "bg-accent",
55
+ success: "bg-success",
56
+ warning: "bg-warning",
57
+ danger: "bg-danger",
58
+ info: "bg-info"
59
+ }, x = d(function({ className: r, variant: s, tone: t = "neutral", dot: o, icon: e, children: i, ...c }, l) {
60
+ return /* @__PURE__ */ u("span", { ref: l, className: a(b({ variant: s, tone: t }), r), ...c, children: [
61
+ e && /* @__PURE__ */ n("span", { "aria-hidden": !0, className: "inline-flex shrink-0 [&_svg]:size-3", children: e }),
62
+ o && !e && /* @__PURE__ */ n(
63
+ "span",
64
+ {
65
+ "aria-hidden": !0,
66
+ className: a("inline-block size-1.5 rounded-full", f[t ?? "neutral"])
67
+ }
68
+ ),
69
+ i
70
+ ] });
71
+ });
72
+ x.displayName = "Badge";
73
+ export {
74
+ x as Badge
75
+ };
76
+ //# sourceMappingURL=Badge.js.map