@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,450 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useReducedMotion } from "motion/react";
5
+ import { Check, Download, Eraser, Play, Redo2, Square, Undo2, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./signature-pad.module.css";
8
+ /** Pad coordinates. Strokes are stored in this space and scale with the pad. */
9
+ export const PAD_WIDTH = 600, PAD_HEIGHT = 260;
10
+ const INK = {
11
+ black: { label: "Black ink", export: "#17171a" },
12
+ blue: { label: "Blue ink", export: "#1d3fae" },
13
+ violet: { label: "Violet ink", export: "#5b35c8" },
14
+ };
15
+ const WIDTHS = {
16
+ fine: { label: "Fine", size: 3.4 },
17
+ medium: { label: "Medium", size: 5.6 },
18
+ bold: { label: "Bold", size: 8.4 },
19
+ };
20
+ const COLORS = Object.keys(INK);
21
+ const SIZES = Object.keys(WIDTHS);
22
+ const add = (a, b) => [a[0] + b[0], a[1] + b[1]];
23
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
24
+ const mul = (a, n) => [a[0] * n, a[1] * n];
25
+ const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
26
+ const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
27
+ const norm = (a) => { const l = Math.hypot(a[0], a[1]); return l ? [a[0] / l, a[1] / l] : [0, 0]; };
28
+ const perp = (a) => [a[1], -a[0]];
29
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1];
30
+ const rotAround = (p, c, r) => {
31
+ const s = Math.sin(r), co = Math.cos(r), px = p[0] - c[0], py = p[1] - c[1];
32
+ return [px * co - py * s + c[0], px * s + py * co + c[1]];
33
+ };
34
+ const easeOut = (t) => t * (2 - t);
35
+ /**
36
+ * Turns sampled points into the outline of a variable width stroke, in the spirit of perfect-freehand. Points are streamlined
37
+ * toward the pointer, width follows pen pressure or, for a mouse or finger, speed, the ends taper, sharp turns get round joins,
38
+ * and both ends get round caps. Returns the outline polygon; pass it to `outlineToPath` for SVG.
39
+ */
40
+ export function getStrokeOutline(input, options) {
41
+ const { size, thinning = .62, streamline = .42, simulatePressure = true, taperStart = 0, taperEnd = 0, last = true } = options;
42
+ const [head] = input;
43
+ if (!head)
44
+ return [];
45
+ const usePen = !simulatePressure && input.some(point => point.p > 0);
46
+ // Streamline: each point eases toward the raw input, which removes jitter without a smoothing pass.
47
+ const pts = [];
48
+ let prev = [head.x, head.y], length = 0;
49
+ const start = { at: prev, pressure: usePen ? head.p : .5, length: 0, vector: [1, 0] };
50
+ pts.push(start);
51
+ for (let i = 1; i < input.length; i++) {
52
+ const sample = input[i];
53
+ const raw = [sample.x, sample.y];
54
+ const at = i === input.length - 1 && last ? raw : lerp(prev, raw, 1 - streamline);
55
+ const d = dist(at, prev);
56
+ if (d < .4)
57
+ continue;
58
+ length += d;
59
+ pts.push({ at, pressure: usePen ? sample.p : .5, length, vector: norm(sub(prev, at)) });
60
+ prev = at;
61
+ }
62
+ const second = pts[1];
63
+ if (second)
64
+ start.vector = second.vector;
65
+ const total = length;
66
+ // A dot: a round mark sized by the first pressure.
67
+ if (pts.length === 1) {
68
+ const r = Math.max(.6, size * (.5 - thinning * (.5 - start.pressure)) * .9);
69
+ return Array.from({ length: 18 }, (_, i) => add(start.at, mul([Math.cos(i / 18 * Math.PI * 2), Math.sin(i / 18 * Math.PI * 2)], r)));
70
+ }
71
+ const left = [], right = [];
72
+ let pressure = start.pressure, prevVector = start.vector;
73
+ const radii = [];
74
+ for (let i = 0; i < pts.length; i++) {
75
+ const point = pts[i];
76
+ if (!usePen) {
77
+ // Speed thins the line: a long step between samples reads as a fast stroke.
78
+ const step = i === 0 ? 0 : dist(point.at, pts[i - 1].at);
79
+ const speed = Math.min(1, step / size);
80
+ const target = Math.min(1, 1 - speed);
81
+ pressure = Math.min(1, pressure + (target - pressure) * speed * .3);
82
+ }
83
+ else {
84
+ pressure = point.pressure;
85
+ }
86
+ let radius = Math.max(.25, size * (.5 - thinning * (.5 - pressure)));
87
+ const ts = taperStart ? easeOut(Math.min(1, point.length / taperStart)) : 1;
88
+ const te = taperEnd && last ? easeOut(Math.min(1, (total - point.length) / taperEnd)) : 1;
89
+ radius = Math.max(.12 * size, radius * Math.min(ts, te));
90
+ radii.push(radius);
91
+ const next = pts[i + 1]?.vector ?? point.vector;
92
+ const offset = mul(perp(lerp(next, point.vector, .5)), radius);
93
+ // A sharp turn: sweep round the corner so the outline does not pinch.
94
+ if (i > 0 && i < pts.length - 1 && dot(point.vector, next) < -.2) {
95
+ const corner = mul(perp(prevVector), radius);
96
+ for (let t = 0; t <= 1; t += 1 / 12) {
97
+ left.push(rotAround(sub(point.at, corner), point.at, Math.PI * t));
98
+ right.push(rotAround(add(point.at, corner), point.at, -Math.PI * t));
99
+ }
100
+ }
101
+ else {
102
+ left.push(sub(point.at, offset));
103
+ right.push(add(point.at, offset));
104
+ }
105
+ prevVector = point.vector;
106
+ }
107
+ // Round caps: half circles swept around the first and last points, on the side facing away from the stroke.
108
+ const cap = (center, from, to, away) => {
109
+ const out = [];
110
+ const r = dist(center, from), start = Math.atan2(from[1] - center[1], from[0] - center[0]);
111
+ const mid = start + Math.PI / 2;
112
+ const sign = dot([Math.cos(mid), Math.sin(mid)], away) >= 0 ? 1 : -1;
113
+ for (let k = 1; k < 12; k++) {
114
+ const a = start + sign * Math.PI * k / 12;
115
+ out.push([center[0] + Math.cos(a) * r, center[1] + Math.sin(a) * r]);
116
+ }
117
+ out.push(to);
118
+ return out;
119
+ };
120
+ const end = pts[pts.length - 1];
121
+ const endCap = cap(end.at, left[left.length - 1], right[right.length - 1], mul(end.vector, -1));
122
+ const startCap = cap(start.at, right[0], left[0], start.vector);
123
+ return [...left, ...endCap, ...right.reverse(), ...startCap];
124
+ }
125
+ const round = (value) => Math.round(value * 100) / 100;
126
+ /** Draws an outline as one smooth closed path, with quadratic curves through the midpoints of its edges. */
127
+ export function outlineToPath(points) {
128
+ if (points.length < 3)
129
+ return "";
130
+ let d = `M${round(points[0][0])} ${round(points[0][1])} Q`;
131
+ for (let i = 0; i < points.length; i++) {
132
+ const [x0, y0] = points[i], [x1, y1] = points[(i + 1) % points.length];
133
+ d += `${round(x0)} ${round(y0)} ${round((x0 + x1) / 2)} ${round((y0 + y1) / 2)} `;
134
+ }
135
+ return `${d}Z`;
136
+ }
137
+ function strokeOptions(stroke, last) {
138
+ const size = WIDTHS[stroke.width].size;
139
+ const pen = stroke.points.some(point => point.p > 0);
140
+ return { size, simulatePressure: !pen, taperStart: size * 1.5, taperEnd: pen ? 0 : size * 5, last };
141
+ }
142
+ /** The SVG path of a stroke, or of its first `upTo` ms while it is replayed. */
143
+ export function strokePath(stroke, upTo = Infinity) {
144
+ const all = stroke.points;
145
+ const done = upTo >= all[all.length - 1].t;
146
+ const points = done ? all : all.filter(point => point.t <= upTo);
147
+ return outlineToPath(getStrokeOutline(points, strokeOptions(stroke, done)));
148
+ }
149
+ /** Tight bounds of the ink, padded, for export. */
150
+ function inkBounds(strokes) {
151
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
152
+ for (const stroke of strokes)
153
+ for (const point of stroke.points) {
154
+ minX = Math.min(minX, point.x);
155
+ minY = Math.min(minY, point.y);
156
+ maxX = Math.max(maxX, point.x);
157
+ maxY = Math.max(maxY, point.y);
158
+ }
159
+ const pad = 16;
160
+ return { x: Math.floor(minX - pad), y: Math.floor(minY - pad), width: Math.ceil(maxX - minX + pad * 2), height: Math.ceil(maxY - minY + pad * 2) };
161
+ }
162
+ /** A standalone SVG of the signature, cropped to the ink, with print ink colors. */
163
+ export function signatureToSvg(strokes) {
164
+ if (!strokes.length)
165
+ return "";
166
+ const box = inkBounds(strokes);
167
+ const paths = strokes.map(stroke => `<path d="${strokePath(stroke)}" fill="${INK[stroke.color].export}"/>`).join("");
168
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${box.x} ${box.y} ${box.width} ${box.height}" width="${box.width}" height="${box.height}">${paths}</svg>`;
169
+ }
170
+ /** A transparent PNG of the signature, cropped to the ink, at `scale` times pad resolution. */
171
+ export function signatureToPng(strokes, scale = 3) {
172
+ return new Promise((resolve, reject) => {
173
+ if (!strokes.length)
174
+ return reject(new Error("Nothing to export"));
175
+ const box = inkBounds(strokes);
176
+ const canvas = document.createElement("canvas");
177
+ canvas.width = box.width * scale;
178
+ canvas.height = box.height * scale;
179
+ const ctx = canvas.getContext("2d");
180
+ if (!ctx)
181
+ return reject(new Error("Canvas is unavailable"));
182
+ ctx.scale(scale, scale);
183
+ ctx.translate(-box.x, -box.y);
184
+ for (const stroke of strokes) {
185
+ ctx.fillStyle = INK[stroke.color].export;
186
+ ctx.fill(new Path2D(strokePath(stroke)));
187
+ }
188
+ canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error("Export failed")), "image/png");
189
+ });
190
+ }
191
+ function download(blob, name) {
192
+ const url = URL.createObjectURL(blob);
193
+ const link = document.createElement("a");
194
+ link.href = url;
195
+ link.download = name;
196
+ document.body.appendChild(link);
197
+ link.click();
198
+ link.remove();
199
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
200
+ }
201
+ const { spring, duration, ease, blur } = motionTokens;
202
+ const subscribe = () => () => { };
203
+ function useReducedFlag() {
204
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
205
+ return !!useReducedMotion() && hydrated;
206
+ }
207
+ export function SignaturePad({ signer, hint = "Sign here", defaultColor = "black", defaultWidth = "medium", onChange, fileName = "signature", label = "Signature pad", className }) {
208
+ const uid = useId().replace(/[^a-zA-Z0-9-]/g, "");
209
+ const reduced = useReducedFlag();
210
+ const [history, setHistory] = useState({ past: [], present: [], future: [], fresh: null });
211
+ const [color, setColor] = useState(defaultColor);
212
+ const [width, setWidth] = useState(defaultWidth);
213
+ const [drawing, setDrawing] = useState(false);
214
+ const [wiping, setWiping] = useState(false);
215
+ const [replay, setReplay] = useState(null);
216
+ const [saved, setSaved] = useState(null);
217
+ const [status, setStatus] = useState("");
218
+ const padRef = useRef(null);
219
+ const inkRef = useRef(null);
220
+ const liveRef = useRef(null);
221
+ const live = useRef(null);
222
+ const replayFrame = useRef(0);
223
+ const savedTimer = useRef(0);
224
+ const wipeAnimation = useRef(null);
225
+ const counter = useRef(0);
226
+ const onChangeRef = useRef(onChange);
227
+ useEffect(() => { onChangeRef.current = onChange; });
228
+ const strokes = history.present;
229
+ const empty = strokes.length === 0;
230
+ const replaying = replay !== null;
231
+ useEffect(() => { onChangeRef.current?.(strokes); }, [strokes]);
232
+ useEffect(() => () => { cancelAnimationFrame(replayFrame.current); window.clearTimeout(savedTimer.current); }, []);
233
+ const commit = useCallback((next, fresh = null) => {
234
+ setHistory(current => ({ past: [...current.past, current.present].slice(-60), present: next, future: [], fresh }));
235
+ }, []);
236
+ const stopReplay = useCallback(() => { cancelAnimationFrame(replayFrame.current); setReplay(null); }, []);
237
+ /** A wipe in flight completes at once, so whatever comes next starts from a clear pad. */
238
+ const settleWipe = useCallback(() => { wipeAnimation.current?.complete(); }, []);
239
+ function toPad(event) {
240
+ const box = padRef.current.getBoundingClientRect();
241
+ return { x: (event.clientX - box.left) / box.width * PAD_WIDTH, y: (event.clientY - box.top) / box.height * PAD_HEIGHT };
242
+ }
243
+ function onPointerDown(event) {
244
+ if (live.current || (event.pointerType === "mouse" && event.button !== 0))
245
+ return;
246
+ event.preventDefault();
247
+ settleWipe();
248
+ if (replaying)
249
+ stopReplay();
250
+ padRef.current?.focus({ preventScroll: true });
251
+ event.currentTarget.setPointerCapture(event.pointerId);
252
+ const { x, y } = toPad(event);
253
+ const now = event.nativeEvent.timeStamp;
254
+ counter.current += 1;
255
+ const pen = event.pointerType === "pen" && event.pressure > 0;
256
+ live.current = { id: event.pointerId, stamp: event.nativeEvent.timeStamp, stroke: { id: `${uid}-${counter.current}`, color, width, at: now, points: [{ x, y, p: pen ? event.pressure : -1, t: 0 }] } };
257
+ setDrawing(true);
258
+ paintLive();
259
+ }
260
+ function onPointerMove(event) {
261
+ const current = live.current;
262
+ if (!current || current.id !== event.pointerId)
263
+ return;
264
+ const samples = typeof event.nativeEvent.getCoalescedEvents === "function" ? event.nativeEvent.getCoalescedEvents() : [];
265
+ const list = samples.length ? samples : [event.nativeEvent];
266
+ for (const sample of list) {
267
+ const { x, y } = toPad(sample);
268
+ const pen = sample.pointerType === "pen" && sample.pressure > 0;
269
+ current.stroke.points.push({ x, y, p: pen ? sample.pressure : -1, t: Math.max(current.stroke.points[current.stroke.points.length - 1].t, sample.timeStamp - current.stamp) });
270
+ }
271
+ paintLive();
272
+ }
273
+ function onPointerUp(event) {
274
+ const current = live.current;
275
+ if (!current || current.id !== event.pointerId)
276
+ return;
277
+ live.current = null;
278
+ commit([...strokes, current.stroke], current.stroke.id);
279
+ liveRef.current?.setAttribute("d", "");
280
+ setDrawing(false);
281
+ setStatus(`Stroke added. ${strokes.length + 1} in the signature.`);
282
+ }
283
+ function paintLive() {
284
+ const current = live.current;
285
+ if (!current || !liveRef.current)
286
+ return;
287
+ liveRef.current.setAttribute("d", outlineToPath(getStrokeOutline(current.stroke.points, strokeOptions(current.stroke, false))));
288
+ }
289
+ function undo() {
290
+ if (!history.past.length || live.current)
291
+ return;
292
+ settleWipe();
293
+ stopReplay();
294
+ setHistory(current => current.past.length ? { past: current.past.slice(0, -1), present: current.past[current.past.length - 1], future: [current.present, ...current.future], fresh: null } : current);
295
+ setStatus("Undone");
296
+ }
297
+ function redo() {
298
+ if (!history.future.length || live.current)
299
+ return;
300
+ settleWipe();
301
+ stopReplay();
302
+ setHistory(current => current.future.length ? { past: [...current.past, current.present], present: current.future[0], future: current.future.slice(1), fresh: null } : current);
303
+ setStatus("Redone");
304
+ }
305
+ function clear() {
306
+ if (empty || wiping || live.current)
307
+ return;
308
+ stopReplay();
309
+ const node = inkRef.current;
310
+ const finish = () => {
311
+ wipeAnimation.current = null;
312
+ commit([]);
313
+ setWiping(false);
314
+ setStatus("Signature cleared. Undo brings it back.");
315
+ // Lift the clip only once the wiped strokes have left, so they never flash back for a frame.
316
+ if (node)
317
+ requestAnimationFrame(() => requestAnimationFrame(() => { node.style.clipPath = ""; }));
318
+ };
319
+ if (!node || reduced) {
320
+ finish();
321
+ return;
322
+ }
323
+ setWiping(true);
324
+ // The ink is cut away behind a squeegee edge that sweeps across the pad; the edge itself is drawn by the sweep element.
325
+ const controls = animate(node, { clipPath: ["inset(0% 0% 0% 0%)", "inset(0% 0% 0% 100%)"] }, { duration: .56, ease: [...ease.inOut] });
326
+ wipeAnimation.current = { stop: () => controls.stop(), complete: () => { controls.complete(); } };
327
+ controls.then(finish);
328
+ }
329
+ function toggleReplay() {
330
+ if (replaying) {
331
+ stopReplay();
332
+ setStatus("Replay stopped");
333
+ return;
334
+ }
335
+ if (empty || live.current)
336
+ return;
337
+ settleWipe();
338
+ // Build a timeline: each stroke keeps its own rhythm, and pauses between strokes are capped so the replay never stalls.
339
+ const offsets = [];
340
+ let cursor = 0;
341
+ strokes.forEach((stroke, index) => {
342
+ if (index > 0) {
343
+ const previous = strokes[index - 1];
344
+ const gap = stroke.at - (previous.at + previous.points[previous.points.length - 1].t);
345
+ cursor += Math.min(260, Math.max(90, gap));
346
+ }
347
+ offsets.push(cursor);
348
+ cursor += stroke.points[stroke.points.length - 1].t;
349
+ });
350
+ const speed = Math.max(1, cursor / 3200);
351
+ const total = cursor + 120;
352
+ const begin = performance.now();
353
+ const tick = (now) => {
354
+ const at = (now - begin) * speed;
355
+ if (at >= total) {
356
+ setReplay(null);
357
+ setStatus("Replay finished");
358
+ return;
359
+ }
360
+ setReplay({ at, offsets });
361
+ replayFrame.current = requestAnimationFrame(tick);
362
+ };
363
+ setReplay({ at: 0, offsets });
364
+ setStatus("Replaying signature");
365
+ replayFrame.current = requestAnimationFrame(tick);
366
+ }
367
+ async function save(kind) {
368
+ if (empty)
369
+ return;
370
+ window.clearTimeout(savedTimer.current);
371
+ try {
372
+ const blob = kind === "svg" ? new Blob([signatureToSvg(strokes)], { type: "image/svg+xml" }) : await signatureToPng(strokes);
373
+ download(blob, `${fileName}.${kind}`);
374
+ setSaved(kind);
375
+ setStatus(`Saved ${fileName}.${kind}`);
376
+ }
377
+ catch {
378
+ setSaved("failed");
379
+ setStatus("Export failed. Try again.");
380
+ }
381
+ savedTimer.current = window.setTimeout(() => setSaved(null), 1800);
382
+ }
383
+ function onKeyDown(event) {
384
+ const mod = event.metaKey || event.ctrlKey;
385
+ const key = event.key.toLowerCase();
386
+ if (mod && key === "z") {
387
+ event.preventDefault();
388
+ if (event.shiftKey)
389
+ redo();
390
+ else
391
+ undo();
392
+ }
393
+ else if (mod && key === "y") {
394
+ event.preventDefault();
395
+ redo();
396
+ }
397
+ else if (!mod && (event.key === "Delete" || event.key === "Backspace") && event.currentTarget === event.target) {
398
+ event.preventDefault();
399
+ clear();
400
+ }
401
+ else if (event.key === "Escape" && replaying) {
402
+ stopReplay();
403
+ }
404
+ }
405
+ // During replay each stroke shows only the part drawn so far, and the nib rides its newest point.
406
+ let nib = null, nibColor = color;
407
+ const visible = [];
408
+ for (let index = 0; index < strokes.length; index++) {
409
+ const stroke = strokes[index];
410
+ if (replay === null) {
411
+ visible.push({ stroke, d: null });
412
+ continue;
413
+ }
414
+ const local = replay.at - (replay.offsets[index] ?? 0);
415
+ const lastT = stroke.points[stroke.points.length - 1].t;
416
+ if (local < 0) {
417
+ visible.push({ stroke, d: "" });
418
+ continue;
419
+ }
420
+ if (local >= lastT) {
421
+ visible.push({ stroke, d: null });
422
+ continue;
423
+ }
424
+ nib = stroke.points.findLast(point => point.t <= local) ?? stroke.points[0];
425
+ nibColor = stroke.color;
426
+ visible.push({ stroke, d: strokePath(stroke, local) });
427
+ }
428
+ const showHint = empty && !drawing && !wiping;
429
+ const hintHidden = reduced ? { opacity: 0 } : { opacity: 0, y: 6, scale: .97, filter: `blur(${blur.soft}px)` };
430
+ const t = reduced ? { duration: duration.instant } : spring.snappy;
431
+ return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), "data-ink": color, children: [_jsxs("div", { ref: padRef, className: styles.pad, tabIndex: 0, role: "img", "aria-roledescription": "signature pad", "aria-label": `${label}. ${empty ? "Empty." : `${strokes.length} ${strokes.length === 1 ? "stroke" : "strokes"}.`} Draw with a pointer, pen, or finger.`, "aria-describedby": `${uid}-keys`, "data-drawing": drawing ? "" : undefined, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, onKeyDown: onKeyDown, children: [_jsxs("div", { className: styles.guide, "aria-hidden": "true", children: [_jsx(X, { className: styles.mark, size: 14, strokeWidth: 1.75 }), _jsx("span", { className: styles.baseline }), signer && _jsx("span", { className: styles.signer, children: signer }), _jsx(AnimatePresence, { initial: false, children: showHint && _jsx(motion.span, { className: styles.hint, initial: hintHidden, animate: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }, exit: { ...hintHidden, transition: { duration: duration.fast, ease: [...ease.standard] } }, transition: reduced ? { duration: duration.instant } : { duration: duration.standard, ease: [...ease.enter] }, children: hint }, "hint") })] }), _jsx("div", { ref: inkRef, className: styles.ink, children: _jsxs("svg", { viewBox: `0 0 ${PAD_WIDTH} ${PAD_HEIGHT}`, preserveAspectRatio: "none", "aria-hidden": "true", children: [_jsx(AnimatePresence, { initial: false, children: visible.map(({ stroke, d }) => _jsx(motion.path, { className: styles.stroke, "data-color": stroke.color, d: d ?? strokePath(stroke), initial: history.fresh === stroke.id ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { duration: wiping ? 0 : duration.standard, ease: [...ease.standard] } }, transition: { duration: duration.standard, ease: [...ease.standard] } }, stroke.id)) }), _jsx("path", { ref: liveRef, className: styles.stroke, "data-color": color }), nib && _jsx("circle", { className: styles.nib, "data-color": nibColor, cx: nib.x, cy: nib.y, r: WIDTHS[width].size * .9 })] }) }), wiping && _jsx(motion.span, { className: styles.sweep, "aria-hidden": "true", initial: { x: "0%", opacity: 0 }, animate: { x: "100%", opacity: [0, 1, 1, 0] }, transition: { duration: .56, ease: [...ease.inOut], opacity: { duration: .56, times: [0, .12, .85, 1] } } })] }), _jsxs("div", { className: styles.toolbar, children: [_jsxs("div", { className: styles.group, children: [_jsx(Choice, { label: "Ink color", options: COLORS, value: color, onChange: next => { setColor(next); setStatus(`${INK[next].label} selected`); }, layoutId: `${uid}-color`, reduced: reduced, render: option => _jsx("span", { className: styles.swatch, "data-color": option }), name: option => INK[option].label }), _jsx("span", { className: styles.divider, "aria-hidden": "true" }), _jsx(Choice, { label: "Stroke width", options: SIZES, value: width, onChange: next => { setWidth(next); setStatus(`${WIDTHS[next].label} nib selected`); }, layoutId: `${uid}-width`, reduced: reduced, render: option => _jsx("span", { className: styles.weight, style: { width: WIDTHS[option].size + 2, height: WIDTHS[option].size + 2 } }), name: option => WIDTHS[option].label })] }), _jsxs("div", { className: styles.group, children: [_jsx(IconButton, { label: "Undo", onClick: undo, disabled: !history.past.length || wiping, children: _jsx(Undo2, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Redo", onClick: redo, disabled: !history.future.length || wiping, children: _jsx(Redo2, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx(IconButton, { label: replaying ? "Stop replay" : "Replay signature", onClick: toggleReplay, disabled: empty || wiping, pressed: replaying, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.swap, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6 }, transition: t, children: replaying ? _jsx(Square, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }) : _jsx(Play, { size: 17, strokeWidth: 1.75, "aria-hidden": "true" }) }, replaying ? "stop" : "play") }) }), _jsx(IconButton, { label: "Clear signature", onClick: clear, disabled: empty || wiping, children: _jsx(Eraser, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }) })] })] }), _jsxs("div", { className: styles.exports, children: [_jsx("span", { className: styles.exportLabel, children: "Save as" }), ["png", "svg"].map(kind => _jsx("button", { type: "button", className: styles.export, onClick: () => save(kind), disabled: empty, "aria-label": `Download ${kind.toUpperCase()}`, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.exportContent, initial: { opacity: 0, y: reduced ? 0 : 4, filter: reduced ? "blur(0px)" : `blur(${blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, y: reduced ? 0 : -4, transition: { duration: duration.instant } }, transition: reduced ? { duration: duration.instant } : { duration: duration.fast, ease: [...ease.enter] }, children: saved === kind ? _jsxs(_Fragment, { children: [_jsx(Check, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), "Saved"] }) : _jsxs(_Fragment, { children: [_jsx(Download, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), kind.toUpperCase()] }) }, saved === kind ? "done" : saved === "failed" ? "failed" : "idle") }) }, kind))] }), _jsx("p", { id: `${uid}-keys`, className: styles.srOnly, children: "Press Control or Command Z to undo, add Shift to redo, and Delete to clear." }), _jsx("p", { className: styles.srOnly, role: "status", "aria-live": "polite", children: status })] });
432
+ }
433
+ function IconButton({ label, onClick, disabled, pressed, children }) {
434
+ return _jsx("button", { type: "button", className: styles.iconButton, "aria-label": label, title: label, onClick: onClick, disabled: disabled, "aria-pressed": pressed, children: children });
435
+ }
436
+ /** A compact radio group with a highlight that glides to the chosen option. Arrow keys move the choice. */
437
+ function Choice({ label, options, value, onChange, render, name, layoutId, reduced }) {
438
+ const refs = useRef([]);
439
+ function onKeyDown(event) {
440
+ const step = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 0;
441
+ if (!step)
442
+ return;
443
+ event.preventDefault();
444
+ const index = (options.indexOf(value) + step + options.length) % options.length;
445
+ onChange(options[index]);
446
+ refs.current[index]?.focus();
447
+ }
448
+ return _jsx("div", { className: styles.choice, role: "radiogroup", "aria-label": label, onKeyDown: onKeyDown, children: options.map((option, index) => _jsxs("button", { ref: node => { refs.current[index] = node; }, type: "button", role: "radio", "aria-checked": option === value, "aria-label": name(option), title: name(option), tabIndex: option === value ? 0 : -1, className: styles.option, onClick: () => onChange(option), children: [option === value && _jsx(motion.span, { layoutId: layoutId, className: styles.selected, transition: reduced ? { duration: 0 } : spring.morph }), render(option)] }, option)) });
449
+ }
450
+ export default SignaturePad;
@@ -0,0 +1,84 @@
1
+ .root {
2
+ --ink-black: var(--foreground);
3
+ --ink-blue: oklch(46% .17 262);
4
+ --ink-violet: oklch(49% .2 292);
5
+ display: grid; gap: 10px; width: 100%; min-width: 0; font-family: var(--font-body); letter-spacing: var(--tracking-body);
6
+ }
7
+ :global(:root[data-theme="dark"]) .root { --ink-blue: oklch(76% .12 258); --ink-violet: oklch(77% .13 295); }
8
+
9
+ /* The pad keeps the 600 by 260 ratio of its coordinate space, so strokes scale with it at every width. */
10
+ .pad {
11
+ position: relative; aspect-ratio: 600 / 260; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape);
12
+ background: var(--surface); cursor: crosshair; touch-action: none; user-select: none; -webkit-user-select: none; isolation: isolate;
13
+ transition: border-color var(--duration-fast) var(--ease-standard);
14
+ }
15
+ .pad[data-drawing] { border-color: var(--border-strong); }
16
+
17
+ .guide { position: absolute; inset: 0; pointer-events: none; }
18
+ .baseline { position: absolute; left: 7%; right: 7%; top: 72%; border-top: 1.5px dashed var(--border-strong); }
19
+ .mark { position: absolute; left: 7%; top: 72%; color: var(--text-muted); translate: 0 calc(-100% - 6px); }
20
+ .signer { position: absolute; left: 7%; top: calc(72% + 8px); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
21
+ .hint {
22
+ position: absolute; left: 50%; top: 72%; translate: -50% calc(-100% - 8px); color: var(--text-muted);
23
+ font-size: var(--text-lg); line-height: var(--leading-body); white-space: nowrap;
24
+ }
25
+
26
+ .ink { position: absolute; inset: 0; pointer-events: none; }
27
+ .ink svg { display: block; width: 100%; height: 100%; overflow: visible; }
28
+ .stroke { fill: var(--ink-black); }
29
+ .stroke[data-color="blue"], .nib[data-color="blue"] { fill: var(--ink-blue); }
30
+ .stroke[data-color="violet"], .nib[data-color="violet"] { fill: var(--ink-violet); }
31
+ .nib { fill: var(--ink-black); opacity: .35; }
32
+
33
+ /* The squeegee edge that clears the ink as it crosses the pad. */
34
+ .sweep { position: absolute; inset: 0; pointer-events: none; }
35
+ .sweep::before {
36
+ content: ""; position: absolute; top: 8%; bottom: 8%; left: -1px; width: 2px; border-radius: var(--radius-full); corner-shape: round;
37
+ background: var(--foreground); opacity: .5;
38
+ }
39
+ .sweep::after {
40
+ content: ""; position: absolute; top: 0; bottom: 0; left: -28px; width: 28px;
41
+ background: linear-gradient(to right, transparent, color-mix(in oklab, var(--foreground) 5%, transparent));
42
+ }
43
+
44
+ .toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
45
+ .group { display: flex; align-items: center; gap: 2px; }
46
+ .divider { width: 1px; height: 20px; margin: 0 6px; background: var(--border); }
47
+
48
+ .choice { display: flex; align-items: center; gap: 2px; }
49
+ .option {
50
+ position: relative; display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round;
51
+ background: transparent; color: var(--foreground); cursor: pointer; -webkit-tap-highlight-color: transparent;
52
+ }
53
+ .selected { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border); }
54
+ .swatch { position: relative; width: 16px; height: 16px; border-radius: 50%; corner-shape: round; background: var(--ink-black); }
55
+ .swatch[data-color="blue"] { background: var(--ink-blue); }
56
+ .swatch[data-color="violet"] { background: var(--ink-violet); }
57
+ .weight { position: relative; border-radius: 50%; corner-shape: round; background: var(--foreground); }
58
+
59
+ .iconButton {
60
+ display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round;
61
+ background: transparent; color: var(--text-secondary); cursor: pointer; -webkit-tap-highlight-color: transparent;
62
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
63
+ }
64
+ .iconButton:active:not(:disabled) { transform: scale(.94); }
65
+ .iconButton:disabled { color: var(--text-muted); opacity: .45; cursor: default; }
66
+ .iconButton[aria-pressed="true"] { background: var(--surface-muted); color: var(--foreground); }
67
+ .swap { display: grid; place-items: center; }
68
+
69
+ .exports { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
70
+ .exportLabel { margin-right: 4px; color: var(--text-muted); font-size: var(--text-xs); }
71
+ .export {
72
+ display: grid; min-width: 76px; height: var(--control-height-sm); padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape);
73
+ background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); cursor: pointer; -webkit-tap-highlight-color: transparent;
74
+ transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
75
+ }
76
+ .export:active:not(:disabled) { transform: scale(.97); }
77
+ .export:disabled { color: var(--text-muted); cursor: default; }
78
+ .exportContent { display: inline-flex; grid-area: 1 / 1; align-items: center; justify-content: center; gap: 6px; }
79
+
80
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
81
+
82
+ @media (hover: hover) and (pointer: fine) {
83
+ .option:hover, .iconButton:hover:not(:disabled), .export:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); }
84
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ export interface SkeletonProps {
3
+ label?: string;
4
+ lines?: number;
5
+ avatar?: boolean;
6
+ className?: string;
7
+ /** Content to reveal once loading finishes. With children, the placeholder crossfades into them. */
8
+ children?: ReactNode;
9
+ /** Keeps the placeholder visible while true. Only used together with children. */
10
+ loading?: boolean;
11
+ }
12
+ export declare function Skeleton({ label, lines, avatar, className, children, loading }: SkeletonProps): import("react").JSX.Element;
13
+ export default Skeleton;
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef } from "react";
4
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./skeleton.module.css";
7
+ /** Follows its content height. After `morphKey` changes, the height springs from the old size to the new one and then returns to auto, so passive reflows (a resize, a font swap) follow instantly. It clips only while moving, so focus rings stay visible at rest. */
8
+ function HeightFrame({ className, busy, reduce, children }) {
9
+ const frame = useRef(null);
10
+ const content = useRef(null);
11
+ const height = useMotionValue("auto");
12
+ const changedAt = useRef(0);
13
+ useLayoutEffect(() => { changedAt.current = performance.now(); }, [busy]);
14
+ useEffect(() => {
15
+ const node = content.current;
16
+ if (!node || typeof ResizeObserver === "undefined")
17
+ return;
18
+ let last;
19
+ let controls;
20
+ const settle = () => { height.jump("auto"); if (frame.current)
21
+ Object.assign(frame.current.style, { overflow: "", height: "auto" }); };
22
+ const observer = new ResizeObserver(([entry]) => {
23
+ if (!entry)
24
+ return;
25
+ const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
26
+ const current = height.get();
27
+ const from = typeof current === "number" ? current : last;
28
+ last = next;
29
+ controls?.stop();
30
+ if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120)
31
+ return settle();
32
+ // Pin the old height before this frame paints, then spring to the new one.
33
+ if (frame.current)
34
+ Object.assign(frame.current.style, { overflow: "hidden", height: `${from}px` });
35
+ controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });
36
+ });
37
+ observer.observe(node);
38
+ return () => { observer.disconnect(); controls?.stop(); };
39
+ }, [height, reduce]);
40
+ return _jsx(motion.div, { ref: frame, className: className, "aria-busy": busy, style: { height }, children: _jsx("div", { ref: content, className: styles.frame, children: children }) });
41
+ }
42
+ export function Skeleton({ label = "Loading content", lines = 3, avatar = false, className, children, loading = true }) {
43
+ const reduce = useReducedMotion();
44
+ const count = Math.min(Math.max(Math.floor(lines), 1), 6);
45
+ // Each block pulses a beat after the one above it, so the wait reads as one calm wave.
46
+ const placeholder = (extra) => _jsxs("div", { className: [styles.root, extra].filter(Boolean).join(" "), role: "status", "aria-label": label, "aria-busy": "true", children: [avatar && _jsx("span", { className: styles.avatar, "aria-hidden": "true" }), _jsx("span", { className: styles.lines, "aria-hidden": "true", children: Array.from({ length: count }, (_, index) => _jsx("span", { className: styles.line, style: { "--index": index + (avatar ? 1 : 0) } }, index)) })] });
47
+ if (children === undefined)
48
+ return placeholder(className);
49
+ return _jsx(HeightFrame, { className: className, busy: loading, reduce: reduce, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: loading
50
+ ? _jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast, ease: [...motionTokens.ease.standard] }, exit: { opacity: 0, transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast } }, children: placeholder() }, "placeholder")
51
+ : _jsx(motion.div, { initial: reduce ? { opacity: 0 } : { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast } }, transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: children }, "content") }) });
52
+ }
53
+ export default Skeleton;
@@ -0,0 +1,11 @@
1
+ .root { display: flex; width: 100%; min-width: 0; align-items: flex-start; gap: var(--space-inset); }
2
+ /* A calm, finite pulse: each block dims a beat after the one above it, then rests after about 20 seconds. */
3
+ .avatar, .line { display: block; flex: 0 0 auto; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--border-strong) 38%, var(--surface)); animation: skeleton-pulse 1.8s var(--ease-in-out) calc(var(--index, 0) * 90ms) 11 both; }
4
+ .avatar { width: var(--control-height-md); height: var(--control-height-md); border-radius: var(--radius-full); corner-shape: round; }
5
+ .lines { display: grid; flex: 1; gap: var(--space-group); padding-top: var(--space-tight); }
6
+ .line { width: 100%; height: var(--space-group); border-radius: var(--radius-full); corner-shape: round; }
7
+ .line:first-child { width: 48%; height: var(--space-inset); }
8
+ .line:last-child { width: 72%; }
9
+ .frame { position: relative; display: flow-root; }
10
+ @keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .52; } }
11
+ @media (prefers-reduced-motion: reduce) { .avatar, .line { animation: none; } }