@hoardodile/ui 0.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 (356) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +70 -0
  3. package/dist/components/alert.d.ts +13 -0
  4. package/dist/components/alert.js +97 -0
  5. package/dist/components/alert.js.map +1 -0
  6. package/dist/components/app-dialog.d.ts +62 -0
  7. package/dist/components/app-dialog.js +656 -0
  8. package/dist/components/app-dialog.js.map +1 -0
  9. package/dist/components/badge.d.ts +11 -0
  10. package/dist/components/badge.js +70 -0
  11. package/dist/components/badge.js.map +1 -0
  12. package/dist/components/breadcrumb.d.ts +12 -0
  13. package/dist/components/breadcrumb.js +617 -0
  14. package/dist/components/breadcrumb.js.map +1 -0
  15. package/dist/components/button.d.ts +17 -0
  16. package/dist/components/button.js +78 -0
  17. package/dist/components/button.js.map +1 -0
  18. package/dist/components/caption-bar.d.ts +44 -0
  19. package/dist/components/caption-bar.js +827 -0
  20. package/dist/components/caption-bar.js.map +1 -0
  21. package/dist/components/chart-card.d.ts +15 -0
  22. package/dist/components/chart-card.js +27 -0
  23. package/dist/components/chart-card.js.map +1 -0
  24. package/dist/components/checkbox.d.ts +6 -0
  25. package/dist/components/checkbox.js +76 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip-button.d.ts +28 -0
  28. package/dist/components/chip-button.js +87 -0
  29. package/dist/components/chip-button.js.map +1 -0
  30. package/dist/components/close-confirm-dialog.d.ts +19 -0
  31. package/dist/components/close-confirm-dialog.js +743 -0
  32. package/dist/components/close-confirm-dialog.js.map +1 -0
  33. package/dist/components/color-picker.d.ts +32 -0
  34. package/dist/components/color-picker.js +667 -0
  35. package/dist/components/color-picker.js.map +1 -0
  36. package/dist/components/combobox.d.ts +32 -0
  37. package/dist/components/combobox.js +1211 -0
  38. package/dist/components/combobox.js.map +1 -0
  39. package/dist/components/confirm-by-typing-dialog.d.ts +48 -0
  40. package/dist/components/confirm-by-typing-dialog.js +759 -0
  41. package/dist/components/confirm-by-typing-dialog.js.map +1 -0
  42. package/dist/components/confirm-dialog.d.ts +34 -0
  43. package/dist/components/confirm-dialog.js +710 -0
  44. package/dist/components/confirm-dialog.js.map +1 -0
  45. package/dist/components/count-badge.d.ts +13 -0
  46. package/dist/components/count-badge.js +39 -0
  47. package/dist/components/count-badge.js.map +1 -0
  48. package/dist/components/dialog.d.ts +40 -0
  49. package/dist/components/dialog.js +571 -0
  50. package/dist/components/dialog.js.map +1 -0
  51. package/dist/components/dropdown-menu.d.ts +45 -0
  52. package/dist/components/dropdown-menu.js +1003 -0
  53. package/dist/components/dropdown-menu.js.map +1 -0
  54. package/dist/components/dropdown-select.d.ts +29 -0
  55. package/dist/components/dropdown-select.js +1036 -0
  56. package/dist/components/dropdown-select.js.map +1 -0
  57. package/dist/components/empty.d.ts +15 -0
  58. package/dist/components/empty.js +119 -0
  59. package/dist/components/empty.js.map +1 -0
  60. package/dist/components/filter-rail.d.ts +48 -0
  61. package/dist/components/filter-rail.js +231 -0
  62. package/dist/components/filter-rail.js.map +1 -0
  63. package/dist/components/fixed-action-bar.d.ts +24 -0
  64. package/dist/components/fixed-action-bar.js +48 -0
  65. package/dist/components/fixed-action-bar.js.map +1 -0
  66. package/dist/components/font-picker.d.ts +45 -0
  67. package/dist/components/font-picker.js +759 -0
  68. package/dist/components/font-picker.js.map +1 -0
  69. package/dist/components/form.d.ts +17 -0
  70. package/dist/components/form.js +150 -0
  71. package/dist/components/form.js.map +1 -0
  72. package/dist/components/group-label.d.ts +12 -0
  73. package/dist/components/group-label.js +10 -0
  74. package/dist/components/group-label.js.map +1 -0
  75. package/dist/components/icon-tile.d.ts +16 -0
  76. package/dist/components/icon-tile.js +67 -0
  77. package/dist/components/icon-tile.js.map +1 -0
  78. package/dist/components/icon-toggle.d.ts +24 -0
  79. package/dist/components/icon-toggle.js +81 -0
  80. package/dist/components/icon-toggle.js.map +1 -0
  81. package/dist/components/icon.d.ts +2 -0
  82. package/dist/components/icon.js +49 -0
  83. package/dist/components/icon.js.map +1 -0
  84. package/dist/components/image-crop-panel.d.ts +80 -0
  85. package/dist/components/image-crop-panel.js +1104 -0
  86. package/dist/components/image-crop-panel.js.map +1 -0
  87. package/dist/components/image-cropper.d.ts +63 -0
  88. package/dist/components/image-cropper.js +166 -0
  89. package/dist/components/image-cropper.js.map +1 -0
  90. package/dist/components/input-group.d.ts +22 -0
  91. package/dist/components/input-group.js +270 -0
  92. package/dist/components/input-group.js.map +1 -0
  93. package/dist/components/input.d.ts +16 -0
  94. package/dist/components/input.js +60 -0
  95. package/dist/components/input.js.map +1 -0
  96. package/dist/components/label.d.ts +5 -0
  97. package/dist/components/label.js +40 -0
  98. package/dist/components/label.js.map +1 -0
  99. package/dist/components/list-empty-row.d.ts +16 -0
  100. package/dist/components/list-empty-row.js +40 -0
  101. package/dist/components/list-empty-row.js.map +1 -0
  102. package/dist/components/management-empty.d.ts +14 -0
  103. package/dist/components/management-empty.js +40 -0
  104. package/dist/components/management-empty.js.map +1 -0
  105. package/dist/components/management-skeleton.d.ts +11 -0
  106. package/dist/components/management-skeleton.js +72 -0
  107. package/dist/components/management-skeleton.js.map +1 -0
  108. package/dist/components/meta-chip.d.ts +17 -0
  109. package/dist/components/meta-chip.js +49 -0
  110. package/dist/components/meta-chip.js.map +1 -0
  111. package/dist/components/mobile-drawer.d.ts +29 -0
  112. package/dist/components/mobile-drawer.js +331 -0
  113. package/dist/components/mobile-drawer.js.map +1 -0
  114. package/dist/components/page-header.d.ts +20 -0
  115. package/dist/components/page-header.js +44 -0
  116. package/dist/components/page-header.js.map +1 -0
  117. package/dist/components/page-scaffold.d.ts +34 -0
  118. package/dist/components/page-scaffold.js +157 -0
  119. package/dist/components/page-scaffold.js.map +1 -0
  120. package/dist/components/pagination-bar.d.ts +22 -0
  121. package/dist/components/pagination-bar.js +703 -0
  122. package/dist/components/pagination-bar.js.map +1 -0
  123. package/dist/components/pagination.d.ts +22 -0
  124. package/dist/components/pagination.js +680 -0
  125. package/dist/components/pagination.js.map +1 -0
  126. package/dist/components/panel-toolbar.d.ts +37 -0
  127. package/dist/components/panel-toolbar.js +781 -0
  128. package/dist/components/panel-toolbar.js.map +1 -0
  129. package/dist/components/pill-tabs.d.ts +33 -0
  130. package/dist/components/pill-tabs.js +66 -0
  131. package/dist/components/pill-tabs.js.map +1 -0
  132. package/dist/components/plugin-download-consent.d.ts +52 -0
  133. package/dist/components/plugin-download-consent.js +780 -0
  134. package/dist/components/plugin-download-consent.js.map +1 -0
  135. package/dist/components/popover.d.ts +26 -0
  136. package/dist/components/popover.js +343 -0
  137. package/dist/components/popover.js.map +1 -0
  138. package/dist/components/progress.d.ts +10 -0
  139. package/dist/components/progress.js +103 -0
  140. package/dist/components/progress.js.map +1 -0
  141. package/dist/components/query-state-view.d.ts +36 -0
  142. package/dist/components/query-state-view.js +17 -0
  143. package/dist/components/query-state-view.js.map +1 -0
  144. package/dist/components/radio-group.d.ts +8 -0
  145. package/dist/components/radio-group.js +60 -0
  146. package/dist/components/radio-group.js.map +1 -0
  147. package/dist/components/scroll-area.d.ts +7 -0
  148. package/dist/components/scroll-area.js +80 -0
  149. package/dist/components/scroll-area.js.map +1 -0
  150. package/dist/components/search-field.d.ts +49 -0
  151. package/dist/components/search-field.js +586 -0
  152. package/dist/components/search-field.js.map +1 -0
  153. package/dist/components/secondary-stat.d.ts +13 -0
  154. package/dist/components/secondary-stat.js +56 -0
  155. package/dist/components/secondary-stat.js.map +1 -0
  156. package/dist/components/section-header.d.ts +23 -0
  157. package/dist/components/section-header.js +76 -0
  158. package/dist/components/section-header.js.map +1 -0
  159. package/dist/components/section-label.d.ts +20 -0
  160. package/dist/components/section-label.js +46 -0
  161. package/dist/components/section-label.js.map +1 -0
  162. package/dist/components/section-tabs.d.ts +47 -0
  163. package/dist/components/section-tabs.js +145 -0
  164. package/dist/components/section-tabs.js.map +1 -0
  165. package/dist/components/separator.d.ts +14 -0
  166. package/dist/components/separator.js +49 -0
  167. package/dist/components/separator.js.map +1 -0
  168. package/dist/components/sheet.d.ts +22 -0
  169. package/dist/components/sheet.js +469 -0
  170. package/dist/components/sheet.js.map +1 -0
  171. package/dist/components/sidebar.d.ts +69 -0
  172. package/dist/components/sidebar.js +1645 -0
  173. package/dist/components/sidebar.js.map +1 -0
  174. package/dist/components/skeleton.d.ts +8 -0
  175. package/dist/components/skeleton.js +41 -0
  176. package/dist/components/skeleton.js.map +1 -0
  177. package/dist/components/slider.d.ts +6 -0
  178. package/dist/components/slider.js +75 -0
  179. package/dist/components/slider.js.map +1 -0
  180. package/dist/components/sortable-chip-list.d.ts +26 -0
  181. package/dist/components/sortable-chip-list.js +59 -0
  182. package/dist/components/sortable-chip-list.js.map +1 -0
  183. package/dist/components/spinner.d.ts +5 -0
  184. package/dist/components/spinner.js +65 -0
  185. package/dist/components/spinner.js.map +1 -0
  186. package/dist/components/surface.d.ts +13 -0
  187. package/dist/components/surface.js +51 -0
  188. package/dist/components/surface.js.map +1 -0
  189. package/dist/components/switch.d.ts +8 -0
  190. package/dist/components/switch.js +53 -0
  191. package/dist/components/switch.js.map +1 -0
  192. package/dist/components/table.d.ts +12 -0
  193. package/dist/components/table.js +129 -0
  194. package/dist/components/table.js.map +1 -0
  195. package/dist/components/textarea.d.ts +15 -0
  196. package/dist/components/textarea.js +58 -0
  197. package/dist/components/textarea.js.map +1 -0
  198. package/dist/components/toast.d.ts +19 -0
  199. package/dist/components/toast.js +805 -0
  200. package/dist/components/toast.js.map +1 -0
  201. package/dist/components/toggle.d.ts +12 -0
  202. package/dist/components/toggle.js +63 -0
  203. package/dist/components/toggle.js.map +1 -0
  204. package/dist/components/tooltip.d.ts +9 -0
  205. package/dist/components/tooltip.js +82 -0
  206. package/dist/components/tooltip.js.map +1 -0
  207. package/dist/hooks/use-dialog-footer-actions.d.ts +18 -0
  208. package/dist/hooks/use-dialog-footer-actions.js +11 -0
  209. package/dist/hooks/use-dialog-footer-actions.js.map +1 -0
  210. package/dist/hooks/use-form-field.d.ts +33 -0
  211. package/dist/hooks/use-form-field.js +36 -0
  212. package/dist/hooks/use-form-field.js.map +1 -0
  213. package/dist/hooks/use-mobile.d.ts +34 -0
  214. package/dist/hooks/use-mobile.js +29 -0
  215. package/dist/hooks/use-mobile.js.map +1 -0
  216. package/dist/hooks/use-pinch-pan.d.ts +56 -0
  217. package/dist/hooks/use-pinch-pan.js +223 -0
  218. package/dist/hooks/use-pinch-pan.js.map +1 -0
  219. package/dist/hooks/useMobileBackToClose.d.ts +61 -0
  220. package/dist/hooks/useMobileBackToClose.js +234 -0
  221. package/dist/hooks/useMobileBackToClose.js.map +1 -0
  222. package/dist/icon-BOgL5bpL.d.ts +85 -0
  223. package/dist/icons/actions.d.ts +4 -0
  224. package/dist/icons/actions.js +520 -0
  225. package/dist/icons/actions.js.map +1 -0
  226. package/dist/icons/icon-style.d.ts +2 -0
  227. package/dist/icons/icon-style.js +51 -0
  228. package/dist/icons/icon-style.js.map +1 -0
  229. package/dist/icons/marks.d.ts +15 -0
  230. package/dist/icons/marks.js +48 -0
  231. package/dist/icons/marks.js.map +1 -0
  232. package/dist/icons/registry.d.ts +177 -0
  233. package/dist/icons/registry.js +1180 -0
  234. package/dist/icons/registry.js.map +1 -0
  235. package/dist/index.d.ts +6 -0
  236. package/dist/index.js +653 -0
  237. package/dist/index.js.map +1 -0
  238. package/dist/lib/merge-config.d.ts +15 -0
  239. package/dist/lib/merge-config.js +20 -0
  240. package/dist/lib/merge-config.js.map +1 -0
  241. package/dist/lib/pagination.d.ts +19 -0
  242. package/dist/lib/pagination.js +26 -0
  243. package/dist/lib/pagination.js.map +1 -0
  244. package/dist/lib/utils.d.ts +5 -0
  245. package/dist/lib/utils.js +26 -0
  246. package/dist/lib/utils.js.map +1 -0
  247. package/dist/styles/theme.css +480 -0
  248. package/dist/styles/theme.css.map +1 -0
  249. package/dist/styles/theme.d.ts +2 -0
  250. package/dist/viewport.d.ts +64 -0
  251. package/dist/viewport.js +13 -0
  252. package/dist/viewport.js.map +1 -0
  253. package/package.json +122 -0
  254. package/src/components/alert.tsx +76 -0
  255. package/src/components/app-dialog.tsx +187 -0
  256. package/src/components/badge.tsx +52 -0
  257. package/src/components/breadcrumb.tsx +129 -0
  258. package/src/components/button.tsx +70 -0
  259. package/src/components/caption-bar.test.tsx +151 -0
  260. package/src/components/caption-bar.tsx +392 -0
  261. package/src/components/chart-card.tsx +31 -0
  262. package/src/components/checkbox.tsx +28 -0
  263. package/src/components/chip-button.tsx +69 -0
  264. package/src/components/close-confirm-dialog.tsx +99 -0
  265. package/src/components/color-picker.tsx +186 -0
  266. package/src/components/combobox.tsx +335 -0
  267. package/src/components/confirm-by-typing-dialog.tsx +130 -0
  268. package/src/components/confirm-dialog.test.tsx +39 -0
  269. package/src/components/confirm-dialog.tsx +98 -0
  270. package/src/components/count-badge.tsx +23 -0
  271. package/src/components/dialog.tsx +315 -0
  272. package/src/components/dropdown-menu.tsx +311 -0
  273. package/src/components/dropdown-select.tsx +211 -0
  274. package/src/components/empty.tsx +104 -0
  275. package/src/components/filter-rail.tsx +139 -0
  276. package/src/components/fixed-action-bar.tsx +46 -0
  277. package/src/components/font-picker.tsx +257 -0
  278. package/src/components/form.tsx +127 -0
  279. package/src/components/group-label.tsx +13 -0
  280. package/src/components/icon-tile.tsx +35 -0
  281. package/src/components/icon-toggle.tsx +57 -0
  282. package/src/components/icon.tsx +66 -0
  283. package/src/components/image-crop-panel.test.tsx +167 -0
  284. package/src/components/image-crop-panel.tsx +465 -0
  285. package/src/components/image-cropper.test.tsx +144 -0
  286. package/src/components/image-cropper.tsx +265 -0
  287. package/src/components/input-group.tsx +156 -0
  288. package/src/components/input.tsx +45 -0
  289. package/src/components/label.tsx +20 -0
  290. package/src/components/list-empty-row.tsx +27 -0
  291. package/src/components/management-empty.tsx +25 -0
  292. package/src/components/management-skeleton.tsx +23 -0
  293. package/src/components/meta-chip.tsx +40 -0
  294. package/src/components/mobile-drawer.test.tsx +52 -0
  295. package/src/components/mobile-drawer.tsx +98 -0
  296. package/src/components/page-header.tsx +47 -0
  297. package/src/components/page-scaffold.test.tsx +24 -0
  298. package/src/components/page-scaffold.tsx +131 -0
  299. package/src/components/pagination-bar.test.tsx +87 -0
  300. package/src/components/pagination-bar.tsx +139 -0
  301. package/src/components/pagination.tsx +139 -0
  302. package/src/components/panel-toolbar.tsx +99 -0
  303. package/src/components/pill-tabs.test.tsx +84 -0
  304. package/src/components/pill-tabs.tsx +77 -0
  305. package/src/components/plugin-download-consent.test.tsx +90 -0
  306. package/src/components/plugin-download-consent.tsx +183 -0
  307. package/src/components/popover.tsx +133 -0
  308. package/src/components/progress.tsx +88 -0
  309. package/src/components/query-state-view.tsx +47 -0
  310. package/src/components/radio-group.tsx +36 -0
  311. package/src/components/scroll-area.tsx +52 -0
  312. package/src/components/search-field.test.tsx +86 -0
  313. package/src/components/search-field.tsx +119 -0
  314. package/src/components/secondary-stat.tsx +24 -0
  315. package/src/components/section-header.tsx +54 -0
  316. package/src/components/section-label.tsx +40 -0
  317. package/src/components/section-tabs.test.tsx +154 -0
  318. package/src/components/section-tabs.tsx +190 -0
  319. package/src/components/separator.tsx +38 -0
  320. package/src/components/sheet.tsx +170 -0
  321. package/src/components/sidebar.tsx +725 -0
  322. package/src/components/skeleton.tsx +18 -0
  323. package/src/components/slider.tsx +52 -0
  324. package/src/components/sortable-chip-list.tsx +56 -0
  325. package/src/components/spinner.tsx +35 -0
  326. package/src/components/surface.tsx +37 -0
  327. package/src/components/switch.tsx +30 -0
  328. package/src/components/table.tsx +116 -0
  329. package/src/components/textarea.tsx +41 -0
  330. package/src/components/toast.tsx +239 -0
  331. package/src/components/toggle.tsx +45 -0
  332. package/src/components/tooltip.tsx +64 -0
  333. package/src/hooks/use-dialog-footer-actions.ts +23 -0
  334. package/src/hooks/use-form-field.ts +62 -0
  335. package/src/hooks/use-mobile.ts +54 -0
  336. package/src/hooks/use-pinch-pan.test.tsx +42 -0
  337. package/src/hooks/use-pinch-pan.ts +340 -0
  338. package/src/hooks/useMobileBackToClose.test.tsx +140 -0
  339. package/src/hooks/useMobileBackToClose.ts +508 -0
  340. package/src/icons/actions.ts +10 -0
  341. package/src/icons/icon-style.test.tsx +69 -0
  342. package/src/icons/icon-style.ts +104 -0
  343. package/src/icons/marks.tsx +72 -0
  344. package/src/icons/registry.test.ts +65 -0
  345. package/src/icons/registry.ts +1163 -0
  346. package/src/index.ts +22 -0
  347. package/src/lib/merge-config.test.ts +109 -0
  348. package/src/lib/merge-config.ts +22 -0
  349. package/src/lib/pagination.test.ts +55 -0
  350. package/src/lib/pagination.ts +42 -0
  351. package/src/lib/utils.ts +6 -0
  352. package/src/styles/scan-contract.test.ts +74 -0
  353. package/src/styles/theme.css +798 -0
  354. package/src/test/i18n.tsx +22 -0
  355. package/src/test/setup.ts +104 -0
  356. package/src/viewport.ts +69 -0
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @hoardodile/ui — the design-system component library for hoardodile
3
+ * (the host app and plugin iframes alike). Components are importable
4
+ * per-path (`@hoardodile/ui/components/button`,
5
+ * `@hoardodile/ui/components/app-dialog`, `@hoardodile/ui/icons/registry`,
6
+ * ...) so bundles stay small; hooks live under
7
+ * `@hoardodile/ui/hooks/*` and shared CSS variables in
8
+ * `@hoardodile/ui/theme.css`. The root entry re-exports only app-owned
9
+ * pieces that don't fit the subpath layout.
10
+ *
11
+ * Theme classes: `@hoardodile/ui/theme.css` defines the `.light` /
12
+ * `.dark` variables and per-palette `.theme-<id>` blocks the host app
13
+ * and plugin iframes both consume.
14
+ */
15
+
16
+ export type { AppDialogProps } from "./components/app-dialog.tsx"
17
+ export { AppDialog } from "./components/app-dialog.tsx"
18
+ export {
19
+ setNavigationResolver,
20
+ useMobileBackToClose,
21
+ } from "./hooks/useMobileBackToClose.ts"
22
+ export { cn } from "./lib/utils.ts"
@@ -0,0 +1,109 @@
1
+ // @vitest-environment node
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { mergeTheme } from "./merge-config"
5
+ import { cn } from "./utils"
6
+
7
+ // merge-config.ts registers the app's custom @theme tokens (theme.css) with
8
+ // tailwind-merge, so custom utilities get correct conflict semantics — e.g.
9
+ // `text-ui` must behave as a font size, not as a text color. Two pins:
10
+ // 1. Token contract: every @theme token in a covered namespace is
11
+ // registered. Add a token, go red until registered — no silent drift.
12
+ // 2. Behavior: the merges that previously misbehaved, pinned.
13
+ // Vitest runs with the package root as cwd (see scan-contract.test.ts).
14
+ const THEME_CSS = join(process.cwd(), "src", "styles", "theme.css")
15
+
16
+ // Namespaces whose tokens the merge config owns; keys mirror the v4
17
+ // `--<namespace>-*` prefixes one-to-one.
18
+ const NAMESPACES = [
19
+ "text",
20
+ "spacing",
21
+ "tracking",
22
+ "shadow",
23
+ "container",
24
+ "font",
25
+ "ease",
26
+ "animate",
27
+ ] as const
28
+
29
+ // Names already covered by tailwind-merge's default config under the same
30
+ // token name — registered there, not here.
31
+ const BUILTIN: Partial<Record<(typeof NAMESPACES)[number], readonly string[]>> =
32
+ {
33
+ font: ["sans"],
34
+ ease: ["in", "out"],
35
+ }
36
+
37
+ function themeTokens(namespace: string): string[] {
38
+ const css = readFileSync(THEME_CSS, "utf8")
39
+ const open = css.indexOf("{", css.indexOf("@theme"))
40
+ let depth = 0
41
+ let end = css.length
42
+ for (let i = open; i < css.length; i++) {
43
+ if (css[i] === "{") depth++
44
+ else if (css[i] === "}") {
45
+ depth--
46
+ if (depth === 0) {
47
+ end = i
48
+ break
49
+ }
50
+ }
51
+ }
52
+ const tokens = new Set<string>()
53
+ for (const match of css.slice(open, end).matchAll(/--([\w-]+)\s*:/g)) {
54
+ // `--text-ui--line-height` is the line-height companion of `text-ui`,
55
+ // not a separate utility — resolve it to its base token.
56
+ const name = match[1]!.replace(/--line-height$/, "")
57
+ if (name.startsWith(`${namespace}-`)) {
58
+ tokens.add(name.slice(namespace.length + 1))
59
+ }
60
+ }
61
+ return [...tokens]
62
+ }
63
+
64
+ describe("tailwind-merge theme config", () => {
65
+ it("registers every custom @theme token", () => {
66
+ for (const namespace of NAMESPACES) {
67
+ const covered = mergeTheme[namespace] as readonly string[]
68
+ const builtin = BUILTIN[namespace] ?? []
69
+ const unregistered = themeTokens(namespace).filter(
70
+ (token) => !covered.includes(token) && !builtin.includes(token),
71
+ )
72
+ expect(
73
+ unregistered,
74
+ `--${namespace}-* tokens missing from mergeTheme.${namespace}`,
75
+ ).toEqual([])
76
+ }
77
+ })
78
+
79
+ it("merges custom font sizes as sizes, not colors", () => {
80
+ expect(cn("text-ui text-foreground")).toBe("text-ui text-foreground")
81
+ expect(cn("text-tiny text-muted-foreground")).toBe(
82
+ "text-tiny text-muted-foreground",
83
+ )
84
+ expect(cn("text-doc-title text-primary")).toBe(
85
+ "text-doc-title text-primary",
86
+ )
87
+ })
88
+
89
+ it("keeps custom font sizes mutually exclusive with the builtin scale", () => {
90
+ expect(cn("text-ui text-sm")).toBe("text-sm")
91
+ expect(cn("text-sm text-ui")).toBe("text-ui")
92
+ })
93
+
94
+ it("merges the other custom token families with correct conflicts", () => {
95
+ expect(cn("h-control h-10")).toBe("h-10")
96
+ expect(cn("max-w-content max-w-md")).toBe("max-w-md")
97
+ expect(cn("tracking-label tracking-wide")).toBe("tracking-wide")
98
+ expect(cn("shadow-card shadow-sm")).toBe("shadow-sm")
99
+ expect(cn("font-doc font-sans")).toBe("font-sans")
100
+ expect(cn("ease-standard ease-out")).toBe("ease-out")
101
+ expect(cn("animate-skel animate-pop")).toBe("animate-pop")
102
+ })
103
+
104
+ it("keeps custom tokens from different groups side by side", () => {
105
+ expect(cn("text-ui leading-[1.75]")).toBe("text-ui leading-[1.75]")
106
+ expect(cn("h-nav min-h-nav")).toBe("h-nav min-h-nav")
107
+ expect(cn("font-doc text-doc-title")).toBe("font-doc text-doc-title")
108
+ })
109
+ })
@@ -0,0 +1,22 @@
1
+ import { extendTailwindMerge } from "tailwind-merge"
2
+
3
+ // The app's `@theme inline` tokens (theme.css) generate utility names
4
+ // tailwind-merge doesn't know, so it misclassifies them — e.g. `text-ui` is
5
+ // parsed as a text color and dropped next to any `text-*` color. The default
6
+ // class groups are theme-backed, so extending the theme (keys mirror the v4
7
+ // token namespaces one-to-one) registers every custom utility with correct
8
+ // conflict semantics. merge-config.test.ts pins this list against theme.css.
9
+ export const mergeTheme = {
10
+ text: ["tiny", "ui", "doc", "doc-title", "doc-heading", "quote"],
11
+ spacing: ["chip", "control", "nav", "sidebar", "panel"],
12
+ tracking: ["label"],
13
+ shadow: ["card", "dialog"],
14
+ container: ["reading", "medium", "content"],
15
+ font: ["doc", "heading"],
16
+ ease: ["standard"],
17
+ animate: ["skel", "pop"],
18
+ } as const
19
+
20
+ export const twMerge = extendTailwindMerge({
21
+ extend: { theme: mergeTheme },
22
+ })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest"
6
+ import { pageCountOf, paginationWindow } from "./pagination"
7
+
8
+ describe("pageCountOf", () => {
9
+ it("returns 1 when there are no rows so the UI never shows '1 / 0'", () => {
10
+ expect(pageCountOf(0, 20)).toBe(1)
11
+ })
12
+
13
+ it("returns 1 when total fits exactly in one page", () => {
14
+ expect(pageCountOf(20, 20)).toBe(1)
15
+ })
16
+
17
+ it("rounds up to cover the partial trailing page", () => {
18
+ expect(pageCountOf(21, 20)).toBe(2)
19
+ expect(pageCountOf(99, 10)).toBe(10)
20
+ })
21
+
22
+ it("returns 1 for negative or NaN input rather than 0 or NaN", () => {
23
+ expect(pageCountOf(-5, 20)).toBe(1)
24
+ })
25
+ })
26
+
27
+ describe("paginationWindow", () => {
28
+ it("shows first, last, the asymmetric window and ellipses for a middle page", () => {
29
+ expect(paginationWindow(6, 68)).toEqual([1, "…", 5, 6, 7, 8, "…", 68])
30
+ })
31
+
32
+ it("renders every page when the range fits without gaps", () => {
33
+ expect(paginationWindow(3, 5)).toEqual([1, 2, 3, 4, 5])
34
+ expect(paginationWindow(3, 6)).toEqual([1, 2, 3, 4, 5, 6])
35
+ expect(paginationWindow(4, 7)).toEqual([1, "…", 3, 4, 5, 6, 7])
36
+ expect(paginationWindow(4, 8)).toEqual([1, "…", 3, 4, 5, 6, "…", 8])
37
+ })
38
+
39
+ it("drops the leading ellipse when the window touches the first page", () => {
40
+ expect(paginationWindow(1, 68)).toEqual([1, 2, 3, "…", 68])
41
+ expect(paginationWindow(3, 68)).toEqual([1, 2, 3, 4, 5, "…", 68])
42
+ })
43
+
44
+ it("drops the trailing ellipse when the window touches the last page", () => {
45
+ expect(paginationWindow(68, 68)).toEqual([1, "…", 67, 68])
46
+ expect(paginationWindow(67, 68)).toEqual([1, "…", 66, 67, 68])
47
+ expect(paginationWindow(66, 68)).toEqual([1, "…", 65, 66, 67, 68])
48
+ })
49
+
50
+ it("stays within bounds for tiny ranges", () => {
51
+ expect(paginationWindow(1, 1)).toEqual([1])
52
+ expect(paginationWindow(1, 2)).toEqual([1, 2])
53
+ expect(paginationWindow(2, 3)).toEqual([1, 2, 3])
54
+ })
55
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Compute the number of pages for a paginated list given a total row count
3
+ * and the page size. Always at least 1 so the UI never renders "1 / 0".
4
+ */
5
+ export function pageCountOf(total: number, pageSize: number): number {
6
+ return Math.max(1, Math.ceil(total / pageSize))
7
+ }
8
+
9
+ /**
10
+ * The page numbers a windowed pager renders: the first and last pages are
11
+ * always shown, with an asymmetric window around the active one (more
12
+ * pages trailing than leading) and an ellipsis wherever the gap between
13
+ * two segments is wider than a single page (adjacent numbers render
14
+ * directly, no ellipsis).
15
+ *
16
+ * @example
17
+ * paginationWindow(6, 68) // [1, "…", 5, 6, 7, 8, "…", 68]
18
+ * paginationWindow(1, 5) // [1, 2, 3, 4, 5]
19
+ */
20
+ export function paginationWindow(
21
+ page: number,
22
+ pageCount: number,
23
+ before = 1,
24
+ after = 2,
25
+ ): readonly (number | "…")[] {
26
+ const windowStart = Math.max(2, page - before)
27
+ const windowEnd = Math.min(pageCount - 1, page + after)
28
+ const items: (number | "…")[] = [1]
29
+ if (windowStart > 2) {
30
+ items.push("…")
31
+ }
32
+ for (let p = windowStart; p <= windowEnd; p++) {
33
+ items.push(p)
34
+ }
35
+ if (windowEnd < pageCount - 1) {
36
+ items.push("…")
37
+ }
38
+ if (pageCount > 1) {
39
+ items.push(pageCount)
40
+ }
41
+ return items
42
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "./merge-config"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,74 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { compile } from "@tailwindcss/node"
4
+ import { Scanner } from "@tailwindcss/oxide"
5
+
6
+ // Consumers import `@hoardodile/ui/theme.css`, which resolves to the dist
7
+ // copy. Its `@source` glob is relative to that file, so it only sees what
8
+ // tsup ships — this test compiles the built stylesheet exactly like a
9
+ // consumer's Tailwind would, pinning the published artifact against the
10
+ // components it must style. It goes red if the glob stops matching the
11
+ // tsup outputs (wrong extension, moved dist layout, minified output, ...).
12
+ // Vitest runs with the package root as cwd; `import.meta.url` is not a
13
+ // file URL under its module runner, so resolve from cwd instead.
14
+ const DIST_STYLES = join(process.cwd(), "dist", "styles")
15
+
16
+ const REPRESENTATIVE_CANDIDATES = [
17
+ // dialog surface + exit animation
18
+ "bg-popover",
19
+ "text-popover-foreground",
20
+ "sm:data-starting-style:scale-95",
21
+ // button variants, icon slots, dark input
22
+ "hover:bg-primary/80",
23
+ "has-data-[icon=inline-end]:pr-2",
24
+ "dark:bg-input/30",
25
+ // button-group slot override
26
+ "in-data-[slot=button-group]:rounded-md",
27
+ // popover anchor animation
28
+ "data-[side=bottom]:slide-in-from-top-2",
29
+ ] as const
30
+
31
+ describe("theme.css scan contract", () => {
32
+ it("scans the tsup outputs and generates the component utilities", async () => {
33
+ const themeCss = readFileSync(join(DIST_STYLES, "theme.css"), "utf8")
34
+ // `tw-animate-css` / `shadcn/tailwind.css` are package-name imports
35
+ // resolved by the consumer's Tailwind toolchain; compile() resolves
36
+ // only relative imports, so strip them. They don't affect the scan
37
+ // contract under test.
38
+ const entry = themeCss.replace(
39
+ /@import "tw-animate-css";\n@import "shadcn\/tailwind.css";\n/g,
40
+ "",
41
+ )
42
+
43
+ const compiled = await compile(entry, {
44
+ base: DIST_STYLES,
45
+ onDependency: () => {},
46
+ })
47
+ expect(compiled.sources.length).toBeGreaterThan(0)
48
+
49
+ const candidates = new Set(
50
+ new Scanner({ sources: compiled.sources }).scan(),
51
+ )
52
+ for (const candidate of REPRESENTATIVE_CANDIDATES) {
53
+ expect([...candidates]).toContain(candidate)
54
+ }
55
+
56
+ const css = compiled.build([...candidates])
57
+ // theme.css itself went through (palettes survive the entry surgery)
58
+ expect(css).toContain(".theme-parchment")
59
+ // build() pretty-prints (`{` on its own line), so assert on the
60
+ // selector text and one rule body rather than brace positions
61
+ for (const fragment of [
62
+ ".bg-popover",
63
+ "background-color: var(--popover);",
64
+ ".text-popover-foreground",
65
+ "data-starting-style\\:scale-95",
66
+ "has-data-\\[icon",
67
+ "in-data-\\[slot\\=button-group\\]",
68
+ "hover\\:bg-primary\\/80",
69
+ ".dark\\:bg-input\\/30",
70
+ ]) {
71
+ expect(css).toContain(fragment)
72
+ }
73
+ })
74
+ })