@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,380 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
5
+ import { RotateCcw, Search, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./shortcut-recorder.module.css";
8
+ const MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift", "meta"];
9
+ const isModifier = (part) => MODIFIER_ORDER.includes(part);
10
+ const CODE_KEYS = {
11
+ Comma: ",", Period: ".", Slash: "/", Semicolon: ";", Quote: "'", BracketLeft: "[", BracketRight: "]", Backslash: "\\", Minus: "-", Equal: "=", Backquote: "`",
12
+ Space: "space", Enter: "enter", NumpadEnter: "enter", Escape: "escape", Backspace: "backspace", Delete: "delete", Tab: "tab",
13
+ ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", Home: "home", End: "end", PageUp: "pageup", PageDown: "pagedown",
14
+ };
15
+ const MODIFIER_KEYS = { Meta: "meta", OS: "meta", Control: "ctrl", Alt: "alt", AltGraph: "alt", Shift: "shift" };
16
+ /** The physical key id for an event: "k", "1", "enter", "meta", "shift"… `null` for keys that never make a shortcut, such as Caps Lock. */
17
+ export function keyId(event) {
18
+ const { code, key } = event;
19
+ if (MODIFIER_KEYS[key])
20
+ return MODIFIER_KEYS[key];
21
+ if (/^Key[A-Z]$/.test(code))
22
+ return code.slice(3).toLowerCase();
23
+ if (/^Digit\d$/.test(code))
24
+ return code.slice(5);
25
+ if (/^Numpad\d$/.test(code))
26
+ return code.slice(6);
27
+ if (CODE_KEYS[code])
28
+ return CODE_KEYS[code];
29
+ if (/^F\d{1,2}$/.test(key))
30
+ return key.toLowerCase();
31
+ if (key === "CapsLock" || key === "Fn" || key === "Dead" || key === "Unidentified" || key === "Process")
32
+ return null;
33
+ return key.length === 1 ? key.toLowerCase() : null;
34
+ }
35
+ function splitShortcut(shortcut) {
36
+ const parts = shortcut.toLowerCase().split("+").map(part => part.trim()).filter(Boolean);
37
+ const key = parts.filter(part => !isModifier(part)).pop() ?? "";
38
+ return { mods: new Set(parts.filter(isModifier)), key };
39
+ }
40
+ /** One spelling per combination: on a Mac, Command is `mod`; elsewhere, Ctrl is. Modifiers come in a fixed order. */
41
+ export function normalizeShortcut(shortcut, platform) {
42
+ const { mods, key } = splitShortcut(shortcut);
43
+ if (platform === "mac" && mods.delete("meta"))
44
+ mods.add("mod");
45
+ if (platform === "other" && mods.delete("ctrl"))
46
+ mods.add("mod");
47
+ return [...MODIFIER_ORDER.filter(mod => mods.has(mod)), key].filter(Boolean).join("+");
48
+ }
49
+ /** Reads a keydown into a shortcut string, or `null` while only modifiers are down. */
50
+ export function shortcutFromEvent(event, platform) {
51
+ const key = keyId(event);
52
+ if (!key || ["meta", "ctrl", "alt", "shift"].includes(key))
53
+ return null;
54
+ const mods = [];
55
+ if (platform === "mac" ? event.metaKey : event.ctrlKey)
56
+ mods.push("mod");
57
+ if (platform === "mac" ? event.ctrlKey : false)
58
+ mods.push("ctrl");
59
+ if (event.altKey)
60
+ mods.push("alt");
61
+ if (event.shiftKey)
62
+ mods.push("shift");
63
+ if (platform === "other" && event.metaKey)
64
+ mods.push("meta");
65
+ return normalizeShortcut([...mods, key].join("+"), platform);
66
+ }
67
+ /** True when a keydown is this shortcut. Use it to run the action a recorded shortcut is bound to. */
68
+ export function matchesShortcut(event, shortcut, platform) {
69
+ const pressed = shortcutFromEvent(event, platform);
70
+ return !!pressed && pressed === normalizeShortcut(shortcut, platform);
71
+ }
72
+ const MAC_KEYS = { enter: ["↩", "Return"], backspace: ["⌫", "Delete"], delete: ["⌦", "Forward delete"], tab: ["⇥", "Tab"], escape: ["esc", "Escape"] };
73
+ const SHARED_KEYS = {
74
+ space: ["Space", "Space"], enter: ["Enter", "Enter"], escape: ["Esc", "Escape"], backspace: ["Backspace", "Backspace"], delete: ["Del", "Delete"], tab: ["Tab", "Tab"],
75
+ up: ["↑", "Up arrow"], down: ["↓", "Down arrow"], left: ["←", "Left arrow"], right: ["→", "Right arrow"], home: ["Home", "Home"], end: ["End", "End"], pageup: ["PgUp", "Page up"], pagedown: ["PgDn", "Page down"],
76
+ ",": [",", "Comma"], ".": [".", "Period"], "/": ["/", "Slash"], ";": [";", "Semicolon"], "'": ["'", "Quote"], "[": ["[", "Left bracket"], "]": ["]", "Right bracket"], "\\": ["\\", "Backslash"], "-": ["-", "Minus"], "=": ["=", "Equals"], "`": ["`", "Backtick"],
77
+ };
78
+ /** Key caps for a shortcut in platform order: ⌃ ⌥ ⇧ ⌘ on a Mac, Ctrl Alt Shift Win elsewhere. */
79
+ export function shortcutTokens(shortcut, platform) {
80
+ const { mods, key } = splitShortcut(normalizeShortcut(shortcut, platform));
81
+ const tokens = [];
82
+ if (platform === "mac") {
83
+ if (mods.has("ctrl"))
84
+ tokens.push({ label: "⌃", id: "ctrl", spoken: "Control" });
85
+ if (mods.has("alt"))
86
+ tokens.push({ label: "⌥", id: "alt", spoken: "Option" });
87
+ if (mods.has("shift"))
88
+ tokens.push({ label: "⇧", id: "shift", spoken: "Shift" });
89
+ if (mods.has("mod") || mods.has("meta"))
90
+ tokens.push({ label: "⌘", id: "meta", spoken: "Command" });
91
+ }
92
+ else {
93
+ if (mods.has("mod") || mods.has("ctrl"))
94
+ tokens.push({ label: "Ctrl", id: "ctrl", spoken: "Control" });
95
+ if (mods.has("alt"))
96
+ tokens.push({ label: "Alt", id: "alt", spoken: "Alt" });
97
+ if (mods.has("shift"))
98
+ tokens.push({ label: "Shift", id: "shift", spoken: "Shift" });
99
+ if (mods.has("meta"))
100
+ tokens.push({ label: "Win", id: "meta", spoken: "Windows" });
101
+ }
102
+ if (key) {
103
+ const named = (platform === "mac" ? MAC_KEYS[key] : undefined) ?? SHARED_KEYS[key];
104
+ tokens.push(named ? { label: named[0], id: key, spoken: named[1] } : { label: key.toUpperCase(), id: key, spoken: key.toUpperCase() });
105
+ }
106
+ return tokens;
107
+ }
108
+ /** "⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere, plus a spoken form for assistive technology. */
109
+ export function formatShortcut(shortcut, platform) {
110
+ const tokens = shortcutTokens(shortcut, platform);
111
+ return { text: tokens.map(token => token.label).join(platform === "mac" ? "" : "+"), spoken: tokens.map(token => token.spoken).join(" ") };
112
+ }
113
+ const subscribe = () => () => { };
114
+ /** Reduced motion that only applies after hydration, so server and client render the same styles. */
115
+ function useReducedFlag() {
116
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
117
+ return !!useReducedMotion() && hydrated;
118
+ }
119
+ const detectPlatform = () => {
120
+ if (typeof navigator === "undefined")
121
+ return "mac";
122
+ const hint = navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent;
123
+ return /mac|iphone|ipad|ipod/i.test(hint) ? "mac" : "other";
124
+ };
125
+ /** The viewer's platform. Server renders assume a Mac and settle on the first client render. */
126
+ export function usePlatform(override) {
127
+ const detected = useSyncExternalStore(subscribe, detectPlatform, () => "mac");
128
+ return override ?? detected;
129
+ }
130
+ /** Keys held down right now, by physical id. Clears on window blur, and after Command lifts (macOS drops the other keyups). */
131
+ export function usePressedKeys(enabled = true) {
132
+ const [held, setHeld] = useState(() => new Set());
133
+ useEffect(() => {
134
+ if (!enabled)
135
+ return;
136
+ const update = (change) => setHeld(current => { const next = new Set(current); change(next); return next.size === current.size && [...next].every(key => current.has(key)) ? current : next; });
137
+ const down = (event) => { const id = keyId(event); if (id)
138
+ update(next => next.add(id)); };
139
+ const up = (event) => {
140
+ const id = keyId(event);
141
+ update(next => {
142
+ if (id)
143
+ next.delete(id);
144
+ if (id === "meta")
145
+ [...next].forEach(key => { if (!["ctrl", "alt", "shift"].includes(key))
146
+ next.delete(key); });
147
+ if (!event.metaKey)
148
+ next.delete("meta");
149
+ if (!event.ctrlKey)
150
+ next.delete("ctrl");
151
+ if (!event.altKey)
152
+ next.delete("alt");
153
+ if (!event.shiftKey)
154
+ next.delete("shift");
155
+ });
156
+ };
157
+ const clear = () => setHeld(current => current.size ? new Set() : current);
158
+ // Capture phase, so keys still register while a focused recorder keeps the chord from bubbling.
159
+ window.addEventListener("keydown", down, true);
160
+ window.addEventListener("keyup", up, true);
161
+ window.addEventListener("blur", clear);
162
+ document.addEventListener("visibilitychange", clear);
163
+ return () => { window.removeEventListener("keydown", down, true); window.removeEventListener("keyup", up, true); window.removeEventListener("blur", clear); document.removeEventListener("visibilitychange", clear); };
164
+ }, [enabled]);
165
+ return enabled ? held : EMPTY;
166
+ }
167
+ const EMPTY = new Set();
168
+ /** A key cap. Use it inline in copy, in menus, or through `ShortcutKeys` for a whole combination. */
169
+ export const ShortcutKbd = forwardRef(function ShortcutKbd({ children, pressed = false, size = "md", className, ...props }, ref) {
170
+ return _jsx("kbd", { ref: ref, ...props, className: [styles.kbd, className].filter(Boolean).join(" "), "data-size": size, "data-pressed": pressed || undefined, children: children });
171
+ });
172
+ ShortcutKbd.displayName = "ShortcutKbd";
173
+ /** @deprecated Renamed to `ShortcutKbd` so it never clashes with the `kbd` component's `Kbd` in a shared index.ts. */
174
+ export const Kbd = ShortcutKbd;
175
+ /** A shortcut as key caps, with a spoken label for assistive technology. */
176
+ export function ShortcutKeys({ shortcut, platform: platformProp, pressed, size = "md", className }) {
177
+ const platform = usePlatform(platformProp);
178
+ const tokens = shortcutTokens(shortcut, platform);
179
+ return _jsx("span", { className: [styles.keys, className].filter(Boolean).join(" "), role: "img", "aria-label": tokens.map(token => token.spoken).join(" "), children: tokens.map((token, index) => _jsx(ShortcutKbd, { size: size, pressed: pressed?.has(token.id), "aria-hidden": "true", children: token.label }, `${index}-${token.id}`)) });
180
+ }
181
+ const RESERVED = [
182
+ { shortcut: "mod+w", label: "Close tab" }, { shortcut: "mod+t", label: "New tab" }, { shortcut: "mod+n", label: "New window" }, { shortcut: "mod+q", label: "Quit" },
183
+ { shortcut: "mod+l", label: "Address bar" }, { shortcut: "mod+r", label: "Reload" }, { shortcut: "mod+shift+t", label: "Reopen tab" },
184
+ { shortcut: "mod+c", label: "Copy" }, { shortcut: "mod+v", label: "Paste" }, { shortcut: "mod+x", label: "Cut" }, { shortcut: "mod+z", label: "Undo" }, { shortcut: "mod+a", label: "Select all" },
185
+ ];
186
+ const chip = {
187
+ enter: { opacity: 0, scale: .72, y: 4, filter: `blur(${motionTokens.blur.subtle}px)` },
188
+ rest: (index) => ({ opacity: 1, scale: 1, y: 0, filter: "blur(0px)", transition: { ...motionTokens.spring.snappy, delay: index * motionTokens.stagger.item, opacity: { duration: .14, delay: index * motionTokens.stagger.item }, filter: { duration: .16, delay: index * motionTokens.stagger.item } } }),
189
+ exit: { opacity: 0, scale: .8, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: .1 } },
190
+ };
191
+ const chipFade = { enter: { opacity: 0 }, rest: { opacity: 1, scale: 1, y: 0, filter: "none", transition: { duration: .1 } }, exit: { opacity: 0, transition: { duration: .06 } } };
192
+ /**
193
+ * A field that records a keyboard shortcut. Press it, then press the combination: held modifiers light up as caps while you
194
+ * build the chord, the finished shortcut settles into place, and a combination that is already taken asks before it is used.
195
+ * Escape cancels, Backspace clears, and the reset button restores the default. Caps follow the platform: ⌘ on a Mac, Ctrl elsewhere.
196
+ */
197
+ export function ShortcutRecorder({ label, hideLabel = false, value, defaultValue = null, onValueChange, resetValue, bindings = [], warnReserved = true, requireModifier = true, platform: platformProp, placeholder = "Record shortcut", description, disabled = false, id, className, }) {
198
+ const reduced = useReducedFlag();
199
+ const platform = usePlatform(platformProp);
200
+ const uid = useId();
201
+ const buttonId = id ?? `${uid}-button`;
202
+ const labelId = `${uid}-label`, messageId = `${uid}-message`;
203
+ const [inner, setInner] = useState(defaultValue);
204
+ const current = value !== undefined ? value : inner;
205
+ const restoreTo = resetValue !== undefined ? resetValue : defaultValue;
206
+ const [recording, setRecording] = useState(false);
207
+ const [live, setLive] = useState([]);
208
+ const [pending, setPending] = useState(null);
209
+ const [nudge, setNudge] = useState(null);
210
+ const [settled, setSettled] = useState(0);
211
+ const [announcement, setAnnouncement] = useState("");
212
+ const buttonRef = useRef(null);
213
+ /* The message slot animates to the measured height of its content, so a swap to a message that wraps grows instead of snapping. */
214
+ const [messageHeight, setMessageHeight] = useState(null);
215
+ const measureMessage = useCallback((node) => {
216
+ if (!node)
217
+ return;
218
+ const observer = new ResizeObserver(() => setMessageHeight(node.getBoundingClientRect().height));
219
+ observer.observe(node);
220
+ return () => { observer.disconnect(); setMessageHeight(null); };
221
+ }, []);
222
+ const spoken = (shortcut) => shortcut ? formatShortcut(shortcut, platform).spoken : "none";
223
+ const commit = useCallback((next, replaced) => {
224
+ setPending(null);
225
+ setNudge(null);
226
+ if (value === undefined)
227
+ setInner(next);
228
+ onValueChange?.(next, { replaced });
229
+ setSettled(count => count + 1);
230
+ setAnnouncement(next ? `Shortcut set to ${formatShortcut(next, platform).spoken}` : "Shortcut cleared");
231
+ }, [onValueChange, platform, value]);
232
+ const start = () => {
233
+ if (disabled)
234
+ return;
235
+ setPending(null);
236
+ setNudge(null);
237
+ setLive([]);
238
+ setRecording(true);
239
+ setAnnouncement("Recording. Press the new shortcut, or Escape to cancel.");
240
+ };
241
+ const stop = () => { setRecording(false); setLive([]); };
242
+ const conflictFor = (shortcut) => {
243
+ const normal = normalizeShortcut(shortcut, platform);
244
+ if (current && normalizeShortcut(current, platform) === normal)
245
+ return null;
246
+ const taken = bindings.find(binding => normalizeShortcut(binding.shortcut, platform) === normal);
247
+ if (taken)
248
+ return { shortcut, conflict: taken, reserved: false };
249
+ const reserved = warnReserved ? RESERVED.find(binding => normalizeShortcut(binding.shortcut, platform) === normal) : undefined;
250
+ return reserved ? { shortcut, conflict: reserved, reserved: true } : null;
251
+ };
252
+ const liveMods = (event) => {
253
+ const mods = [];
254
+ if (event.ctrlKey && platform === "mac")
255
+ mods.push("ctrl");
256
+ if (event.altKey)
257
+ mods.push("alt");
258
+ if (event.shiftKey)
259
+ mods.push("shift");
260
+ if (platform === "mac" ? event.metaKey : event.ctrlKey)
261
+ mods.push("mod");
262
+ if (platform === "other" && event.metaKey)
263
+ mods.push("meta");
264
+ return mods;
265
+ };
266
+ function onKeyDown(event) {
267
+ if (!recording) {
268
+ if ((event.key === "Backspace" || event.key === "Delete") && current) {
269
+ event.preventDefault();
270
+ commit(null);
271
+ }
272
+ return;
273
+ }
274
+ if (event.key === "Tab" && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
275
+ stop();
276
+ return;
277
+ }
278
+ // The chord belongs to the recorder: keep it from the page's own shortcuts, such as a ⌘K command menu.
279
+ event.preventDefault();
280
+ event.stopPropagation();
281
+ const bare = !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
282
+ if (bare && event.key === "Escape") {
283
+ stop();
284
+ setAnnouncement("Recording canceled");
285
+ return;
286
+ }
287
+ if (bare && (event.key === "Backspace" || event.key === "Delete")) {
288
+ stop();
289
+ commit(null);
290
+ return;
291
+ }
292
+ const shortcut = shortcutFromEvent(event.nativeEvent, platform);
293
+ if (!shortcut) {
294
+ setLive(liveMods(event));
295
+ return;
296
+ }
297
+ const { mods, key } = splitShortcut(shortcut);
298
+ const strong = mods.has("mod") || mods.has("ctrl") || mods.has("alt") || mods.has("meta");
299
+ if (requireModifier && !strong && !/^f\d+$/.test(key)) {
300
+ const hint = platform === "mac" ? "Include ⌘, ⌃, or ⌥" : "Include Ctrl or Alt";
301
+ setNudge(hint);
302
+ setAnnouncement(hint);
303
+ setLive(liveMods(event));
304
+ return;
305
+ }
306
+ stop();
307
+ const conflict = conflictFor(shortcut);
308
+ if (conflict) {
309
+ setPending(conflict);
310
+ setAnnouncement(`${formatShortcut(shortcut, platform).spoken} is ${conflict.reserved ? "reserved for" : "used by"} ${conflict.conflict.label}. Use anyway, or record another.`);
311
+ return;
312
+ }
313
+ commit(shortcut);
314
+ }
315
+ function onKeyUp(event) { if (recording)
316
+ setLive(liveMods(event)); }
317
+ const shown = pending?.shortcut ?? current;
318
+ const tokens = recording ? shortcutTokens(live.join("+"), platform) : shown ? shortcutTokens(shown, platform) : [];
319
+ const state = recording ? "recording" : pending ? "conflict" : "idle";
320
+ const canReset = !recording && (pending !== null || (current ?? null) !== (restoreTo ?? null));
321
+ const canClear = !recording && !!current && !pending;
322
+ const message = pending
323
+ ? { tone: "warning", key: `c-${pending.shortcut}`, body: _jsxs(_Fragment, { children: [pending.reserved ? "Reserved for" : "Already used by", " ", _jsx("strong", { className: styles.strong, children: pending.conflict.label })] }) }
324
+ : nudge ? { tone: "hint", key: `n-${nudge}`, body: _jsx(_Fragment, { children: nudge }) }
325
+ : recording ? { tone: "hint", key: "rec", body: _jsx(_Fragment, { children: "Esc to cancel, Backspace to clear" }) }
326
+ : description ? { tone: "hint", key: "d", body: _jsx(_Fragment, { children: description }) } : null;
327
+ useEffect(() => {
328
+ if (!settled)
329
+ return;
330
+ const timer = window.setTimeout(() => setSettled(0), 700);
331
+ return () => window.clearTimeout(timer);
332
+ }, [settled]);
333
+ return _jsxs("div", { className: [styles.field, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, children: [_jsx("span", { id: labelId, className: hideLabel ? styles.srOnly : styles.label, children: label }), _jsxs("div", { className: styles.control, "data-state": state, children: [_jsxs("button", { ref: buttonRef, id: buttonId, type: "button", className: styles.recorder, disabled: disabled, "aria-labelledby": `${labelId} ${buttonId}`, "aria-describedby": message ? messageId : undefined, "aria-pressed": recording, onClick: event => { if (recording) {
334
+ if (event.detail !== 0) {
335
+ stop();
336
+ setAnnouncement("Recording canceled");
337
+ }
338
+ return;
339
+ } start(); }, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onBlur: () => { if (recording) {
340
+ stop();
341
+ setNudge(null);
342
+ } }, children: [_jsx("span", { className: styles.srOnly, children: recording ? "Recording" : shown ? spoken(shown) : placeholder }), _jsxs("span", { className: styles.row, "aria-hidden": "true", children: [recording && _jsx("span", { className: styles.dot, "data-reduced": reduced || undefined }), _jsxs(AnimatePresence, { initial: false, mode: "popLayout", children: [tokens.map((token, index) => _jsx(motion.span, { layout: reduced ? false : "position", custom: recording ? 0 : index, variants: reduced ? chipFade : chip, initial: "enter", animate: "rest", exit: "exit", transition: reduced ? { duration: 0 } : { layout: motionTokens.spring.snappy }, className: styles.chipSlot, children: _jsx(ShortcutKbd, { pressed: recording || (settled > 0 && !pending), "data-tone": pending ? "warning" : undefined, children: token.label }) }, `${token.id}`)), tokens.length === 0 && _jsx(motion.span, { className: styles.placeholder, "data-recording": recording || undefined, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: .08 } }, transition: reduced ? { duration: .1 } : { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, children: recording ? "Press keys" : placeholder }, recording ? "prompt" : "placeholder")] })] })] }), _jsx("span", { className: styles.actions, children: _jsxs(AnimatePresence, { initial: false, children: [canReset && _jsx(motion.button, { type: "button", className: styles.action, "aria-label": `Reset to ${restoreTo ? formatShortcut(restoreTo, platform).spoken : "none"}`, onClick: () => { commit(restoreTo ?? null); buttonRef.current?.focus(); }, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: .1 } }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: _jsx(RotateCcw, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }, "reset"), canClear && _jsx(motion.button, { type: "button", className: styles.action, "aria-label": "Clear shortcut", onClick: () => { commit(null); buttonRef.current?.focus(); }, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: .1 } }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }, "clear")] }) })] }), _jsx(AnimatePresence, { initial: false, children: message && _jsx(motion.div, { className: styles.messageSlot, initial: reduced ? false : { height: 0, opacity: 0 }, animate: { height: messageHeight ?? "auto", opacity: 1 }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: .1 } } }, transition: reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast } }, children: _jsx("div", { ref: measureMessage, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsxs(motion.div, { id: messageId, className: styles.message, "data-tone": message.tone, initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: .08 } }, transition: { duration: reduced ? .1 : motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: [_jsx("span", { className: styles.messageText, children: message.body }), pending && _jsxs("span", { className: styles.messageActions, children: [_jsx("button", { type: "button", className: styles.link, onClick: () => { const taken = pending; commit(taken.shortcut, taken.reserved ? undefined : taken.conflict); buttonRef.current?.focus(); }, children: "Use anyway" }), _jsx("button", { type: "button", className: styles.link, "data-quiet": "", onClick: () => { setPending(null); setAnnouncement("Kept the previous shortcut"); buttonRef.current?.focus(); }, children: "Cancel" })] })] }, message.key) }) }) }, "slot") }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
343
+ }
344
+ /**
345
+ * A searchable, grouped cheatsheet. Holding a modifier lights that cap in every row and fades rows that do not use it, so people
346
+ * can explore by holding keys; a row whose whole combination is down lights up. Search matches names, groups, and key names such as "cmd k".
347
+ */
348
+ export function ShortcutList({ groups, label = "Keyboard shortcuts", searchable = true, searchPlaceholder = "Search shortcuts", highlightPressed = true, platform: platformProp, className }) {
349
+ const reduced = useReducedFlag();
350
+ const platform = usePlatform(platformProp);
351
+ const held = usePressedKeys(highlightPressed);
352
+ const [query, setQuery] = useState("");
353
+ const searchRef = useRef(null);
354
+ const heldMods = useMemo(() => [...held].filter(key => ["meta", "ctrl", "alt", "shift"].includes(key)), [held]);
355
+ const words = query.trim().toLowerCase().replace(/[⌘]/g, " cmd ").replace(/[⇧]/g, " shift ").replace(/[⌥]/g, " alt ").replace(/[⌃]/g, " ctrl ").split(/[\s+]+/).filter(Boolean);
356
+ const aliases = { cmd: "command", command: "command", ctrl: "control", control: "control", opt: "option", option: platform === "mac" ? "option" : "alt", alt: platform === "mac" ? "option" : "alt", win: "windows", esc: "escape", return: platform === "mac" ? "return" : "enter" };
357
+ const filtered = groups.map(group => ({
358
+ ...group,
359
+ items: group.items.filter(item => {
360
+ if (!words.length)
361
+ return true;
362
+ const tokens = shortcutTokens(item.shortcut, platform);
363
+ const haystack = `${item.label} ${group.label} ${item.keywords ?? ""} ${tokens.map(token => `${token.spoken} ${token.label}`).join(" ")}`.toLowerCase();
364
+ return words.every(word => haystack.includes(aliases[word] ?? word) || haystack.includes(word));
365
+ }),
366
+ })).filter(group => group.items.length);
367
+ const total = filtered.reduce((sum, group) => sum + group.items.length, 0);
368
+ const rowTransition = reduced ? { duration: 0 } : motionTokens.spring.smooth;
369
+ return _jsxs("section", { className: [styles.list, className].filter(Boolean).join(" "), "aria-label": label, children: [searchable && _jsxs("div", { className: styles.searchRow, children: [_jsx(Search, { className: styles.searchIcon, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { ref: searchRef, className: styles.search, type: "search", placeholder: searchPlaceholder, value: query, "aria-label": searchPlaceholder, onChange: event => setQuery(event.target.value), onKeyDown: event => { if (event.key === "Escape" && query) {
370
+ event.preventDefault();
371
+ setQuery("");
372
+ } }, autoComplete: "off", spellCheck: false }), _jsx("span", { className: styles.count, "aria-live": "polite", children: query ? `${total} ${total === 1 ? "match" : "matches"}` : "" }), _jsx(AnimatePresence, { initial: false, children: query && _jsx(motion.button, { type: "button", className: styles.clear, "aria-label": "Clear search", onClick: () => { setQuery(""); searchRef.current?.focus(); }, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: .1 } }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }, "clear") })] }), _jsxs("div", { className: styles.groups, children: [_jsx(AnimatePresence, { initial: false, children: filtered.map(group => _jsxs(motion.section, { className: styles.group, "aria-label": group.label, initial: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, transition: rowTransition, children: [_jsx("h4", { className: styles.groupLabel, children: group.label }), _jsx("ul", { className: styles.rows, children: _jsx(AnimatePresence, { initial: false, children: group.items.map(item => {
373
+ const tokens = shortcutTokens(item.shortcut, platform);
374
+ const ids = tokens.map(token => token.id);
375
+ const match = held.size > 0 && ids.every(key => held.has(key));
376
+ const dim = heldMods.length > 0 && !match && !heldMods.every(mod => ids.includes(mod));
377
+ return _jsx(motion.li, { className: styles.item, "data-match": match || undefined, "data-dim": dim || undefined, initial: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, transition: rowTransition, children: _jsxs("span", { className: styles.itemInner, children: [_jsx("span", { className: styles.itemLabel, children: item.label }), _jsx(ShortcutKeys, { shortcut: item.shortcut, platform: platform, pressed: held, size: "sm" })] }) }, item.label);
378
+ }) }) })] }, group.label)) }), total === 0 && _jsxs("p", { className: styles.empty, children: ["No shortcuts match \u201C", query.trim(), "\u201D"] })] })] });
379
+ }
380
+ export default ShortcutRecorder;
@@ -0,0 +1,85 @@
1
+ /* ---------------------------------------------- Kbd ---------------------------------------------- */
2
+
3
+ /* A key cap: a one pixel edge with a slightly heavier bottom, so it reads as a key without a shadow. Held, it drops a pixel and takes the accent. */
4
+ .kbd { display: inline-grid; min-width: 26px; height: 26px; box-sizing: border-box; place-items: center; padding: 0 7px 1px; border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); font-family: var(--font-body); font-size: var(--text-xs); font-weight: 500; line-height: 1; letter-spacing: 0; font-variant-numeric: tabular-nums; white-space: nowrap; transition: transform var(--duration-spring) var(--ease-spring), border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-bottom-width var(--duration-fast) var(--ease-standard); }
5
+ .kbd[data-size="sm"] { min-width: 22px; height: 22px; padding: 0 5px 1px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); font-size: var(--text-xs); }
6
+ .kbd[data-pressed] { transform: translateY(1px); border-color: color-mix(in oklab, var(--accent) 55%, var(--border-strong)); border-bottom-width: 1px; background: var(--accent-subtle); color: var(--accent-strong); transition-duration: 90ms; }
7
+ .kbd[data-tone="warning"] { border-color: color-mix(in oklab, var(--warning) 55%, var(--border-strong)); background: color-mix(in oklab, var(--warning) 12%, var(--surface-raised)); color: var(--foreground); }
8
+ .keys { display: inline-flex; flex: none; align-items: center; gap: 4px; }
9
+
10
+ /* ---------------------------------------------- Recorder ---------------------------------------------- */
11
+
12
+ .field { display: grid; min-width: 0; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
13
+ .label { justify-self: start; margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
14
+
15
+ .control { position: relative; display: flex; min-width: 0; height: var(--control-height-md); box-sizing: border-box; align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
16
+ @media (hover: hover) and (pointer: fine) { .control[data-state="idle"]:hover { border-color: var(--foreground); } }
17
+ .control:has(.recorder:focus-visible) { border-color: var(--foreground); }
18
+ .control[data-state="recording"] { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 4%, var(--surface)); }
19
+ .control[data-state="conflict"] { border-color: var(--warning); }
20
+ .field[data-disabled] .control { opacity: .5; background: var(--surface-muted); }
21
+
22
+ .recorder { display: flex; min-width: 0; height: 100%; flex: 1; align-items: center; padding: 0 6px 0 9px; border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; outline: none; }
23
+ .recorder:disabled { cursor: not-allowed; }
24
+ .row { position: relative; display: flex; min-width: 0; align-items: center; gap: 4px; }
25
+ .chipSlot { display: inline-flex; }
26
+ .placeholder { padding-left: 3px; color: var(--text-muted); font-size: var(--text-sm); white-space: nowrap; }
27
+ .placeholder[data-recording] { color: var(--text-secondary); }
28
+ /* A recording light: steady in reduced motion, a slow breath otherwise. */
29
+ .dot { width: 7px; height: 7px; flex: none; margin: 0 6px 0 3px; border-radius: 50%; corner-shape: round; background: var(--accent); animation: breathe 1.4s var(--ease-in-out) infinite; }
30
+ .dot[data-reduced] { animation: none; }
31
+ @keyframes breathe { 50% { opacity: .35; } }
32
+
33
+ .actions { display: flex; flex: none; align-items: center; gap: 2px; padding-right: 4px; }
34
+ .action { display: grid; width: 32px; height: 32px; place-items: center; padding: 0; border: 0; border-radius: calc(var(--radius-control) - 5px); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
35
+ @media (hover: hover) and (pointer: fine) { .action:hover { background: var(--surface-muted); color: var(--foreground); } }
36
+ .action:active, .action:focus-visible { background: var(--surface-muted); color: var(--foreground); outline: none; }
37
+
38
+ .messageSlot { position: relative; overflow: hidden; }
39
+ .message { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px var(--space-group); padding-top: var(--space-related); font-size: var(--text-xs); line-height: var(--leading-body); }
40
+ .message[data-tone="hint"] { color: var(--text-muted); }
41
+ .message[data-tone="warning"] { color: var(--text-secondary); }
42
+ .message[data-tone="warning"] .messageText::before { content: ""; display: inline-block; width: 6px; height: 6px; margin: 0 7px 1px 1px; border-radius: 50%; corner-shape: round; background: var(--warning); vertical-align: middle; }
43
+ .strong { color: var(--foreground); font-weight: 500; }
44
+ .messageActions { display: inline-flex; gap: var(--space-group); }
45
+ .link { padding: 0; border: 0; background: none; color: var(--foreground); cursor: pointer; font: inherit; font-weight: 500; text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; transition: text-decoration-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
46
+ .link:focus-visible { text-decoration-color: currentColor; outline: none; }
47
+ .link[data-quiet] { color: var(--text-muted); font-weight: 400; text-decoration: none; }
48
+ @media (hover: hover) and (pointer: fine) { .link:hover { text-decoration-color: currentColor; } .link[data-quiet]:hover { color: var(--foreground); } }
49
+
50
+ /* ---------------------------------------------- Cheatsheet ---------------------------------------------- */
51
+
52
+ .list { container-type: inline-size; display: grid; min-width: 0; gap: var(--space-group); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
53
+ .searchRow { display: flex; height: var(--control-height-sm); box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 6px 0 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
54
+ .searchRow:focus-within { border-color: var(--foreground); }
55
+ .searchIcon { flex: none; color: var(--text-muted); }
56
+ .search { min-width: 0; height: 100%; flex: 1; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); outline: none; -webkit-appearance: none; appearance: none; }
57
+ .search::placeholder { color: var(--text-muted); }
58
+ .search::-webkit-search-cancel-button { display: none; }
59
+ .count { flex: none; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
60
+ .clear { display: grid; width: 24px; height: 24px; flex: none; place-items: center; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 8%, transparent); color: var(--text-secondary); cursor: pointer; }
61
+ .clear:focus-visible { background: color-mix(in oklab, var(--foreground) 14%, transparent); color: var(--foreground); outline: none; }
62
+
63
+ .groups { display: grid; align-items: start; gap: 0 var(--space-section); }
64
+ @container (min-width: 520px) { .groups { grid-template-columns: 1fr 1fr; } }
65
+ .group { overflow: hidden; margin-inline: -8px; padding-inline: 8px; }
66
+ .groupLabel { margin: 0; padding: var(--space-group) 0 var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
67
+ .rows { display: grid; margin: 0; padding: 0; list-style: none; }
68
+ .item { overflow: hidden; margin-inline: -8px; }
69
+ /* Rows fade when a held modifier is not in them, and light when their whole combination is down. */
70
+ .itemInner { display: flex; min-height: 36px; align-items: center; justify-content: space-between; gap: var(--space-group); padding: 0 8px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); transition: opacity var(--duration-standard) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
71
+ .item + .item .itemInner { box-shadow: inset 0 1px 0 var(--border-subtle); }
72
+ .item[data-dim] .itemInner { opacity: .38; }
73
+ .item[data-match] .itemInner { background: var(--accent-subtle); box-shadow: none; }
74
+ .item[data-match] + .item .itemInner { box-shadow: none; }
75
+ .itemLabel { min-width: 0; overflow: hidden; color: var(--text-secondary); font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; transition: color var(--duration-fast) var(--ease-standard); }
76
+ .item[data-match] .itemLabel { color: var(--foreground); }
77
+ .empty { margin: 0; padding: var(--space-inset) 0; color: var(--text-muted); font-size: var(--text-sm); }
78
+
79
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .kbd, .control, .itemInner { transition-duration: 0ms; }
83
+ .kbd[data-pressed] { transform: none; }
84
+ .dot { animation: none; }
85
+ }
@@ -0,0 +1,72 @@
1
+ /** A sampled point: position in pad units, pen pressure between 0 and 1 (or -1 when the device has none), and ms since the stroke began. */
2
+ export interface InkPoint {
3
+ x: number;
4
+ y: number;
5
+ p: number;
6
+ t: number;
7
+ }
8
+ export type InkColor = "black" | "blue" | "violet";
9
+ export type InkWidth = "fine" | "medium" | "bold";
10
+ export interface InkStroke {
11
+ id: string;
12
+ points: InkPoint[];
13
+ color: InkColor;
14
+ width: InkWidth;
15
+ /** When the stroke began, in ms. Replay keeps the real rhythm between strokes. */
16
+ at: number;
17
+ }
18
+ export interface OutlineOptions {
19
+ /** Diameter of the stroke at half pressure, in pad units. */
20
+ size: number;
21
+ /** How much pressure changes the width, from 0 to 1. */
22
+ thinning?: number;
23
+ /** How far each point trails the pointer, from 0 to 1. Higher is smoother but lags more. */
24
+ streamline?: number;
25
+ /** Derive pressure from speed when the device reports none: fast is thin, slow is full. */
26
+ simulatePressure?: boolean;
27
+ /** Length over which the start and the end taper, in pad units. */
28
+ taperStart?: number;
29
+ taperEnd?: number;
30
+ /** False while the stroke is still being drawn, so the end does not taper yet. */
31
+ last?: boolean;
32
+ }
33
+ /** Pad coordinates. Strokes are stored in this space and scale with the pad. */
34
+ export declare const PAD_WIDTH = 600, PAD_HEIGHT = 260;
35
+ type Vec = [number, number];
36
+ /**
37
+ * Turns sampled points into the outline of a variable width stroke, in the spirit of perfect-freehand. Points are streamlined
38
+ * toward the pointer, width follows pen pressure or, for a mouse or finger, speed, the ends taper, sharp turns get round joins,
39
+ * and both ends get round caps. Returns the outline polygon; pass it to `outlineToPath` for SVG.
40
+ */
41
+ export declare function getStrokeOutline(input: InkPoint[], options: OutlineOptions): Vec[];
42
+ /** Draws an outline as one smooth closed path, with quadratic curves through the midpoints of its edges. */
43
+ export declare function outlineToPath(points: Vec[]): string;
44
+ /** The SVG path of a stroke, or of its first `upTo` ms while it is replayed. */
45
+ export declare function strokePath(stroke: Pick<InkStroke, "width" | "points">, upTo?: number): string;
46
+ /** A standalone SVG of the signature, cropped to the ink, with print ink colors. */
47
+ export declare function signatureToSvg(strokes: InkStroke[]): string;
48
+ /** A transparent PNG of the signature, cropped to the ink, at `scale` times pad resolution. */
49
+ export declare function signatureToPng(strokes: InkStroke[], scale?: number): Promise<Blob>;
50
+ /**
51
+ * A signature field with real ink. Width follows pen pressure, or speed for a mouse or finger, and the ends taper like a nib
52
+ * lifting off. It keeps undo and redo, clears with a wipe across the pad, replays the signature in its original rhythm,
53
+ * and exports a cropped PNG or SVG. A "Sign here" hint rests on the baseline until the first stroke.
54
+ * Keyboard: Mod+Z undoes, Shift+Mod+Z or Mod+Y redoes, and Delete clears.
55
+ */
56
+ export interface SignaturePadProps {
57
+ /** Printed under the baseline, such as the signer's name. */
58
+ signer?: string;
59
+ /** Resting hint on the baseline before the first stroke. */
60
+ hint?: string;
61
+ defaultColor?: InkColor;
62
+ defaultWidth?: InkWidth;
63
+ /** Receives the strokes after every change. */
64
+ onChange?: (strokes: InkStroke[]) => void;
65
+ /** File name for exports, without extension. */
66
+ fileName?: string;
67
+ /** Accessible name of the drawing surface. */
68
+ label?: string;
69
+ className?: string;
70
+ }
71
+ export declare function SignaturePad({ signer, hint, defaultColor, defaultWidth, onChange, fileName, label, className }: SignaturePadProps): import("react").JSX.Element;
72
+ export default SignaturePad;