@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
@@ -0,0 +1,69 @@
1
+ import { BoltIcon as BoltBoldWeight } from "@solar-icons/react/bold/bolt"
2
+ import { BoltIcon as BoltBoldDuotone } from "@solar-icons/react/bold-duotone/bolt"
3
+ import { BoltIcon as BoltLinear } from "@solar-icons/react/linear/bolt"
4
+ import { act, render, waitFor } from "@testing-library/react"
5
+ import { beforeEach, describe, expect, it } from "vitest"
6
+ import { createIcon } from "./icon-style"
7
+
8
+ const Bolt = createIcon({
9
+ bold: BoltBoldWeight,
10
+ boldDuotone: BoltBoldDuotone,
11
+ linear: BoltLinear,
12
+ })
13
+
14
+ function svgClass(container: HTMLElement): string | null {
15
+ return container.querySelector("svg")?.getAttribute("class") ?? null
16
+ }
17
+
18
+ function setIconStyle(style: "duotone" | "grayscale" | "linear" | undefined) {
19
+ act(() => {
20
+ if (style === undefined) {
21
+ delete document.documentElement.dataset.iconStyle
22
+ } else {
23
+ document.documentElement.dataset.iconStyle = style
24
+ }
25
+ })
26
+ }
27
+
28
+ beforeEach(() => {
29
+ delete document.documentElement.dataset.iconStyle
30
+ })
31
+
32
+ describe("createIcon", () => {
33
+ it("renders the boldDuotone glyph by default with the hd-icon hook", () => {
34
+ const { container } = render(<Bolt className="size-4" />)
35
+ expect(svgClass(container)).toContain("solar-bolt-bold-duotone")
36
+ expect(svgClass(container)).toContain("hd-icon")
37
+ expect(svgClass(container)).toContain("size-4")
38
+ })
39
+
40
+ it("swaps to the linear glyph in linear style", async () => {
41
+ const { container } = render(<Bolt />)
42
+ setIconStyle("linear")
43
+ await waitFor(() => {
44
+ expect(svgClass(container)).toContain("solar-bolt-linear")
45
+ })
46
+ })
47
+
48
+ it("restores the boldDuotone glyph when the style leaves linear", async () => {
49
+ const { container } = render(<Bolt />)
50
+ setIconStyle("linear")
51
+ await waitFor(() => {
52
+ expect(svgClass(container)).toContain("solar-bolt-linear")
53
+ })
54
+ setIconStyle("grayscale")
55
+ await waitFor(() => {
56
+ expect(svgClass(container)).toContain("solar-bolt-bold-duotone")
57
+ })
58
+ })
59
+
60
+ it("renders the bold glyph when the mode prop is set", () => {
61
+ const { container } = render(<Bolt mode="bold" />)
62
+ expect(svgClass(container)).toContain("solar-bolt-bold")
63
+ })
64
+
65
+ it("renders each mode explicitly", () => {
66
+ const { container } = render(<Bolt mode="linear" />)
67
+ expect(svgClass(container)).toContain("solar-bolt-linear")
68
+ })
69
+ })
@@ -0,0 +1,104 @@
1
+ import { type ComponentType, createElement, useSyncExternalStore } from "react"
2
+ import type { IconType } from "../components/icon.tsx"
3
+
4
+ /**
5
+ * Icon-mode-aware glyph wrapper (Settings → Icons in the host app).
6
+ *
7
+ * The active icon style lives as `data-icon-style` on `document.documentElement`
8
+ * — the host app writes it from its preference store, plugin iframes receive
9
+ * it through the `themeChanged` push — so this module needs no context or
10
+ * injected state: a single shared `MutationObserver` watches the attribute
11
+ * and every wrapped component re-renders through `useSyncExternalStore`
12
+ * when it flips.
13
+ *
14
+ * Only `linear` needs a component swap: the boldDuotone glyphs change to
15
+ * their thin-line linear counterparts. `grayscale` is pure CSS (theme.css
16
+ * recolors the `.hd-icon` hook), so the store only tracks the mode.
17
+ */
18
+ export type IconStyleValue = "duotone" | "grayscale" | "linear"
19
+
20
+ /**
21
+ * The parallel Solar weights an icon registers — named after Solar's own
22
+ * weights (DESIGN.md — Iconography). Every icon carries all of them;
23
+ * adding a mode here forces every registry entry to fill it in.
24
+ */
25
+ export type IconMode = "bold" | "boldDuotone" | "linear"
26
+
27
+ /** All three mode glyphs of one icon. Mandatory and complete by type. */
28
+ export type IconVariants = Record<IconMode, IconType>
29
+
30
+ function readIconMode(): IconMode {
31
+ return document.documentElement.dataset.iconStyle === "linear"
32
+ ? "linear"
33
+ : "boldDuotone"
34
+ }
35
+
36
+ // Never read `document` at module load — pure-logic consumers (node test
37
+ // environments) import this module without a DOM. The first subscribe
38
+ // (component mount) snapshots the current value.
39
+ let currentMode: IconMode = "boldDuotone"
40
+ let observer: MutationObserver | undefined
41
+ const listeners = new Set<() => void>()
42
+
43
+ function ensureObserver(): void {
44
+ if (observer !== undefined || typeof document === "undefined") return
45
+ observer = new MutationObserver(() => {
46
+ const next = readIconMode()
47
+ if (next === currentMode) return
48
+ currentMode = next
49
+ for (const cb of listeners) cb()
50
+ })
51
+ observer.observe(document.documentElement, {
52
+ attributes: true,
53
+ attributeFilter: ["data-icon-style"],
54
+ })
55
+ }
56
+
57
+ function subscribeIconStyle(callback: () => void): () => void {
58
+ ensureObserver()
59
+ currentMode = readIconMode()
60
+ listeners.add(callback)
61
+ return () => {
62
+ listeners.delete(callback)
63
+ }
64
+ }
65
+
66
+ function getIconStyleSnapshot(): IconMode {
67
+ return currentMode
68
+ }
69
+
70
+ /**
71
+ * Wraps a Solar glyph family so its render follows the host's icon style.
72
+ *
73
+ * `createIcon({ bold, boldDuotone, linear })` — the three parallel weights
74
+ * of one icon. Without a `mode` prop the wrapper renders the glyph for the
75
+ * active icon style; an explicit `mode` overrides it (selected states pass
76
+ * `"bold"` through `Icon`).
77
+ *
78
+ * Every wrapped render merges the `hd-icon` hook class, so the duotone /
79
+ * grayscale CSS in theme.css applies to direct renders too. Returns
80
+ * `undefined`-free components callable through the `Icon` wrapper or bare
81
+ * JSX alike.
82
+ */
83
+ export function createIcon(
84
+ variants: IconVariants,
85
+ ): ComponentType<Record<string, unknown>> {
86
+ function Icon(props: Record<string, unknown>) {
87
+ const current = useSyncExternalStore(
88
+ subscribeIconStyle,
89
+ getIconStyleSnapshot,
90
+ () => "boldDuotone",
91
+ )
92
+ const { mode, ...rest } = props as { readonly mode?: IconMode } & Record<
93
+ string,
94
+ unknown
95
+ >
96
+ const Component = variants[(mode ?? current) as IconMode]
97
+ const className =
98
+ typeof rest.className === "string"
99
+ ? `hd-icon ${rest.className}`
100
+ : "hd-icon"
101
+ return createElement(Component, { ...rest, className })
102
+ }
103
+ return Icon
104
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Local utility marks — the three plain glyphs Solar's vocabulary lacks
3
+ * (its ✓/×/+ only exist inside Circle/Square containers, whose marks occupy
4
+ * barely a third of the box and go muddy below 16px). These are drawn
5
+ * in-house for the spots where a bare mark is the whole meaning: checkbox
6
+ * ticks, selected badges, pill clears, add affordances.
7
+ *
8
+ * Check and Cross are strokes in Solar's language (24 viewBox, round caps)
9
+ * at 2 units — diagonals anti-alias gracefully at any tier. Plus is a
10
+ * filled cross instead: its pure orthogonal edges land on integer pixels
11
+ * at the md tier (3-unit arms render as a crisp 2px bar at 16px), where a
12
+ * 1.33px stroke would straddle pixel boundaries and blur. Weight-insensitive:
13
+ * no tone layer, no weight variants. Original geometry — not derived
14
+ * from Solar assets.
15
+ *
16
+ * Marks ride the same `mode` channel as registry icons (declared, ignored):
17
+ * a check is a check in every weight.
18
+ */
19
+ import type { IconMode } from "./icon-style.ts"
20
+
21
+ type MarkProps = {
22
+ className?: string
23
+ mode?: IconMode
24
+ }
25
+
26
+ function Mark({
27
+ d,
28
+ filled,
29
+ className,
30
+ }: MarkProps & { d: string; filled?: boolean }) {
31
+ return (
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ className={className}
37
+ >
38
+ {filled ? (
39
+ <path d={d} fill="currentColor" />
40
+ ) : (
41
+ <path
42
+ d={d}
43
+ stroke="currentColor"
44
+ strokeWidth="2"
45
+ strokeLinecap="round"
46
+ strokeLinejoin="round"
47
+ />
48
+ )}
49
+ </svg>
50
+ )
51
+ }
52
+
53
+ /** Plain ✓ — checkbox ticks, selected badges. */
54
+ export function Check(props: MarkProps) {
55
+ return <Mark {...props} d="M5 12.8l4.7 4.7L19 7.2" />
56
+ }
57
+
58
+ /** Plain × — pill and chip clears. */
59
+ export function Cross(props: MarkProps) {
60
+ return <Mark {...props} d="M6 6l12 12M18 6L6 18" />
61
+ }
62
+
63
+ /** Plain + — add affordances. Filled cross, 3-unit arms spanning 5–19. */
64
+ export function Plus(props: MarkProps) {
65
+ return (
66
+ <Mark
67
+ {...props}
68
+ filled
69
+ d="M10.5 5h3v5.5H19v3h-5.5V19h-3v-5.5H5v-3h5.5V5Z"
70
+ />
71
+ )
72
+ }
@@ -0,0 +1,65 @@
1
+ // @vitest-environment node
2
+ import { readFileSync } from "node:fs"
3
+ import { describe, expect, it } from "vitest"
4
+ import * as icons from "./registry"
5
+
6
+ /**
7
+ * Source-structure guard for the icon registry. The file is excluded from
8
+ * biome (packages/ui/src/icons), so this test is the only thing that
9
+ * keeps the single export section ordered and every icon's three parallel
10
+ * Solar weights registered — see the header comment in registry.ts for the
11
+ * conventions.
12
+ */
13
+ const src = readFileSync(new URL("./registry.ts", import.meta.url), "utf8")
14
+ const lines = src.split(/\r?\n/)
15
+
16
+ /** One parsed registry entry: the export name. */
17
+ function parseIcons(): string[] {
18
+ const names: string[] = []
19
+ for (let i = 0; i < lines.length; ) {
20
+ const m = /^export const (\w+) = createIcon\(\{$/.exec(lines[i]!)
21
+ if (m === null) {
22
+ i++
23
+ continue
24
+ }
25
+ const name = m[1]!
26
+ expect(lines[i + 1]).toBe(`\tbold: ${name}BoldWeight,`)
27
+ expect(lines[i + 2]).toBe(`\tboldDuotone: ${name}BoldDuotone,`)
28
+ // MenuDots is the one documented exception: its thin-line dots read
29
+ // as noise at small sizes, so the filled weight stays in every mode.
30
+ expect(lines[i + 3]).toBe(
31
+ name === "MenuDots"
32
+ ? "\tlinear: MenuDotsBoldWeight,"
33
+ : `\tlinear: ${name}Linear,`,
34
+ )
35
+ expect(lines[i + 4]).toBe("})")
36
+ names.push(name)
37
+ i += 5
38
+ }
39
+ return names
40
+ }
41
+
42
+ const iconNames = parseIcons()
43
+
44
+ const ordinal = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
45
+
46
+ describe("icons registry structure", () => {
47
+ it("keeps the export section alphabetically ordered (ordinal)", () => {
48
+ expect([...iconNames].sort(ordinal)).toEqual(iconNames)
49
+ })
50
+
51
+ it("registers every icon with all three Solar weights", () => {
52
+ for (const name of iconNames) {
53
+ // biome-ignore lint/performance/noDynamicNamespaceImportAccess: the registry is a data module — accessing entries by parsed name is the point of the guard.
54
+ const icon = icons[name as keyof typeof icons] as (
55
+ props: Record<string, unknown>,
56
+ ) => unknown
57
+ expect(typeof icon).toBe("function")
58
+ }
59
+ })
60
+
61
+ it("exports exactly the names declared in the file", () => {
62
+ expect(new Set(iconNames)).toEqual(new Set(Object.keys(icons)))
63
+ expect(iconNames).toHaveLength(Object.keys(icons).length)
64
+ })
65
+ })