@seekalfred/primitives 0.5.1 β†’ 0.7.1

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 (446) hide show
  1. package/README.md +682 -19
  2. package/dist/accordion/accordion.js +10 -4
  3. package/dist/accordion/accordion.js.map +1 -1
  4. package/dist/action-button/action-button.d.ts +50 -0
  5. package/dist/action-button/action-button.js +159 -0
  6. package/dist/action-button/action-button.js.map +1 -0
  7. package/dist/action-button/index.d.ts +2 -0
  8. package/dist/action-button/index.js +2 -0
  9. package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
  10. package/dist/activity-heatmap/activity-heatmap.js +699 -0
  11. package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
  12. package/dist/activity-heatmap/index.d.ts +2 -0
  13. package/dist/activity-heatmap/index.js +2 -0
  14. package/dist/alert/alert.d.ts +19 -1
  15. package/dist/alert/alert.js +180 -23
  16. package/dist/alert/alert.js.map +1 -1
  17. package/dist/alert-dialog/alert-dialog.d.ts +45 -18
  18. package/dist/alert-dialog/alert-dialog.js +128 -9
  19. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  20. package/dist/alert-dialog/index.d.ts +2 -2
  21. package/dist/alert-dialog/index.js +2 -2
  22. package/dist/animated-counter/animated-counter.d.ts +38 -0
  23. package/dist/animated-counter/animated-counter.js +230 -0
  24. package/dist/animated-counter/animated-counter.js.map +1 -0
  25. package/dist/animated-counter/index.d.ts +2 -0
  26. package/dist/animated-counter/index.js +2 -0
  27. package/dist/announcement-bar/announcement-bar.d.ts +67 -0
  28. package/dist/announcement-bar/announcement-bar.js +364 -0
  29. package/dist/announcement-bar/announcement-bar.js.map +1 -0
  30. package/dist/announcement-bar/index.d.ts +2 -0
  31. package/dist/announcement-bar/index.js +2 -0
  32. package/dist/avatar/avatar.d.ts +6 -0
  33. package/dist/avatar/avatar.js +47 -16
  34. package/dist/avatar/avatar.js.map +1 -1
  35. package/dist/badge/badge.d.ts +10 -3
  36. package/dist/badge/badge.js +35 -5
  37. package/dist/badge/badge.js.map +1 -1
  38. package/dist/bar-chart/bar-chart.d.ts +51 -0
  39. package/dist/bar-chart/bar-chart.js +565 -0
  40. package/dist/bar-chart/bar-chart.js.map +1 -0
  41. package/dist/bar-chart/index.d.ts +2 -0
  42. package/dist/bar-chart/index.js +2 -0
  43. package/dist/breadcrumb/breadcrumb.d.ts +2 -29
  44. package/dist/breadcrumb/breadcrumb.js +32 -9
  45. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  46. package/dist/brush-chart/brush-chart.d.ts +64 -0
  47. package/dist/brush-chart/brush-chart.js +1056 -0
  48. package/dist/brush-chart/brush-chart.js.map +1 -0
  49. package/dist/brush-chart/index.d.ts +2 -0
  50. package/dist/brush-chart/index.js +2 -0
  51. package/dist/button/button.d.ts +36 -23
  52. package/dist/button/button.js +198 -45
  53. package/dist/button/button.js.map +1 -1
  54. package/dist/button-group/button-group.d.ts +76 -0
  55. package/dist/button-group/button-group.js +402 -0
  56. package/dist/button-group/button-group.js.map +1 -0
  57. package/dist/button-group/index.d.ts +2 -0
  58. package/dist/button-group/index.js +2 -0
  59. package/dist/calendar/calendar.d.ts +1 -1
  60. package/dist/calendar/calendar.js +257 -30
  61. package/dist/calendar/calendar.js.map +1 -1
  62. package/dist/card/card.d.ts +9 -2
  63. package/dist/card/card.js +16 -5
  64. package/dist/card/card.js.map +1 -1
  65. package/dist/card-stack/card-stack.d.ts +54 -0
  66. package/dist/card-stack/card-stack.js +492 -0
  67. package/dist/card-stack/card-stack.js.map +1 -0
  68. package/dist/card-stack/index.d.ts +2 -0
  69. package/dist/card-stack/index.js +2 -0
  70. package/dist/carousel/carousel.d.ts +56 -0
  71. package/dist/carousel/carousel.js +389 -0
  72. package/dist/carousel/carousel.js.map +1 -0
  73. package/dist/carousel/index.d.ts +2 -0
  74. package/dist/carousel/index.js +2 -0
  75. package/dist/checkbox/checkbox.d.ts +0 -7
  76. package/dist/checkbox/checkbox.js +26 -29
  77. package/dist/checkbox/checkbox.js.map +1 -1
  78. package/dist/chip-group/chip-group.d.ts +37 -0
  79. package/dist/chip-group/chip-group.js +148 -0
  80. package/dist/chip-group/chip-group.js.map +1 -0
  81. package/dist/chip-group/index.d.ts +2 -0
  82. package/dist/chip-group/index.js +2 -0
  83. package/dist/confirm-morph/confirm-morph.d.ts +85 -0
  84. package/dist/confirm-morph/confirm-morph.js +652 -0
  85. package/dist/confirm-morph/confirm-morph.js.map +1 -0
  86. package/dist/confirm-morph/index.d.ts +2 -0
  87. package/dist/confirm-morph/index.js +2 -0
  88. package/dist/copy-button/copy-button.d.ts +32 -0
  89. package/dist/copy-button/copy-button.js +134 -0
  90. package/dist/copy-button/copy-button.js.map +1 -0
  91. package/dist/copy-button/index.d.ts +2 -0
  92. package/dist/copy-button/index.js +2 -0
  93. package/dist/countdown/countdown.d.ts +45 -0
  94. package/dist/countdown/countdown.js +298 -0
  95. package/dist/countdown/countdown.js.map +1 -0
  96. package/dist/countdown/index.d.ts +2 -0
  97. package/dist/countdown/index.js +2 -0
  98. package/dist/date-picker/date-picker.d.ts +51 -0
  99. package/dist/date-picker/date-picker.js +241 -0
  100. package/dist/date-picker/date-picker.js.map +1 -0
  101. package/dist/date-picker/date-range-picker.d.ts +78 -0
  102. package/dist/date-picker/date-range-picker.js +1015 -0
  103. package/dist/date-picker/date-range-picker.js.map +1 -0
  104. package/dist/date-picker/index.d.ts +4 -0
  105. package/dist/date-picker/index.js +4 -0
  106. package/dist/date-picker/picker-parts.d.ts +46 -0
  107. package/dist/date-picker/picker-parts.js +135 -0
  108. package/dist/date-picker/picker-parts.js.map +1 -0
  109. package/dist/dialog/dialog.d.ts +10 -14
  110. package/dist/dialog/dialog.js +40 -12
  111. package/dist/dialog/dialog.js.map +1 -1
  112. package/dist/donut-chart/donut-chart.d.ts +64 -0
  113. package/dist/donut-chart/donut-chart.js +576 -0
  114. package/dist/donut-chart/donut-chart.js.map +1 -0
  115. package/dist/donut-chart/index.d.ts +2 -0
  116. package/dist/donut-chart/index.js +2 -0
  117. package/dist/empty-state/empty-state.d.ts +55 -0
  118. package/dist/empty-state/empty-state.js +89 -0
  119. package/dist/empty-state/empty-state.js.map +1 -0
  120. package/dist/empty-state/index.d.ts +2 -0
  121. package/dist/empty-state/index.js +2 -0
  122. package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
  123. package/dist/expanding-button-group/expanding-button-group.js +350 -0
  124. package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
  125. package/dist/expanding-button-group/index.d.ts +2 -0
  126. package/dist/expanding-button-group/index.js +2 -0
  127. package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
  128. package/dist/file-drop-zone/file-drop-zone.js +116 -0
  129. package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
  130. package/dist/file-drop-zone/index.d.ts +2 -0
  131. package/dist/file-drop-zone/index.js +2 -0
  132. package/dist/filter-bar/filter-bar.d.ts +142 -0
  133. package/dist/filter-bar/filter-bar.js +416 -0
  134. package/dist/filter-bar/filter-bar.js.map +1 -0
  135. package/dist/filter-bar/index.d.ts +2 -0
  136. package/dist/filter-bar/index.js +2 -0
  137. package/dist/floating-button-group/floating-button-group.d.ts +61 -0
  138. package/dist/floating-button-group/floating-button-group.js +340 -0
  139. package/dist/floating-button-group/floating-button-group.js.map +1 -0
  140. package/dist/floating-button-group/index.d.ts +2 -0
  141. package/dist/floating-button-group/index.js +2 -0
  142. package/dist/gauge/gauge.d.ts +45 -0
  143. package/dist/gauge/gauge.js +219 -0
  144. package/dist/gauge/gauge.js.map +1 -0
  145. package/dist/gauge/index.d.ts +2 -0
  146. package/dist/gauge/index.js +2 -0
  147. package/dist/icon-morph/icon-morph.d.ts +55 -0
  148. package/dist/icon-morph/icon-morph.js +331 -0
  149. package/dist/icon-morph/icon-morph.js.map +1 -0
  150. package/dist/icon-morph/index.d.ts +2 -0
  151. package/dist/icon-morph/index.js +2 -0
  152. package/dist/in-view-title/in-view-title.d.ts +33 -0
  153. package/dist/in-view-title/in-view-title.js +119 -0
  154. package/dist/in-view-title/in-view-title.js.map +1 -0
  155. package/dist/in-view-title/index.d.ts +2 -0
  156. package/dist/in-view-title/index.js +2 -0
  157. package/dist/index.d.ts +71 -6
  158. package/dist/index.js +73 -11
  159. package/dist/input/input.d.ts +22 -0
  160. package/dist/input/input.js +36 -12
  161. package/dist/input/input.js.map +1 -1
  162. package/dist/lib/cn.js +11 -1
  163. package/dist/lib/cn.js.map +1 -1
  164. package/dist/lib/count-to.js +113 -0
  165. package/dist/lib/count-to.js.map +1 -0
  166. package/dist/lib/eye-morph.js +62 -0
  167. package/dist/lib/eye-morph.js.map +1 -0
  168. package/dist/lib/field-message.js +354 -0
  169. package/dist/lib/field-message.js.map +1 -0
  170. package/dist/lib/field-shell.js +81 -0
  171. package/dist/lib/field-shell.js.map +1 -0
  172. package/dist/lib/field-surface.js +11 -8
  173. package/dist/lib/field-surface.js.map +1 -1
  174. package/dist/lib/flag-emoji.js +50 -0
  175. package/dist/lib/flag-emoji.js.map +1 -0
  176. package/dist/lib/flip.js +88 -0
  177. package/dist/lib/flip.js.map +1 -0
  178. package/dist/lib/height-follow.js +59 -0
  179. package/dist/lib/height-follow.js.map +1 -0
  180. package/dist/lib/icon-swap.js +47 -0
  181. package/dist/lib/icon-swap.js.map +1 -0
  182. package/dist/lib/leaving-list.js +57 -0
  183. package/dist/lib/leaving-list.js.map +1 -0
  184. package/dist/lib/menu-highlight.js +64 -0
  185. package/dist/lib/menu-highlight.js.map +1 -0
  186. package/dist/lib/merge-refs.js +3 -3
  187. package/dist/lib/merge-refs.js.map +1 -1
  188. package/dist/lib/morph-text.js +128 -0
  189. package/dist/lib/morph-text.js.map +1 -0
  190. package/dist/lib/motion.js +169 -0
  191. package/dist/lib/motion.js.map +1 -0
  192. package/dist/lib/pausable-timeout.js +67 -0
  193. package/dist/lib/pausable-timeout.js.map +1 -0
  194. package/dist/lib/rolling-value.js +31 -0
  195. package/dist/lib/rolling-value.js.map +1 -0
  196. package/dist/lib/spring-progress.js +114 -0
  197. package/dist/lib/spring-progress.js.map +1 -0
  198. package/dist/lib/spring-tween.js +500 -0
  199. package/dist/lib/spring-tween.js.map +1 -0
  200. package/dist/lib/svg-id.js +10 -0
  201. package/dist/lib/svg-id.js.map +1 -0
  202. package/dist/lib/swap-text.js +79 -0
  203. package/dist/lib/swap-text.js.map +1 -0
  204. package/dist/lib/use-copy-feedback.js +36 -0
  205. package/dist/lib/use-copy-feedback.js.map +1 -0
  206. package/dist/line-chart/index.d.ts +2 -0
  207. package/dist/line-chart/index.js +2 -0
  208. package/dist/line-chart/line-chart.d.ts +76 -0
  209. package/dist/line-chart/line-chart.js +611 -0
  210. package/dist/line-chart/line-chart.js.map +1 -0
  211. package/dist/mention-input/index.d.ts +2 -0
  212. package/dist/mention-input/index.js +2 -0
  213. package/dist/mention-input/mention-input.d.ts +98 -0
  214. package/dist/mention-input/mention-input.js +576 -0
  215. package/dist/mention-input/mention-input.js.map +1 -0
  216. package/dist/menu/menu.d.ts +2 -0
  217. package/dist/menu/menu.js +23 -12
  218. package/dist/menu/menu.js.map +1 -1
  219. package/dist/money-input/index.d.ts +2 -0
  220. package/dist/money-input/index.js +2 -0
  221. package/dist/money-input/money-input.d.ts +65 -0
  222. package/dist/money-input/money-input.js +607 -0
  223. package/dist/money-input/money-input.js.map +1 -0
  224. package/dist/morph-select/index.d.ts +2 -0
  225. package/dist/morph-select/index.js +2 -0
  226. package/dist/morph-select/morph-select.d.ts +71 -0
  227. package/dist/morph-select/morph-select.js +693 -0
  228. package/dist/morph-select/morph-select.js.map +1 -0
  229. package/dist/multi-select/index.d.ts +2 -0
  230. package/dist/multi-select/index.js +2 -0
  231. package/dist/multi-select/multi-select.d.ts +36 -0
  232. package/dist/multi-select/multi-select.js +152 -0
  233. package/dist/multi-select/multi-select.js.map +1 -0
  234. package/dist/number-field/index.d.ts +2 -0
  235. package/dist/number-field/index.js +2 -0
  236. package/dist/number-field/number-field.d.ts +42 -0
  237. package/dist/number-field/number-field.js +199 -0
  238. package/dist/number-field/number-field.js.map +1 -0
  239. package/dist/onboarding-checklist/index.d.ts +2 -0
  240. package/dist/onboarding-checklist/index.js +2 -0
  241. package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
  242. package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
  243. package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
  244. package/dist/page-header/index.d.ts +2 -0
  245. package/dist/page-header/index.js +2 -0
  246. package/dist/page-header/page-header.d.ts +75 -0
  247. package/dist/page-header/page-header.js +83 -0
  248. package/dist/page-header/page-header.js.map +1 -0
  249. package/dist/password-field/index.d.ts +2 -0
  250. package/dist/password-field/index.js +2 -0
  251. package/dist/password-field/password-field.d.ts +37 -0
  252. package/dist/password-field/password-field.js +101 -0
  253. package/dist/password-field/password-field.js.map +1 -0
  254. package/dist/password-strength/index.d.ts +2 -0
  255. package/dist/password-strength/index.js +2 -0
  256. package/dist/password-strength/password-strength.d.ts +127 -0
  257. package/dist/password-strength/password-strength.js +455 -0
  258. package/dist/password-strength/password-strength.js.map +1 -0
  259. package/dist/phone-input/index.d.ts +3 -0
  260. package/dist/phone-input/index.js +3 -0
  261. package/dist/phone-input/phone-countries.d.ts +29 -0
  262. package/dist/phone-input/phone-countries.js +170 -0
  263. package/dist/phone-input/phone-countries.js.map +1 -0
  264. package/dist/phone-input/phone-input.d.ts +66 -0
  265. package/dist/phone-input/phone-input.js +440 -0
  266. package/dist/phone-input/phone-input.js.map +1 -0
  267. package/dist/popover/popover.d.ts +20 -0
  268. package/dist/popover/popover.js +33 -9
  269. package/dist/popover/popover.js.map +1 -1
  270. package/dist/progress/progress.js +46 -5
  271. package/dist/progress/progress.js.map +1 -1
  272. package/dist/radio-group/radio-group.js +8 -7
  273. package/dist/radio-group/radio-group.js.map +1 -1
  274. package/dist/reactions/index.d.ts +2 -0
  275. package/dist/reactions/index.js +2 -0
  276. package/dist/reactions/reactions.d.ts +50 -0
  277. package/dist/reactions/reactions.js +315 -0
  278. package/dist/reactions/reactions.js.map +1 -0
  279. package/dist/rich-text-editor/index.d.ts +3 -0
  280. package/dist/rich-text-editor/index.js +2 -0
  281. package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
  282. package/dist/rich-text-editor/rich-text-editor.js +909 -0
  283. package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
  284. package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
  285. package/dist/rich-text-editor/rich-text-engine.js +366 -0
  286. package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
  287. package/dist/rich-text-editor/rich-text-html.js +174 -0
  288. package/dist/rich-text-editor/rich-text-html.js.map +1 -0
  289. package/dist/rich-text-editor/rich-text-icons.js +43 -0
  290. package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
  291. package/dist/ridgeline/index.d.ts +2 -0
  292. package/dist/ridgeline/index.js +2 -0
  293. package/dist/ridgeline/ridgeline.d.ts +55 -0
  294. package/dist/ridgeline/ridgeline.js +701 -0
  295. package/dist/ridgeline/ridgeline.js.map +1 -0
  296. package/dist/scroll-area/index.d.ts +2 -2
  297. package/dist/scroll-area/index.js +2 -2
  298. package/dist/scroll-area/scroll-area.d.ts +75 -28
  299. package/dist/scroll-area/scroll-area.js +212 -41
  300. package/dist/scroll-area/scroll-area.js.map +1 -1
  301. package/dist/scrub-input/evaluate.d.ts +14 -0
  302. package/dist/scrub-input/evaluate.js +72 -0
  303. package/dist/scrub-input/evaluate.js.map +1 -0
  304. package/dist/scrub-input/index.d.ts +3 -0
  305. package/dist/scrub-input/index.js +3 -0
  306. package/dist/scrub-input/scrub-input.d.ts +74 -0
  307. package/dist/scrub-input/scrub-input.js +486 -0
  308. package/dist/scrub-input/scrub-input.js.map +1 -0
  309. package/dist/search-field/index.d.ts +2 -0
  310. package/dist/search-field/index.js +2 -0
  311. package/dist/search-field/search-field.d.ts +27 -0
  312. package/dist/search-field/search-field.js +155 -0
  313. package/dist/search-field/search-field.js.map +1 -0
  314. package/dist/select/select.d.ts +3 -1
  315. package/dist/select/select.js +95 -25
  316. package/dist/select/select.js.map +1 -1
  317. package/dist/separator/separator.d.ts +1 -1
  318. package/dist/sheet/sheet.js +4 -2
  319. package/dist/sheet/sheet.js.map +1 -1
  320. package/dist/shortcut-recorder/index.d.ts +3 -0
  321. package/dist/shortcut-recorder/index.js +3 -0
  322. package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
  323. package/dist/shortcut-recorder/shortcut-model.js +206 -0
  324. package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
  325. package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
  326. package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
  327. package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
  328. package/dist/side-nav/index.d.ts +2 -0
  329. package/dist/side-nav/index.js +2 -0
  330. package/dist/side-nav/side-nav.d.ts +72 -0
  331. package/dist/side-nav/side-nav.js +142 -0
  332. package/dist/side-nav/side-nav.js.map +1 -0
  333. package/dist/skeleton/skeleton.d.ts +8 -2
  334. package/dist/skeleton/skeleton.js +6 -2
  335. package/dist/skeleton/skeleton.js.map +1 -1
  336. package/dist/slider/index.d.ts +2 -2
  337. package/dist/slider/slider.d.ts +46 -14
  338. package/dist/slider/slider.js +188 -50
  339. package/dist/slider/slider.js.map +1 -1
  340. package/dist/slope-chart/index.d.ts +2 -0
  341. package/dist/slope-chart/index.js +2 -0
  342. package/dist/slope-chart/slope-chart.d.ts +54 -0
  343. package/dist/slope-chart/slope-chart.js +568 -0
  344. package/dist/slope-chart/slope-chart.js.map +1 -0
  345. package/dist/sparkline/index.d.ts +2 -0
  346. package/dist/sparkline/index.js +2 -0
  347. package/dist/sparkline/sparkline.d.ts +46 -0
  348. package/dist/sparkline/sparkline.js +390 -0
  349. package/dist/sparkline/sparkline.js.map +1 -0
  350. package/dist/split-bar/index.d.ts +2 -0
  351. package/dist/split-bar/index.js +2 -0
  352. package/dist/split-bar/split-bar.d.ts +62 -0
  353. package/dist/split-bar/split-bar.js +513 -0
  354. package/dist/split-bar/split-bar.js.map +1 -0
  355. package/dist/split-button/index.d.ts +2 -0
  356. package/dist/split-button/index.js +2 -0
  357. package/dist/split-button/split-button.d.ts +46 -0
  358. package/dist/split-button/split-button.js +123 -0
  359. package/dist/split-button/split-button.js.map +1 -0
  360. package/dist/stat-card/index.d.ts +2 -0
  361. package/dist/stat-card/index.js +2 -0
  362. package/dist/stat-card/stat-card.d.ts +101 -0
  363. package/dist/stat-card/stat-card.js +249 -0
  364. package/dist/stat-card/stat-card.js.map +1 -0
  365. package/dist/stepper/index.d.ts +2 -0
  366. package/dist/stepper/index.js +2 -0
  367. package/dist/stepper/stepper.d.ts +51 -0
  368. package/dist/stepper/stepper.js +215 -0
  369. package/dist/stepper/stepper.js.map +1 -0
  370. package/dist/streamgraph/index.d.ts +2 -0
  371. package/dist/streamgraph/index.js +2 -0
  372. package/dist/streamgraph/streamgraph.d.ts +70 -0
  373. package/dist/streamgraph/streamgraph.js +613 -0
  374. package/dist/streamgraph/streamgraph.js.map +1 -0
  375. package/dist/styles.css +1 -1
  376. package/dist/swipe-actions/index.d.ts +2 -0
  377. package/dist/swipe-actions/index.js +2 -0
  378. package/dist/swipe-actions/swipe-actions.d.ts +67 -0
  379. package/dist/swipe-actions/swipe-actions.js +552 -0
  380. package/dist/swipe-actions/swipe-actions.js.map +1 -0
  381. package/dist/switch/switch.js +10 -9
  382. package/dist/switch/switch.js.map +1 -1
  383. package/dist/table/index.d.ts +2 -2
  384. package/dist/table/index.js +2 -2
  385. package/dist/table/table.d.ts +87 -4
  386. package/dist/table/table.js +441 -165
  387. package/dist/table/table.js.map +1 -1
  388. package/dist/tabs/tabs.js +143 -7
  389. package/dist/tabs/tabs.js.map +1 -1
  390. package/dist/tag-input/index.d.ts +2 -0
  391. package/dist/tag-input/index.js +2 -0
  392. package/dist/tag-input/tag-input.d.ts +40 -0
  393. package/dist/tag-input/tag-input.js +269 -0
  394. package/dist/tag-input/tag-input.js.map +1 -0
  395. package/dist/text-morph/index.d.ts +2 -0
  396. package/dist/text-morph/index.js +2 -0
  397. package/dist/text-morph/text-morph.d.ts +23 -0
  398. package/dist/text-morph/text-morph.js +21 -0
  399. package/dist/text-morph/text-morph.js.map +1 -0
  400. package/dist/text-reveal/index.d.ts +2 -0
  401. package/dist/text-reveal/index.js +2 -0
  402. package/dist/text-reveal/text-reveal.d.ts +27 -0
  403. package/dist/text-reveal/text-reveal.js +51 -0
  404. package/dist/text-reveal/text-reveal.js.map +1 -0
  405. package/dist/text-shimmer/index.d.ts +2 -0
  406. package/dist/text-shimmer/index.js +2 -0
  407. package/dist/text-shimmer/text-shimmer.d.ts +29 -0
  408. package/dist/text-shimmer/text-shimmer.js +29 -0
  409. package/dist/text-shimmer/text-shimmer.js.map +1 -0
  410. package/dist/textarea/textarea.d.ts +23 -0
  411. package/dist/textarea/textarea.js +24 -2
  412. package/dist/textarea/textarea.js.map +1 -1
  413. package/dist/theme-switch/index.d.ts +2 -0
  414. package/dist/theme-switch/index.js +2 -0
  415. package/dist/theme-switch/theme-switch.d.ts +37 -0
  416. package/dist/theme-switch/theme-switch.js +123 -0
  417. package/dist/theme-switch/theme-switch.js.map +1 -0
  418. package/dist/time-picker/index.d.ts +2 -0
  419. package/dist/time-picker/index.js +2 -0
  420. package/dist/time-picker/time-picker.d.ts +54 -0
  421. package/dist/time-picker/time-picker.js +201 -0
  422. package/dist/time-picker/time-picker.js.map +1 -0
  423. package/dist/toast/index.d.ts +2 -2
  424. package/dist/toast/index.js +2 -2
  425. package/dist/toast/toast.d.ts +6 -0
  426. package/dist/toast/toast.js +98 -36
  427. package/dist/toast/toast.js.map +1 -1
  428. package/dist/tooltip/tooltip.d.ts +10 -3
  429. package/dist/tooltip/tooltip.js +67 -18
  430. package/dist/tooltip/tooltip.js.map +1 -1
  431. package/dist/treemap/index.d.ts +2 -0
  432. package/dist/treemap/index.js +2 -0
  433. package/dist/treemap/treemap.d.ts +53 -0
  434. package/dist/treemap/treemap.js +699 -0
  435. package/dist/treemap/treemap.js.map +1 -0
  436. package/dist/usage-meter/index.d.ts +2 -0
  437. package/dist/usage-meter/index.js +2 -0
  438. package/dist/usage-meter/usage-meter.d.ts +50 -0
  439. package/dist/usage-meter/usage-meter.js +440 -0
  440. package/dist/usage-meter/usage-meter.js.map +1 -0
  441. package/dist/waffle-chart/index.d.ts +2 -0
  442. package/dist/waffle-chart/index.js +2 -0
  443. package/dist/waffle-chart/waffle-chart.d.ts +57 -0
  444. package/dist/waffle-chart/waffle-chart.js +426 -0
  445. package/dist/waffle-chart/waffle-chart.js.map +1 -0
  446. package/package.json +245 -3
@@ -0,0 +1,354 @@
1
+ import { cn } from "./cn.js";
2
+ import { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from "./motion.js";
3
+ import { SwapText } from "./swap-text.js";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/lib/field-message.tsx
7
+ const isNumber = (word) => /^\d[\d.,/:]*%?$/.test(word);
8
+ const WORD_OUT = {
9
+ settle: "absolute inline-block whitespace-pre motion-safe:animate-field-word-out",
10
+ quick: "absolute inline-block whitespace-pre motion-safe:animate-field-word-out-quick",
11
+ sharp: "absolute inline-block whitespace-pre motion-safe:animate-field-word-out-sharp"
12
+ };
13
+ /** The longest a spring exit takes (736ms) plus a margin, for the backstop that removes a node whose animation never ends. */
14
+ const SPRING_BACKSTOP = 900;
15
+ const rectOf = (node) => ({
16
+ left: node.offsetLeft,
17
+ top: node.offsetTop,
18
+ width: node.offsetWidth,
19
+ height: node.offsetHeight
20
+ });
21
+ function LeavingWordNode({ item, onDone, exit }) {
22
+ const { id } = item;
23
+ const remove = useCallback(() => onDone(id), [onDone, id]);
24
+ useExitBackstop(true, remove);
25
+ return /* @__PURE__ */ jsx("span", {
26
+ "aria-hidden": "true",
27
+ onAnimationEnd: (event) => {
28
+ if (event.target === event.currentTarget) remove();
29
+ },
30
+ style: item.rect,
31
+ className: WORD_OUT[exit],
32
+ children: item.word
33
+ });
34
+ }
35
+ function LeavingDigit({ id, rect, onDone, children }) {
36
+ const remove = useCallback(() => onDone(id), [onDone, id]);
37
+ useExitBackstop(true, remove, SPRING_BACKSTOP);
38
+ useIsomorphicLayoutEffect(() => {
39
+ if (prefersReducedMotion()) remove();
40
+ }, [remove]);
41
+ return /* @__PURE__ */ jsx("span", {
42
+ "aria-hidden": "true",
43
+ onAnimationEnd: (event) => {
44
+ if (event.target === event.currentTarget && event.animationName === "field-digit-move-out") remove();
45
+ },
46
+ style: rect,
47
+ className: "absolute motion-safe:animate-field-digit-out",
48
+ children
49
+ });
50
+ }
51
+ /** One digit, rolling in or not as it was when it mounted: a later change of `animate` must not replay it. */
52
+ function Digit({ nodeRef, animate, children }) {
53
+ const [rolls] = useState(animate);
54
+ return /* @__PURE__ */ jsx("span", {
55
+ ref: nodeRef,
56
+ className: rolls ? "motion-safe:animate-field-digit-in" : void 0,
57
+ children
58
+ });
59
+ }
60
+ function DigitColumn({ column, direction, leaving, still, onDone }) {
61
+ const digitRef = useRef(null);
62
+ const digits = useLeavingItems(column.char, column.char, () => digitRef.current);
63
+ const [shownDigit, setShownDigit] = useState({
64
+ char: column.char,
65
+ changes: 0
66
+ });
67
+ if (shownDigit.char !== column.char) setShownDigit({
68
+ char: column.char,
69
+ changes: shownDigit.changes + 1
70
+ });
71
+ const done = useCallback(() => onDone(column.key), [onDone, column.key]);
72
+ useExitBackstop(leaving, done, SPRING_BACKSTOP);
73
+ useIsomorphicLayoutEffect(() => {
74
+ if (leaving && prefersReducedMotion()) done();
75
+ }, [leaving, done]);
76
+ return /* @__PURE__ */ jsxs("span", {
77
+ style: { "--roll": direction },
78
+ onAnimationEnd: (event) => {
79
+ if (leaving && event.target === event.currentTarget && event.animationName === "field-column-shrink") done();
80
+ },
81
+ className: cn("field-number-column", leaving ? "motion-safe:animate-field-column-out" : column.seq > 0 && "motion-safe:animate-field-column-in"),
82
+ children: [(still ? [] : digits.leaving).map((item) => /* @__PURE__ */ jsx(LeavingDigit, {
83
+ id: item.id,
84
+ rect: item.rect,
85
+ onDone: digits.remove,
86
+ children: item.value
87
+ }, item.id)), /* @__PURE__ */ jsx(Digit, {
88
+ nodeRef: digitRef,
89
+ animate: shownDigit.changes > 0 && !still,
90
+ children: column.char
91
+ }, column.char)]
92
+ });
93
+ }
94
+ /**
95
+ * A count in the copy, such as "12 characters": only the digits that changed
96
+ * roll, and a gained or lost digit opens or closes its width. Digits arrive
97
+ * from below when the number grew and from above when it shrank.
98
+ */
99
+ function RollingNumber({ word, direction: forced, still = false }) {
100
+ const [state, setState] = useState(() => ({
101
+ word,
102
+ direction: 1,
103
+ seq: 0,
104
+ columns: toColumns(word, 0, /* @__PURE__ */ new Map()),
105
+ gone: []
106
+ }));
107
+ let { columns, gone, direction } = state;
108
+ if (state.word !== word) {
109
+ const seq = state.seq + 1;
110
+ direction = forced ?? (parseFloat(word.replace(/,/g, "")) < parseFloat(state.word.replace(/,/g, "")) ? -1 : 1);
111
+ columns = toColumns(word, still ? 0 : seq, new Map(state.columns.map((column) => [column.key, column])));
112
+ const kept = new Set(columns.map((column) => column.key));
113
+ gone = still ? [] : [...state.gone.filter((column) => !kept.has(column.key)), ...state.columns.filter((column) => !kept.has(column.key))];
114
+ setState({
115
+ word,
116
+ direction,
117
+ seq,
118
+ columns,
119
+ gone
120
+ });
121
+ }
122
+ const finish = useCallback((key) => setState((current) => ({
123
+ ...current,
124
+ gone: current.gone.filter((column) => column.key !== key)
125
+ })), []);
126
+ const all = [...columns.map((column) => ({
127
+ column,
128
+ leaving: false
129
+ })), ...gone.map((column) => ({
130
+ column,
131
+ leaving: true
132
+ }))].sort((a, b) => b.column.key - a.column.key);
133
+ return /* @__PURE__ */ jsx(Fragment$1, { children: all.map(({ column, leaving }) => /* @__PURE__ */ jsx(DigitColumn, {
134
+ column,
135
+ direction,
136
+ leaving,
137
+ still,
138
+ onDone: finish
139
+ }, column.key)) });
140
+ }
141
+ function toColumns(word, seq, previous) {
142
+ const chars = word.split("");
143
+ return chars.map((char, index) => {
144
+ const key = chars.length - index;
145
+ const old = previous.get(key);
146
+ return {
147
+ key,
148
+ char,
149
+ seq: old ? old.seq : seq
150
+ };
151
+ });
152
+ }
153
+ /**
154
+ * Copy that changes word by word. Words are keyed by position (numbers keep
155
+ * their identity, so only their digits move), so inserting a word near the
156
+ * front replaces every word after it. A changed word rises in from a soft blur
157
+ * and the old one lifts away from where it stood. Assistive technology reads
158
+ * the plain copy; the animated copy is hidden from it.
159
+ */
160
+ function MotionText({ text, exit, numbers }) {
161
+ const [state, setState] = useState(() => ({
162
+ text,
163
+ seq: 0,
164
+ words: toWords(text, 0, /* @__PURE__ */ new Map(), numbers)
165
+ }));
166
+ let { words } = state;
167
+ if (state.text !== text) {
168
+ const seq = state.seq + 1;
169
+ words = toWords(text, seq, new Map(state.words.map((word) => [word.key, word])), numbers);
170
+ setState({
171
+ text,
172
+ seq,
173
+ words
174
+ });
175
+ }
176
+ const rowRef = useRef(null);
177
+ const before = useRef(/* @__PURE__ */ new Map());
178
+ const nextId = useRef(0);
179
+ const [leaving, setLeaving] = useState([]);
180
+ useIsomorphicLayoutEffect(() => {
181
+ const row = rowRef.current;
182
+ if (!row) return;
183
+ const now = /* @__PURE__ */ new Map();
184
+ for (const node of row.querySelectorAll("[data-word]")) now.set(node.dataset.word, {
185
+ rect: rectOf(node),
186
+ word: node.textContent ?? ""
187
+ });
188
+ const gone = [];
189
+ if (!prefersReducedMotion()) {
190
+ for (const [key, was] of before.current) if (!now.has(key) && was.rect.width && was.rect.height) gone.push({
191
+ id: nextId.current++,
192
+ word: was.word,
193
+ rect: was.rect
194
+ });
195
+ }
196
+ before.current = now;
197
+ if (gone.length) setLeaving((list) => [...list, ...gone]);
198
+ });
199
+ const finish = useCallback((id) => setLeaving((list) => list.filter((item) => item.id !== id)), []);
200
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
201
+ className: "sr-only",
202
+ children: text
203
+ }), /* @__PURE__ */ jsxs("span", {
204
+ ref: rowRef,
205
+ "aria-hidden": "true",
206
+ className: "relative block",
207
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingWordNode, {
208
+ item,
209
+ onDone: finish,
210
+ exit
211
+ }, item.id)), words.map((entry, index) => /* @__PURE__ */ jsxs("span", {
212
+ "data-word": entry.key,
213
+ className: cn("inline-block whitespace-pre", entry.seq > 0 && "motion-safe:animate-field-word-in motion-reduce:animate-fade-in"),
214
+ children: [numbers && isNumber(entry.word) ? /* @__PURE__ */ jsx(RollingNumber, { word: entry.word }) : entry.word, index < words.length - 1 ? " " : null]
215
+ }, entry.key))]
216
+ })] });
217
+ }
218
+ function toWords(text, seq, previous, numbers) {
219
+ return text.split(" ").map((word, index) => {
220
+ const key = `${index}:${numbers && isNumber(word) ? "#" : word}`;
221
+ const old = previous.get(key);
222
+ return {
223
+ key,
224
+ word,
225
+ seq: old ? old.seq : seq
226
+ };
227
+ });
228
+ }
229
+ /** Writes the copy's height where the slot's height transition reads it. */
230
+ function syncHeight(slot, copy) {
231
+ const height = copy.getBoundingClientRect().height;
232
+ if (height) slot.style.setProperty("--field-message-h", height + "px");
233
+ }
234
+ /**
235
+ * A field's helper or error row. `text` empty means no row. A row that appears
236
+ * after mount opens its height on a spring and its copy settles in; one that
237
+ * goes away closes the same way, still showing its last text while it does, and
238
+ * text that comes back mid-close reopens the same row. A row present at first
239
+ * render never animates. Error copy is an alert.
240
+ *
241
+ * The slot's height is written from script (a transition cannot start from or
242
+ * end at auto): measured in a layout effect before the row opens, and again
243
+ * whenever the copy resizes, so a message that wraps opens its next line.
244
+ */
245
+ function FieldMessage({ id, text, tone, wordExit = "settle", numbers = true, copy = "words" }) {
246
+ const [row, setRow] = useState(() => text ? {
247
+ text,
248
+ phase: "open",
249
+ animate: false
250
+ } : null);
251
+ if (text && (!row || row.text !== text || row.phase === "closing")) setRow(row ? {
252
+ text,
253
+ phase: row.phase === "closing" ? "open" : row.phase,
254
+ animate: row.animate
255
+ } : {
256
+ text,
257
+ phase: "entering",
258
+ animate: true
259
+ });
260
+ else if (!text && row && row.phase !== "closing") setRow({
261
+ ...row,
262
+ phase: "closing"
263
+ });
264
+ const slotRef = useRef(null);
265
+ const copyRef = useRef(null);
266
+ const phase = row?.phase;
267
+ useIsomorphicLayoutEffect(() => {
268
+ const slot = slotRef.current;
269
+ const copy = copyRef.current;
270
+ if (!slot || !copy || phase === "closing") return;
271
+ syncHeight(slot, copy);
272
+ if (phase === "entering") {
273
+ slot.offsetHeight;
274
+ setRow((current) => current?.phase === "entering" ? {
275
+ ...current,
276
+ phase: "open"
277
+ } : current);
278
+ }
279
+ });
280
+ useIsomorphicLayoutEffect(() => {
281
+ if (phase !== "closing") return;
282
+ const slot = slotRef.current;
283
+ const gone = () => setRow((current) => current?.phase === "closing" ? null : current);
284
+ if (!slot || prefersReducedMotion()) {
285
+ gone();
286
+ return;
287
+ }
288
+ const style = getComputedStyle(slot);
289
+ if (parseFloat(style.height) === 0 || style.opacity === "0") {
290
+ gone();
291
+ return;
292
+ }
293
+ const onEnd = (event) => {
294
+ if (event.target === slot && event.propertyName === "height") gone();
295
+ };
296
+ slot.addEventListener("transitionend", onEnd);
297
+ const backstop = setTimeout(gone, SPRING_BACKSTOP);
298
+ return () => {
299
+ slot.removeEventListener("transitionend", onEnd);
300
+ clearTimeout(backstop);
301
+ };
302
+ }, [phase]);
303
+ useEffect(() => {
304
+ const slot = slotRef.current;
305
+ const copy = copyRef.current;
306
+ if (!slot || !copy || typeof ResizeObserver === "undefined") return;
307
+ let frame = 0;
308
+ const observer = new ResizeObserver(([entry]) => {
309
+ const height = entry?.borderBoxSize[0]?.blockSize ?? copy.offsetHeight;
310
+ if (!height) return;
311
+ cancelAnimationFrame(frame);
312
+ frame = requestAnimationFrame(() => slot.style.setProperty("--field-message-h", height + "px"));
313
+ });
314
+ observer.observe(copy);
315
+ return () => {
316
+ cancelAnimationFrame(frame);
317
+ observer.disconnect();
318
+ };
319
+ }, [row !== null]);
320
+ if (!row) return null;
321
+ const closing = row.phase === "closing";
322
+ return /* @__PURE__ */ jsx("span", {
323
+ ref: slotRef,
324
+ "data-slot": "field-message-slot",
325
+ "data-state": closing || row.phase === "entering" ? "closed" : "open",
326
+ className: "field-message-slot",
327
+ children: /* @__PURE__ */ jsx("span", {
328
+ ref: copyRef,
329
+ id: closing ? void 0 : id,
330
+ role: tone === "error" && !closing ? "alert" : void 0,
331
+ className: cn("block pt-2 text-sm leading-(--leading-normal) " + (tone === "error" ? "text-destructive-strong" : "text-muted-foreground") + (row.animate ? " motion-safe:animate-field-copy-in" : "")),
332
+ children: copy === "swap" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
333
+ className: "sr-only",
334
+ children: row.text
335
+ }), /* @__PURE__ */ jsx("span", {
336
+ "aria-hidden": "true",
337
+ className: "block",
338
+ children: /* @__PURE__ */ jsx(SwapText, {
339
+ swapKey: row.text,
340
+ className: "block",
341
+ children: row.text
342
+ })
343
+ })] }) : /* @__PURE__ */ jsx(MotionText, {
344
+ text: row.text,
345
+ exit: wordExit,
346
+ numbers
347
+ })
348
+ })
349
+ });
350
+ }
351
+ //#endregion
352
+ export { FieldMessage, RollingNumber };
353
+
354
+ //# sourceMappingURL=field-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-message.js","names":[],"sources":["../../src/lib/field-message.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\n\nimport { cn } from \"./cn.js\";\nimport {\n prefersReducedMotion,\n useExitBackstop,\n useIsomorphicLayoutEffect,\n useLeavingItems,\n type Rect,\n} from \"./motion.js\";\nimport { SwapText } from \"./swap-text.js\";\n\n/*\n * The helper and error copy under a text field. Three layers of presence, each\n * a small piece of React because CSS cannot keep a node that React has already\n * unmounted: the row (opens and closes its height), the words (a changed word\n * rises in while the old one lifts away) and the numbers (one column per digit,\n * a gained or lost digit opens or closes its width, a changed digit rolls).\n *\n * Nothing animates on first mount at any layer: a row present at first render,\n * words in a row that has just opened, and digits in a column that has just\n * opened all appear as they are.\n */\n\nconst isNumber = (word: string) => /^\\d[\\d.,/:]*%?$/.test(word);\n\n/** How a replaced word leaves: Input's 180ms settle, Textarea's 160ms, or the sharper ease-exit curve Password field uses. */\nexport type WordExit = \"settle\" | \"quick\" | \"sharp\";\n\nconst WORD_OUT: Record<WordExit, string> = {\n settle: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out\",\n quick: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-quick\",\n sharp: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-sharp\",\n};\n\n/** The longest a spring exit takes (736ms) plus a margin, for the backstop that removes a node whose animation never ends. */\nconst SPRING_BACKSTOP = 900;\n\nconst rectOf = (node: HTMLElement): Rect => ({\n left: node.offsetLeft,\n top: node.offsetTop,\n width: node.offsetWidth,\n height: node.offsetHeight,\n});\n\ninterface Word {\n key: string;\n word: string;\n /** 0 for the words the text mounted with, which do not animate in. */\n seq: number;\n}\n\ninterface LeavingWord {\n id: number;\n word: string;\n rect: Rect;\n}\n\nfunction LeavingWordNode({\n item,\n onDone,\n exit,\n}: {\n item: LeavingWord;\n onDone: (id: number) => void;\n exit: WordExit;\n}) {\n const { id } = item;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={item.rect}\n className={WORD_OUT[exit]}\n >\n {item.word}\n </span>\n );\n}\n\ninterface Column {\n /** Distance from the end of the number, so a new leading digit adds a column on the left. */\n key: number;\n char: string;\n seq: number;\n}\n\nfunction LeavingDigit({\n id,\n rect,\n onDone,\n children,\n}: {\n id: number;\n rect: Rect;\n onDone: (id: number) => void;\n children: string;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove, SPRING_BACKSTOP);\n // Reduced motion swaps instantly, and an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (prefersReducedMotion()) remove();\n }, [remove]);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget && event.animationName === \"field-digit-move-out\")\n remove();\n }}\n style={rect}\n className=\"absolute motion-safe:animate-field-digit-out\"\n >\n {children}\n </span>\n );\n}\n\n/** One digit, rolling in or not as it was when it mounted: a later change of `animate` must not replay it. */\nfunction Digit({\n nodeRef,\n animate,\n children,\n}: {\n nodeRef: RefObject<HTMLSpanElement | null>;\n animate: boolean;\n children: string;\n}) {\n const [rolls] = useState(animate);\n return (\n <span ref={nodeRef} className={rolls ? \"motion-safe:animate-field-digit-in\" : undefined}>\n {children}\n </span>\n );\n}\n\nfunction DigitColumn({\n column,\n direction,\n leaving,\n still,\n onDone,\n}: {\n column: Column;\n direction: number;\n leaving: boolean;\n /** Show the digits as they are, without rolling or opening columns. */\n still: boolean;\n onDone: (key: number) => void;\n}) {\n const digitRef = useRef<HTMLSpanElement>(null);\n const digits = useLeavingItems(column.char, column.char, () => digitRef.current);\n // A digit that arrives after the column is already there rolls in; the one it mounted with does not.\n const [shownDigit, setShownDigit] = useState({ char: column.char, changes: 0 });\n if (shownDigit.char !== column.char)\n setShownDigit({ char: column.char, changes: shownDigit.changes + 1 });\n const done = useCallback(() => onDone(column.key), [onDone, column.key]);\n useExitBackstop(leaving, done, SPRING_BACKSTOP);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) done();\n }, [leaving, done]);\n return (\n <span\n style={{ \"--roll\": direction } as CSSProperties}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-column-shrink\"\n )\n done();\n }}\n className={cn(\n \"field-number-column\",\n leaving\n ? \"motion-safe:animate-field-column-out\"\n : column.seq > 0 && \"motion-safe:animate-field-column-in\",\n )}\n >\n {(still ? [] : digits.leaving).map((item) => (\n <LeavingDigit key={item.id} id={item.id} rect={item.rect} onDone={digits.remove}>\n {item.value}\n </LeavingDigit>\n ))}\n <Digit key={column.char} nodeRef={digitRef} animate={shownDigit.changes > 0 && !still}>\n {column.char}\n </Digit>\n </span>\n );\n}\n\n/**\n * A count in the copy, such as \"12 characters\": only the digits that changed\n * roll, and a gained or lost digit opens or closes its width. Digits arrive\n * from below when the number grew and from above when it shrank.\n */\nexport function RollingNumber({\n word,\n direction: forced,\n still = false,\n}: {\n word: string;\n /** Which way the digits roll (1 up, -1 down). Read from the number's own change by default, which fails for text such as \"$12\". */\n direction?: 1 | -1;\n /** Follow the text at once, with no rolling: for a number that is being typed. */\n still?: boolean;\n}) {\n const [state, setState] = useState(() => ({\n word,\n direction: 1,\n seq: 0,\n columns: toColumns(word, 0, new Map<number, Column>()),\n gone: [] as Column[],\n }));\n let { columns, gone, direction } = state;\n if (state.word !== word) {\n const seq = state.seq + 1;\n direction =\n forced ??\n (parseFloat(word.replace(/,/g, \"\")) < parseFloat(state.word.replace(/,/g, \"\")) ? -1 : 1);\n // A still number opens its new columns at once: seq 0 is the seq of a column the text mounted with.\n columns = toColumns(\n word,\n still ? 0 : seq,\n new Map(state.columns.map((column) => [column.key, column])),\n );\n const kept = new Set(columns.map((column) => column.key));\n // A column that comes back while it is closing is shown again, not kept twice.\n gone = still\n ? []\n : [\n ...state.gone.filter((column) => !kept.has(column.key)),\n ...state.columns.filter((column) => !kept.has(column.key)),\n ];\n setState({ word, direction, seq, columns, gone });\n }\n const finish = useCallback(\n (key: number) =>\n setState((current) => ({\n ...current,\n gone: current.gone.filter((column) => column.key !== key),\n })),\n [],\n );\n // Left to right is key descending, so a closing column keeps its place while it shrinks.\n const all = [\n ...columns.map((column) => ({ column, leaving: false })),\n ...gone.map((column) => ({ column, leaving: true })),\n ].sort((a, b) => b.column.key - a.column.key);\n return (\n <>\n {all.map(({ column, leaving }) => (\n <DigitColumn\n key={column.key}\n column={column}\n direction={direction}\n leaving={leaving}\n still={still}\n onDone={finish}\n />\n ))}\n </>\n );\n}\n\nfunction toColumns(word: string, seq: number, previous: Map<number, Column>): Column[] {\n const chars = word.split(\"\");\n return chars.map((char, index) => {\n const key = chars.length - index;\n const old = previous.get(key);\n return { key, char, seq: old ? old.seq : seq };\n });\n}\n\n/**\n * Copy that changes word by word. Words are keyed by position (numbers keep\n * their identity, so only their digits move), so inserting a word near the\n * front replaces every word after it. A changed word rises in from a soft blur\n * and the old one lifts away from where it stood. Assistive technology reads\n * the plain copy; the animated copy is hidden from it.\n */\nfunction MotionText({ text, exit, numbers }: { text: string; exit: WordExit; numbers: boolean }) {\n const [state, setState] = useState(() => ({\n text,\n seq: 0,\n words: toWords(text, 0, new Map<string, Word>(), numbers),\n }));\n let { words } = state;\n if (state.text !== text) {\n const seq = state.seq + 1;\n words = toWords(text, seq, new Map(state.words.map((word) => [word.key, word])), numbers);\n setState({ text, seq, words });\n }\n\n const rowRef = useRef<HTMLSpanElement>(null);\n const before = useRef(new Map<string, { rect: Rect; word: string }>());\n const nextId = useRef(0);\n const [leaving, setLeaving] = useState<LeavingWord[]>([]);\n\n // After every commit: remember where each word stands, and keep the ones that are gone, where they stood.\n useIsomorphicLayoutEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n const now = new Map<string, { rect: Rect; word: string }>();\n for (const node of row.querySelectorAll<HTMLElement>(\"[data-word]\")) {\n now.set(node.dataset.word!, { rect: rectOf(node), word: node.textContent ?? \"\" });\n }\n const gone: LeavingWord[] = [];\n // Reduced motion removes a replaced word at once, and a box with no size has nothing to lift away.\n if (!prefersReducedMotion()) {\n for (const [key, was] of before.current) {\n if (!now.has(key) && was.rect.width && was.rect.height)\n gone.push({ id: nextId.current++, word: was.word, rect: was.rect });\n }\n }\n before.current = now;\n if (gone.length) setLeaving((list) => [...list, ...gone]);\n });\n\n const finish = useCallback(\n (id: number) => setLeaving((list) => list.filter((item) => item.id !== id)),\n [],\n );\n\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span ref={rowRef} aria-hidden=\"true\" className=\"relative block\">\n {leaving.map((item) => (\n <LeavingWordNode key={item.id} item={item} onDone={finish} exit={exit} />\n ))}\n {words.map((entry, index) => (\n <span\n key={entry.key}\n data-word={entry.key}\n className={cn(\n \"inline-block whitespace-pre\",\n entry.seq > 0 && \"motion-safe:animate-field-word-in motion-reduce:animate-fade-in\",\n )}\n >\n {numbers && isNumber(entry.word) ? <RollingNumber word={entry.word} /> : entry.word}\n {index < words.length - 1 ? \" \" : null}\n </span>\n ))}\n </span>\n </>\n );\n}\n\nfunction toWords(text: string, seq: number, previous: Map<string, Word>, numbers: boolean): Word[] {\n return text.split(\" \").map((word, index) => {\n const key = `${index}:${numbers && isNumber(word) ? \"#\" : word}`;\n const old = previous.get(key);\n return { key, word, seq: old ? old.seq : seq };\n });\n}\n\ninterface Row {\n text: string;\n /** entering: mounted closed, about to open. closing: kept on screen while it shuts. */\n phase: \"entering\" | \"open\" | \"closing\";\n /** False for a row the field mounted with, whose copy does not settle in. */\n animate: boolean;\n}\n\n/** Writes the copy's height where the slot's height transition reads it. */\nfunction syncHeight(slot: HTMLElement, copy: HTMLElement) {\n const height = copy.getBoundingClientRect().height;\n if (height) slot.style.setProperty(\"--field-message-h\", height + \"px\");\n}\n\n/**\n * A field's helper or error row. `text` empty means no row. A row that appears\n * after mount opens its height on a spring and its copy settles in; one that\n * goes away closes the same way, still showing its last text while it does, and\n * text that comes back mid-close reopens the same row. A row present at first\n * render never animates. Error copy is an alert.\n *\n * The slot's height is written from script (a transition cannot start from or\n * end at auto): measured in a layout effect before the row opens, and again\n * whenever the copy resizes, so a message that wraps opens its next line.\n */\nexport function FieldMessage({\n id,\n text,\n tone,\n wordExit = \"settle\",\n numbers = true,\n copy = \"words\",\n}: {\n id?: string | undefined;\n text?: string | undefined;\n tone: \"description\" | \"error\";\n /** How a replaced word leaves (Input settles in 180ms, Textarea in 160ms, Password field uses the sharper exit curve). */\n wordExit?: WordExit;\n /** Roll digits in counts such as \"12 characters\". Off for copy that is words only. */\n numbers?: boolean;\n /** `\"words\"` animates a changed message word by word; `\"swap\"` replaces it as a whole, rising in as the old one lifts away. */\n copy?: \"words\" | \"swap\";\n}) {\n // Decided during render, so the id exists in the same commit as the aria-describedby that names it.\n const [row, setRow] = useState<Row | null>(() =>\n text ? { text, phase: \"open\", animate: false } : null,\n );\n if (text && (!row || row.text !== text || row.phase === \"closing\")) {\n setRow(\n row\n ? { text, phase: row.phase === \"closing\" ? \"open\" : row.phase, animate: row.animate }\n : { text, phase: \"entering\", animate: true },\n );\n } else if (!text && row && row.phase !== \"closing\") {\n setRow({ ...row, phase: \"closing\" });\n }\n\n const slotRef = useRef<HTMLSpanElement>(null);\n const copyRef = useRef<HTMLSpanElement>(null);\n const phase = row?.phase;\n\n // Measure before the row opens, and force the closed style to be computed first so there is something to transition from.\n useIsomorphicLayoutEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || phase === \"closing\") return;\n syncHeight(slot, copy);\n if (phase === \"entering\") {\n void slot.offsetHeight;\n setRow((current) =>\n current?.phase === \"entering\" ? { ...current, phase: \"open\" } : current,\n );\n }\n });\n\n // Remove the row once its height has closed. A row that never opened, or reduced motion, has nothing to wait for.\n useIsomorphicLayoutEffect(() => {\n if (phase !== \"closing\") return;\n const slot = slotRef.current;\n const gone = () => setRow((current) => (current?.phase === \"closing\" ? null : current));\n if (!slot || prefersReducedMotion()) {\n gone();\n return;\n }\n const style = getComputedStyle(slot);\n if (parseFloat(style.height) === 0 || style.opacity === \"0\") {\n gone();\n return;\n }\n const onEnd = (event: TransitionEvent) => {\n if (event.target === slot && event.propertyName === \"height\") gone();\n };\n slot.addEventListener(\"transitionend\", onEnd);\n const backstop = setTimeout(gone, SPRING_BACKSTOP);\n return () => {\n slot.removeEventListener(\"transitionend\", onEnd);\n clearTimeout(backstop);\n };\n }, [phase]);\n\n // Later size changes only (the first measurement is the layout effect above). A frame later, so the write cannot report a ResizeObserver loop.\n const mounted = row !== null;\n useEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || typeof ResizeObserver === \"undefined\") return;\n let frame = 0;\n const observer = new ResizeObserver(([entry]) => {\n const height = entry?.borderBoxSize[0]?.blockSize ?? copy.offsetHeight;\n if (!height) return;\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() =>\n slot.style.setProperty(\"--field-message-h\", height + \"px\"),\n );\n });\n observer.observe(copy);\n return () => {\n cancelAnimationFrame(frame);\n observer.disconnect();\n };\n }, [mounted]);\n\n if (!row) return null;\n const closing = row.phase === \"closing\";\n return (\n <span\n ref={slotRef}\n data-slot=\"field-message-slot\"\n data-state={closing || row.phase === \"entering\" ? \"closed\" : \"open\"}\n className=\"field-message-slot\"\n >\n <span\n ref={copyRef}\n // A closing row no longer describes the field: it gives up its id and its alert role.\n id={closing ? undefined : id}\n role={tone === \"error\" && !closing ? \"alert\" : undefined}\n className={cn(\n \"block pt-2 text-sm leading-(--leading-normal) \" +\n (tone === \"error\" ? \"text-destructive-strong\" : \"text-muted-foreground\") +\n (row.animate ? \" motion-safe:animate-field-copy-in\" : \"\"),\n )}\n >\n {copy === \"swap\" ? (\n <>\n <span className=\"sr-only\">{row.text}</span>\n <span aria-hidden=\"true\" className=\"block\">\n <SwapText swapKey={row.text} className=\"block\">\n {row.text}\n </SwapText>\n </span>\n </>\n ) : (\n <MotionText text={row.text} exit={wordExit} numbers={numbers} />\n )}\n </span>\n </span>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { Label as Label$1 } from "../label/label.js";
2
+ import { FieldMessage } from "./field-message.js";
3
+ import { useId } from "react";
4
+ import { InputContext, LabelContext, useSlottedContext } from "react-aria-components";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/lib/field-shell.tsx
7
+ /**
8
+ * True when any field prop was passed at all, even as `undefined`. Deciding by
9
+ * presence, not value, is what keeps `error={maybeMessage}` from moving the
10
+ * control between a wrapper and no wrapper when the message appears: that would
11
+ * remount the control and drop focus and an uncontrolled value.
12
+ */
13
+ const hasFieldProps = (props) => "label" in props || "description" in props || "error" in props;
14
+ /** A control inside a React Aria `TextField` already has its label and messages supplied by the field. */
15
+ function useInsideReactAriaField() {
16
+ const label = useSlottedContext(LabelContext);
17
+ const input = useSlottedContext(InputContext);
18
+ return Object.keys(label ?? {}).length > 0 || Object.keys(input ?? {}).length > 0;
19
+ }
20
+ /**
21
+ * The ids and aria wiring for a control with field props. `active` is false
22
+ * when no field prop was passed, or inside a React Aria field, and then
23
+ * `controlProps` is empty: the control's own props are untouched, and a bare
24
+ * control has no generated `id`.
25
+ */
26
+ function useFieldWiring({ enabled, id, describedBy, invalid, description, error }) {
27
+ const generated = useId();
28
+ const inside = useInsideReactAriaField();
29
+ const active = enabled && !inside;
30
+ const controlId = id ?? generated;
31
+ const hintId = active && description ? `${controlId}-description` : void 0;
32
+ const errorId = active && error ? `${controlId}-error` : void 0;
33
+ return {
34
+ active,
35
+ controlId,
36
+ hintId,
37
+ errorId,
38
+ controlProps: active ? {
39
+ id: controlId,
40
+ "aria-invalid": error ? true : invalid,
41
+ "aria-describedby": [
42
+ describedBy,
43
+ hintId,
44
+ errorId
45
+ ].filter(Boolean).join(" ") || void 0
46
+ } : {}
47
+ };
48
+ }
49
+ /** The wrapper: label above the control, then the helper row and the error row. Each row owns its top space, so a closed row costs nothing. */
50
+ function FieldShell({ label, controlId, description, hintId, error, errorId, wordExit, numbers, children }) {
51
+ return /* @__PURE__ */ jsxs("div", {
52
+ "data-slot": "field",
53
+ className: "grid min-w-0 font-sans",
54
+ children: [
55
+ label == null ? null : /* @__PURE__ */ jsx(Label$1, {
56
+ htmlFor: controlId,
57
+ className: "mb-1.5",
58
+ children: label
59
+ }),
60
+ children,
61
+ /* @__PURE__ */ jsx(FieldMessage, {
62
+ id: hintId,
63
+ text: description,
64
+ tone: "description",
65
+ wordExit,
66
+ numbers
67
+ }),
68
+ /* @__PURE__ */ jsx(FieldMessage, {
69
+ id: errorId,
70
+ text: error,
71
+ tone: "error",
72
+ wordExit,
73
+ numbers
74
+ })
75
+ ]
76
+ });
77
+ }
78
+ //#endregion
79
+ export { FieldShell, hasFieldProps, useFieldWiring };
80
+
81
+ //# sourceMappingURL=field-shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-shell.js","names":["Label"],"sources":["../../src/lib/field-shell.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { InputContext, LabelContext, useSlottedContext } from \"react-aria-components\";\n\nimport { Label } from \"../label/label.js\";\nimport { FieldMessage, type WordExit } from \"./field-message.js\";\n\n/*\n * The label, helper text and error text around a text control, for Input and\n * Textarea. The first lib file that imports a sibling component (Label): the\n * layer rule allows primitives to use primitives, and a field's label is the\n * same Label everywhere else.\n */\n\n/**\n * True when any field prop was passed at all, even as `undefined`. Deciding by\n * presence, not value, is what keeps `error={maybeMessage}` from moving the\n * control between a wrapper and no wrapper when the message appears: that would\n * remount the control and drop focus and an uncontrolled value.\n */\nexport const hasFieldProps = (props: object) =>\n \"label\" in props || \"description\" in props || \"error\" in props;\n\n/** A control inside a React Aria `TextField` already has its label and messages supplied by the field. */\nfunction useInsideReactAriaField() {\n const label = useSlottedContext(LabelContext);\n const input = useSlottedContext(InputContext);\n return Object.keys(label ?? {}).length > 0 || Object.keys(input ?? {}).length > 0;\n}\n\n/**\n * The ids and aria wiring for a control with field props. `active` is false\n * when no field prop was passed, or inside a React Aria field, and then\n * `controlProps` is empty: the control's own props are untouched, and a bare\n * control has no generated `id`.\n */\nexport function useFieldWiring({\n enabled,\n id,\n describedBy,\n invalid,\n description,\n error,\n}: {\n enabled: boolean;\n id: string | undefined;\n describedBy: string | undefined;\n invalid: boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\" | undefined;\n description: string | undefined;\n error: string | undefined;\n}) {\n const generated = useId();\n const inside = useInsideReactAriaField();\n const active = enabled && !inside;\n const controlId = id ?? generated;\n const hintId = active && description ? `${controlId}-description` : undefined;\n const errorId = active && error ? `${controlId}-error` : undefined;\n return {\n active,\n controlId,\n hintId,\n errorId,\n controlProps: active\n ? {\n id: controlId,\n \"aria-invalid\": error ? true : invalid,\n \"aria-describedby\": [describedBy, hintId, errorId].filter(Boolean).join(\" \") || undefined,\n }\n : {},\n };\n}\n\n/** The wrapper: label above the control, then the helper row and the error row. Each row owns its top space, so a closed row costs nothing. */\nexport function FieldShell({\n label,\n controlId,\n description,\n hintId,\n error,\n errorId,\n wordExit,\n numbers,\n children,\n}: {\n label: ReactNode;\n controlId: string;\n description: string | undefined;\n hintId: string | undefined;\n error: string | undefined;\n errorId: string | undefined;\n wordExit?: WordExit;\n numbers?: boolean;\n children: ReactNode;\n}) {\n return (\n <div data-slot=\"field\" className=\"grid min-w-0 font-sans\">\n {label == null ? null : (\n // 6px, the gap Select and DatePicker keep between their label and control, so a labelled Input sits level with them.\n <Label htmlFor={controlId} className=\"mb-1.5\">\n {label}\n </Label>\n )}\n {children}\n <FieldMessage\n id={hintId}\n text={description}\n tone=\"description\"\n wordExit={wordExit}\n numbers={numbers}\n />\n <FieldMessage id={errorId} text={error} tone=\"error\" wordExit={wordExit} numbers={numbers} />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAa,iBAAiB,UAC5B,WAAW,SAAS,iBAAiB,SAAS,WAAW;;AAG3D,SAAS,0BAA0B;CACjC,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;AAClF;;;;;;;AAQA,SAAgB,eAAe,EAC7B,SACA,IACA,aACA,SACA,aACA,SAQC;CACD,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,wBAAwB;CACvC,MAAM,SAAS,WAAW,CAAC;CAC3B,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,UAAU,cAAc,GAAG,UAAU,gBAAgB,KAAA;CACpE,MAAM,UAAU,UAAU,QAAQ,GAAG,UAAU,UAAU,KAAA;CACzD,OAAO;EACL;EACA;EACA;EACA;EACA,cAAc,SACV;GACE,IAAI;GACJ,gBAAgB,QAAQ,OAAO;GAC/B,oBAAoB;IAAC;IAAa;IAAQ;GAAO,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;EAClF,IACA,CAAC;CACP;AACF;;AAGA,SAAgB,WAAW,EACzB,OACA,WACA,aACA,QACA,OACA,SACA,UACA,SACA,YAWC;CACD,OACE,qBAAC,OAAD;EAAK,aAAU;EAAQ,WAAU;EAAjC,UAAA;GACG,SAAS,OAAO,OAEf,oBAACA,SAAD;IAAO,SAAS;IAAW,WAAU;IAClC,UAAA;GACI,CAAA;GAER;GACD,oBAAC,cAAD;IACE,IAAI;IACJ,MAAM;IACN,MAAK;IACK;IACD;GACV,CAAA;GACD,oBAAC,cAAD;IAAc,IAAI;IAAS,MAAM;IAAO,MAAK;IAAkB;IAAmB;GAAU,CAAA;EACzF;;AAET"}
@@ -20,19 +20,22 @@ import { cva } from "class-variance-authority";
20
20
  const fieldSurfaceVariants = cva([
21
21
  "w-full min-w-0",
22
22
  "box-border font-sans",
23
- "rounded-md border-(length:--border-width) border-border",
24
- "bg-background text-foreground",
23
+ "rounded-control border-(length:--border-width) border-border",
24
+ "bg-surface text-foreground",
25
25
  "shadow-surface",
26
26
  "placeholder:text-muted-foreground",
27
27
  "selection:bg-primary selection:text-primary-foreground",
28
- "motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard",
28
+ "motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle",
29
29
  "outline-none",
30
- "data-[focus-visible]:outline-solid",
31
- "data-[focus-visible]:outline-(length:--focus-ring-width)",
32
- "data-[focus-visible]:outline-offset-(--focus-ring-offset)",
33
- "data-[focus-visible]:outline-(--color-ring)",
30
+ "forced-colors:data-[focus-visible]:outline-solid",
31
+ "forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)",
32
+ "forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)",
34
33
  "data-[invalid]:border-destructive-strong",
35
- "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)"
34
+ "[&:where([data-hovered]:not([data-disabled]))]:border-border-strong",
35
+ "data-[focus-visible]:not-data-[invalid]:not-group-data-[invalid]:border-foreground",
36
+ "[&:is(input,textarea):focus:not([data-invalid])]:border-foreground",
37
+ "[&:has(:is(input,textarea,[role=spinbutton]):focus):not([data-invalid]):not(:where(.group[data-invalid]_*))]:border-foreground",
38
+ "data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:opacity-(--opacity-disabled)"
36
39
  ]);
37
40
  //#endregion
38
41
  export { fieldSurfaceVariants };
@@ -1 +1 @@
1
- {"version":3,"file":"field-surface.js","names":[],"sources":["../../src/lib/field-surface.ts"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\n/**\n * The shared surface of a text entry control.\n *\n * Extracted at the Rule of Two, and for a reason the legacy demonstrates:\n * its `input.tsx` and `textarea.tsx` styled the same surface twice and had\n * already drifted. The input is `bg-transparent`, the textarea is\n * `bg-background`. The input focuses with `focus-visible:border-ring` plus a\n * 3px ring; the textarea uses a 2px ring with an offset. The input has\n * `aria-invalid` treatment, the textarea has none. None of those differences\n * is a decision anyone made -- they are two files edited at different times.\n *\n * Everything that must look identical between an Input and a Textarea lives\n * here. Only the box geometry differs, and that is the caller's variant.\n *\n * Every string is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would never be emitted.\n */\nexport const fieldSurfaceVariants = cva([\n \"w-full min-w-0\",\n\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <input> keeps the user-agent border, background and Arial face,\n // and content-box would add the padding on top of the width.\n \"box-border font-sans\",\n\n \"rounded-md border-(length:--border-width) border-border\",\n \"bg-background text-foreground\",\n \"shadow-surface\",\n\n // --color-muted-foreground is 5.25:1 on the page, so the placeholder is\n // legible rather than the usual near-invisible grey. It is still text and\n // the contrast contract holds it to the 4.5 floor.\n \"placeholder:text-muted-foreground\",\n \"selection:bg-primary selection:text-primary-foreground\",\n\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // outline-solid is load-bearing. `outline-none` sets BOTH\n // outline-style:none and --tw-outline-style:none, and the width utility\n // resolves its style from that same variable -- so a focus rule can apply\n // a width and a colour while faithfully re-applying none, and paint\n // nothing. Button shipped exactly that.\n //\n // An outline, not the legacy `ring-[3px]` box-shadow: box-shadows are\n // discarded under forced-colors, taking the only focus indication with\n // them, and axe has no focus-indicator rule to notice.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n // React Aria sets data-invalid from the native `aria-invalid` attribute,\n // so this costs no API surface and works both bare and inside a\n // TextField. destructive-strong rather than destructive: the fill token\n // is 2.44:1-class and cannot describe a boundary (WCAG 1.4.11).\n //\n // The border CHANGES COLOUR only. Colour alone is not a state cue\n // (WCAG 1.4.1), which is why an invalid field must also carry a message --\n // that is the consumer's job, and the migration table says so.\n \"data-[invalid]:border-destructive-strong\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,uBAAuB,IAAI;CACtC;CAKA;CAEA;CACA;CACA;CAKA;CACA;CAEA;CAWA;CACA;CACA;CACA;CACA;CAUA;CAEA;AACF,CAAC"}
1
+ {"version":3,"file":"field-surface.js","names":[],"sources":["../../src/lib/field-surface.ts"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\n/**\n * The shared surface of a text entry control.\n *\n * Extracted at the Rule of Two, and for a reason the legacy demonstrates:\n * its `input.tsx` and `textarea.tsx` styled the same surface twice and had\n * already drifted. The input is `bg-transparent`, the textarea is\n * `bg-background`. The input focuses with `focus-visible:border-ring` plus a\n * 3px ring; the textarea uses a 2px ring with an offset. The input has\n * `aria-invalid` treatment, the textarea has none. None of those differences\n * is a decision anyone made -- they are two files edited at different times.\n *\n * Everything that must look identical between an Input and a Textarea lives\n * here. Only the box geometry differs, and that is the caller's variant.\n *\n * Every string is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would never be emitted.\n */\nexport const fieldSurfaceVariants = cva([\n \"w-full min-w-0\",\n\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <input> keeps the user-agent border, background and Arial face,\n // and content-box would add the padding on top of the width.\n \"box-border font-sans\",\n\n \"rounded-control border-(length:--border-width) border-border\",\n \"bg-surface text-foreground\",\n \"shadow-surface\",\n\n // --color-muted-foreground is 5.25:1 on the page, so the placeholder is\n // legible rather than the usual near-invisible grey. It is still text and\n // the contrast contract holds it to the 4.5 floor.\n \"placeholder:text-muted-foreground\",\n \"selection:bg-primary selection:text-primary-foreground\",\n\n // Only the two properties that change, on the settle curve: the border darkens\n // on hover and focus, and the background turns muted when disabled. The focus\n // outline is not animated (it switches between none and solid).\n \"motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n\n // outline-solid is load-bearing. `outline-none` sets BOTH\n // outline-style:none and --tw-outline-style:none, and the width utility\n // resolves its style from that same variable -- so a focus rule can apply\n // a width and a colour while faithfully re-applying none, and paint\n // nothing. Button shipped exactly that.\n //\n // A text field shows focus on its own border (below), not a separate ring:\n // UX decision (Oct 2026). The outline comes back only under forced colours,\n // where a border colour is overridden and would leave no focus indicator.\n \"outline-none\",\n \"forced-colors:data-[focus-visible]:outline-solid\",\n \"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n\n // React Aria sets data-invalid from the native `aria-invalid` attribute,\n // so this costs no API surface and works both bare and inside a\n // TextField. destructive-strong rather than destructive: the fill token\n // is 2.44:1-class and cannot describe a boundary (WCAG 1.4.11).\n //\n // The border CHANGES COLOUR only. Colour alone is not a state cue\n // (WCAG 1.4.1), which is why an invalid field must also carry a message --\n // that is the consumer's job, and the migration table says so.\n \"data-[invalid]:border-destructive-strong\",\n\n // Hover and focus darken the border to border-strong, in addition to the\n // outline ring above (which stays: forced-colours drops a border colour, and\n // WCAG 2.4.7 wants a visible indicator). The conditions sit inside :where(),\n // which adds no specificity, so the invalid border always wins by specificity\n // rather than by the order Tailwind happens to emit utilities in -- on an\n // Input through data-[invalid], on a Select or DatePicker trigger through\n // group-data-[invalid], because their ROOT carries the state. A disabled field\n // does not darken: React Aria's TextArea still reports hover while disabled.\n \"[&:where([data-hovered]:not([data-disabled]))]:border-border-strong\",\n // Focus turns the border itself dark, same width, no gap. An invalid field\n // keeps its red border while focused.\n \"data-[focus-visible]:not-data-[invalid]:not-group-data-[invalid]:border-foreground\",\n // A mouse click into a text box is not \"focus-visible\" to React Aria, but the\n // border must still show focus: key off real focus on the input itself, or on\n // the input, textarea or date segment inside a wrapping box.\n \"[&:is(input,textarea):focus:not([data-invalid])]:border-foreground\",\n \"[&:has(:is(input,textarea,[role=spinbutton]):focus):not([data-invalid]):not(:where(.group[data-invalid]_*))]:border-foreground\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,uBAAuB,IAAI;CACtC;CAKA;CAEA;CACA;CACA;CAKA;CACA;CAKA;CAWA;CACA;CACA;CACA;CAUA;CAUA;CAGA;CAIA;CACA;CAEA;AACF,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { useSyncExternalStore } from "react";
2
+ //#region src/lib/flag-emoji.ts
3
+ /**
4
+ * Whether this platform draws flag emoji. Windows ships none: a flag there comes
5
+ * out as two boxed letters, so the picker shows the plain ISO code instead.
6
+ *
7
+ * The test draws a flag in black on an offscreen canvas. A real flag glyph is
8
+ * coloured whatever the fill colour is; the fallback letters take the fill and
9
+ * stay black. Any saturated pixel means the platform can draw it.
10
+ */
11
+ function detect() {
12
+ try {
13
+ const canvas = document.createElement("canvas");
14
+ canvas.width = 48;
15
+ canvas.height = 32;
16
+ const context = canvas.getContext("2d", { willReadFrequently: true });
17
+ if (!context) return false;
18
+ context.textBaseline = "top";
19
+ context.font = "24px sans-serif";
20
+ context.fillStyle = "black";
21
+ context.fillText("πŸ‡ΊπŸ‡Έ", 0, 2);
22
+ const { data } = context.getImageData(0, 0, canvas.width, canvas.height);
23
+ for (let i = 0; i < data.length; i += 4) {
24
+ const spread = Math.max(data[i], data[i + 1], data[i + 2]) - Math.min(data[i], data[i + 1], data[i + 2]);
25
+ if (data[i + 3] > 0 && spread > 40) return true;
26
+ }
27
+ return false;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ let cached;
33
+ /** The check runs once per page and its answer is kept. Browser only: it needs a canvas. */
34
+ function supportsFlagEmoji() {
35
+ if (typeof document === "undefined") return false;
36
+ return cached ??= detect();
37
+ }
38
+ const subscribe = () => () => {};
39
+ const serverSnapshot = () => false;
40
+ /**
41
+ * `false` on the server and for the hydrating render (so the markup matches),
42
+ * then the real answer. Never reads `window` at import time.
43
+ */
44
+ function useFlagEmoji() {
45
+ return useSyncExternalStore(subscribe, supportsFlagEmoji, serverSnapshot);
46
+ }
47
+ //#endregion
48
+ export { supportsFlagEmoji, useFlagEmoji };
49
+
50
+ //# sourceMappingURL=flag-emoji.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flag-emoji.js","names":[],"sources":["../../src/lib/flag-emoji.ts"],"sourcesContent":["import { useSyncExternalStore } from \"react\";\n\n/**\n * Whether this platform draws flag emoji. Windows ships none: a flag there comes\n * out as two boxed letters, so the picker shows the plain ISO code instead.\n *\n * The test draws a flag in black on an offscreen canvas. A real flag glyph is\n * coloured whatever the fill colour is; the fallback letters take the fill and\n * stay black. Any saturated pixel means the platform can draw it.\n */\nfunction detect(): boolean {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 48;\n canvas.height = 32;\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) return false;\n context.textBaseline = \"top\";\n context.font = \"24px sans-serif\";\n context.fillStyle = \"black\";\n context.fillText(\"\\u{1F1FA}\\u{1F1F8}\", 0, 2);\n const { data } = context.getImageData(0, 0, canvas.width, canvas.height);\n for (let i = 0; i < data.length; i += 4) {\n const spread = Math.max(data[i]!, data[i + 1]!, data[i + 2]!) - Math.min(data[i]!, data[i + 1]!, data[i + 2]!);\n if (data[i + 3]! > 0 && spread > 40) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\nlet cached: boolean | undefined;\n\n/** The check runs once per page and its answer is kept. Browser only: it needs a canvas. */\nexport function supportsFlagEmoji(): boolean {\n if (typeof document === \"undefined\") return false;\n return (cached ??= detect());\n}\n\nconst subscribe = () => () => {};\nconst serverSnapshot = () => false;\n\n/**\n * `false` on the server and for the hydrating render (so the markup matches),\n * then the real answer. Never reads `window` at import time.\n */\nexport function useFlagEmoji(): boolean {\n return useSyncExternalStore(subscribe, supportsFlagEmoji, serverSnapshot);\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAS,SAAkB;CACzB,IAAI;EACF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,QAAQ;EACf,OAAO,SAAS;EAChB,MAAM,UAAU,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;EACpE,IAAI,CAAC,SAAS,OAAO;EACrB,QAAQ,eAAe;EACvB,QAAQ,OAAO;EACf,QAAQ,YAAY;EACpB,QAAQ,SAAS,QAAsB,GAAG,CAAC;EAC3C,MAAM,EAAE,SAAS,QAAQ,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;EACvE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;GACvC,MAAM,SAAS,KAAK,IAAI,KAAK,IAAK,KAAK,IAAI,IAAK,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,KAAK,IAAK,KAAK,IAAI,IAAK,KAAK,IAAI,EAAG;GAC7G,IAAI,KAAK,IAAI,KAAM,KAAK,SAAS,IAAI,OAAO;EAC9C;EACA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,IAAI;;AAGJ,SAAgB,oBAA6B;CAC3C,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,OAAQ,WAAW,OAAO;AAC5B;AAEA,MAAM,wBAAwB,CAAC;AAC/B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,eAAwB;CACtC,OAAO,qBAAqB,WAAW,mBAAmB,cAAc;AAC1E"}