@dowel-ui/react 0.4.0 → 0.7.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 (335) hide show
  1. package/README.md +9 -5
  2. package/dist/blocks/admin-dashboard/meta.js +31 -0
  3. package/dist/blocks/admin-dashboard/meta.js.map +1 -0
  4. package/dist/blocks/agent-console/meta.js +27 -0
  5. package/dist/blocks/agent-console/meta.js.map +1 -0
  6. package/dist/blocks/ai-dashboard/meta.js +25 -0
  7. package/dist/blocks/ai-dashboard/meta.js.map +1 -0
  8. package/dist/blocks/ai-workspace/meta.js +33 -0
  9. package/dist/blocks/ai-workspace/meta.js.map +1 -0
  10. package/dist/blocks/analytics/meta.js +24 -0
  11. package/dist/blocks/analytics/meta.js.map +1 -0
  12. package/dist/blocks/billing/meta.js +25 -0
  13. package/dist/blocks/billing/meta.js.map +1 -0
  14. package/dist/blocks/command-center/meta.js +28 -0
  15. package/dist/blocks/command-center/meta.js.map +1 -0
  16. package/dist/blocks/crm/meta.js +32 -0
  17. package/dist/blocks/crm/meta.js.map +1 -0
  18. package/dist/blocks/onboarding/meta.js +23 -0
  19. package/dist/blocks/onboarding/meta.js.map +1 -0
  20. package/dist/components/accordion/accordion.js +1 -1
  21. package/dist/components/accordion/accordion.js.map +1 -1
  22. package/dist/components/activity-feed/activity-feed.js +1 -1
  23. package/dist/components/activity-feed/activity-feed.js.map +1 -1
  24. package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
  25. package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
  26. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
  27. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
  28. package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
  29. package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
  30. package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
  31. package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
  32. package/dist/components/ai-extraction-review/extraction-model.js +91 -0
  33. package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
  34. package/dist/components/ai-extraction-review/index.d.ts +3 -0
  35. package/dist/components/ai-extraction-review/index.js +3 -0
  36. package/dist/components/ai-extraction-review/meta.js +17 -0
  37. package/dist/components/ai-extraction-review/meta.js.map +1 -0
  38. package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
  39. package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
  40. package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
  41. package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
  42. package/dist/components/ai-response/ai-response.js +2 -2
  43. package/dist/components/ai-response/ai-response.js.map +1 -1
  44. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
  45. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
  46. package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
  47. package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
  48. package/dist/components/ai-suggested-value/index.d.ts +2 -0
  49. package/dist/components/ai-suggested-value/index.js +2 -0
  50. package/dist/components/ai-suggested-value/meta.js +17 -0
  51. package/dist/components/ai-suggested-value/meta.js.map +1 -0
  52. package/dist/components/ai-tool/ai-tool.js +2 -2
  53. package/dist/components/ai-tool/ai-tool.js.map +1 -1
  54. package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
  55. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
  56. package/dist/components/breadcrumb/breadcrumb.js +141 -0
  57. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  58. package/dist/components/breadcrumb/index.d.ts +2 -0
  59. package/dist/components/breadcrumb/index.js +2 -0
  60. package/dist/components/breadcrumb/meta.js +17 -0
  61. package/dist/components/breadcrumb/meta.js.map +1 -0
  62. package/dist/components/calendar/calendar.js +2 -2
  63. package/dist/components/calendar/calendar.js.map +1 -1
  64. package/dist/components/code-block/code-block.js +1 -1
  65. package/dist/components/code-block/code-block.js.map +1 -1
  66. package/dist/components/collapsible/collapsible.d.ts +30 -0
  67. package/dist/components/collapsible/collapsible.d.ts.map +1 -0
  68. package/dist/components/collapsible/collapsible.js +38 -0
  69. package/dist/components/collapsible/collapsible.js.map +1 -0
  70. package/dist/components/collapsible/index.d.ts +2 -0
  71. package/dist/components/collapsible/index.js +2 -0
  72. package/dist/components/collapsible/meta.js +17 -0
  73. package/dist/components/collapsible/meta.js.map +1 -0
  74. package/dist/components/combobox/combobox.js +2 -2
  75. package/dist/components/combobox/combobox.js.map +1 -1
  76. package/dist/components/command/command.js +1 -1
  77. package/dist/components/command/command.js.map +1 -1
  78. package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
  79. package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
  80. package/dist/components/confirm-typed/confirm-typed.js +106 -0
  81. package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
  82. package/dist/components/confirm-typed/index.d.ts +2 -0
  83. package/dist/components/confirm-typed/index.js +2 -0
  84. package/dist/components/confirm-typed/meta.js +17 -0
  85. package/dist/components/confirm-typed/meta.js.map +1 -0
  86. package/dist/components/cron-editor/cron-editor.d.ts +36 -0
  87. package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
  88. package/dist/components/cron-editor/cron-editor.js +493 -0
  89. package/dist/components/cron-editor/cron-editor.js.map +1 -0
  90. package/dist/components/cron-editor/cron-expression.d.ts +82 -0
  91. package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
  92. package/dist/components/cron-editor/cron-expression.js +352 -0
  93. package/dist/components/cron-editor/cron-expression.js.map +1 -0
  94. package/dist/components/cron-editor/index.d.ts +3 -0
  95. package/dist/components/cron-editor/index.js +3 -0
  96. package/dist/components/cron-editor/meta.js +17 -0
  97. package/dist/components/cron-editor/meta.js.map +1 -0
  98. package/dist/components/data-table/data-table.d.ts.map +1 -1
  99. package/dist/components/data-table/data-table.js +4 -1
  100. package/dist/components/data-table/data-table.js.map +1 -1
  101. package/dist/components/dialog/dialog.js +2 -2
  102. package/dist/components/dialog/dialog.js.map +1 -1
  103. package/dist/components/diff-viewer/diff-viewer.js +4 -4
  104. package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
  105. package/dist/components/direction/direction.d.ts +35 -0
  106. package/dist/components/direction/direction.d.ts.map +1 -0
  107. package/dist/components/direction/direction.js +14 -0
  108. package/dist/components/direction/direction.js.map +1 -0
  109. package/dist/components/direction/index.d.ts +2 -0
  110. package/dist/components/direction/index.js +2 -0
  111. package/dist/components/direction/meta.js +17 -0
  112. package/dist/components/direction/meta.js.map +1 -0
  113. package/dist/components/dns-record/dns-record.d.ts +61 -0
  114. package/dist/components/dns-record/dns-record.d.ts.map +1 -0
  115. package/dist/components/dns-record/dns-record.js +227 -0
  116. package/dist/components/dns-record/dns-record.js.map +1 -0
  117. package/dist/components/dns-record/index.d.ts +2 -0
  118. package/dist/components/dns-record/index.js +2 -0
  119. package/dist/components/dns-record/meta.js +17 -0
  120. package/dist/components/dns-record/meta.js.map +1 -0
  121. package/dist/components/drawer/drawer.js +1 -1
  122. package/dist/components/drawer/drawer.js.map +1 -1
  123. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  124. package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
  125. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
  126. package/dist/components/input/input.js +1 -1
  127. package/dist/components/input/input.js.map +1 -1
  128. package/dist/components/log-viewer/log-viewer.js +2 -2
  129. package/dist/components/log-viewer/log-viewer.js.map +1 -1
  130. package/dist/components/meter/meter.js +1 -1
  131. package/dist/components/meter/meter.js.map +1 -1
  132. package/dist/components/pagination/pagination.d.ts.map +1 -1
  133. package/dist/components/pagination/pagination.js +3 -2
  134. package/dist/components/pagination/pagination.js.map +1 -1
  135. package/dist/components/permission-matrix/index.d.ts +3 -0
  136. package/dist/components/permission-matrix/index.js +3 -0
  137. package/dist/components/permission-matrix/meta.js +17 -0
  138. package/dist/components/permission-matrix/meta.js.map +1 -0
  139. package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
  140. package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
  141. package/dist/components/permission-matrix/permission-matrix.js +199 -0
  142. package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
  143. package/dist/components/permission-matrix/permission-model.d.ts +53 -0
  144. package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
  145. package/dist/components/permission-matrix/permission-model.js +64 -0
  146. package/dist/components/permission-matrix/permission-model.js.map +1 -0
  147. package/dist/components/record-diff/record-diff.js +4 -4
  148. package/dist/components/record-diff/record-diff.js.map +1 -1
  149. package/dist/components/secret-field/index.d.ts +2 -0
  150. package/dist/components/secret-field/index.js +2 -0
  151. package/dist/components/secret-field/meta.js +17 -0
  152. package/dist/components/secret-field/meta.js.map +1 -0
  153. package/dist/components/secret-field/secret-field.d.ts +58 -0
  154. package/dist/components/secret-field/secret-field.d.ts.map +1 -0
  155. package/dist/components/secret-field/secret-field.js +168 -0
  156. package/dist/components/secret-field/secret-field.js.map +1 -0
  157. package/dist/components/select/select.js +2 -2
  158. package/dist/components/select/select.js.map +1 -1
  159. package/dist/components/session-expiry/index.d.ts +2 -0
  160. package/dist/components/session-expiry/index.js +2 -0
  161. package/dist/components/session-expiry/meta.js +17 -0
  162. package/dist/components/session-expiry/meta.js.map +1 -0
  163. package/dist/components/session-expiry/session-expiry.d.ts +64 -0
  164. package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
  165. package/dist/components/session-expiry/session-expiry.js +185 -0
  166. package/dist/components/session-expiry/session-expiry.js.map +1 -0
  167. package/dist/components/sheet/sheet.d.ts.map +1 -1
  168. package/dist/components/sheet/sheet.js +2 -2
  169. package/dist/components/sheet/sheet.js.map +1 -1
  170. package/dist/components/shortcut-recorder/index.d.ts +3 -0
  171. package/dist/components/shortcut-recorder/index.js +3 -0
  172. package/dist/components/shortcut-recorder/meta.js +17 -0
  173. package/dist/components/shortcut-recorder/meta.js.map +1 -0
  174. package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
  175. package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
  176. package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
  177. package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
  178. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
  179. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
  180. package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
  181. package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
  182. package/dist/components/sidebar/index.d.ts +2 -0
  183. package/dist/components/sidebar/index.js +2 -0
  184. package/dist/components/sidebar/meta.js +17 -0
  185. package/dist/components/sidebar/meta.js.map +1 -0
  186. package/dist/components/sidebar/sidebar.d.ts +69 -0
  187. package/dist/components/sidebar/sidebar.d.ts.map +1 -0
  188. package/dist/components/sidebar/sidebar.js +256 -0
  189. package/dist/components/sidebar/sidebar.js.map +1 -0
  190. package/dist/components/sync-status/index.d.ts +2 -0
  191. package/dist/components/sync-status/index.js +2 -0
  192. package/dist/components/sync-status/meta.js +17 -0
  193. package/dist/components/sync-status/meta.js.map +1 -0
  194. package/dist/components/sync-status/sync-status.d.ts +65 -0
  195. package/dist/components/sync-status/sync-status.d.ts.map +1 -0
  196. package/dist/components/sync-status/sync-status.js +103 -0
  197. package/dist/components/sync-status/sync-status.js.map +1 -0
  198. package/dist/components/table/table.js +2 -2
  199. package/dist/components/table/table.js.map +1 -1
  200. package/dist/components/tags-input/tags-input.js +1 -1
  201. package/dist/components/tags-input/tags-input.js.map +1 -1
  202. package/dist/components/textarea/index.d.ts +2 -0
  203. package/dist/components/textarea/index.js +2 -0
  204. package/dist/components/textarea/meta.js +17 -0
  205. package/dist/components/textarea/meta.js.map +1 -0
  206. package/dist/components/textarea/textarea.d.ts +43 -0
  207. package/dist/components/textarea/textarea.d.ts.map +1 -0
  208. package/dist/components/textarea/textarea.js +119 -0
  209. package/dist/components/textarea/textarea.js.map +1 -0
  210. package/dist/components/time-range-picker/time-range-picker.js +3 -3
  211. package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
  212. package/dist/components/toast/toast.d.ts.map +1 -1
  213. package/dist/components/toast/toast.js +2 -2
  214. package/dist/components/toast/toast.js.map +1 -1
  215. package/dist/index.d.ts +36 -2
  216. package/dist/index.js +22 -3
  217. package/dist/lib/styles.d.ts +16 -1
  218. package/dist/lib/styles.d.ts.map +1 -1
  219. package/dist/lib/styles.js +16 -1
  220. package/dist/lib/styles.js.map +1 -1
  221. package/dist/registry/blocks.d.ts.map +1 -1
  222. package/dist/registry/blocks.js +27 -9
  223. package/dist/registry/blocks.js.map +1 -1
  224. package/dist/registry/components.d.ts.map +1 -1
  225. package/dist/registry/components.js +84 -54
  226. package/dist/registry/components.js.map +1 -1
  227. package/dist/registry/schema.d.ts +8 -0
  228. package/dist/registry/schema.d.ts.map +1 -1
  229. package/dist/registry/schema.js.map +1 -1
  230. package/package.json +5 -4
  231. package/src/components/accordion/accordion.tsx +1 -1
  232. package/src/components/activity-feed/activity-feed.tsx +1 -1
  233. package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
  234. package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
  235. package/src/components/ai-extraction-review/extraction-model.ts +159 -0
  236. package/src/components/ai-extraction-review/index.ts +13 -0
  237. package/src/components/ai-extraction-review/meta.ts +24 -0
  238. package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
  239. package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
  240. package/src/components/ai-response/ai-response.tsx +2 -2
  241. package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
  242. package/src/components/ai-suggested-value/index.ts +6 -0
  243. package/src/components/ai-suggested-value/meta.ts +24 -0
  244. package/src/components/ai-tool/ai-tool.tsx +2 -2
  245. package/src/components/breadcrumb/breadcrumb.tsx +157 -0
  246. package/src/components/breadcrumb/index.ts +10 -0
  247. package/src/components/breadcrumb/meta.ts +19 -0
  248. package/src/components/calendar/calendar.tsx +2 -2
  249. package/src/components/code-block/code-block.tsx +1 -1
  250. package/src/components/collapsible/collapsible.tsx +63 -0
  251. package/src/components/collapsible/index.ts +6 -0
  252. package/src/components/collapsible/meta.ts +16 -0
  253. package/src/components/combobox/combobox.tsx +2 -2
  254. package/src/components/command/command.tsx +1 -1
  255. package/src/components/confirm-typed/confirm-typed.tsx +191 -0
  256. package/src/components/confirm-typed/index.ts +1 -0
  257. package/src/components/confirm-typed/meta.ts +22 -0
  258. package/src/components/cron-editor/cron-editor.tsx +676 -0
  259. package/src/components/cron-editor/cron-expression.ts +552 -0
  260. package/src/components/cron-editor/index.ts +23 -0
  261. package/src/components/cron-editor/meta.ts +23 -0
  262. package/src/components/data-table/data-table.tsx +14 -3
  263. package/src/components/dialog/dialog.tsx +2 -2
  264. package/src/components/diff-viewer/diff-viewer.tsx +4 -4
  265. package/src/components/direction/direction.tsx +38 -0
  266. package/src/components/direction/index.ts +1 -0
  267. package/src/components/direction/meta.ts +19 -0
  268. package/src/components/dns-record/dns-record.tsx +315 -0
  269. package/src/components/dns-record/index.ts +7 -0
  270. package/src/components/dns-record/meta.ts +21 -0
  271. package/src/components/drawer/drawer.tsx +1 -1
  272. package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
  273. package/src/components/input/input.tsx +1 -1
  274. package/src/components/log-viewer/log-viewer.tsx +2 -2
  275. package/src/components/meter/meter.tsx +1 -1
  276. package/src/components/pagination/pagination.tsx +13 -2
  277. package/src/components/permission-matrix/index.ts +14 -0
  278. package/src/components/permission-matrix/meta.ts +23 -0
  279. package/src/components/permission-matrix/permission-matrix.tsx +368 -0
  280. package/src/components/permission-matrix/permission-model.ts +120 -0
  281. package/src/components/record-diff/record-diff.tsx +4 -4
  282. package/src/components/secret-field/index.ts +6 -0
  283. package/src/components/secret-field/meta.ts +21 -0
  284. package/src/components/secret-field/secret-field.tsx +284 -0
  285. package/src/components/select/select.tsx +2 -2
  286. package/src/components/session-expiry/index.ts +9 -0
  287. package/src/components/session-expiry/meta.ts +23 -0
  288. package/src/components/session-expiry/session-expiry.tsx +321 -0
  289. package/src/components/sheet/sheet.tsx +8 -4
  290. package/src/components/shortcut-recorder/index.ts +19 -0
  291. package/src/components/shortcut-recorder/meta.ts +22 -0
  292. package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
  293. package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
  294. package/src/components/sidebar/index.ts +19 -0
  295. package/src/components/sidebar/meta.ts +25 -0
  296. package/src/components/sidebar/sidebar.tsx +374 -0
  297. package/src/components/sync-status/index.ts +9 -0
  298. package/src/components/sync-status/meta.ts +21 -0
  299. package/src/components/sync-status/sync-status.tsx +217 -0
  300. package/src/components/table/table.tsx +3 -3
  301. package/src/components/tags-input/tags-input.tsx +1 -1
  302. package/src/components/textarea/index.ts +1 -0
  303. package/src/components/textarea/meta.ts +21 -0
  304. package/src/components/textarea/textarea.tsx +210 -0
  305. package/src/components/time-range-picker/time-range-picker.tsx +3 -3
  306. package/src/components/toast/toast.tsx +14 -6
  307. package/src/index.ts +15 -0
  308. package/src/lib/styles.ts +16 -0
  309. package/src/registry/blocks.ts +18 -0
  310. package/src/registry/components.ts +30 -0
  311. package/src/registry/schema.ts +8 -0
  312. package/src/blocks/admin-users/admin-users.tsx +0 -251
  313. package/src/blocks/admin-users/index.ts +0 -1
  314. package/src/blocks/admin-users/meta.ts +0 -27
  315. package/src/blocks/ai-chat/ai-chat.tsx +0 -280
  316. package/src/blocks/ai-chat/index.ts +0 -7
  317. package/src/blocks/ai-chat/meta.ts +0 -32
  318. package/src/blocks/dashboard/dashboard.tsx +0 -192
  319. package/src/blocks/dashboard/index.ts +0 -6
  320. package/src/blocks/dashboard/meta.ts +0 -18
  321. package/src/blocks/forgot-password/forgot-password.tsx +0 -158
  322. package/src/blocks/forgot-password/index.ts +0 -1
  323. package/src/blocks/forgot-password/meta.ts +0 -18
  324. package/src/blocks/login/index.ts +0 -1
  325. package/src/blocks/login/login.tsx +0 -178
  326. package/src/blocks/login/meta.ts +0 -18
  327. package/src/blocks/pricing/index.ts +0 -1
  328. package/src/blocks/pricing/meta.ts +0 -18
  329. package/src/blocks/pricing/pricing.tsx +0 -176
  330. package/src/blocks/settings/index.ts +0 -6
  331. package/src/blocks/settings/meta.ts +0 -29
  332. package/src/blocks/settings/settings.tsx +0 -317
  333. package/src/blocks/signup/index.ts +0 -1
  334. package/src/blocks/signup/meta.ts +0 -18
  335. package/src/blocks/signup/signup.tsx +0 -240
@@ -0,0 +1,19 @@
1
+ export {
2
+ ShortcutRecorder,
3
+ type ShortcutRecorderProps,
4
+ type TakenShortcut,
5
+ } from "./shortcut-recorder";
6
+ export {
7
+ RESERVED_KEYS,
8
+ describeShortcut,
9
+ detectPlatform,
10
+ formatShortcut,
11
+ needsModifier,
12
+ parseShortcut,
13
+ serializeShortcut,
14
+ shortcutFromKey,
15
+ shortcutParts,
16
+ shortcutsEqual,
17
+ type Platform,
18
+ type Shortcut,
19
+ } from "./shortcut-model";
@@ -0,0 +1,22 @@
1
+ import { defineMeta } from "@/registry/schema";
2
+
3
+ export const meta = defineMeta({
4
+ name: "shortcut-recorder",
5
+ title: "Shortcut Recorder",
6
+ description:
7
+ "Press the keys you want: records a chord, platform-aware, and says when it clashes.",
8
+ category: "form",
9
+ status: "stable",
10
+ dependencies: [],
11
+ registryDependencies: [],
12
+ files: ["shortcut-model.ts", "shortcut-recorder.tsx"],
13
+ a11y:
14
+ "The recorder is a button named by what the shortcut does and described by its value in " +
15
+ "words — Command Shift K — while the key caps on it are hidden from assistive technology, " +
16
+ "since ⌘⇧K read aloud is noise. While recording it is aria-pressed and its description " +
17
+ "says what to do; Tab always leaves and Escape always cancels, so it is never a keyboard " +
18
+ "trap. Every outcome is announced through a status region present from the start: recorded, " +
19
+ "refused with the reason, clashing with which command, cancelled, cleared. Symbols are shown " +
20
+ "on a Mac and words elsewhere, and the description uses the platform's own names for its " +
21
+ "keys.",
22
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * A keyboard shortcut as a value: parsed from the string an app stores,
3
+ * built from a keydown, and written back out for a Mac, for everyone else,
4
+ * and for the ear.
5
+ *
6
+ * Pure, so the modifier canon, the platform rendering and the conflict rule
7
+ * are tested without a keyboard, and so the same `parseShortcut` can run on
8
+ * the server that validates a saved binding.
9
+ *
10
+ * The stored form is platform-neutral: `Mod+Shift+K`, where `Mod` is Command
11
+ * on a Mac and Control elsewhere. That is the one decision every app makes
12
+ * and few make explicitly — a binding saved as `Ctrl+K` on Windows is wrong
13
+ * the moment the same account opens on a Mac.
14
+ */
15
+
16
+ export type Platform = "mac" | "other";
17
+
18
+ export interface Shortcut {
19
+ /** The non-modifier key, canonical: `K`, `1`, `ArrowUp`, `F5`, `Enter`, `Space`. */
20
+ key: string;
21
+ /** Command on a Mac, Control elsewhere. */
22
+ mod: boolean;
23
+ /** Control, held explicitly — on a Mac this is a different key from Mod. */
24
+ ctrl: boolean;
25
+ alt: boolean;
26
+ shift: boolean;
27
+ }
28
+
29
+ const MODIFIER_KEYS = new Set(["Meta", "Control", "Alt", "Shift", "OS", "AltGraph", "Fn"]);
30
+
31
+ /** Keys that cannot be a shortcut, because they already mean something. */
32
+ export const RESERVED_KEYS = new Set(["Tab", "Escape"]);
33
+
34
+ const KEY_ALIASES: Record<string, string> = {
35
+ " ": "Space",
36
+ spacebar: "Space",
37
+ esc: "Escape",
38
+ return: "Enter",
39
+ del: "Delete",
40
+ up: "ArrowUp",
41
+ down: "ArrowDown",
42
+ left: "ArrowLeft",
43
+ right: "ArrowRight",
44
+ cmd: "Mod",
45
+ command: "Mod",
46
+ meta: "Mod",
47
+ control: "Ctrl",
48
+ option: "Alt",
49
+ };
50
+
51
+ /** The canonical key name for a token someone typed or stored. */
52
+ function canonicalKey(token: string): string {
53
+ const alias = KEY_ALIASES[token.toLowerCase()];
54
+ if (alias) return alias;
55
+ if (token.length === 1) return token.toUpperCase();
56
+ // ArrowUp, F5, Enter, Backspace, Delete, Home, End, PageUp…
57
+ return token.charAt(0).toUpperCase() + token.slice(1);
58
+ }
59
+
60
+ /**
61
+ * `Mod+Shift+K` to a shortcut. Accepts the spellings people use — `Cmd`,
62
+ * `Command`, `Ctrl`, `Option` — and throws on an empty or modifier-only one.
63
+ */
64
+ export function parseShortcut(text: string): Shortcut {
65
+ const tokens = text
66
+ .split("+")
67
+ .map((token) => token.trim())
68
+ .filter(Boolean);
69
+ if (tokens.length === 0) throw new Error("Enter a shortcut.");
70
+
71
+ const shortcut: Shortcut = { key: "", mod: false, ctrl: false, alt: false, shift: false };
72
+ for (const token of tokens) {
73
+ const canonical = canonicalKey(token);
74
+ if (canonical === "Mod") shortcut.mod = true;
75
+ else if (canonical === "Ctrl") shortcut.ctrl = true;
76
+ else if (canonical === "Alt") shortcut.alt = true;
77
+ else if (canonical === "Shift") shortcut.shift = true;
78
+ else if (shortcut.key) throw new Error(`"${text}" has two keys; a shortcut has one.`);
79
+ else shortcut.key = canonical;
80
+ }
81
+ if (!shortcut.key) throw new Error(`"${text}" is only modifiers; it needs a key.`);
82
+ return shortcut;
83
+ }
84
+
85
+ /** The stored, platform-neutral form. Modifiers in a fixed order, so equal shortcuts are equal strings. */
86
+ export function serializeShortcut(shortcut: Shortcut): string {
87
+ const parts: string[] = [];
88
+ if (shortcut.mod) parts.push("Mod");
89
+ if (shortcut.ctrl) parts.push("Ctrl");
90
+ if (shortcut.alt) parts.push("Alt");
91
+ if (shortcut.shift) parts.push("Shift");
92
+ parts.push(shortcut.key);
93
+ return parts.join("+");
94
+ }
95
+
96
+ export function shortcutsEqual(a: Shortcut, b: Shortcut): boolean {
97
+ return serializeShortcut(a) === serializeShortcut(b);
98
+ }
99
+
100
+ interface KeyLike {
101
+ key: string;
102
+ code?: string;
103
+ metaKey: boolean;
104
+ ctrlKey: boolean;
105
+ altKey: boolean;
106
+ shiftKey: boolean;
107
+ }
108
+
109
+ /**
110
+ * The shortcut a keydown amounts to, or null while only modifiers are held.
111
+ *
112
+ * Letters and digits come from `code`, not `key`: with Shift or Option held,
113
+ * `key` is `K`, `˚` or `∆` depending on the layout and the platform, and a
114
+ * shortcut recorded as Option-∆ is one nobody can read back. `Mod` is Meta
115
+ * on a Mac and Control elsewhere; the other one, when held, is kept as
116
+ * itself.
117
+ */
118
+ export function shortcutFromKey(event: KeyLike, platform: Platform): Shortcut | null {
119
+ if (MODIFIER_KEYS.has(event.key)) return null;
120
+
121
+ let key: string;
122
+ const code = event.code ?? "";
123
+ if (/^Key[A-Z]$/.test(code)) key = code.slice(3);
124
+ else if (/^Digit[0-9]$/.test(code)) key = code.slice(5);
125
+ else key = canonicalKey(event.key);
126
+
127
+ const mac = platform === "mac";
128
+ return {
129
+ key,
130
+ mod: mac ? event.metaKey : event.ctrlKey,
131
+ ctrl: mac ? event.ctrlKey : false,
132
+ alt: event.altKey,
133
+ shift: event.shiftKey,
134
+ };
135
+ }
136
+
137
+ /** Printable keys need a modifier: a bare K as a shortcut fires while someone types a sentence. */
138
+ export function needsModifier(shortcut: Shortcut): boolean {
139
+ const printable = shortcut.key.length === 1 || shortcut.key === "Space";
140
+ return printable && !shortcut.mod && !shortcut.ctrl && !shortcut.alt;
141
+ }
142
+
143
+ const MAC_SYMBOLS: Record<string, string> = {
144
+ Mod: "⌘",
145
+ Ctrl: "⌃",
146
+ Alt: "⌥",
147
+ Shift: "⇧",
148
+ Enter: "↩",
149
+ Backspace: "⌫",
150
+ Delete: "⌦",
151
+ ArrowUp: "↑",
152
+ ArrowDown: "↓",
153
+ ArrowLeft: "←",
154
+ ArrowRight: "→",
155
+ Escape: "⎋",
156
+ Space: "Space",
157
+ };
158
+
159
+ const SPOKEN: Record<Platform, Record<string, string>> = {
160
+ mac: { Mod: "Command", Ctrl: "Control", Alt: "Option", Shift: "Shift" },
161
+ other: { Mod: "Control", Ctrl: "Control", Alt: "Alt", Shift: "Shift" },
162
+ };
163
+
164
+ /** The parts to draw, in order: symbols on a Mac, words elsewhere. */
165
+ export function shortcutParts(shortcut: Shortcut, platform: Platform): string[] {
166
+ const parts: string[] = [];
167
+ const name = (modifier: string) =>
168
+ platform === "mac"
169
+ ? (MAC_SYMBOLS[modifier] ?? modifier)
170
+ : (SPOKEN.other[modifier] ?? modifier);
171
+ if (shortcut.mod) parts.push(name("Mod"));
172
+ if (shortcut.ctrl && !(platform === "other" && shortcut.mod)) parts.push(name("Ctrl"));
173
+ if (shortcut.alt) parts.push(name("Alt"));
174
+ if (shortcut.shift) parts.push(name("Shift"));
175
+ parts.push(platform === "mac" ? (MAC_SYMBOLS[shortcut.key] ?? shortcut.key) : shortcut.key);
176
+ return parts;
177
+ }
178
+
179
+ /** "⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere. */
180
+ export function formatShortcut(shortcut: Shortcut, platform: Platform): string {
181
+ return shortcutParts(shortcut, platform).join(platform === "mac" ? "" : "+");
182
+ }
183
+
184
+ /** "Command Shift K" — words, for the ear, whatever the screen shows. */
185
+ export function describeShortcut(shortcut: Shortcut, platform: Platform): string {
186
+ const words: string[] = [];
187
+ const spoken = SPOKEN[platform];
188
+ if (shortcut.mod) words.push(spoken.Mod ?? "Mod");
189
+ if (shortcut.ctrl && !(platform === "other" && shortcut.mod))
190
+ words.push(spoken.Ctrl ?? "Ctrl");
191
+ if (shortcut.alt) words.push(spoken.Alt ?? "Alt");
192
+ if (shortcut.shift) words.push(spoken.Shift ?? "Shift");
193
+ words.push(shortcut.key.replace(/^Arrow/, "Arrow "));
194
+ return words.join(" ");
195
+ }
196
+
197
+ /** Whether the browser is on a Mac, by the only signal it gives. */
198
+ export function detectPlatform(): Platform {
199
+ if (typeof navigator === "undefined") return "other";
200
+ const hint =
201
+ (navigator as { userAgentData?: { platform?: string } }).userAgentData?.platform ??
202
+ navigator.platform;
203
+ return /mac|iphone|ipad|ipod/i.test(hint) ? "mac" : "other";
204
+ }
@@ -0,0 +1,336 @@
1
+ "use client";
2
+
3
+ import {
4
+ useId,
5
+ useState,
6
+ useSyncExternalStore,
7
+ type ComponentPropsWithRef,
8
+ type KeyboardEvent,
9
+ } from "react";
10
+
11
+ import { disabledStyles, focusRing } from "@/lib/styles";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ import {
15
+ describeShortcut,
16
+ detectPlatform,
17
+ needsModifier,
18
+ parseShortcut,
19
+ serializeShortcut,
20
+ shortcutFromKey,
21
+ shortcutParts,
22
+ shortcutsEqual,
23
+ type Platform,
24
+ type Shortcut,
25
+ } from "./shortcut-model";
26
+
27
+ /**
28
+ * Press the keys you want.
29
+ *
30
+ * Linear, Slack, VS Code, Figma and Superhuman each wrote one, and no
31
+ * component library ships the recorder — only the `<kbd>` that displays the
32
+ * result. The recorder is the hard part: a button that, when pressed, stops
33
+ * being a button and starts being a keyboard listener, and has to come back.
34
+ *
35
+ * Three things it does that a listener bolted onto an input does not. It
36
+ * reads letters from `code`, not `key`, so Option-K records as Option K and
37
+ * not as the ˚ the Mac produced. It stores `Mod`, not Command or Control,
38
+ * so a binding saved on one machine is right on the other. And it refuses a
39
+ * bare printable key by saying so, because a shortcut that fires while
40
+ * someone types a sentence is the bug every app that allowed it later fixed.
41
+ *
42
+ * Tab and Escape are never recorded: Tab leaves, Escape cancels, and a
43
+ * recorder that captures both is a keyboard trap with a nice label. A
44
+ * chord that another command already uses is said, with that command's
45
+ * name, and applied only if the person says to use it anyway.
46
+ */
47
+
48
+ export interface TakenShortcut {
49
+ /** Stored form: `Mod+K`. */
50
+ shortcut: string;
51
+ /** What it does: "Search". */
52
+ label: string;
53
+ }
54
+
55
+ export interface ShortcutRecorderProps extends Omit<
56
+ ComponentPropsWithRef<"div">,
57
+ "onChange" | "defaultValue"
58
+ > {
59
+ /** What the shortcut triggers: "Open search". Names the control. */
60
+ label: string;
61
+ /** Stored form, `Mod+Shift+K`, or null for none. Controlled. */
62
+ value?: string | null;
63
+ defaultValue?: string | null;
64
+ onChange?: (value: string | null) => void;
65
+ /** Shortcuts other commands already use. A clash is said, and applied only on request. */
66
+ taken?: TakenShortcut[];
67
+ /** Mac shows ⌘⇧K; everything else shows Ctrl+Shift+K. Detected if omitted. */
68
+ platform?: Platform;
69
+ /** Printable keys need a modifier. On by default, for the reason in the source. */
70
+ requireModifier?: boolean;
71
+ }
72
+
73
+ /* The platform, as an external store that never changes: a constant that the
74
+ server cannot know, so the server snapshot is the neutral one and the real
75
+ answer arrives after hydration without a mismatch. */
76
+ const subscribeToNothing = () => () => undefined;
77
+ const readPlatform = () => detectPlatform();
78
+ const readNeutralPlatform = (): Platform => "other";
79
+
80
+ export function ShortcutRecorder({
81
+ className,
82
+ label,
83
+ value: valueProp,
84
+ defaultValue = null,
85
+ onChange,
86
+ taken = [],
87
+ platform: platformProp,
88
+ requireModifier = true,
89
+ ...props
90
+ }: ShortcutRecorderProps) {
91
+ const id = useId();
92
+ const detected = useSyncExternalStore(subscribeToNothing, readPlatform, readNeutralPlatform);
93
+ const platform = platformProp ?? detected;
94
+
95
+ const [uncontrolled, setUncontrolled] = useState<string | null>(defaultValue);
96
+ const value = valueProp === undefined ? uncontrolled : valueProp;
97
+ const current = value ? safeParse(value) : null;
98
+
99
+ const [recording, setRecording] = useState(false);
100
+ /** Modifiers held mid-chord, shown so the person sees the recorder listening. */
101
+ const [held, setHeld] = useState<Shortcut | null>(null);
102
+ const [conflict, setConflict] = useState<{ shortcut: Shortcut; with: TakenShortcut } | null>(
103
+ null,
104
+ );
105
+ const [announcement, setAnnouncement] = useState("");
106
+
107
+ const commit = (next: Shortcut | null) => {
108
+ const serialized = next ? serializeShortcut(next) : null;
109
+ setUncontrolled(serialized);
110
+ onChange?.(serialized);
111
+ };
112
+
113
+ const stop = () => {
114
+ setRecording(false);
115
+ setHeld(null);
116
+ };
117
+
118
+ const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
119
+ if (!recording) return;
120
+ // Tab leaves. A recorder that keeps it is a keyboard trap.
121
+ if (event.key === "Tab") {
122
+ stop();
123
+ setAnnouncement("Cancelled.");
124
+ return;
125
+ }
126
+ event.preventDefault();
127
+ if (event.key === "Escape") {
128
+ stop();
129
+ setAnnouncement("Cancelled.");
130
+ return;
131
+ }
132
+ if ((event.key === "Backspace" || event.key === "Delete") && !hasModifier(event)) {
133
+ stop();
134
+ commit(null);
135
+ setAnnouncement("Cleared.");
136
+ return;
137
+ }
138
+
139
+ const chord = shortcutFromKey(event.nativeEvent, platform);
140
+ if (!chord) {
141
+ // Only modifiers so far. Show them, keep listening.
142
+ setHeld({
143
+ key: "",
144
+ mod: platform === "mac" ? event.metaKey : event.ctrlKey,
145
+ ctrl: platform === "mac" ? event.ctrlKey : false,
146
+ alt: event.altKey,
147
+ shift: event.shiftKey,
148
+ });
149
+ return;
150
+ }
151
+
152
+ if (requireModifier && needsModifier(chord)) {
153
+ setAnnouncement(
154
+ `${describeShortcut(chord, platform)} needs a modifier — it would fire while typing. Try again.`,
155
+ );
156
+ setHeld(null);
157
+ return;
158
+ }
159
+
160
+ const clash = taken.find((entry) =>
161
+ shortcutsEqual(safeParse(entry.shortcut) ?? chord, chord),
162
+ );
163
+ stop();
164
+ if (clash && !(current && shortcutsEqual(current, chord))) {
165
+ setConflict({ shortcut: chord, with: clash });
166
+ setAnnouncement(
167
+ `${describeShortcut(chord, platform)} is already used by ${clash.label}.`,
168
+ );
169
+ return;
170
+ }
171
+ commit(chord);
172
+ setAnnouncement(`Recorded ${describeShortcut(chord, platform)}.`);
173
+ };
174
+
175
+ const onKeyUp = (event: KeyboardEvent<HTMLButtonElement>) => {
176
+ if (!recording || !held) return;
177
+ setHeld({
178
+ key: "",
179
+ mod: platform === "mac" ? event.metaKey : event.ctrlKey,
180
+ ctrl: platform === "mac" ? event.ctrlKey : false,
181
+ alt: event.altKey,
182
+ shift: event.shiftKey,
183
+ });
184
+ };
185
+
186
+ const shown = recording ? held : current;
187
+ const description = recording
188
+ ? "Press the keys you want. Escape cancels, Backspace clears."
189
+ : current
190
+ ? `${describeShortcut(current, platform)}. Press to change.`
191
+ : "Not set. Press to record.";
192
+
193
+ return (
194
+ <div
195
+ data-slot="shortcut-recorder"
196
+ data-state={recording ? "recording" : conflict ? "conflict" : current ? "set" : "empty"}
197
+ className={cn("flex flex-col gap-1.5", className)}
198
+ {...props}
199
+ >
200
+ <span id={`${id}-label`} className="text-xs font-medium">
201
+ {label}
202
+ </span>
203
+
204
+ <div className="flex flex-wrap items-center gap-2">
205
+ <button
206
+ type="button"
207
+ aria-labelledby={`${id}-label`}
208
+ aria-describedby={`${id}-description`}
209
+ aria-pressed={recording}
210
+ className={cn(
211
+ "flex h-9 min-w-32 items-center justify-center gap-1 rounded-md border px-3 font-mono text-sm",
212
+ recording
213
+ ? "border-primary bg-primary/5 text-muted-foreground"
214
+ : "border-input bg-background hover:bg-accent",
215
+ focusRing,
216
+ disabledStyles,
217
+ )}
218
+ onClick={() => {
219
+ if (recording) return;
220
+ setConflict(null);
221
+ setRecording(true);
222
+ setAnnouncement("");
223
+ }}
224
+ onKeyDown={onKeyDown}
225
+ onKeyUp={onKeyUp}
226
+ onBlur={() => {
227
+ if (recording) {
228
+ stop();
229
+ setAnnouncement("Cancelled.");
230
+ }
231
+ }}
232
+ >
233
+ {/* Symbols and key caps are for the eye; the description says the
234
+ same thing in words. */}
235
+ <span aria-hidden="true" className="flex items-center gap-1">
236
+ {shown && (shown.key || shown.mod || shown.ctrl || shown.alt || shown.shift) ? (
237
+ shortcutParts(shown, platform)
238
+ .filter(Boolean)
239
+ .map((part, index) => (
240
+ <kbd
241
+ key={index}
242
+ className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-xs"
243
+ >
244
+ {part}
245
+ </kbd>
246
+ ))
247
+ ) : recording ? (
248
+ <span className="text-xs">Press keys…</span>
249
+ ) : (
250
+ <span className="text-xs text-muted-foreground">Not set</span>
251
+ )}
252
+ </span>
253
+ </button>
254
+
255
+ {current && !recording ? (
256
+ <button
257
+ type="button"
258
+ aria-label={`Clear — ${label}`}
259
+ className={cn(
260
+ "rounded-md border border-input bg-background px-2.5 py-1 text-xs font-medium hover:bg-accent",
261
+ focusRing,
262
+ )}
263
+ onClick={() => {
264
+ commit(null);
265
+ setConflict(null);
266
+ setAnnouncement("Cleared.");
267
+ }}
268
+ >
269
+ Clear
270
+ </button>
271
+ ) : null}
272
+ </div>
273
+
274
+ <p id={`${id}-description`} className="text-xs text-muted-foreground">
275
+ {description}
276
+ </p>
277
+
278
+ {conflict ? (
279
+ <div
280
+ data-slot="shortcut-conflict"
281
+ className="flex flex-wrap items-center gap-2 rounded-md border border-warning/50 bg-warning/5 px-2.5 py-1.5 text-xs"
282
+ >
283
+ <span className="flex-1 text-warning">
284
+ {describeShortcut(conflict.shortcut, platform)} is already used by{" "}
285
+ <strong>{conflict.with.label}</strong>.
286
+ </span>
287
+ <button
288
+ type="button"
289
+ className={cn(
290
+ "rounded-md border border-input bg-background px-2 py-0.5 font-medium hover:bg-accent",
291
+ focusRing,
292
+ )}
293
+ onClick={() => {
294
+ commit(conflict.shortcut);
295
+ setConflict(null);
296
+ setAnnouncement(
297
+ `Recorded ${describeShortcut(conflict.shortcut, platform)}. ${conflict.with.label} no longer has a shortcut.`,
298
+ );
299
+ }}
300
+ >
301
+ Use anyway
302
+ </button>
303
+ <button
304
+ type="button"
305
+ className={cn(
306
+ "rounded-md border border-input bg-background px-2 py-0.5 font-medium hover:bg-accent",
307
+ focusRing,
308
+ )}
309
+ onClick={() => {
310
+ setConflict(null);
311
+ }}
312
+ >
313
+ Keep the old one
314
+ </button>
315
+ </div>
316
+ ) : null}
317
+
318
+ {/* Present from the start, so the first outcome is heard. */}
319
+ <span role="status" aria-live="polite" className="sr-only">
320
+ {announcement}
321
+ </span>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ function hasModifier(event: KeyboardEvent): boolean {
327
+ return event.metaKey || event.ctrlKey || event.altKey || event.shiftKey;
328
+ }
329
+
330
+ function safeParse(text: string): Shortcut | null {
331
+ try {
332
+ return parseShortcut(text);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ Sidebar,
3
+ SidebarContent,
4
+ SidebarFooter,
5
+ SidebarGroup,
6
+ SidebarGroupLabel,
7
+ SidebarHeader,
8
+ SidebarInset,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ SidebarMenuLabel,
13
+ SidebarProvider,
14
+ SidebarTrigger,
15
+ type SidebarMenuButtonProps,
16
+ type SidebarProps,
17
+ type SidebarProviderProps,
18
+ type SidebarTriggerProps,
19
+ } from "./sidebar";
@@ -0,0 +1,25 @@
1
+ import { defineMeta } from "@/registry/schema";
2
+
3
+ export const meta = defineMeta({
4
+ name: "sidebar",
5
+ title: "Sidebar",
6
+ description:
7
+ "The application's own navigation: a collapsible rail on a wide screen, an overlay on a narrow one.",
8
+ category: "navigation",
9
+ status: "stable",
10
+ dependencies: ["radix-ui"],
11
+ registryDependencies: ["sheet"],
12
+ files: ["sidebar.tsx"],
13
+ a11y:
14
+ "The navigation landmark must be named — a page has several, and three regions all called " +
15
+ '"navigation" is a list nobody can choose from. The active entry carries aria-current=' +
16
+ '"page" rather than only a background colour. On a narrow screen it is a Sheet, not a ' +
17
+ "styled div: an overlay needs a focus trap and an Escape key, and without them the page " +
18
+ "behind stays reachable by Tab while the menu covers it. When the rail collapses, labels " +
19
+ "are visually hidden rather than removed — removing them leaves controls whose only content " +
20
+ "is an icon, which is how a collapsed sidebar becomes a column of links all announced as " +
21
+ '"link". The trigger says what pressing it will do, not what the state currently is. The ' +
22
+ "overlay is mounted only on a narrow screen and has its own open state: a modal dialog that " +
23
+ "CSS hides is still modal, and mounting it always made every wide screen aria-hidden and " +
24
+ "unclickable whenever the rail was open.",
25
+ });