makersclaw 0.8.0 → 0.8.2

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 (401) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/r/activity-log.json +52 -0
  4. package/r/agent-changes.json +51 -0
  5. package/r/agent-chat.json +53 -0
  6. package/r/agent-column.json +85 -0
  7. package/r/agent-dock.json +57 -0
  8. package/r/agent-kit.json +49 -0
  9. package/r/agent-marks.json +49 -0
  10. package/r/agent-pane.json +93 -0
  11. package/r/ai-action.json +56 -0
  12. package/r/annotate.json +3 -3
  13. package/r/approval-request.json +2 -2
  14. package/r/approval.json +54 -0
  15. package/r/ask-bar.json +50 -0
  16. package/r/audio-player.json +2 -2
  17. package/r/auth-form.json +2 -2
  18. package/r/automation-editor.json +66 -0
  19. package/r/automation-list.json +57 -0
  20. package/r/automation-model.json +118 -0
  21. package/r/automations-screen.json +71 -0
  22. package/r/board.json +2 -2
  23. package/r/calendar-view.json +3 -3
  24. package/r/cart.json +2 -2
  25. package/r/chart.json +2 -2
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +2 -2
  28. package/r/code-editor.json +2 -2
  29. package/r/comment-box.json +3 -3
  30. package/r/connect-card.json +51 -0
  31. package/r/contact-section.json +2 -2
  32. package/r/core.json +13 -7
  33. package/r/cost.json +51 -0
  34. package/r/csv-import.json +3 -3
  35. package/r/data-picker.json +60 -0
  36. package/r/data-table.json +2 -2
  37. package/r/diagram.json +2 -2
  38. package/r/doc-editor.json +2 -2
  39. package/r/doc-page.json +3 -3
  40. package/r/email-render.json +2 -2
  41. package/r/faq.json +2 -2
  42. package/r/file-upload.json +2 -2
  43. package/r/first-run.json +63 -0
  44. package/r/fix-note.json +57 -0
  45. package/r/flow-canvas.json +2 -2
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +2 -2
  48. package/r/gantt.json +3 -3
  49. package/r/job-panel.json +2 -2
  50. package/r/json-editor.json +3 -3
  51. package/r/library-shelf.json +56 -0
  52. package/r/map.json +2 -2
  53. package/r/margin-notes.json +67 -0
  54. package/r/menubar.json +2 -2
  55. package/r/mind-map.json +3 -3
  56. package/r/navigation-menu.json +2 -2
  57. package/r/needs-you.json +55 -0
  58. package/r/order-detail.json +2 -2
  59. package/r/org-chart.json +3 -3
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +2 -2
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +2 -2
  65. package/r/product-detail.json +2 -2
  66. package/r/product-grid.json +3 -3
  67. package/r/record-form.json +2 -2
  68. package/r/record-grid.json +52 -0
  69. package/r/record-notes.json +4 -4
  70. package/r/registry.json +4086 -1512
  71. package/r/run-history.json +52 -0
  72. package/r/run-view.json +80 -0
  73. package/r/schedule-card.json +54 -0
  74. package/r/schedule-engine.json +79 -0
  75. package/r/schedule-gantt.json +83 -0
  76. package/r/schedule-ruler.json +64 -0
  77. package/r/search-dialog.json +2 -2
  78. package/r/settings-list.json +2 -2
  79. package/r/setup-checklist.json +2 -2
  80. package/r/share-dialog.json +2 -2
  81. package/r/sidebar.json +2 -2
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +2 -2
  84. package/r/source-receipt.json +52 -0
  85. package/r/spreadsheet.json +3 -3
  86. package/r/stat-cards.json +3 -3
  87. package/r/step-picker.json +53 -0
  88. package/r/step-settings.json +72 -0
  89. package/r/suggestion.json +58 -0
  90. package/r/sum-bar.json +64 -0
  91. package/r/task-list.json +3 -3
  92. package/r/task-model.json +2 -2
  93. package/r/testimonials.json +2 -2
  94. package/r/tier-map.json +67 -0
  95. package/r/time-grid.json +73 -0
  96. package/r/time-strip.json +64 -0
  97. package/r/top-bar.json +66 -0
  98. package/r/transaction-list.json +2 -2
  99. package/r/trigger-picker.json +61 -0
  100. package/r/whiteboard.json +3 -3
  101. package/r/workflow-view.json +55 -0
  102. package/registry/blocks/activity-log/README.md +112 -0
  103. package/registry/blocks/activity-log/activity-log.jsx +351 -0
  104. package/registry/blocks/activity-log/block.json +33 -0
  105. package/registry/blocks/activity-log/demo.jsx +48 -0
  106. package/registry/blocks/activity-log/index.js +4 -0
  107. package/registry/blocks/agent-changes/README.md +94 -0
  108. package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
  109. package/registry/blocks/agent-changes/block.json +28 -0
  110. package/registry/blocks/agent-changes/demo.jsx +39 -0
  111. package/registry/blocks/agent-changes/index.js +3 -0
  112. package/registry/blocks/agent-chat/README.md +145 -0
  113. package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
  114. package/registry/blocks/agent-chat/block.json +30 -0
  115. package/registry/blocks/agent-chat/demo.jsx +165 -0
  116. package/registry/blocks/agent-chat/index.js +3 -0
  117. package/registry/blocks/agent-column/README.md +197 -0
  118. package/registry/blocks/agent-column/agent-column.jsx +492 -0
  119. package/registry/blocks/agent-column/block.json +67 -0
  120. package/registry/blocks/agent-column/column-picker.jsx +163 -0
  121. package/registry/blocks/agent-column/demo.jsx +50 -0
  122. package/registry/blocks/agent-column/index.js +7 -0
  123. package/registry/blocks/agent-column/model.js +164 -0
  124. package/registry/blocks/agent-column/receipt.jsx +483 -0
  125. package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
  126. package/registry/blocks/agent-dock/README.md +210 -0
  127. package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
  128. package/registry/blocks/agent-dock/block.json +47 -0
  129. package/registry/blocks/agent-dock/demo.jsx +153 -0
  130. package/registry/blocks/agent-dock/dock-model.js +118 -0
  131. package/registry/blocks/agent-dock/index.js +5 -0
  132. package/registry/blocks/agent-kit/README.md +73 -0
  133. package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
  134. package/registry/blocks/agent-kit/block.json +10 -0
  135. package/registry/blocks/agent-kit/demo.jsx +35 -0
  136. package/registry/blocks/agent-kit/index.js +3 -0
  137. package/registry/blocks/agent-marks/README.md +80 -0
  138. package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
  139. package/registry/blocks/agent-marks/block.json +27 -0
  140. package/registry/blocks/agent-marks/demo.jsx +49 -0
  141. package/registry/blocks/agent-marks/index.js +3 -0
  142. package/registry/blocks/agent-pane/README.md +234 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +246 -0
  144. package/registry/blocks/agent-pane/block.json +41 -0
  145. package/registry/blocks/agent-pane/composer.jsx +6 -0
  146. package/registry/blocks/agent-pane/demo.jsx +233 -0
  147. package/registry/blocks/agent-pane/dock-context.js +10 -0
  148. package/registry/blocks/agent-pane/index.js +4 -0
  149. package/registry/blocks/agent-pane/prose.jsx +84 -0
  150. package/registry/blocks/agent-pane/questions.jsx +134 -0
  151. package/registry/blocks/agent-pane/transcript.jsx +268 -0
  152. package/registry/blocks/agent-pane/work.jsx +368 -0
  153. package/registry/blocks/ai-action/README.md +94 -0
  154. package/registry/blocks/ai-action/ai-action.jsx +188 -0
  155. package/registry/blocks/ai-action/block.json +34 -0
  156. package/registry/blocks/ai-action/demo.jsx +93 -0
  157. package/registry/blocks/ai-action/index.js +3 -0
  158. package/registry/blocks/approval/README.md +145 -0
  159. package/registry/blocks/approval/approval.jsx +163 -0
  160. package/registry/blocks/approval/block.json +31 -0
  161. package/registry/blocks/approval/demo.jsx +121 -0
  162. package/registry/blocks/approval/index.js +3 -0
  163. package/registry/blocks/ask-bar/README.md +105 -0
  164. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  165. package/registry/blocks/ask-bar/block.json +26 -0
  166. package/registry/blocks/ask-bar/demo.jsx +78 -0
  167. package/registry/blocks/ask-bar/index.js +3 -0
  168. package/registry/blocks/automation-editor/README.md +291 -0
  169. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  170. package/registry/blocks/automation-editor/block.json +39 -0
  171. package/registry/blocks/automation-editor/demo.jsx +284 -0
  172. package/registry/blocks/automation-editor/index.js +4 -0
  173. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  174. package/registry/blocks/automation-list/README.md +129 -0
  175. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  176. package/registry/blocks/automation-list/block.json +35 -0
  177. package/registry/blocks/automation-list/demo.jsx +67 -0
  178. package/registry/blocks/automation-list/index.js +3 -0
  179. package/registry/blocks/automation-model/README.md +109 -0
  180. package/registry/blocks/automation-model/block.json +135 -0
  181. package/registry/blocks/automation-model/catalog.js +256 -0
  182. package/registry/blocks/automation-model/check.js +161 -0
  183. package/registry/blocks/automation-model/demo.jsx +42 -0
  184. package/registry/blocks/automation-model/edit.js +262 -0
  185. package/registry/blocks/automation-model/fixes.js +287 -0
  186. package/registry/blocks/automation-model/glyph.jsx +13 -0
  187. package/registry/blocks/automation-model/graph.js +410 -0
  188. package/registry/blocks/automation-model/index.js +33 -0
  189. package/registry/blocks/automation-model/overlay.js +175 -0
  190. package/registry/blocks/automation-model/parts.jsx +94 -0
  191. package/registry/blocks/automation-model/schedule.js +53 -0
  192. package/registry/blocks/automation-model/templates.js +128 -0
  193. package/registry/blocks/automation-model/values.js +29 -0
  194. package/registry/blocks/automation-model/words.js +439 -0
  195. package/registry/blocks/automations-screen/README.md +207 -0
  196. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  197. package/registry/blocks/automations-screen/block.json +47 -0
  198. package/registry/blocks/automations-screen/demo.jsx +364 -0
  199. package/registry/blocks/automations-screen/index.js +4 -0
  200. package/registry/blocks/automations-screen/screen-model.js +129 -0
  201. package/registry/blocks/chat-pane/README.md +143 -0
  202. package/registry/blocks/chat-pane/block.json +48 -0
  203. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  204. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  205. package/registry/blocks/chat-pane/chat-context.js +17 -0
  206. package/registry/blocks/chat-pane/chat-model.js +43 -0
  207. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  208. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  209. package/registry/blocks/chat-pane/demo.jsx +103 -0
  210. package/registry/blocks/chat-pane/index.js +9 -0
  211. package/registry/blocks/connect-card/README.md +99 -0
  212. package/registry/blocks/connect-card/block.json +10 -0
  213. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  214. package/registry/blocks/connect-card/demo.jsx +49 -0
  215. package/registry/blocks/connect-card/index.js +3 -0
  216. package/registry/blocks/cost/README.md +115 -0
  217. package/registry/blocks/cost/block.json +10 -0
  218. package/registry/blocks/cost/cost.jsx +176 -0
  219. package/registry/blocks/cost/demo.jsx +66 -0
  220. package/registry/blocks/cost/index.js +3 -0
  221. package/registry/blocks/data-picker/README.md +81 -0
  222. package/registry/blocks/data-picker/block.json +38 -0
  223. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  224. package/registry/blocks/data-picker/demo.jsx +34 -0
  225. package/registry/blocks/data-picker/fields.js +89 -0
  226. package/registry/blocks/data-picker/index.js +4 -0
  227. package/registry/blocks/data-table/demo.jsx +1 -1
  228. package/registry/blocks/first-run/README.md +118 -0
  229. package/registry/blocks/first-run/block.json +11 -0
  230. package/registry/blocks/first-run/demo.jsx +88 -0
  231. package/registry/blocks/first-run/first-run-model.js +51 -0
  232. package/registry/blocks/first-run/first-run.jsx +156 -0
  233. package/registry/blocks/first-run/index.js +5 -0
  234. package/registry/blocks/first-run/probe-rules.js +26 -0
  235. package/registry/blocks/fix-note/README.md +79 -0
  236. package/registry/blocks/fix-note/block.json +35 -0
  237. package/registry/blocks/fix-note/demo.jsx +30 -0
  238. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  239. package/registry/blocks/fix-note/index.js +3 -0
  240. package/registry/blocks/flow-editor/README.md +108 -0
  241. package/registry/blocks/flow-editor/block.json +44 -0
  242. package/registry/blocks/flow-editor/demo.jsx +115 -0
  243. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  244. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  245. package/registry/blocks/flow-editor/index.js +6 -0
  246. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  247. package/registry/blocks/flow-editor/outline.jsx +134 -0
  248. package/registry/blocks/library-shelf/README.md +117 -0
  249. package/registry/blocks/library-shelf/block.json +28 -0
  250. package/registry/blocks/library-shelf/demo.jsx +68 -0
  251. package/registry/blocks/library-shelf/index.js +3 -0
  252. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  253. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  254. package/registry/blocks/margin-notes/README.md +222 -0
  255. package/registry/blocks/margin-notes/block.json +10 -0
  256. package/registry/blocks/margin-notes/demo.jsx +144 -0
  257. package/registry/blocks/margin-notes/index.js +20 -0
  258. package/registry/blocks/margin-notes/margin-model.js +224 -0
  259. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  260. package/registry/blocks/needs-you/README.md +177 -0
  261. package/registry/blocks/needs-you/block.json +34 -0
  262. package/registry/blocks/needs-you/demo.jsx +143 -0
  263. package/registry/blocks/needs-you/index.js +3 -0
  264. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  265. package/registry/blocks/pace-bar/README.md +230 -0
  266. package/registry/blocks/pace-bar/block.json +10 -0
  267. package/registry/blocks/pace-bar/demo.jsx +255 -0
  268. package/registry/blocks/pace-bar/index.js +22 -0
  269. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  270. package/registry/blocks/pace-bar/pace-model.js +288 -0
  271. package/registry/blocks/record-grid/README.md +100 -0
  272. package/registry/blocks/record-grid/block.json +28 -0
  273. package/registry/blocks/record-grid/demo.jsx +43 -0
  274. package/registry/blocks/record-grid/index.js +3 -0
  275. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  276. package/registry/blocks/run-history/README.md +138 -0
  277. package/registry/blocks/run-history/block.json +10 -0
  278. package/registry/blocks/run-history/demo.jsx +74 -0
  279. package/registry/blocks/run-history/index.js +3 -0
  280. package/registry/blocks/run-history/run-history.jsx +218 -0
  281. package/registry/blocks/run-view/README.md +174 -0
  282. package/registry/blocks/run-view/block.json +50 -0
  283. package/registry/blocks/run-view/demo.jsx +286 -0
  284. package/registry/blocks/run-view/index.js +5 -0
  285. package/registry/blocks/run-view/run-controls.jsx +266 -0
  286. package/registry/blocks/run-view/run-view.jsx +236 -0
  287. package/registry/blocks/run-view/runs-list.jsx +99 -0
  288. package/registry/blocks/schedule-card/README.md +121 -0
  289. package/registry/blocks/schedule-card/block.json +10 -0
  290. package/registry/blocks/schedule-card/demo.jsx +82 -0
  291. package/registry/blocks/schedule-card/index.js +3 -0
  292. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  293. package/registry/blocks/schedule-engine/README.md +137 -0
  294. package/registry/blocks/schedule-engine/block.json +10 -0
  295. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  296. package/registry/blocks/schedule-engine/engine.js +769 -0
  297. package/registry/blocks/schedule-engine/index.js +9 -0
  298. package/registry/blocks/schedule-engine/ops.js +71 -0
  299. package/registry/blocks/schedule-engine/place.js +294 -0
  300. package/registry/blocks/schedule-engine/tree.js +79 -0
  301. package/registry/blocks/schedule-engine/waits.js +80 -0
  302. package/registry/blocks/schedule-gantt/README.md +159 -0
  303. package/registry/blocks/schedule-gantt/block.json +10 -0
  304. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  305. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  306. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  307. package/registry/blocks/schedule-gantt/index.js +26 -0
  308. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  309. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  310. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  311. package/registry/blocks/schedule-ruler/README.md +148 -0
  312. package/registry/blocks/schedule-ruler/block.json +10 -0
  313. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  314. package/registry/blocks/schedule-ruler/index.js +21 -0
  315. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  316. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  317. package/registry/blocks/source-receipt/README.md +97 -0
  318. package/registry/blocks/source-receipt/block.json +30 -0
  319. package/registry/blocks/source-receipt/demo.jsx +71 -0
  320. package/registry/blocks/source-receipt/index.js +3 -0
  321. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  322. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  323. package/registry/blocks/step-picker/README.md +78 -0
  324. package/registry/blocks/step-picker/block.json +31 -0
  325. package/registry/blocks/step-picker/demo.jsx +23 -0
  326. package/registry/blocks/step-picker/index.js +3 -0
  327. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  328. package/registry/blocks/step-settings/README.md +100 -0
  329. package/registry/blocks/step-settings/block.json +44 -0
  330. package/registry/blocks/step-settings/demo.jsx +93 -0
  331. package/registry/blocks/step-settings/fields.jsx +58 -0
  332. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  333. package/registry/blocks/step-settings/index.js +5 -0
  334. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  335. package/registry/blocks/suggestion/README.md +171 -0
  336. package/registry/blocks/suggestion/block.json +40 -0
  337. package/registry/blocks/suggestion/demo.jsx +91 -0
  338. package/registry/blocks/suggestion/index.js +3 -0
  339. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  340. package/registry/blocks/sum-bar/README.md +122 -0
  341. package/registry/blocks/sum-bar/block.json +10 -0
  342. package/registry/blocks/sum-bar/demo.jsx +110 -0
  343. package/registry/blocks/sum-bar/index.js +5 -0
  344. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  345. package/registry/blocks/sum-bar/sum-model.js +126 -0
  346. package/registry/blocks/tier-map/README.md +217 -0
  347. package/registry/blocks/tier-map/block.json +10 -0
  348. package/registry/blocks/tier-map/demo.jsx +222 -0
  349. package/registry/blocks/tier-map/index.js +24 -0
  350. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  351. package/registry/blocks/tier-map/tier-model.js +318 -0
  352. package/registry/blocks/time-grid/README.md +305 -0
  353. package/registry/blocks/time-grid/block.json +10 -0
  354. package/registry/blocks/time-grid/demo.jsx +279 -0
  355. package/registry/blocks/time-grid/grid-model.js +322 -0
  356. package/registry/blocks/time-grid/index.js +25 -0
  357. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  358. package/registry/blocks/time-strip/README.md +141 -0
  359. package/registry/blocks/time-strip/block.json +10 -0
  360. package/registry/blocks/time-strip/demo.jsx +129 -0
  361. package/registry/blocks/time-strip/index.js +19 -0
  362. package/registry/blocks/time-strip/strip-model.js +178 -0
  363. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  364. package/registry/blocks/top-bar/README.md +149 -0
  365. package/registry/blocks/top-bar/block.json +35 -0
  366. package/registry/blocks/top-bar/demo.jsx +78 -0
  367. package/registry/blocks/top-bar/index.js +5 -0
  368. package/registry/blocks/top-bar/probe-rules.js +74 -0
  369. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  370. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  371. package/registry/blocks/trigger-picker/README.md +98 -0
  372. package/registry/blocks/trigger-picker/block.json +38 -0
  373. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  374. package/registry/blocks/trigger-picker/index.js +3 -0
  375. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  376. package/registry/blocks/workflow-view/README.md +123 -0
  377. package/registry/blocks/workflow-view/block.json +10 -0
  378. package/registry/blocks/workflow-view/demo.jsx +90 -0
  379. package/registry/blocks/workflow-view/index.js +3 -0
  380. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  381. package/src/install.mjs +77 -16
  382. package/src/registry.mjs +3 -0
  383. package/templates/next/browser/lib/data.js +6 -1
  384. package/templates/next/files/README.md +627 -24
  385. package/templates/next/files/check/automation-format.ts +2103 -0
  386. package/templates/next/files/check/automation-package.ts +142 -0
  387. package/templates/next/files/check/run.mjs +968 -9
  388. package/templates/next/files/components/ui/field.jsx +17 -4
  389. package/templates/next/files/components/ui/icon.jsx +13 -0
  390. package/templates/next/files/components/ui/index.js +2 -1
  391. package/templates/next/files/components/ui/layout.jsx +77 -2
  392. package/templates/next/files/components/ui/notice.jsx +70 -0
  393. package/templates/next/files/components/ui/page.jsx +124 -0
  394. package/templates/next/files/components/ui/stat.jsx +22 -7
  395. package/templates/next/files/components/ui/toast.jsx +6 -3
  396. package/templates/next/files/lib/automations.js +660 -0
  397. package/templates/next/files/lib/claw.js +241 -5
  398. package/templates/next/files/lib/door.js +92 -0
  399. package/templates/next/files/lib/hooks.js +1677 -8
  400. package/templates/next/files/lib/platform.js +529 -21
  401. package/templates/next/template.json +13 -3
package/r/core.json CHANGED
@@ -129,7 +129,7 @@
129
129
  "path": "templates/next/files/components/ui/field.jsx",
130
130
  "type": "registry:ui",
131
131
  "target": "@components/makersclaw/ui/field.jsx",
132
- "content": "\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { cx } from \"./cx\";\n\n// Form parts. Each is the native element with the workspace's look and a\n// 44px target; the browser supplies the behaviour (focus, keyboard,\n// validation, autofill). Wrap one in <Field> for its label, hint and\n// error — never a bare <input> with a <div> label beside it.\n\nconst BOX =\n \"min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground \" +\n \"placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive\";\nconst CONTROL = `w-full ${BOX}`;\n\n// An Input is as wide as its column unless the caller's className gives\n// it a width (w-24, w-auto, w-[12rem], size-10), which then stands in\n// for w-full. cx does not merge classes: with both, the rule later in the\n// stylesheet wins, and Tailwind writes w-full after w-24, w-auto, w-fit\n// and w-[…] — until 0.5.1 those did nothing on an Input. A width under a\n// breakpoint (sm:w-40) keeps w-full below it.\nconst ownWidth = (className) => /(?:^|\\s)!?(?:w|size)-/.test(className ?? \"\");\n\n/** Label, control, hint and error in one stack. Pass `id`; the label\n * points at it. `error` (a string) marks the control invalid. */\nexport function Field({ id, label, hint, error, required, className, children }) {\n return (\n <div className={cx(\"grid gap-1.5\", className)}>\n {label ? (\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n {required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </label>\n ) : null}\n {children}\n {error ? (\n <p id={id ? `${id}-error` : undefined} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : hint ? (\n <p id={id ? `${id}-hint` : undefined} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, invalid, ...rest }) {\n return <input className={cx(!ownWidth(className) && \"w-full\", BOX, className)} aria-invalid={invalid ? true : undefined} {...rest} />;\n}\n\n/**\n * A time of day: the native <input type=\"time\"> (the browser's segments,\n * keyboard and picker; `value` is \"HH:MM\", or \"HH:MM:SS\" with `step` under\n * 60), in the workspace's look. The browser's own clock button is drawn\n * by the engine in its own grey and ignores the theme, so it is laid\n * invisibly over the kit's clock glyph: the glyph is what shows, the\n * native button is what a press opens. min, max, step, required and every\n * other prop go to the input.\n */\nexport function TimeInput({ className, invalid, ...rest }) {\n return (\n <span className=\"relative block\">\n <input\n type=\"time\"\n className={cx(\n CONTROL,\n \"appearance-none pr-10 tabular-nums [&::-webkit-date-and-time-value]:text-left\",\n \"[&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:top-1/2\",\n \"[&::-webkit-calendar-picker-indicator]:size-5 [&::-webkit-calendar-picker-indicator]:-translate-y-1/2 [&::-webkit-calendar-picker-indicator]:m-0\",\n \"[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-0\",\n className,\n )}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n />\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\" />\n </svg>\n </span>\n );\n}\n\n/**\n * Keeps a <textarea> as tall as its text, between `minRows` and `maxRows`\n * lines; past `maxRows` it scrolls. Returns the ref callback to put on the\n * textarea (it also sets `outer`, a ref of the caller's). It measures on\n * every value change, typed or set (a composer clearing after send), and\n * when the width changes the wrapping. Textarea autoGrow and InputGroup\n * multiline use it; it is not a surface's tool.\n */\nexport function useAutoGrow({ enabled = true, minRows = 1, maxRows = 8, value, outer }) {\n const node = useRef(null);\n\n const fit = useCallback(() => {\n const el = node.current;\n if (!el || !enabled) return;\n const cs = getComputedStyle(el);\n const line = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5 || 20;\n const frame =\n parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom) +\n (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n const low = line * Math.max(1, minRows) + frame;\n const high = Number.isFinite(maxRows) && maxRows > 0 ? line * Math.max(minRows, maxRows) + frame : Infinity;\n el.style.height = \"auto\";\n const content = el.scrollHeight + (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n el.style.height = `${Math.min(Math.max(content, low), high)}px`;\n el.style.overflowY = content > high ? \"auto\" : \"hidden\";\n }, [enabled, minRows, maxRows]);\n\n useLayoutEffect(fit, [fit, value]);\n\n // A width change re-wraps the text; a height change is our own doing.\n useLayoutEffect(() => {\n const el = node.current;\n if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n let width = el.clientWidth;\n const ro = new ResizeObserver(() => {\n if (el.clientWidth === width) return;\n width = el.clientWidth;\n fit();\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [enabled, fit]);\n\n const ref = useCallback(\n (el) => {\n node.current = el;\n if (typeof outer === \"function\") return outer(el);\n if (outer) outer.current = el;\n },\n [outer],\n );\n return { ref, fit };\n}\n\n/**\n * The native <textarea>. `rows` sets its height (default 4) and it\n * resizes by its corner. `autoGrow` makes it as tall as its text instead:\n * from `minRows` (default 1) to `maxRows` (default 8) lines, then it\n * scrolls; no resize corner.\n */\nexport function Textarea({ className, invalid, rows = 4, autoGrow = false, minRows = 1, maxRows = 8, onInput, ref, ...rest }) {\n const grow = useAutoGrow({ enabled: autoGrow, minRows, maxRows, value: rest.value, outer: ref });\n return (\n <textarea\n ref={grow.ref}\n rows={autoGrow ? minRows : rows}\n className={cx(CONTROL, autoGrow ? \"resize-none py-2.5 leading-normal\" : \"min-h-24 resize-y py-2.5 leading-normal\", className)}\n aria-invalid={invalid ? true : undefined}\n onInput={(e) => {\n if (autoGrow) grow.fit();\n onInput?.(e);\n }}\n {...rest}\n />\n );\n}\n\n/**\n * The native <select>, styled. `options` is [{ value, label }]; children\n * work too. A select with nothing to choose from is not a control — when\n * `options` is empty it renders `empty` (a short muted line, or nothing\n * when `empty` is null) rather than a disabled dropdown. For \"pick a\n * saved record or make a new one\" use RecordSwitcher (components/parts).\n */\nexport function Select({ options, empty = \"Nothing to choose yet.\", className, invalid, children, ...rest }) {\n if (options && options.length === 0 && !children) {\n return empty ? <p className=\"text-sm text-muted-foreground\">{empty}</p> : null;\n }\n return (\n <span className=\"relative block\">\n <select\n className={cx(CONTROL, \"appearance-none pr-9\", className)}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n >\n {options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}\n </select>\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n\n/** An on/off. A checkbox underneath (role=switch), so forms, labels and\n * keyboards work; the track is drawn from tokens. */\nexport function Switch({ id, checked, onChange, label, disabled, className }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}>\n <span className=\"relative inline-flex h-6 w-10 shrink-0\">\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n className=\"peer sr-only\"\n checked={checked}\n onChange={onChange}\n disabled={disabled}\n />\n <span className=\"absolute inset-0 rounded-full border border-input bg-muted transition-colors peer-checked:bg-primary peer-focus-visible:ring-2 peer-focus-visible:ring-ring\" />\n <span className=\"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-foreground transition-transform peer-checked:translate-x-4 peer-checked:bg-primary-foreground\" />\n </span>\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n\nexport function Checkbox({ id, label, className, ...rest }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm\", className)}>\n <input\n id={id}\n type=\"checkbox\"\n className=\"h-4 w-4 rounded-sm border-input accent-[var(--color-primary)] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n {...rest}\n />\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n"
132
+ "content": "\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { cx } from \"./cx\";\n\n// Form parts. Each is the native element with the workspace's look and a\n// 44px target; the browser supplies the behaviour (focus, keyboard,\n// validation, autofill). Wrap one in <Field> for its label, hint and\n// error — never a bare <input> with a <div> label beside it.\n\nconst BOX =\n \"min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground \" +\n \"placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive\";\nconst CONTROL = `w-full ${BOX}`;\n\n// An Input is as wide as its column unless the caller's className gives\n// it a width (w-24, w-auto, w-[12rem], size-10), which then stands in\n// for w-full. cx does not merge classes: with both, the rule later in the\n// stylesheet wins, and Tailwind writes w-full after w-24, w-auto, w-fit\n// and w-[…] — until 0.5.1 those did nothing on an Input. A width under a\n// breakpoint (sm:w-40) keeps w-full below it.\nconst ownWidth = (className) => /(?:^|\\s)!?(?:w|size)-/.test(className ?? \"\");\n\n/** Label, control, hint and error in one stack. Pass `id`; the label\n * points at it. `error` (a string) marks the control invalid.\n * Two Fields side by side in a grid row are stretched to the taller one;\n * `content-start` keeps the shorter one's label and control at the top\n * where they were (without it the grid spread them apart, and Start\n * date's box sat lower than Target date's when only Target had a hint). */\nexport function Field({ id, label, hint, error, required, className, children }) {\n return (\n <div className={cx(\"grid content-start gap-1.5\", className)}>\n {label ? (\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n {required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </label>\n ) : null}\n {children}\n {error ? (\n <p id={id ? `${id}-error` : undefined} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : hint ? (\n <p id={id ? `${id}-hint` : undefined} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, invalid, ...rest }) {\n return <input className={cx(!ownWidth(className) && \"w-full\", BOX, className)} aria-invalid={invalid ? true : undefined} {...rest} />;\n}\n\n/**\n * A time of day: the native <input type=\"time\"> (the browser's segments,\n * keyboard and picker; `value` is \"HH:MM\", or \"HH:MM:SS\" with `step` under\n * 60), in the workspace's look. The browser's own clock button is drawn\n * by the engine in its own grey and ignores the theme, so it is laid\n * invisibly over the kit's clock glyph: the glyph is what shows, the\n * native button is what a press opens. min, max, step, required and every\n * other prop go to the input.\n */\nexport function TimeInput({ className, invalid, ...rest }) {\n return (\n <span className=\"relative block\">\n <input\n type=\"time\"\n className={cx(\n CONTROL,\n \"appearance-none pr-10 tabular-nums [&::-webkit-date-and-time-value]:text-left\",\n \"[&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:top-1/2\",\n \"[&::-webkit-calendar-picker-indicator]:size-5 [&::-webkit-calendar-picker-indicator]:-translate-y-1/2 [&::-webkit-calendar-picker-indicator]:m-0\",\n \"[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-0\",\n className,\n )}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n />\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\" />\n </svg>\n </span>\n );\n}\n\n/**\n * Keeps a <textarea> as tall as its text, between `minRows` and `maxRows`\n * lines; past `maxRows` it scrolls. Returns the ref callback to put on the\n * textarea (it also sets `outer`, a ref of the caller's). It measures on\n * every value change, typed or set (a composer clearing after send), and\n * when the width changes the wrapping. Textarea autoGrow and InputGroup\n * multiline use it; it is not a surface's tool.\n */\nexport function useAutoGrow({ enabled = true, minRows = 1, maxRows = 8, value, outer }) {\n const node = useRef(null);\n\n const fit = useCallback(() => {\n const el = node.current;\n if (!el || !enabled) return;\n const cs = getComputedStyle(el);\n const line = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5 || 20;\n const frame =\n parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom) +\n (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n const low = line * Math.max(1, minRows) + frame;\n const high = Number.isFinite(maxRows) && maxRows > 0 ? line * Math.max(minRows, maxRows) + frame : Infinity;\n el.style.height = \"auto\";\n const content = el.scrollHeight + (cs.boxSizing === \"border-box\" ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) : 0);\n el.style.height = `${Math.min(Math.max(content, low), high)}px`;\n el.style.overflowY = content > high ? \"auto\" : \"hidden\";\n }, [enabled, minRows, maxRows]);\n\n useLayoutEffect(fit, [fit, value]);\n\n // A width change re-wraps the text; a height change is our own doing.\n useLayoutEffect(() => {\n const el = node.current;\n if (!el || !enabled || typeof ResizeObserver === \"undefined\") return;\n let width = el.clientWidth;\n const ro = new ResizeObserver(() => {\n if (el.clientWidth === width) return;\n width = el.clientWidth;\n fit();\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [enabled, fit]);\n\n const ref = useCallback(\n (el) => {\n node.current = el;\n if (typeof outer === \"function\") return outer(el);\n if (outer) outer.current = el;\n },\n [outer],\n );\n return { ref, fit };\n}\n\n/**\n * The native <textarea>. `rows` sets its height (default 4) and it\n * resizes by its corner. `autoGrow` makes it as tall as its text instead:\n * from `minRows` (default 1) to `maxRows` (default 8) lines, then it\n * scrolls; no resize corner.\n */\nexport function Textarea({ className, invalid, rows = 4, autoGrow = false, minRows = 1, maxRows = 8, onInput, ref, ...rest }) {\n const grow = useAutoGrow({ enabled: autoGrow, minRows, maxRows, value: rest.value, outer: ref });\n return (\n <textarea\n ref={grow.ref}\n rows={autoGrow ? minRows : rows}\n className={cx(CONTROL, autoGrow ? \"resize-none py-2.5 leading-normal\" : \"min-h-24 resize-y py-2.5 leading-normal\", className)}\n aria-invalid={invalid ? true : undefined}\n onInput={(e) => {\n if (autoGrow) grow.fit();\n onInput?.(e);\n }}\n {...rest}\n />\n );\n}\n\n/**\n * The native <select>, styled. `options` is [{ value, label }]; children\n * work too. A select with nothing to choose from is not a control — when\n * `options` is empty it renders `empty` (a short muted line, or nothing\n * when `empty` is null) rather than a disabled dropdown. For \"pick a\n * saved record or make a new one\" use RecordSwitcher (components/parts).\n * A width in `className` (w-40, w-fit, w-[12rem], size-10) sets the width of\n * the whole control, as it does on an Input (0.8.1).\n */\nexport function Select({ options, empty = \"Nothing to choose yet.\", className, invalid, children, ...rest }) {\n if (options && options.length === 0 && !children) {\n return empty ? <p className=\"text-sm text-muted-foreground\">{empty}</p> : null;\n }\n // As Input: a width in className (w-40, w-auto, w-[12rem], size-10) is\n // honoured. The chevron sits on the wrapper, so the width goes to the\n // wrapper and the select fills it; the rest of className stays on the\n // select. Without one it is as wide as its column, as before.\n const tokens = String(className ?? \"\").split(/\\s+/).filter(Boolean);\n const own = tokens.filter((t) => /^!?(?:w|size)-/.test(t));\n const others = tokens.filter((t) => !own.includes(t));\n return (\n <span className={cx(\"relative block\", own.join(\" \"))}>\n <select\n className={cx(CONTROL, \"appearance-none pr-9\", others.join(\" \"))}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n >\n {options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}\n </select>\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n\n/** An on/off. A checkbox underneath (role=switch), so forms, labels and\n * keyboards work; the track is drawn from tokens. */\nexport function Switch({ id, checked, onChange, label, disabled, className }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}>\n <span className=\"relative inline-flex h-6 w-10 shrink-0\">\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n className=\"peer sr-only\"\n checked={checked}\n onChange={onChange}\n disabled={disabled}\n />\n <span className=\"absolute inset-0 rounded-full border border-input bg-muted transition-colors peer-checked:bg-primary peer-focus-visible:ring-2 peer-focus-visible:ring-ring\" />\n <span className=\"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-foreground transition-transform peer-checked:translate-x-4 peer-checked:bg-primary-foreground\" />\n </span>\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n\nexport function Checkbox({ id, label, className, ...rest }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm\", className)}>\n <input\n id={id}\n type=\"checkbox\"\n className=\"h-4 w-4 rounded-sm border-input accent-[var(--color-primary)] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n {...rest}\n />\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n"
133
133
  },
134
134
  {
135
135
  "path": "templates/next/files/components/ui/file-input.jsx",
@@ -141,7 +141,7 @@
141
141
  "path": "templates/next/files/components/ui/icon.jsx",
142
142
  "type": "registry:ui",
143
143
  "target": "@components/makersclaw/ui/icon.jsx",
144
- "content": "// The kit's icons: one inline sprite, no icon package to install. Every\n// glyph is a 24-box stroke path (1.8 wide, round caps, currentColor); add\n// one here, never an <svg> in a surface. `name` is a key below; unknown\n// names render nothing (and `npm run check` names them).\n//\n// `size` is pixels (16, the default) or one of the kit's size names:\n// xs 12 · sm 14 · md 16 · lg 20 · xl 24. Every other part of the kit\n// takes size=\"sm\", so a surface writes it here too — and until 0.3.3 a\n// name went onto the <svg> as width=\"sm\", which is not a length: the\n// browser logged \"Expected length, 'sm'\" and drew the glyph at its\n// 300×150 default, a calendar as big as the card around it (a podcast\n// guest board, 2026-09-25).\n//\n// A glyph added for a build takes lucide's name when lucide has one\n// (calendar-check, trash-2), because that is the name a build reaches for\n// first; the path is our own, drawn to the same 24 box, never copied.\n//\n// BRANDS holds the brand marks: google, github, apple, microsoft,\n// x-logo (the X / Twitter mark; `x` is the close glyph), linkedin,\n// instagram, youtube, facebook. Unlike PATHS they are FILLED shapes\n// (currentColor, even-odd so letters and holes knock out), one colour,\n// drawn by hand and simplified, never copied from an icon library.\n\nconst PATHS = {\n plus: \"M12 5v14M5 12h14\",\n x: \"M18 6 6 18M6 6l12 12\",\n check: \"m5 12 5 5L20 7\",\n \"chevron-down\": \"m6 9 6 6 6-6\",\n \"chevron-up\": \"m18 15-6-6-6 6\",\n \"chevron-left\": \"m15 18-6-6 6-6\",\n \"chevron-right\": \"m9 18 6-6-6-6\",\n \"chevrons-up-down\": \"m7 15 5 5 5-5M7 9l5-5 5 5\",\n \"arrow-left\": \"M19 12H5m7-7-7 7 7 7\",\n \"arrow-right\": \"M5 12h14m-7-7 7 7-7 7\",\n \"arrow-up\": \"M12 19V5m-7 7 7-7 7 7\",\n \"arrow-down\": \"M12 5v14m7-7-7 7-7-7\",\n search: \"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3\",\n trash: \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14\",\n pencil: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n copy: \"M8 8h12v12H8zM4 16V4h12\",\n more: \"M5 12h.01M12 12h.01M19 12h.01\",\n external: \"M14 3h7v7m0-7L10 14M21 14v7H3V3h7\",\n refresh: \"M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6\",\n filter: \"M3 5h18l-7 8v6l-4 2v-8Z\",\n calendar: \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18\",\n clock: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\",\n user: \"M20 21a8 8 0 0 0-16 0M12 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z\",\n users: \"M17 21a4 4 0 0 0-8 0M13 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM23 21a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M3 21a4 4 0 0 1 3-3.9M8 3.1a4 4 0 0 0 0 7.8\",\n mail: \"M3 5h18v14H3zM3 7l9 6 9-6\",\n link: \"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1\",\n grip: \"M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01\",\n info: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 5h.01M12 12v4\",\n warning: \"M12 3 2 20h20L12 3Zm0 7v4m0 3h.01\",\n sparkles: \"m12 3 2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6ZM19 15l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z\",\n play: \"m6 4 14 8-14 8V4Z\",\n pause: \"M6 4h4v16H6zM14 4h4v16h-4z\",\n download: \"M12 3v12m-5-5 5 5 5-5M4 21h16\",\n upload: \"M12 21V9m-5 5 5-5 5 5M4 3h16\",\n eye: \"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Zm10-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\",\n \"eye-off\": \"M10.6 5.1A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-2.4 3.2M6.6 6.6C3.8 8.4 2 12 2 12s4 7 10 7a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20\",\n send: \"m22 2-7 20-4-9-9-4 20-7Z\",\n home: \"m3 11 9-8 9 8v10h-6v-6H9v6H3V11Z\",\n settings: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8 4-1.5-.5.3-1.6 1.4-.9-1-1.7-1.6.5-1.2-1.2.5-1.6-1.7-1-.9 1.4-1.6-.3L12 4l-.5 1.5-1.6.3-.9-1.4-1.7 1 .5 1.6-1.2 1.2-1.6-.5-1 1.7 1.4.9.3 1.6L4 12l1.5.5-.3 1.6-1.4.9 1 1.7 1.6-.5 1.2 1.2-.5 1.6 1.7 1 .9-1.4 1.6.3L12 20l.5-1.5 1.6-.3.9 1.4 1.7-1-.5-1.6 1.2-1.2 1.6.5 1-1.7-1.4-.9-.3-1.6L20 12Z\",\n reply: \"M9 17 4 12l5-5M4 12h10a6 6 0 0 1 6 6v1\",\n repeat: \"m17 2 4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3\",\n heart: \"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z\",\n chart: \"M3 3v18h18M7 16l4-5 4 3 5-7\",\n share: \"M12 3v12M7 8l5-5 5 5M5 14v6h14v-6\",\n star: \"m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9L12 3Z\",\n bell: \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0\",\n tag: \"M20 12 12 20l-9-9V3h8l9 9ZM7 7h.01\",\n folder: \"M3 7V5h6l2 2h10v13H3V7Z\",\n file: \"M14 3H6v18h12V7l-4-4ZM14 3v4h4\",\n image: \"M3 5h18v14H3zM8 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM21 15l-5-5-9 9\",\n phone: \"M22 16.9v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7l.8 3.6a2 2 0 0 1-.5 1.9L8.1 10.5a16 16 0 0 0 5.4 5.4l1.3-1.3a2 2 0 0 1 1.9-.5l3.6.8a2 2 0 0 1 1.7 2Z\",\n globe: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\",\n lock: \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4\",\n zap: \"M13 2 3 14h8l-1 8 10-12h-8l1-8Z\",\n dollar: \"M12 2v20M17 6H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\",\n map: \"m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6ZM9 3v15M15 6v15\",\n minus: \"M5 12h14\",\n archive: \"M3 4h18v4H3zM5 8v12h14V8M10 12h4\",\n inbox: \"M3 13h5l2 3h4l2-3h5M3 13V5h18v8M3 13v6h18v-6\",\n pin: \"M12 17v5M8 3h8M9 3v7l-3 4v3h12v-3l-3-4V3\",\n bold: \"M7 4h6a4 4 0 0 1 0 8H7zM7 12h7a4 4 0 0 1 0 8H7z\",\n italic: \"M19 4h-9M14 20H5M15 4 9 20\",\n underline: \"M6 4v6a6 6 0 0 0 12 0V4M4 21h16\",\n list: \"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\",\n grid: \"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z\",\n \"align-left\": \"M3 6h18M3 12h12M3 18h16\",\n \"align-center\": \"M3 6h18M6 12h12M4 18h16\",\n \"align-right\": \"M3 6h18M9 12h12M5 18h16\",\n paperclip: \"m21 11-9 9a6 6 0 0 1-8.5-8.5l8.6-8.6a4 4 0 0 1 5.7 5.7l-8.6 8.5a2 2 0 0 1-2.8-2.8L15 6.9\",\n stop: \"M6 6h12v12H6z\",\n menu: \"M4 6h16M4 12h16M4 18h16\",\n \"panel-left\": \"M3 4h18v16H3zM9 4v16\",\n \"log-out\": \"M9 21H4V3h5M16 17l5-5-5-5M21 12H9\",\n code: \"m16 18 6-6-6-6M8 6l-6 6 6 6\",\n shield: \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3Z\",\n \"shield-check\": \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3ZM9 12l2 2 4-4\",\n \"circle-check\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n // The same glyph under its older name, so either one renders.\n \"check-circle\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n \"circle-x\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM15 9l-6 6M9 9l6 6\",\n \"message-square\": \"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z\",\n \"git-branch\": \"M6 3v12M18 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 9a9 9 0 0 1-9 9\",\n \"git-pull-request\": \"M6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 9v12M12 6h4a2 2 0 0 1 2 2v7m-3-12-3 3 3 3\",\n server: \"M3 3h18v8H3zM3 13h18v8H3zM7 7h.01M7 17h.01\",\n leaf: \"M5 19C5 10 10 4 20 4c0 10-6 15-15 15ZM3 21 14 10\",\n monitor: \"M3 4h18v12H3zM12 16v4M8 20h8\",\n smartphone: \"M8 3h8v18H8zM11 17.5h2\",\n video: \"M3 6h12v12H3zM15 10l6-3v10l-6-3\",\n tablet: \"M5 2h14v20H5zM11 18.5h2\",\n sun: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\",\n moon: \"M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\",\n database: \"M4 6a8 3 0 1 0 16 0 8 3 0 1 0-16 0ZM4 6v12a8 3 0 0 0 16 0V6M4 12a8 3 0 0 0 16 0\",\n \"git-commit\": \"M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM3 12h6M15 12h6\",\n lightbulb: \"M12 3a6 6 0 0 0-3.5 10.9c.6.4.9 1.1.9 1.8V16h5.2v-.3c0-.7.3-1.4.9-1.8A6 6 0 0 0 12 3ZM9.5 19h5M10.5 22h3\",\n \"life-buoy\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM5.6 5.6l3.6 3.6M14.8 14.8l3.6 3.6M18.4 5.6l-3.6 3.6M9.2 14.8l-3.6 3.6\",\n \"log-in\": \"M15 3h5v18h-5M10 17l5-5-5-5M15 12H3\",\n \"user-plus\": \"M2 21a7 7 0 0 1 14 0M9 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM19 8v6M16 11h6\",\n palette: \"M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.3 0-1.1.9-1.9 2-1.9h2a4 4 0 0 0 4-4C21 6.4 17 3 12 3ZM7.5 11.5h.01M9 7.5h.01M14 6.5h.01M17.5 9.5h.01\",\n layers: \"M12 3 2 8l10 5 10-5-10-5ZM2 12l10 5 10-5M2 16l10 5 10-5\",\n type: \"M4 7V4h16v3M12 4v16M9 20h6\",\n package: \"M12 2 3 7v10l9 5 9-5V7l-9-5ZM3 7l9 5 9-5M12 12v10M7.5 4.5l9 5\",\n // Task states: an empty ring is \"to do\", a broken ring is \"doing\".\n circle: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z\",\n \"circle-dashed\": \"M13.6 3.1A9 9 0 0 1 17.2 4.6M19.4 6.8A9 9 0 0 1 20.9 10.4M20.9 13.6A9 9 0 0 1 19.4 17.2M17.2 19.4A9 9 0 0 1 13.6 20.9M10.4 20.9A9 9 0 0 1 6.8 19.4M4.6 17.2A9 9 0 0 1 3.1 13.6M3.1 10.4A9 9 0 0 1 4.6 6.8M6.8 4.6A9 9 0 0 1 10.4 3.1\",\n loader: \"M12 2v4M12 18v4M2 12h4M18 12h4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8\",\n \"trending-up\": \"m3 17 6-6 4 4 8-8M15 7h6v6\",\n \"trending-down\": \"m3 7 6 6 4-4 8 8M15 17h6v-6\",\n \"message-circle\": \"M21 12a9 9 0 0 1-13.3 7.9L3 21l1.1-4.7A9 9 0 1 1 21 12Z\",\n \"credit-card\": \"M2 5h20v14H2zM2 10h20M6 15h4\",\n \"shopping-cart\": \"M2 3h3l2.4 12h11.2L21 7H5.8M8 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0ZM16 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0Z\",\n key: \"M7.5 11a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM10.7 12.3 21 2M16 7l3 3M18.5 4.5l2 2\",\n quote: \"M4 18h5v-6H4v6Zm0-6c0-4 1.5-6 5-7M15 18h5v-6h-5v6Zm0-6c0-4 1.5-6 5-7\",\n truck: \"M2 5h12v11H2zM14 9h4.5L21 12v4h-7M4 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0ZM15 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0Z\",\n laptop: \"M4 5h16v11H4zM2 19h20\",\n receipt: \"M5 3h14v18l-2.3-1.5-2.4 1.5-2.3-1.5-2.3 1.5-2.4-1.5L5 21V3ZM9 8h6M9 12h6M9 16h3\",\n mic: \"M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3ZM5 11a7 7 0 0 0 14 0M12 18v4M8 22h8\",\n \"thumbs-up\": \"M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5.5a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7\",\n \"thumbs-down\": \"M7 14V3H3v11h4Zm0 0 4 8a3 3 0 0 0 3-3v-4h5.5a2 2 0 0 0 2-2.3l-1.4-8a2 2 0 0 0-2-1.7H7\",\n history: \"M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5M12 7v5l3 2\",\n wand: \"M3 21 14 10M16 3v4M14 5h4M20 9v3M18.5 10.5h3M9 3v2M8 4h2\",\n building: \"M4 21V3h12v18M16 9h4v12M2 21h20M8 7h4M8 11h4M8 15h4\",\n // Canvas tools (flow-canvas): the rail, the zoom bar, the arrange buttons.\n \"mouse-pointer\": \"M5 3l5.8 16.5 2.4-6.8 6.8-2.4L5 3ZM13.2 12.7 19 18.5\",\n hand: \"M8 12V5.5a1.5 1.5 0 0 1 3 0V11M11 10.5V4a1.5 1.5 0 0 1 3 0v6.5M14 10.5V5.5a1.5 1.5 0 0 1 3 0V14a7 7 0 0 1-7 7h-.4a6 6 0 0 1-4.7-2.3l-2.3-2.9a1.6 1.6 0 0 1 2.4-2.1L8 16\",\n \"sticky-note\": \"M4 4h16v10l-6 6H4V4ZM14 20v-6h6\",\n square: \"M4 4h16v16H4z\",\n frame: \"M7 3v18M17 3v18M3 7h18M3 17h18\",\n connector: \"M4 20C4 11 10 5 19 5m-4-3.5L19 5l-4 3.5M4 20h.01\",\n maximize: \"M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5\",\n undo: \"M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3\",\n redo: \"m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3\",\n keyboard: \"M2 6h20v12H2zM6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10\",\n // Sketch tools (sketch-canvas): the diamond shape and the eraser.\n diamond: \"M12 3 21 12 12 21 3 12Z\",\n eraser: \"M20 20H8.5l-4-4a2 2 0 0 1 0-2.8l8.8-8.8a2 2 0 0 1 2.8 0l3.5 3.5a2 2 0 0 1 0 2.8L12 20M8.5 9.5l6 6\",\n highlighter: \"m14.5 4.5 5 5-7.5 7.5H8.5L7 15.5v-3.5ZM11 8l5 5M7 15.5 4 18.5l2 1.5h5M14 21h6\",\n \"lock-open\": \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 7.7-1.5\",\n \"bring-front\": \"M12 14V3M8 7l4-4 4 4M4 15l8 5 8-5\",\n \"send-back\": \"M12 3v11M8 10l4 4 4-4M4 15l8 5 8-5\",\n // Column types (spreadsheet): a number field, a checkbox field.\n hash: \"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\",\n \"square-check\": \"M4 4h16v16H4zM8 12l3 3 5-6\",\n target: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10ZM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z\",\n flag: \"M5 21V4m0 0h11l-2 4 2 4H5\",\n \"circle-help\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.5 9.2a2.6 2.6 0 0 1 5 .9c0 1.7-2.5 2.3-2.5 3.9M12 17h.01\",\n // Playback (audio-player): skip back / forward, sound on / off.\n \"rotate-ccw\": \"M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5\",\n \"rotate-cw\": \"M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5\",\n volume: \"M11 5 6 9H2v6h4l5 4V5ZM15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14\",\n \"volume-x\": \"M11 5 6 9H2v6h4l5 4V5ZM22 9l-6 6M16 9l6 6\",\n navigation: \"M3 11 21 3l-8 18-2-8Z\",\n locate: \"M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM12 2v3M12 19v3M2 12h3M19 12h3\",\n route: \"M6 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\",\n // View kinds (work-views): a board's columns, a table's grid, a gantt's bars.\n kanban: \"M4 4h4v14H4zM10 4h4v9h-4zM16 4h4v16h-4z\",\n table: \"M3 4h18v16H3zM3 10h18M3 15h18M9 10v10\",\n gantt: \"M4 4v16h16M8 8h6M11 12h7M9 16h5\",\n // Names builds reached for that the sprite lacked, so `npm run check`\n // refused the ship and the build spent a round renaming them (the\n // launch build test, 2026-09-25, and the local build logs before it):\n // a day that is booked, the bin with its slats, a small tree of nodes.\n \"calendar-check\": \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18M9 15.5l2 2 4-4\",\n \"trash-2\": \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14M10 11v6M14 11v6\",\n network: \"M9 2h6v6H9zM2 16h6v6H2zM16 16h6v6h-6zM12 8v4M5 16v-4h14v4\",\n};\n\nconst BRANDS = {\n google: \"M18.9 6.2A9 9 0 1 0 21 12v-1.8h-8.8v3.4h4.96A5.4 5.4 0 1 1 16.14 8.53Z\",\n github: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10 21.3v-2.9c-2.8-.4-4.5-2-4.5-5 0-1.1.4-2.1 1.1-2.9-.2-.7-.2-1.7.2-2.7 1 0 2 .5 2.8 1.1a8 8 0 0 1 4.8 0c.8-.6 1.8-1.1 2.8-1.1.4 1 .4 2 .2 2.7.7.8 1.1 1.8 1.1 2.9 0 3-1.7 4.6-4.5 5v2.9ZM10 20.2c-2.4.4-3.5-.5-4.3-2l1-.4c.6 1.1 1.4 1.7 3.3 1.3Z\",\n apple: \"M12 7.8C10.5 6.8 8.8 6.6 7.4 7.4 4.8 8.9 4.4 12.8 5.9 16.2c1 2.4 2.5 4.4 4.1 4.4.9 0 1.3-.5 2-.5s1.1.5 2 .5c1.6 0 3-1.7 4-3.6.5-.8.9-1.4 1.3-2A3.2 3.2 0 0 1 18.3 9.3c-.8-1.2-2-2.1-3.5-2.2-1 0-1.9.3-2.8.7ZM12.2 6.3c0-1.8 1.3-3.2 3.1-3.3 0 1.8-1.3 3.2-3.1 3.3Z\",\n microsoft: \"M3 3h8.5v8.5H3zM12.5 3H21v8.5h-8.5zM3 12.5h8.5V21H3zM12.5 12.5H21V21h-8.5z\",\n \"x-logo\": \"M2.8 3h6l12.4 18h-6ZM5.3 4.3l10.6 15.4h2.8L8.1 4.3ZM19.3 3h1.9l-6.6 7.4-.8-1ZM4.7 21H2.8l6.6-7.4.8 1Z\",\n linkedin: \"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM6.4 7.6a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0ZM6.6 10h2.3v7.5H6.6ZM10.8 10H13v1.1c.5-.8 1.4-1.3 2.5-1.3 2 0 3 1.2 3 3.4v4.3h-2.3v-3.9c0-1.1-.4-1.8-1.4-1.8s-1.7.7-1.7 1.8v3.9h-2.3Z\",\n instagram: \"M7.5 2.5h9a5 5 0 0 1 5 5v9a5 5 0 0 1-5 5h-9a5 5 0 0 1-5-5v-9a5 5 0 0 1 5-5ZM7.5 4.3a3.2 3.2 0 0 0-3.2 3.2v9a3.2 3.2 0 0 0 3.2 3.2h9a3.2 3.2 0 0 0 3.2-3.2v-9a3.2 3.2 0 0 0-3.2-3.2ZM12 7.3a4.7 4.7 0 1 0 0 9.4 4.7 4.7 0 0 0 0-9.4Zm0 1.8a2.9 2.9 0 1 1 0 5.8 2.9 2.9 0 0 1 0-5.8ZM16.2 6.7a1.1 1.1 0 1 0 2.2 0 1.1 1.1 0 1 0-2.2 0Z\",\n youtube: \"M2.5 7.5c.2-1.6 1.2-2.6 2.8-2.7 2.1-.2 4.4-.3 6.7-.3s4.6.1 6.7.3c1.6.1 2.6 1.1 2.8 2.7.2 1.5.3 3 .3 4.5s-.1 3-.3 4.5c-.2 1.6-1.2 2.6-2.8 2.7-2.1.2-4.4.3-6.7.3s-4.6-.1-6.7-.3c-1.6-.1-2.6-1.1-2.8-2.7-.2-1.5-.3-3-.3-4.5s.1-3 .3-4.5ZM10 8.8v6.4l5.5-3.2Z\",\n facebook: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10.3 21.4v-7.6H8.4v-2.5h1.9V9.4c0-2.5 1.4-3.9 3.7-3.9.9 0 1.6.1 1.9.1v2.2h-1.3c-1 0-1.4.6-1.4 1.4v2.1h2.6l-.4 2.5h-2.2v7.6Z\",\n};\n\nexport const ICON_NAMES = [...Object.keys(PATHS), ...Object.keys(BRANDS)];\n\n// The kit's size names in pixels. A number, or a length the <svg> can\n// read (\"18\", \"1.25em\"), is kept as it was; anything else is the default,\n// never an attribute the browser cannot read.\nconst SIZES = { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 };\nconst LENGTH = /^(\\d+\\.?\\d*|\\.\\d+)(px|em|rem|%)?$/;\n\nfunction pixels(size) {\n if (typeof size === \"number\" && size > 0) return size;\n if (typeof size === \"string\") {\n if (SIZES[size]) return SIZES[size];\n if (LENGTH.test(size)) return size;\n }\n return SIZES.md;\n}\n\nexport function Icon({ name, size = 16, className, label }) {\n const brand = BRANDS[name];\n const d = PATHS[name] || brand;\n if (!d) return null;\n const px = pixels(size);\n return (\n <svg\n width={px}\n height={px}\n viewBox=\"0 0 24 24\"\n fill={brand ? \"currentColor\" : \"none\"}\n fillRule={brand ? \"evenodd\" : undefined}\n stroke={brand ? \"none\" : \"currentColor\"}\n strokeWidth={brand ? undefined : \"1.8\"}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden={label ? undefined : true}\n role={label ? \"img\" : undefined}\n aria-label={label}\n className={className}\n >\n <path d={d} />\n </svg>\n );\n}\n"
144
+ "content": "// The kit's icons: one inline sprite, no icon package to install. Every\n// glyph is a 24-box stroke path (1.8 wide, round caps, currentColor); add\n// one here, never an <svg> in a surface. `name` is a key below; unknown\n// names render nothing (and `npm run check` names them).\n//\n// `size` is pixels (16, the default) or one of the kit's size names:\n// xs 12 · sm 14 · md 16 · lg 20 · xl 24. Every other part of the kit\n// takes size=\"sm\", so a surface writes it here too — and until 0.3.3 a\n// name went onto the <svg> as width=\"sm\", which is not a length: the\n// browser logged \"Expected length, 'sm'\" and drew the glyph at its\n// 300×150 default, a calendar as big as the card around it (a podcast\n// guest board, 2026-09-25).\n//\n// A glyph added for a build takes lucide's name when lucide has one\n// (calendar-check, trash-2), because that is the name a build reaches for\n// first; the path is our own, drawn to the same 24 box, never copied.\n//\n// BRANDS holds the brand marks: google, github, apple, microsoft,\n// x-logo (the X / Twitter mark; `x` is the close glyph), linkedin,\n// instagram, youtube, facebook. Unlike PATHS they are FILLED shapes\n// (currentColor, even-odd so letters and holes knock out), one colour,\n// drawn by hand and simplified, never copied from an icon library.\n\nconst PATHS = {\n plus: \"M12 5v14M5 12h14\",\n x: \"M18 6 6 18M6 6l12 12\",\n check: \"m5 12 5 5L20 7\",\n \"chevron-down\": \"m6 9 6 6 6-6\",\n \"chevron-up\": \"m18 15-6-6-6 6\",\n \"chevron-left\": \"m15 18-6-6 6-6\",\n \"chevron-right\": \"m9 18 6-6-6-6\",\n \"chevrons-up-down\": \"m7 15 5 5 5-5M7 9l5-5 5 5\",\n \"arrow-left\": \"M19 12H5m7-7-7 7 7 7\",\n \"arrow-right\": \"M5 12h14m-7-7 7 7-7 7\",\n \"arrow-up\": \"M12 19V5m-7 7 7-7 7 7\",\n \"arrow-down\": \"M12 5v14m7-7-7 7-7-7\",\n search: \"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3\",\n trash: \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14\",\n pencil: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n copy: \"M8 8h12v12H8zM4 16V4h12\",\n more: \"M5 12h.01M12 12h.01M19 12h.01\",\n external: \"M14 3h7v7m0-7L10 14M21 14v7H3V3h7\",\n refresh: \"M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6\",\n filter: \"M3 5h18l-7 8v6l-4 2v-8Z\",\n calendar: \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18\",\n clock: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\",\n user: \"M20 21a8 8 0 0 0-16 0M12 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z\",\n users: \"M17 21a4 4 0 0 0-8 0M13 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM23 21a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M3 21a4 4 0 0 1 3-3.9M8 3.1a4 4 0 0 0 0 7.8\",\n mail: \"M3 5h18v14H3zM3 7l9 6 9-6\",\n link: \"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1\",\n grip: \"M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01\",\n info: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 5h.01M12 12v4\",\n warning: \"M12 3 2 20h20L12 3Zm0 7v4m0 3h.01\",\n sparkles: \"m12 3 2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6ZM19 15l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z\",\n play: \"m6 4 14 8-14 8V4Z\",\n pause: \"M6 4h4v16H6zM14 4h4v16h-4z\",\n download: \"M12 3v12m-5-5 5 5 5-5M4 21h16\",\n upload: \"M12 21V9m-5 5 5-5 5 5M4 3h16\",\n eye: \"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Zm10-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\",\n \"eye-off\": \"M10.6 5.1A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-2.4 3.2M6.6 6.6C3.8 8.4 2 12 2 12s4 7 10 7a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20\",\n send: \"m22 2-7 20-4-9-9-4 20-7Z\",\n home: \"m3 11 9-8 9 8v10h-6v-6H9v6H3V11Z\",\n settings: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8 4-1.5-.5.3-1.6 1.4-.9-1-1.7-1.6.5-1.2-1.2.5-1.6-1.7-1-.9 1.4-1.6-.3L12 4l-.5 1.5-1.6.3-.9-1.4-1.7 1 .5 1.6-1.2 1.2-1.6-.5-1 1.7 1.4.9.3 1.6L4 12l1.5.5-.3 1.6-1.4.9 1 1.7 1.6-.5 1.2 1.2-.5 1.6 1.7 1 .9-1.4 1.6.3L12 20l.5-1.5 1.6-.3.9 1.4 1.7-1-.5-1.6 1.2-1.2 1.6.5 1-1.7-1.4-.9-.3-1.6L20 12Z\",\n reply: \"M9 17 4 12l5-5M4 12h10a6 6 0 0 1 6 6v1\",\n repeat: \"m17 2 4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3\",\n heart: \"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z\",\n chart: \"M3 3v18h18M7 16l4-5 4 3 5-7\",\n share: \"M12 3v12M7 8l5-5 5 5M5 14v6h14v-6\",\n star: \"m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9L12 3Z\",\n bell: \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0\",\n tag: \"M20 12 12 20l-9-9V3h8l9 9ZM7 7h.01\",\n folder: \"M3 7V5h6l2 2h10v13H3V7Z\",\n file: \"M14 3H6v18h12V7l-4-4ZM14 3v4h4\",\n image: \"M3 5h18v14H3zM8 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM21 15l-5-5-9 9\",\n phone: \"M22 16.9v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7l.8 3.6a2 2 0 0 1-.5 1.9L8.1 10.5a16 16 0 0 0 5.4 5.4l1.3-1.3a2 2 0 0 1 1.9-.5l3.6.8a2 2 0 0 1 1.7 2Z\",\n globe: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\",\n lock: \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4\",\n zap: \"M13 2 3 14h8l-1 8 10-12h-8l1-8Z\",\n dollar: \"M12 2v20M17 6H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\",\n map: \"m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6ZM9 3v15M15 6v15\",\n minus: \"M5 12h14\",\n archive: \"M3 4h18v4H3zM5 8v12h14V8M10 12h4\",\n inbox: \"M3 13h5l2 3h4l2-3h5M3 13V5h18v8M3 13v6h18v-6\",\n pin: \"M12 17v5M8 3h8M9 3v7l-3 4v3h12v-3l-3-4V3\",\n bold: \"M7 4h6a4 4 0 0 1 0 8H7zM7 12h7a4 4 0 0 1 0 8H7z\",\n italic: \"M19 4h-9M14 20H5M15 4 9 20\",\n underline: \"M6 4v6a6 6 0 0 0 12 0V4M4 21h16\",\n list: \"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\",\n grid: \"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z\",\n \"align-left\": \"M3 6h18M3 12h12M3 18h16\",\n \"align-center\": \"M3 6h18M6 12h12M4 18h16\",\n \"align-right\": \"M3 6h18M9 12h12M5 18h16\",\n paperclip: \"m21 11-9 9a6 6 0 0 1-8.5-8.5l8.6-8.6a4 4 0 0 1 5.7 5.7l-8.6 8.5a2 2 0 0 1-2.8-2.8L15 6.9\",\n stop: \"M6 6h12v12H6z\",\n menu: \"M4 6h16M4 12h16M4 18h16\",\n \"panel-left\": \"M3 4h18v16H3zM9 4v16\",\n \"log-out\": \"M9 21H4V3h5M16 17l5-5-5-5M21 12H9\",\n code: \"m16 18 6-6-6-6M8 6l-6 6 6 6\",\n shield: \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3Z\",\n \"shield-check\": \"M12 3 4 6v6c0 5 3.4 8.2 8 9 4.6-.8 8-4 8-9V6l-8-3ZM9 12l2 2 4-4\",\n \"circle-check\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n // The same glyph under its older name, so either one renders.\n \"check-circle\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9 3 3 5-5\",\n \"circle-x\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM15 9l-6 6M9 9l6 6\",\n \"message-square\": \"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z\",\n \"git-branch\": \"M6 3v12M18 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 9a9 9 0 0 1-9 9\",\n \"git-pull-request\": \"M6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM6 9v12M12 6h4a2 2 0 0 1 2 2v7m-3-12-3 3 3 3\",\n server: \"M3 3h18v8H3zM3 13h18v8H3zM7 7h.01M7 17h.01\",\n leaf: \"M5 19C5 10 10 4 20 4c0 10-6 15-15 15ZM3 21 14 10\",\n monitor: \"M3 4h18v12H3zM12 16v4M8 20h8\",\n smartphone: \"M8 3h8v18H8zM11 17.5h2\",\n video: \"M3 6h12v12H3zM15 10l6-3v10l-6-3\",\n tablet: \"M5 2h14v20H5zM11 18.5h2\",\n sun: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\",\n moon: \"M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\",\n database: \"M4 6a8 3 0 1 0 16 0 8 3 0 1 0-16 0ZM4 6v12a8 3 0 0 0 16 0V6M4 12a8 3 0 0 0 16 0\",\n \"git-commit\": \"M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM3 12h6M15 12h6\",\n lightbulb: \"M12 3a6 6 0 0 0-3.5 10.9c.6.4.9 1.1.9 1.8V16h5.2v-.3c0-.7.3-1.4.9-1.8A6 6 0 0 0 12 3ZM9.5 19h5M10.5 22h3\",\n \"life-buoy\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM5.6 5.6l3.6 3.6M14.8 14.8l3.6 3.6M18.4 5.6l-3.6 3.6M9.2 14.8l-3.6 3.6\",\n \"log-in\": \"M15 3h5v18h-5M10 17l5-5-5-5M15 12H3\",\n \"user-plus\": \"M2 21a7 7 0 0 1 14 0M9 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM19 8v6M16 11h6\",\n palette: \"M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.3 0-1.1.9-1.9 2-1.9h2a4 4 0 0 0 4-4C21 6.4 17 3 12 3ZM7.5 11.5h.01M9 7.5h.01M14 6.5h.01M17.5 9.5h.01\",\n layers: \"M12 3 2 8l10 5 10-5-10-5ZM2 12l10 5 10-5M2 16l10 5 10-5\",\n type: \"M4 7V4h16v3M12 4v16M9 20h6\",\n package: \"M12 2 3 7v10l9 5 9-5V7l-9-5ZM3 7l9 5 9-5M12 12v10M7.5 4.5l9 5\",\n // Task states: an empty ring is \"to do\", a broken ring is \"doing\".\n circle: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z\",\n \"circle-dashed\": \"M13.6 3.1A9 9 0 0 1 17.2 4.6M19.4 6.8A9 9 0 0 1 20.9 10.4M20.9 13.6A9 9 0 0 1 19.4 17.2M17.2 19.4A9 9 0 0 1 13.6 20.9M10.4 20.9A9 9 0 0 1 6.8 19.4M4.6 17.2A9 9 0 0 1 3.1 13.6M3.1 10.4A9 9 0 0 1 4.6 6.8M6.8 4.6A9 9 0 0 1 10.4 3.1\",\n loader: \"M12 2v4M12 18v4M2 12h4M18 12h4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8\",\n \"trending-up\": \"m3 17 6-6 4 4 8-8M15 7h6v6\",\n \"trending-down\": \"m3 7 6 6 4-4 8 8M15 17h6v-6\",\n \"message-circle\": \"M21 12a9 9 0 0 1-13.3 7.9L3 21l1.1-4.7A9 9 0 1 1 21 12Z\",\n \"credit-card\": \"M2 5h20v14H2zM2 10h20M6 15h4\",\n \"shopping-cart\": \"M2 3h3l2.4 12h11.2L21 7H5.8M8 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0ZM16 19.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0-3 0Z\",\n key: \"M7.5 11a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM10.7 12.3 21 2M16 7l3 3M18.5 4.5l2 2\",\n quote: \"M4 18h5v-6H4v6Zm0-6c0-4 1.5-6 5-7M15 18h5v-6h-5v6Zm0-6c0-4 1.5-6 5-7\",\n truck: \"M2 5h12v11H2zM14 9h4.5L21 12v4h-7M4 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0ZM15 18a2 2 0 1 0 4 0 2 2 0 1 0-4 0Z\",\n laptop: \"M4 5h16v11H4zM2 19h20\",\n receipt: \"M5 3h14v18l-2.3-1.5-2.4 1.5-2.3-1.5-2.3 1.5-2.4-1.5L5 21V3ZM9 8h6M9 12h6M9 16h3\",\n mic: \"M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3ZM5 11a7 7 0 0 0 14 0M12 18v4M8 22h8\",\n \"thumbs-up\": \"M7 10v11H3V10h4Zm0 0 4-8a3 3 0 0 1 3 3v4h5.5a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7\",\n \"thumbs-down\": \"M7 14V3H3v11h4Zm0 0 4 8a3 3 0 0 0 3-3v-4h5.5a2 2 0 0 0 2-2.3l-1.4-8a2 2 0 0 0-2-1.7H7\",\n history: \"M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5M12 7v5l3 2\",\n wand: \"M3 21 14 10M16 3v4M14 5h4M20 9v3M18.5 10.5h3M9 3v2M8 4h2\",\n building: \"M4 21V3h12v18M16 9h4v12M2 21h20M8 7h4M8 11h4M8 15h4\",\n // Canvas tools (flow-canvas): the rail, the zoom bar, the arrange buttons.\n \"mouse-pointer\": \"M5 3l5.8 16.5 2.4-6.8 6.8-2.4L5 3ZM13.2 12.7 19 18.5\",\n hand: \"M8 12V5.5a1.5 1.5 0 0 1 3 0V11M11 10.5V4a1.5 1.5 0 0 1 3 0v6.5M14 10.5V5.5a1.5 1.5 0 0 1 3 0V14a7 7 0 0 1-7 7h-.4a6 6 0 0 1-4.7-2.3l-2.3-2.9a1.6 1.6 0 0 1 2.4-2.1L8 16\",\n \"sticky-note\": \"M4 4h16v10l-6 6H4V4ZM14 20v-6h6\",\n square: \"M4 4h16v16H4z\",\n frame: \"M7 3v18M17 3v18M3 7h18M3 17h18\",\n connector: \"M4 20C4 11 10 5 19 5m-4-3.5L19 5l-4 3.5M4 20h.01\",\n maximize: \"M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5\",\n undo: \"M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3\",\n redo: \"m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3\",\n keyboard: \"M2 6h20v12H2zM6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10\",\n // Sketch tools (sketch-canvas): the diamond shape and the eraser.\n diamond: \"M12 3 21 12 12 21 3 12Z\",\n eraser: \"M20 20H8.5l-4-4a2 2 0 0 1 0-2.8l8.8-8.8a2 2 0 0 1 2.8 0l3.5 3.5a2 2 0 0 1 0 2.8L12 20M8.5 9.5l6 6\",\n highlighter: \"m14.5 4.5 5 5-7.5 7.5H8.5L7 15.5v-3.5ZM11 8l5 5M7 15.5 4 18.5l2 1.5h5M14 21h6\",\n \"lock-open\": \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 7.7-1.5\",\n \"bring-front\": \"M12 14V3M8 7l4-4 4 4M4 15l8 5 8-5\",\n \"send-back\": \"M12 3v11M8 10l4 4 4-4M4 15l8 5 8-5\",\n // Column types (spreadsheet): a number field, a checkbox field.\n hash: \"M4 9h16M4 15h16M10 3 8 21M16 3l-2 18\",\n \"square-check\": \"M4 4h16v16H4zM8 12l3 3 5-6\",\n target: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10ZM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z\",\n flag: \"M5 21V4m0 0h11l-2 4 2 4H5\",\n \"circle-help\": \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.5 9.2a2.6 2.6 0 0 1 5 .9c0 1.7-2.5 2.3-2.5 3.9M12 17h.01\",\n // Playback (audio-player): skip back / forward, sound on / off.\n \"rotate-ccw\": \"M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5\",\n \"rotate-cw\": \"M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5\",\n volume: \"M11 5 6 9H2v6h4l5 4V5ZM15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14\",\n \"volume-x\": \"M11 5 6 9H2v6h4l5 4V5ZM22 9l-6 6M16 9l6 6\",\n navigation: \"M3 11 21 3l-8 18-2-8Z\",\n locate: \"M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM12 2v3M12 19v3M2 12h3M19 12h3\",\n route: \"M6 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM18 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\",\n // View kinds (work-views): a board's columns, a table's grid, a gantt's bars.\n kanban: \"M4 4h4v14H4zM10 4h4v9h-4zM16 4h4v16h-4z\",\n table: \"M3 4h18v16H3zM3 10h18M3 15h18M9 10v10\",\n gantt: \"M4 4v16h16M8 8h6M11 12h7M9 16h5\",\n // Names builds reached for that the sprite lacked, so `npm run check`\n // refused the ship and the build spent a round renaming them (the\n // launch build test, 2026-09-25, and the local build logs before it):\n // a day that is booked, the bin with its slats, a small tree of nodes.\n \"calendar-check\": \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18M9 15.5l2 2 4-4\",\n \"trash-2\": \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14M10 11v6M14 11v6\",\n network: \"M9 2h6v6H9zM2 16h6v6H2zM16 16h6v6h-6zM12 8v4M5 16v-4h14v4\",\n // Automations (flow-editor and the blocks around it): one glyph per kind\n // of step, drawn to the same 24 box. calendar-clock is a schedule's\n // trigger, split a condition's fork, braces a set of values, timer a\n // wait, plug a connected tool, bot the agent, workflow another\n // automation, book-open a read of the company brain.\n \"calendar-clock\": \"M12 21H5V5h14v7M3 10h18M8 3v4M16 3v4M17.5 14a4 4 0 1 0 .01 0M17.5 16v2l1.2.8\",\n split: \"M3 12h5c3 0 3-6 6-6h6M8 12c3 0 3 6 6 6h6M17 3l3 3-3 3M17 15l3 3-3 3\",\n braces: \"M8 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1M16 3h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-1\",\n timer: \"M10 2h4M12 8v5l3 2M12 6a8 8 0 1 0 0 16 8 8 0 0 0 0-16Z\",\n plug: \"M9 2v5M15 2v5M6 7h12v4a6 6 0 0 1-12 0V7ZM12 17v5\",\n bot: \"M12 3v3M5 8h14v11H5ZM9 13h.01M15 13h.01M2 13v3M22 13v3M9 17h6\",\n workflow: \"M3 4h7v7H3zM14 13h7v7h-7zM10 7.5h2.5a2 2 0 0 1 2 2V13\",\n \"book-open\": \"M12 6c-2-2-5-3-9-3v15c4 0 7 1 9 3 2-2 5-3 9-3V3c-4 0-7 1-9 3ZM12 6v15\",\n};\n\nconst BRANDS = {\n google: \"M18.9 6.2A9 9 0 1 0 21 12v-1.8h-8.8v3.4h4.96A5.4 5.4 0 1 1 16.14 8.53Z\",\n github: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10 21.3v-2.9c-2.8-.4-4.5-2-4.5-5 0-1.1.4-2.1 1.1-2.9-.2-.7-.2-1.7.2-2.7 1 0 2 .5 2.8 1.1a8 8 0 0 1 4.8 0c.8-.6 1.8-1.1 2.8-1.1.4 1 .4 2 .2 2.7.7.8 1.1 1.8 1.1 2.9 0 3-1.7 4.6-4.5 5v2.9ZM10 20.2c-2.4.4-3.5-.5-4.3-2l1-.4c.6 1.1 1.4 1.7 3.3 1.3Z\",\n apple: \"M12 7.8C10.5 6.8 8.8 6.6 7.4 7.4 4.8 8.9 4.4 12.8 5.9 16.2c1 2.4 2.5 4.4 4.1 4.4.9 0 1.3-.5 2-.5s1.1.5 2 .5c1.6 0 3-1.7 4-3.6.5-.8.9-1.4 1.3-2A3.2 3.2 0 0 1 18.3 9.3c-.8-1.2-2-2.1-3.5-2.2-1 0-1.9.3-2.8.7ZM12.2 6.3c0-1.8 1.3-3.2 3.1-3.3 0 1.8-1.3 3.2-3.1 3.3Z\",\n microsoft: \"M3 3h8.5v8.5H3zM12.5 3H21v8.5h-8.5zM3 12.5h8.5V21H3zM12.5 12.5H21V21h-8.5z\",\n \"x-logo\": \"M2.8 3h6l12.4 18h-6ZM5.3 4.3l10.6 15.4h2.8L8.1 4.3ZM19.3 3h1.9l-6.6 7.4-.8-1ZM4.7 21H2.8l6.6-7.4.8 1Z\",\n linkedin: \"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM6.4 7.6a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0ZM6.6 10h2.3v7.5H6.6ZM10.8 10H13v1.1c.5-.8 1.4-1.3 2.5-1.3 2 0 3 1.2 3 3.4v4.3h-2.3v-3.9c0-1.1-.4-1.8-1.4-1.8s-1.7.7-1.7 1.8v3.9h-2.3Z\",\n instagram: \"M7.5 2.5h9a5 5 0 0 1 5 5v9a5 5 0 0 1-5 5h-9a5 5 0 0 1-5-5v-9a5 5 0 0 1 5-5ZM7.5 4.3a3.2 3.2 0 0 0-3.2 3.2v9a3.2 3.2 0 0 0 3.2 3.2h9a3.2 3.2 0 0 0 3.2-3.2v-9a3.2 3.2 0 0 0-3.2-3.2ZM12 7.3a4.7 4.7 0 1 0 0 9.4 4.7 4.7 0 0 0 0-9.4Zm0 1.8a2.9 2.9 0 1 1 0 5.8 2.9 2.9 0 0 1 0-5.8ZM16.2 6.7a1.1 1.1 0 1 0 2.2 0 1.1 1.1 0 1 0-2.2 0Z\",\n youtube: \"M2.5 7.5c.2-1.6 1.2-2.6 2.8-2.7 2.1-.2 4.4-.3 6.7-.3s4.6.1 6.7.3c1.6.1 2.6 1.1 2.8 2.7.2 1.5.3 3 .3 4.5s-.1 3-.3 4.5c-.2 1.6-1.2 2.6-2.8 2.7-2.1.2-4.4.3-6.7.3s-4.6-.1-6.7-.3c-1.6-.1-2.6-1.1-2.8-2.7-.2-1.5-.3-3-.3-4.5s.1-3 .3-4.5ZM10 8.8v6.4l5.5-3.2Z\",\n facebook: \"M12 2.5a9.5 9.5 0 1 0 0 19 9.5 9.5 0 0 0 0-19ZM10.3 21.4v-7.6H8.4v-2.5h1.9V9.4c0-2.5 1.4-3.9 3.7-3.9.9 0 1.6.1 1.9.1v2.2h-1.3c-1 0-1.4.6-1.4 1.4v2.1h2.6l-.4 2.5h-2.2v7.6Z\",\n};\n\nexport const ICON_NAMES = [...Object.keys(PATHS), ...Object.keys(BRANDS)];\n\n// The kit's size names in pixels. A number, or a length the <svg> can\n// read (\"18\", \"1.25em\"), is kept as it was; anything else is the default,\n// never an attribute the browser cannot read.\nconst SIZES = { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 };\nconst LENGTH = /^(\\d+\\.?\\d*|\\.\\d+)(px|em|rem|%)?$/;\n\nfunction pixels(size) {\n if (typeof size === \"number\" && size > 0) return size;\n if (typeof size === \"string\") {\n if (SIZES[size]) return SIZES[size];\n if (LENGTH.test(size)) return size;\n }\n return SIZES.md;\n}\n\nexport function Icon({ name, size = 16, className, label }) {\n const brand = BRANDS[name];\n const d = PATHS[name] || brand;\n if (!d) return null;\n const px = pixels(size);\n return (\n <svg\n width={px}\n height={px}\n viewBox=\"0 0 24 24\"\n fill={brand ? \"currentColor\" : \"none\"}\n fillRule={brand ? \"evenodd\" : undefined}\n stroke={brand ? \"none\" : \"currentColor\"}\n strokeWidth={brand ? undefined : \"1.8\"}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden={label ? undefined : true}\n role={label ? \"img\" : undefined}\n aria-label={label}\n className={className}\n >\n <path d={d} />\n </svg>\n );\n}\n"
145
145
  },
146
146
  {
147
147
  "path": "templates/next/files/components/ui/image.jsx",
@@ -153,7 +153,7 @@
153
153
  "path": "templates/next/files/components/ui/index.js",
154
154
  "type": "registry:ui",
155
155
  "target": "@components/makersclaw/ui/index.js",
156
- "content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n toLocalDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { AppLayout, DocumentLayout } from \"./layout\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
156
+ "content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, ActionCard, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n toLocalDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { AppLayout, DocumentLayout } from \"./layout\";\nexport { PageLayout } from \"./page\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
157
157
  },
158
158
  {
159
159
  "path": "templates/next/files/components/ui/input-group.jsx",
@@ -177,7 +177,7 @@
177
177
  "path": "templates/next/files/components/ui/layout.jsx",
178
178
  "type": "registry:ui",
179
179
  "target": "@components/makersclaw/ui/layout.jsx",
180
- "content": "\"use client\";\n\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Sheet } from \"./sheet\";\n\n// Layouts — who owns the window's height, and who scrolls.\n//\n// Every design-first build of 2026-10-07 shipped a shell of `min-h-dvh`\n// that grew with its content: the page itself scrolled, no pane scrolled\n// on its own, and a footer, an inspector or a button below a long\n// article sat past the end of the iframe, where nothing could reach it.\n// So the kit owns the window's height, once, and the app's code never\n// sizes anything by the window (`npm run check` refuses min-h-dvh,\n// h-screen, 100vh, h-[80vh]… outside the kit, rule 12).\n//\n// AppLayout — the app frame. Exactly one owns the viewport: the\n// outermost. A layout inside another fills the slot it is in instead,\n// whatever `height` says, so there is never a second viewport owner. The\n// shell (app/shell.jsx) is the outermost one in every app the template\n// lays; a surface that needs an inspector or a fill of its own returns\n// an AppLayout as its outermost element, and it fills the shell's\n// content edge to edge (the content's padding drops for it).\n//\n// header the top bar, always in view (title, the surface's\n// commands). The bar draws its own bottom border\n// navigation the surfaces, a page tree: a rail beside the content\n// from md (768px); below md a Sheet from the leading\n// edge, opened by the menu button in the top bar. A\n// press on a link or a button inside closes it\n// navigationLabel the Sheet's title and the menu button's words\n// (\"Navigation\")\n// navigationWidth the rail, in px (176; 160 to 320)\n// children the content. It scrolls inside its own region\n// contentMode scroll (the default): the content region scrolls ·\n// fill: the region is a bounded flex column and the\n// child owns its own scrolling (a canvas, a grid, a\n// spreadsheet, an editor that scrolls itself)\n// contentLabel the content region's name for a screen reader (\"Content\")\n// contentClassName spacing and token backgrounds for the content\n// region (p-4 sm:p-6, bg-card). Never a height or an\n// overflow: the layout owns those\n// scrollKey when it changes the content scrolls back to its top\n// and the navigation Sheet closes: pass the route\n// (usePathname()), since the frame stays mounted while\n// the surfaces change inside it\n// inspector the details, review or feedback panel beside the\n// content, scrolling on its own\n// inspectorLabel the inspector region's name (\"Details\")\n// inspectorWidth in px (360; 240 to 560). Builds have needed 300 and 440\n// inspectorAt md · lg (the default) · xl: the width from which the\n// inspector sits beside the content\n// footer under the content, always in view (the save state,\n// the composer, the commands for what is above)\n// height viewport (the default): the frame is the window's\n// height · parent: it fills a bounded parent. Nested,\n// it is parent whatever this says\n// className the frame's own tokens; never a height or an overflow\n//\n// Below `inspectorAt`, where there is no room for two columns:\n// - contentMode scroll: the content and the inspector share ONE scroll\n// region, the inspector after the content. Everything is reachable\n// by scrolling, and nothing is hidden behind a button.\n// - contentMode fill: the content keeps the room and the inspector is\n// a bounded pane under it (at most 45% of the height), scrolling on\n// its own. A fill child has no flow to stack into, and a pane with\n// no bound under it is cut off (workspace-layout's fault, 2026-10-07).\n// The footer stays under both, in view.\n//\n// In print the frame lets go: no bound, no overflow, no navigation, no\n// header, inspector or footer; the content flows onto pages. A page meant\n// for print or export is a DocumentLayout inside the content.\n\nconst Nested = createContext(false);\n\nconst px = (value, fallback, min, max) => {\n const n = Number(value);\n return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;\n};\n\n// Tailwind reads class names whole from the source, so every breakpoint\n// is written out.\nconst BESIDE = {\n md: {\n main: \"md:grid md:grid-cols-[minmax(0,1fr)_var(--app-inspector)] md:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"md:contents\",\n content: \"md:col-start-1 md:row-start-1\",\n scrolls: \"md:min-h-0 md:overflow-y-auto md:overscroll-contain\",\n inspector: \"md:col-start-2 md:row-span-2 md:row-start-1 md:max-h-none md:min-h-0 md:overflow-y-auto md:border-l md:border-t-0\",\n footer: \"md:col-start-1 md:row-start-2\",\n },\n lg: {\n main: \"lg:grid lg:grid-cols-[minmax(0,1fr)_var(--app-inspector)] lg:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"lg:contents\",\n content: \"lg:col-start-1 lg:row-start-1\",\n scrolls: \"lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain\",\n inspector: \"lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:max-h-none lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0\",\n footer: \"lg:col-start-1 lg:row-start-2\",\n },\n xl: {\n main: \"xl:grid xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"xl:contents\",\n content: \"xl:col-start-1 xl:row-start-1\",\n scrolls: \"xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain\",\n inspector: \"xl:col-start-2 xl:row-span-2 xl:row-start-1 xl:max-h-none xl:min-h-0 xl:overflow-y-auto xl:border-l xl:border-t-0\",\n footer: \"xl:col-start-1 xl:row-start-2\",\n },\n};\n\nconst REGION = \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\";\nconst LETS_GO = \"print:block print:h-auto print:min-h-0 print:overflow-visible\";\n\nexport function AppLayout({\n header,\n navigation,\n navigationLabel = \"Navigation\",\n navigationWidth,\n children,\n contentMode = \"scroll\",\n contentLabel = \"Content\",\n contentClassName,\n scrollKey,\n inspector,\n inspectorLabel = \"Details\",\n inspectorWidth,\n inspectorAt = \"lg\",\n footer,\n height = \"viewport\",\n className,\n}) {\n const nested = useContext(Nested);\n const bounded = nested || height === \"parent\";\n const fill = contentMode === \"fill\";\n const at = BESIDE[inspectorAt] ?? BESIDE.lg;\n const hasInspector = inspector != null && inspector !== false;\n const hasNavigation = navigation != null && navigation !== false;\n const shared = hasInspector && !fill; // below `at`: content and inspector in one scroll region\n const [open, setOpen] = useState(false);\n const flowRef = useRef(null);\n const contentRef = useRef(null);\n\n useEffect(() => {\n if (flowRef.current) flowRef.current.scrollTop = 0;\n if (contentRef.current) contentRef.current.scrollTop = 0;\n setOpen(false);\n }, [scrollKey]);\n\n return (\n <Nested.Provider value={true}>\n <div\n data-app-layout={bounded ? \"parent\" : \"viewport\"}\n style={{ \"--app-nav\": px(navigationWidth, 176, 160, 320), \"--app-inspector\": px(inspectorWidth, 360, 240, 560) }}\n className={cx(\n \"flex min-w-0 flex-col overflow-hidden bg-background text-foreground\",\n bounded ? \"h-full min-h-0 flex-1\" : \"h-dvh\",\n LETS_GO,\n className,\n )}\n >\n {header != null && header !== false ? (\n <div data-layout-header className=\"flex shrink-0 items-center gap-1 border-b border-border print:hidden\">\n {hasNavigation ? (\n <IconButton icon=\"menu\" label={`Open ${navigationLabel.toLowerCase()}`} onClick={() => setOpen(true)} className=\"ml-1 md:hidden\" />\n ) : null}\n <div className=\"min-w-0 flex-1\">{header}</div>\n </div>\n ) : hasNavigation ? (\n <div data-layout-header className=\"flex shrink-0 items-center border-b border-border px-1 py-1 md:hidden print:hidden\">\n <Button variant=\"quiet\" size=\"sm\" icon=\"menu\" onClick={() => setOpen(true)}>\n {navigationLabel}\n </Button>\n </div>\n ) : null}\n\n {hasNavigation ? (\n <Sheet open={open} onClose={() => setOpen(false)} title={navigationLabel} side=\"start\">\n {open ? (\n <div\n onClick={(e) => {\n if (e.target instanceof Element && e.target.closest(\"a,button\")) setOpen(false);\n }}\n >\n {navigation}\n </div>\n ) : null}\n </Sheet>\n ) : null}\n\n <div data-layout-body className={cx(\"flex min-h-0 min-w-0 flex-1\", LETS_GO)}>\n {hasNavigation ? (\n <div\n data-layout-navigation\n className=\"hidden shrink-0 flex-col overflow-y-auto overscroll-contain border-r border-border md:flex md:w-(--app-nav) print:hidden\"\n >\n {navigation}\n </div>\n ) : null}\n\n <div data-layout-main className={cx(\"flex min-h-0 min-w-0 flex-1 flex-col\", hasInspector && at.main, LETS_GO)}>\n <div\n ref={flowRef}\n data-layout-flow\n className={cx(\n \"flex min-h-0 min-w-0 flex-1 flex-col\",\n shared && \"overflow-y-auto overscroll-contain\",\n hasInspector && at.flow,\n LETS_GO,\n )}\n >\n <div\n ref={contentRef}\n data-layout-content\n role=\"region\"\n aria-label={contentLabel}\n tabIndex={0}\n className={cx(\n \"flex min-w-0 flex-col\",\n REGION,\n fill\n ? \"min-h-0 flex-1 overflow-hidden\"\n : shared\n ? cx(\"shrink-0\", at.scrolls)\n : \"min-h-0 flex-1 overflow-y-auto overscroll-contain\",\n hasInspector && at.content,\n LETS_GO,\n contentClassName,\n // A surface that is a frame of its own runs to the edges.\n \"has-[>[data-app-layout]]:p-0\",\n )}\n >\n {children}\n </div>\n {hasInspector ? (\n <div\n data-layout-inspector\n role=\"region\"\n aria-label={inspectorLabel}\n tabIndex={0}\n className={cx(\n \"min-w-0 shrink-0 border-t border-border print:hidden\",\n REGION,\n fill && \"max-h-[45%] overflow-y-auto overscroll-contain\",\n at.inspector,\n )}\n >\n {inspector}\n </div>\n ) : null}\n </div>\n {footer != null && footer !== false ? (\n <div data-layout-footer className={cx(\"shrink-0 border-t border-border print:hidden\", hasInspector && at.footer)}>\n {footer}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n </Nested.Provider>\n );\n}\n\n// DocumentLayout — a page that flows: a report, an invoice, an article\n// meant for print or export. It owns no height and scrolls nothing; it\n// sits in the AppLayout's content (contentMode scroll), which scrolls it,\n// and in print the frame lets go and it flows onto pages.\n//\n// children the document\n// toolbar a bar held at the top while the document scrolls under it\n// (Print, Export, the zoom); never printed\n// width prose (the default: a measure for reading) · page (A4,\n// 210mm) · letter (8.5in) · full. In print it is the paper's\n// paper true: drawn as a sheet on the desk on screen (bg-card, a\n// hairline, padding); plain in print\n// align start (the default) · center\n// label the document's name for a screen reader (\"Document\")\n// className the document's own spacing and tokens; never a height\nconst WIDTHS = {\n prose: \"max-w-prose\",\n page: \"max-w-[210mm]\",\n letter: \"max-w-[8.5in]\",\n full: \"max-w-none\",\n};\n\nexport function DocumentLayout({ children, toolbar, width = \"prose\", paper = false, align = \"start\", label = \"Document\", className }) {\n return (\n <div data-document-layout className=\"flex min-w-0 flex-col\">\n {toolbar != null && toolbar !== false ? (\n <div data-document-toolbar className=\"sticky top-0 z-10 border-b border-border bg-background print:hidden\">\n {toolbar}\n </div>\n ) : null}\n <article\n aria-label={label}\n className={cx(\n \"w-full min-w-0 print:max-w-none\",\n WIDTHS[width] ?? WIDTHS.prose,\n align === \"center\" && \"mx-auto\",\n paper && \"my-6 rounded-lg border border-border bg-card p-8 text-card-foreground shadow-sm print:my-0 print:rounded-none print:border-0 print:p-0 print:shadow-none\",\n className,\n )}\n >\n {children}\n </article>\n </div>\n );\n}\n"
180
+ "content": "\"use client\";\n\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Sheet } from \"./sheet\";\n\n// Layouts — who owns the window's height, and who scrolls.\n//\n// Every design-first build of 2026-10-07 shipped a shell of `min-h-dvh`\n// that grew with its content: the page itself scrolled, no pane scrolled\n// on its own, and a footer, an inspector or a button below a long\n// article sat past the end of the iframe, where nothing could reach it.\n// So the kit owns the window's height, once, and the app's code never\n// sizes anything by the window (`npm run check` refuses min-h-dvh,\n// h-screen, 100vh, h-[80vh]… outside the kit, rule 12).\n//\n// AppLayout — the app frame. Exactly one owns the viewport: the\n// outermost. A layout inside another fills the slot it is in instead,\n// whatever `height` says, so there is never a second viewport owner. The\n// shell (app/shell.jsx) is the outermost one in every app the template\n// lays; a surface that needs an inspector or a fill of its own returns\n// an AppLayout as its outermost element, and it fills the shell's\n// content edge to edge (the content's padding drops for it).\n//\n// header the top bar, always in view (title, the surface's\n// commands). The bar draws its own bottom border\n// navigation the surfaces, a page tree: a rail beside the content\n// from md (768px); below md a Sheet from the leading\n// edge, opened by the menu button in the top bar. A\n// press on a link or a button inside closes it\n// navigationLabel the Sheet's title and the menu button's words\n// (\"Navigation\")\n// navigationWidth the rail, in px (176; 160 to 320)\n// children the content. It scrolls inside its own region\n// contentMode scroll (the default): the content region scrolls ·\n// fill: the region is a bounded flex column and the\n// child owns its own scrolling (a canvas, a grid, a\n// spreadsheet, an editor that scrolls itself)\n// contentLabel the content region's name for a screen reader (\"Content\")\n// contentClassName spacing and token backgrounds for the content\n// region (p-4 sm:p-6, bg-card). Never a height or an\n// overflow: the layout owns those\n// scrollKey when it changes the content scrolls back to its top\n// and the navigation Sheet closes: pass the route\n// (usePathname()), since the frame stays mounted while\n// the surfaces change inside it\n// inspector the details, review or feedback panel beside the\n// content, scrolling on its own\n// inspectorLabel the inspector region's name (\"Details\")\n// inspectorWidth in px (360; 240 to 560). Builds have needed 300 and 440\n// inspectorAt md · lg (the default) · xl: the width from which the\n// inspector sits beside the content\n// inspectorIn window (the default): inspectorAt is the window's\n// width (768 · 1024 · 1280 px) · frame: it is the\n// width of the body between the rail and the dock\n// (42 · 48 · 64 rem), so an agent dock that takes a\n// quarter of the window makes the inspector go under\n// the work when there is no room, not squeeze it\n// footer under the content, always in view (the save state,\n// the composer, the commands for what is above)\n// dock a column beside the whole body (rail, content and\n// inspector), under the header, that the app's own\n// block draws: the agent's dock (blocks/agent-dock).\n// It sizes itself, remembers its width and, where the\n// body is too narrow, lays itself over the app as a\n// sheet; the layout only gives it a place in the row\n// and takes no part in its width. Nothing when it is\n// not given\n// height viewport (the default): the frame is the window's\n// height · parent: it fills a bounded parent. Nested,\n// it is parent whatever this says\n// className the frame's own tokens; never a height or an overflow\n//\n// Below `inspectorAt`, where there is no room for two columns:\n// - contentMode scroll: the content and the inspector share ONE scroll\n// region, the inspector after the content. Everything is reachable\n// by scrolling, and nothing is hidden behind a button.\n// - contentMode fill: the content keeps the room and the inspector is\n// a bounded pane under it (at most 45% of the height), scrolling on\n// its own. A fill child has no flow to stack into, and a pane with\n// no bound under it is cut off (workspace-layout's fault, 2026-10-07).\n// The footer stays under both, in view.\n//\n// In print the frame lets go: no bound, no overflow, no navigation, no\n// header, inspector or footer; the content flows onto pages. A page meant\n// for print or export is a DocumentLayout inside the content.\n\nconst Nested = createContext(false);\n\nconst px = (value, fallback, min, max) => {\n const n = Number(value);\n return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;\n};\n\n// Tailwind reads class names whole from the source, so every breakpoint\n// is written out.\nconst BESIDE = {\n md: {\n main: \"md:grid md:grid-cols-[minmax(0,1fr)_var(--app-inspector)] md:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"md:contents\",\n content: \"md:col-start-1 md:row-start-1\",\n scrolls: \"md:min-h-0 md:overflow-y-auto md:overscroll-contain\",\n inspector: \"md:col-start-2 md:row-span-2 md:row-start-1 md:max-h-none md:min-h-0 md:overflow-y-auto md:border-l md:border-t-0\",\n footer: \"md:col-start-1 md:row-start-2\",\n },\n lg: {\n main: \"lg:grid lg:grid-cols-[minmax(0,1fr)_var(--app-inspector)] lg:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"lg:contents\",\n content: \"lg:col-start-1 lg:row-start-1\",\n scrolls: \"lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain\",\n inspector: \"lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:max-h-none lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0\",\n footer: \"lg:col-start-1 lg:row-start-2\",\n },\n xl: {\n main: \"xl:grid xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"xl:contents\",\n content: \"xl:col-start-1 xl:row-start-1\",\n scrolls: \"xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain\",\n inspector: \"xl:col-start-2 xl:row-span-2 xl:row-start-1 xl:max-h-none xl:min-h-0 xl:overflow-y-auto xl:border-l xl:border-t-0\",\n footer: \"xl:col-start-1 xl:row-start-2\",\n },\n};\n\n// The same classes by the width of the body's own column (a container\n// query on the frame between the rail and the dock), for an app whose dock\n// takes a quarter of the window: md is a body 42rem wide, lg 48rem, xl 64rem.\nconst BESIDE_FRAME = {\n md: {\n main: \"@2xl:grid @2xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] @2xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"@2xl:contents\",\n content: \"@2xl:col-start-1 @2xl:row-start-1\",\n scrolls: \"@2xl:min-h-0 @2xl:overflow-y-auto @2xl:overscroll-contain\",\n inspector: \"@2xl:col-start-2 @2xl:row-span-2 @2xl:row-start-1 @2xl:max-h-none @2xl:min-h-0 @2xl:overflow-y-auto @2xl:border-l @2xl:border-t-0\",\n footer: \"@2xl:col-start-1 @2xl:row-start-2\",\n },\n lg: {\n main: \"@3xl:grid @3xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] @3xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"@3xl:contents\",\n content: \"@3xl:col-start-1 @3xl:row-start-1\",\n scrolls: \"@3xl:min-h-0 @3xl:overflow-y-auto @3xl:overscroll-contain\",\n inspector: \"@3xl:col-start-2 @3xl:row-span-2 @3xl:row-start-1 @3xl:max-h-none @3xl:min-h-0 @3xl:overflow-y-auto @3xl:border-l @3xl:border-t-0\",\n footer: \"@3xl:col-start-1 @3xl:row-start-2\",\n },\n xl: {\n main: \"@5xl:grid @5xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] @5xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"@5xl:contents\",\n content: \"@5xl:col-start-1 @5xl:row-start-1\",\n scrolls: \"@5xl:min-h-0 @5xl:overflow-y-auto @5xl:overscroll-contain\",\n inspector: \"@5xl:col-start-2 @5xl:row-span-2 @5xl:row-start-1 @5xl:max-h-none @5xl:min-h-0 @5xl:overflow-y-auto @5xl:border-l @5xl:border-t-0\",\n footer: \"@5xl:col-start-1 @5xl:row-start-2\",\n },\n};\n\n// inspectorIn=\"frame\": the body's column is the container the BESIDE_FRAME\n// classes answer to. Off, it is nothing: the children, in the same place.\nfunction FrameBox({ on, children }) {\n if (!on) return children;\n return (\n <div data-layout-frame className={cx(\"@container flex min-h-0 min-w-0 flex-1 flex-col\", LETS_GO)}>\n {children}\n </div>\n );\n}\n\nconst REGION = \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\";\n\n// The inspector holds its width and its contents fit it (UI review X4: a\n// shelf's row ran out of the panel and its title was cut at the edge). A\n// grid with no grid-cols-* has one `auto` column, and `auto` grows to its\n// widest child's minimum width, past the panel; the usual inspector is\n// exactly `<div className=\"grid content-start gap-3 p-4\">`. So the panel's\n// own direct children may not grow it (min-w-0), a direct child that is a\n// grid with no columns of its own gets the one column that can shrink\n// (minmax(0,1fr)), and nothing paints past the panel's edge (overflow-x\n// hidden: a wide child wraps or truncates, it does not sideways-scroll).\nconst INSPECTOR_FIT =\n \"min-w-0 max-w-full overflow-x-hidden [&>*]:min-w-0 [&>.grid:not([class*=grid-cols])]:grid-cols-[minmax(0,1fr)]\";\nconst LETS_GO = \"print:block print:h-auto print:min-h-0 print:overflow-visible\";\n\nexport function AppLayout({\n header,\n navigation,\n navigationLabel = \"Navigation\",\n navigationWidth,\n children,\n contentMode = \"scroll\",\n contentLabel = \"Content\",\n contentClassName,\n scrollKey,\n inspector,\n inspectorLabel = \"Details\",\n inspectorWidth,\n inspectorAt = \"lg\",\n inspectorIn = \"window\",\n footer,\n dock,\n height = \"viewport\",\n className,\n}) {\n const nested = useContext(Nested);\n const bounded = nested || height === \"parent\";\n const fill = contentMode === \"fill\";\n const byFrame = inspectorIn === \"frame\";\n const table = byFrame ? BESIDE_FRAME : BESIDE;\n const at = table[inspectorAt] ?? table.lg;\n const hasInspector = inspector != null && inspector !== false;\n const hasNavigation = navigation != null && navigation !== false;\n const shared = hasInspector && !fill; // below `at`: content and inspector in one scroll region\n const [open, setOpen] = useState(false);\n const flowRef = useRef(null);\n const contentRef = useRef(null);\n\n useEffect(() => {\n if (flowRef.current) flowRef.current.scrollTop = 0;\n if (contentRef.current) contentRef.current.scrollTop = 0;\n setOpen(false);\n }, [scrollKey]);\n\n return (\n <Nested.Provider value={true}>\n <div\n data-app-layout={bounded ? \"parent\" : \"viewport\"}\n style={{ \"--app-nav\": px(navigationWidth, 176, 160, 320), \"--app-inspector\": px(inspectorWidth, 360, 240, 560) }}\n className={cx(\n \"flex min-w-0 flex-col overflow-hidden bg-background text-foreground\",\n bounded ? \"h-full min-h-0 flex-1\" : \"h-dvh\",\n LETS_GO,\n className,\n )}\n >\n {header != null && header !== false ? (\n <div data-layout-header className=\"flex shrink-0 items-center gap-1 border-b border-border print:hidden\">\n {hasNavigation ? (\n <IconButton icon=\"menu\" label={`Open ${navigationLabel.toLowerCase()}`} onClick={() => setOpen(true)} className=\"ml-1 md:hidden\" />\n ) : null}\n <div className=\"min-w-0 flex-1\">{header}</div>\n </div>\n ) : hasNavigation ? (\n <div data-layout-header className=\"flex shrink-0 items-center border-b border-border px-1 py-1 md:hidden print:hidden\">\n <Button variant=\"quiet\" size=\"sm\" icon=\"menu\" onClick={() => setOpen(true)}>\n {navigationLabel}\n </Button>\n </div>\n ) : null}\n\n {hasNavigation ? (\n <Sheet open={open} onClose={() => setOpen(false)} title={navigationLabel} side=\"start\">\n {open ? (\n <div\n onClick={(e) => {\n if (e.target instanceof Element && e.target.closest(\"a,button\")) setOpen(false);\n }}\n >\n {navigation}\n </div>\n ) : null}\n </Sheet>\n ) : null}\n\n <div data-layout-body className={cx(\"flex min-h-0 min-w-0 flex-1\", LETS_GO)}>\n {hasNavigation ? (\n <div\n data-layout-navigation\n className=\"hidden shrink-0 flex-col overflow-y-auto overscroll-contain border-r border-border md:flex md:w-(--app-nav) print:hidden\"\n >\n {navigation}\n </div>\n ) : null}\n\n <FrameBox on={byFrame && hasInspector}>\n <div data-layout-main className={cx(\"flex min-h-0 min-w-0 flex-1 flex-col\", hasInspector && at.main, LETS_GO)}>\n <div\n ref={flowRef}\n data-layout-flow\n className={cx(\n \"flex min-h-0 min-w-0 flex-1 flex-col\",\n shared && \"overflow-y-auto overscroll-contain\",\n hasInspector && at.flow,\n LETS_GO,\n )}\n >\n <div\n ref={contentRef}\n data-layout-content\n role=\"region\"\n aria-label={contentLabel}\n tabIndex={0}\n className={cx(\n \"flex min-w-0 flex-col\",\n REGION,\n fill\n ? \"min-h-0 flex-1 overflow-hidden\"\n : shared\n ? cx(\"shrink-0\", at.scrolls)\n : \"min-h-0 flex-1 overflow-y-auto overscroll-contain\",\n hasInspector && at.content,\n LETS_GO,\n contentClassName,\n // A surface that is a frame of its own runs to the edges.\n \"has-[>[data-app-layout]]:p-0\",\n )}\n >\n {children}\n </div>\n {hasInspector ? (\n <div\n data-layout-inspector\n role=\"region\"\n aria-label={inspectorLabel}\n tabIndex={0}\n className={cx(\n \"shrink-0 border-t border-border print:hidden\",\n INSPECTOR_FIT,\n REGION,\n fill && \"max-h-[45%] overflow-y-auto overscroll-contain\",\n at.inspector,\n )}\n >\n {inspector}\n </div>\n ) : null}\n </div>\n {footer != null && footer !== false ? (\n <div data-layout-footer className={cx(\"shrink-0 border-t border-border print:hidden\", hasInspector && at.footer)}>\n {footer}\n </div>\n ) : null}\n </div>\n </FrameBox>\n {dock != null && dock !== false ? dock : null}\n </div>\n </div>\n </Nested.Provider>\n );\n}\n\n// DocumentLayout — a page that flows: a report, an invoice, an article\n// meant for print or export. It owns no height and scrolls nothing; it\n// sits in the AppLayout's content (contentMode scroll), which scrolls it,\n// and in print the frame lets go and it flows onto pages.\n//\n// children the document\n// toolbar a bar held at the top while the document scrolls under it\n// (Print, Export, the zoom); never printed\n// width prose (the default: a measure for reading) · page (A4,\n// 210mm) · letter (8.5in) · full. In print it is the paper's\n// paper true: drawn as a sheet on the desk on screen (bg-card, a\n// hairline, padding); plain in print\n// align start (the default) · center\n// label the document's name for a screen reader (\"Document\")\n// className the document's own spacing and tokens; never a height\nconst WIDTHS = {\n prose: \"max-w-prose\",\n page: \"max-w-[210mm]\",\n letter: \"max-w-[8.5in]\",\n full: \"max-w-none\",\n};\n\nexport function DocumentLayout({ children, toolbar, width = \"prose\", paper = false, align = \"start\", label = \"Document\", className }) {\n return (\n <div data-document-layout className=\"flex min-w-0 flex-col\">\n {toolbar != null && toolbar !== false ? (\n <div data-document-toolbar className=\"sticky top-0 z-10 border-b border-border bg-background print:hidden\">\n {toolbar}\n </div>\n ) : null}\n <article\n aria-label={label}\n className={cx(\n \"w-full min-w-0 print:max-w-none\",\n WIDTHS[width] ?? WIDTHS.prose,\n align === \"center\" && \"mx-auto\",\n paper && \"my-6 rounded-lg border border-border bg-card p-8 text-card-foreground shadow-sm print:my-0 print:rounded-none print:border-0 print:p-0 print:shadow-none\",\n className,\n )}\n >\n {children}\n </article>\n </div>\n );\n}\n"
181
181
  },
182
182
  {
183
183
  "path": "templates/next/files/components/ui/menu.jsx",
@@ -189,7 +189,13 @@
189
189
  "path": "templates/next/files/components/ui/notice.jsx",
190
190
  "type": "registry:ui",
191
191
  "target": "@components/makersclaw/ui/notice.jsx",
192
- "content": "import { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\n\n// Plain parts: a card, a notice, a badge, a kicker, the empty state.\n// Server-safe (no hooks), so a server surface can use them directly.\n\nconst TONES = {\n neutral: { box: \"border-border bg-card text-card-foreground\", icon: \"info\" },\n info: { box: \"border-info/40 bg-card text-card-foreground\", icon: \"info\" },\n success: { box: \"border-success/40 bg-card text-card-foreground\", icon: \"check\" },\n warning: { box: \"border-warning/50 bg-card text-card-foreground\", icon: \"warning\" },\n danger: { box: \"border-destructive/50 bg-card text-card-foreground\", icon: \"warning\" },\n};\n\n/** A raised panel with an optional heading row.\n * padding md (default, 16px) · sm (12px, a dense rail) · none (the\n * body runs to the border — a table, a list of rows; the\n * heading row keeps its own 16px inset)\n * titleSize md (default, 16px) · lg (20px, a page-level card's title)\n * Pick these here, never with a className: cx does not merge classes,\n * so a `p-0` laid over the default padding wins only by CSS order. */\nconst CARD_PADDING = { none: \"\", sm: \"p-3\", md: \"p-4\" };\nconst CARD_TITLE = { md: \"text-base\", lg: \"text-xl\" };\n\nexport function Card({\n title,\n description,\n actions,\n padding = \"md\",\n titleSize = \"md\",\n className,\n children,\n as: Tag = \"section\",\n}) {\n const pad = CARD_PADDING[padding] ?? CARD_PADDING.md;\n const bare = padding === \"none\";\n return (\n <Tag className={cx(\"rounded-lg border border-border bg-card text-card-foreground\", pad, bare && \"overflow-hidden\", className)}>\n {title || actions ? (\n <div className={cx(\"flex items-start justify-between gap-3\", bare ? \"px-4 pt-4 pb-3\" : \"mb-3\")}>\n <div className=\"min-w-0\">\n {title ? (\n <h2 className={cx(\"font-semibold leading-tight\", CARD_TITLE[titleSize] ?? CARD_TITLE.md)}>{title}</h2>\n ) : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex shrink-0 gap-2\">{actions}</div> : null}\n </div>\n ) : null}\n {children}\n </Tag>\n );\n}\n\n/** One line the user should read, with an optional action. `tone`\n * neutral · info · success · warning · danger. Colour is the signal, so\n * status tones are for status only — never decoration. */\nexport function Notice({ tone = \"neutral\", title, action, onDismiss, className, children }) {\n const t = TONES[tone] ?? TONES.neutral;\n return (\n <div role={tone === \"danger\" ? \"alert\" : \"status\"} className={cx(\"flex items-start gap-3 rounded-lg border p-3 text-sm\", t.box, className)}>\n <Icon name={t.icon} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n {title ? <p className=\"font-medium\">{title}</p> : null}\n {children ? <div className={cx(title && \"mt-0.5\", \"text-muted-foreground\")}>{children}</div> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {onDismiss ? (\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" onClick={onDismiss} className=\"-my-1 -mr-1\" />\n ) : null}\n </div>\n );\n}\n\n/** A small label for a state or a count.\n * tone neutral · primary · success · warning · danger · info\n * variant label (default: small caps, wide tracking; primary is a\n * solid fill, every other tone a tint) · soft (the case as\n * written, normal tracking, EVERY tone a tint — primary\n * included: \"Ready\", \"Daily over\", \"Feature\") · outline (the\n * case as written, a tone-coloured hairline on no fill) ·\n * code (the mono face, the case as written, normal tracking\n * — a version tag \"v2.4.0\", a commit \"a3f8c2d\", a path)\n * hue 1..5 — a categorical colour, read from --color-chart-N,\n * for KINDS of things (a DNS type, a platform, a content\n * type). Tone is for status; hue is for category, and a\n * tone given with it wins. With every variant: soft is the\n * case as written on a tint of the hue with a hue dot and\n * foreground text; label (and code) is filled with the hue\n * and black or white text, whichever reads; outline is a\n * hue hairline with foreground text. The text is never the\n * hue itself: a chart colour is not made to be read as type\n * Case is a variant, not its own prop: uppercase needs wide tracking and\n * sentence case needs none, so the two travel together, and a `case`\n * prop would have to be ignored by code. A soft primary lives on `soft`\n * because `label` primary is a solid fill apps already rely on. The\n * outline's ring is inset, so every variant is the same size in a row. */\nconst BADGE_LOOK = {\n label: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary text-primary-foreground\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n soft: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary/15 text-primary\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n outline: {\n neutral: \"bg-transparent text-foreground ring-1 ring-inset ring-border\",\n primary: \"bg-transparent text-primary ring-1 ring-inset ring-primary/50\",\n success: \"bg-transparent text-success ring-1 ring-inset ring-success/50\",\n warning: \"bg-transparent text-warning ring-1 ring-inset ring-warning/50\",\n danger: \"bg-transparent text-destructive ring-1 ring-inset ring-destructive/50\",\n info: \"bg-transparent text-info ring-1 ring-inset ring-info/50\",\n },\n};\nBADGE_LOOK.code = BADGE_LOOK.label;\n\nconst BADGE_FACE = {\n label: \"font-medium uppercase tracking-wide\",\n soft: \"font-medium\",\n outline: \"font-medium\",\n code: \"font-mono font-normal\",\n};\n\n// Categorical hues, literal so Tailwind sees every class. A filled hue's\n// text is black or white, picked from the fill's relative luminance (the\n// WCAG formula, on the clipped sRGB channels): what CSS contrast-color()\n// does, which no token can. A chart colour sits mid-lightness (chart-2…5\n// are clamped to OKLCH L 0.55–0.8, often past sRGB), where the theme's\n// light text fails on a light theme and its dark text on a dark primary.\n// Black below Y 0.179, white above: the better of the two is always\n// 4.58:1 or more. The soft tint is light (8%) so foreground text keeps\n// its contrast.\nconst HUE_LOOK = {\n label: {\n 1: \"bg-chart-1 text-[color:color(from_var(--color-chart-1)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 2: \"bg-chart-2 text-[color:color(from_var(--color-chart-2)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 3: \"bg-chart-3 text-[color:color(from_var(--color-chart-3)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 4: \"bg-chart-4 text-[color:color(from_var(--color-chart-4)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 5: \"bg-chart-5 text-[color:color(from_var(--color-chart-5)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n },\n soft: {\n 1: \"bg-chart-1/8 text-foreground\",\n 2: \"bg-chart-2/8 text-foreground\",\n 3: \"bg-chart-3/8 text-foreground\",\n 4: \"bg-chart-4/8 text-foreground\",\n 5: \"bg-chart-5/8 text-foreground\",\n },\n outline: {\n 1: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-1\",\n 2: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-2\",\n 3: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-3\",\n 4: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-4\",\n 5: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-5\",\n },\n};\nHUE_LOOK.code = HUE_LOOK.label;\nconst HUE_DOT = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nexport function Badge({ tone, hue, variant = \"label\", className, children }) {\n const v = BADGE_FACE[variant] ? variant : \"label\";\n const h = tone == null && HUE_DOT[hue] ? hue : null;\n const look = h ? HUE_LOOK[v][h] : BADGE_LOOK[v][tone ?? \"neutral\"] ?? BADGE_LOOK[v].neutral;\n return (\n <span className={cx(\"inline-flex items-center rounded-sm px-1.5 py-0.5 text-2xs\", h && v === \"soft\" && \"gap-1\", BADGE_FACE[v], look, className)}>\n {h && v === \"soft\" ? <span aria-hidden=\"true\" className={cx(\"size-1.5 shrink-0 rounded-full\", HUE_DOT[h])} /> : null}\n {children}\n </span>\n );\n}\n\n/** The small uppercase line above a heading. */\nexport function Kicker({ className, children }) {\n return <p className={cx(\"text-2xs font-medium uppercase tracking-wider text-muted-foreground\", className)}>{children}</p>;\n}\n\n/** What a surface shows when a real collection is empty AND there is no\n * fixture to show (a filter with no matches, a search with no results).\n * Says what would appear here and offers the one way to make it. */\nexport function Empty({ icon = \"search\", title, body, action, className }) {\n return (\n <div className={cx(\"grid place-items-center gap-2 rounded-lg border border-dashed border-border px-6 py-10 text-center\", className)}>\n <Icon name={icon} size={22} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium\">{title}</p>\n {body ? <p className=\"max-w-prose text-sm text-muted-foreground\">{body}</p> : null}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n );\n}\n"
192
+ "content": "import { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\n\n// Plain parts: a card, a notice, a badge, a kicker, the empty state.\n// Server-safe (no hooks), so a server surface can use them directly.\n\nconst TONES = {\n neutral: { box: \"border-border bg-card text-card-foreground\", icon: \"info\" },\n info: { box: \"border-info/40 bg-card text-card-foreground\", icon: \"info\" },\n success: { box: \"border-success/40 bg-card text-card-foreground\", icon: \"check\" },\n warning: { box: \"border-warning/50 bg-card text-card-foreground\", icon: \"warning\" },\n danger: { box: \"border-destructive/50 bg-card text-card-foreground\", icon: \"warning\" },\n};\n\n/** A raised panel with an optional heading row.\n * padding md (default, 16px) · sm (12px, a dense rail) · none (the\n * body runs to the border — a table, a list of rows; the\n * heading row keeps its own 16px inset)\n * titleSize md (default, 16px) · lg (20px, a page-level card's title)\n * Pick these here, never with a className: cx does not merge classes,\n * so a `p-0` laid over the default padding wins only by CSS order. */\nconst CARD_PADDING = { none: \"\", sm: \"p-3\", md: \"p-4\" };\nconst CARD_TITLE = { md: \"text-base\", lg: \"text-xl\" };\n\nexport function Card({\n title,\n description,\n actions,\n padding = \"md\",\n titleSize = \"md\",\n className,\n children,\n as: Tag = \"section\",\n}) {\n const pad = CARD_PADDING[padding] ?? CARD_PADDING.md;\n const bare = padding === \"none\";\n return (\n <Tag className={cx(\"rounded-lg border border-border bg-card text-card-foreground\", pad, bare && \"overflow-hidden\", className)}>\n {title || actions ? (\n <div className={cx(\"flex items-start justify-between gap-3\", bare ? \"px-4 pt-4 pb-3\" : \"mb-3\")}>\n <div className=\"min-w-0\">\n {title ? (\n <h2 className={cx(\"font-semibold leading-tight\", CARD_TITLE[titleSize] ?? CARD_TITLE.md)}>{title}</h2>\n ) : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex shrink-0 gap-2\">{actions}</div> : null}\n </div>\n ) : null}\n {children}\n </Tag>\n );\n}\n\nconst STATUS_ICON = {\n success: { name: \"circle-check\", box: \"text-success\" },\n warning: { name: \"warning\", box: \"text-warning\" },\n danger: { name: \"warning\", box: \"text-destructive\" },\n neutral: { name: \"info\", box: \"text-muted-foreground\" },\n};\n\n/** A card that is one step the member takes: a title (with an icon), what\n * it does, optionally the state it is in, and ONE action under it.\n * Stacked, always: the status line above a full-width button, so neither\n * squeezes the other and the button's label has the room it needs.\n *\n * Cards in a row are the same height, and their buttons line up (UI\n * review X6). A card stretches to the height of its row, and the status\n * and the action are pinned to the card's bottom: title, words, children,\n * then whatever room is left, then the status, then the button. So in a\n * row of four the buttons sit on one line, however long each card's words\n * are, and a status line in only some of them sits just above its button.\n * It was `self-start` (each card only as tall as its own words) from 0.8.1's\n * first cut: that meant ragged heights and buttons at different heights.\n * The big empty block it was meant to stop came from a squeezed row (check\n * rule 16, squeeze), not from stretching, and it cannot come back here:\n * the spare room is a gap above the footer, never a blank box below it.\n * icon a kit icon name, before the title\n * title the step (\"Connect your mailbox\")\n * description what it does, in a sentence or two (muted)\n * children anything between the description and the state (a price\n * line, a form, a list of rows)\n * status { tone: success · warning · danger · neutral, children } the\n * state it is in, with its mark, above the action. Say\n * \"Connected\" only when the thing exists (an account, a file).\n * action the one button (or a link), laid full width below\n * fit true: only as tall as its own words (self-start), for ONE\n * card on its own beside something unrelated. Never for\n * cards in a row: check rule 19 (stretch) refuses a row of\n * ActionCards that sets self-start, or items-start on the row\n * For a card that holds a table or a list, use Card (padding none). For\n * a screen's boxes in general: Card, ActionCard, Notice, never a hand-drawn\n * bordered div. */\nexport function ActionCard({ icon, title, description, status, action, className, children, fit = false, as: Tag = \"section\" }) {\n const mark = status ? (STATUS_ICON[status.tone] ?? STATUS_ICON.neutral) : null;\n return (\n <Tag\n className={cx(\n \"flex min-w-0 flex-col gap-2.5 rounded-lg border border-border bg-card p-4 text-sm text-card-foreground\",\n fit && \"self-start\",\n className,\n )}\n >\n <h2 className=\"flex min-w-0 items-center gap-2 font-semibold leading-tight\">\n {icon ? <Icon name={icon} size={16} className=\"shrink-0 text-muted-foreground\" /> : null}\n <span className=\"min-w-0\">{title}</span>\n </h2>\n {description ? <p className=\"text-muted-foreground\">{description}</p> : null}\n {children}\n {status || action ? (\n <div className=\"mt-auto grid gap-2.5\">\n {status ? (\n <div className=\"flex items-start gap-2 border-t border-border pt-2.5 text-[13px]\">\n <Icon name={mark.name} size={14} className={cx(\"mt-0.5 shrink-0\", mark.box)} />\n <div className=\"min-w-0\">{status.children}</div>\n </div>\n ) : null}\n {action ? <div className=\"grid\">{action}</div> : null}\n </div>\n ) : null}\n </Tag>\n );\n}\n\n/** One line the user should read, with an optional action. `tone`\n * neutral · info · success · warning · danger. Colour is the signal, so\n * status tones are for status only — never decoration. */\nexport function Notice({ tone = \"neutral\", title, action, onDismiss, className, children }) {\n const t = TONES[tone] ?? TONES.neutral;\n return (\n <div role={tone === \"danger\" ? \"alert\" : \"status\"} className={cx(\"flex items-start gap-3 rounded-lg border p-3 text-sm\", t.box, className)}>\n <Icon name={t.icon} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n {title ? <p className=\"font-medium\">{title}</p> : null}\n {children ? <div className={cx(title && \"mt-0.5\", \"text-muted-foreground\")}>{children}</div> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {onDismiss ? (\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" onClick={onDismiss} className=\"-my-1 -mr-1\" />\n ) : null}\n </div>\n );\n}\n\n/** A small label for a state or a count.\n * tone neutral · primary · success · warning · danger · info\n * variant label (default: small caps, wide tracking; primary is a\n * solid fill, every other tone a tint) · soft (the case as\n * written, normal tracking, EVERY tone a tint — primary\n * included: \"Ready\", \"Daily over\", \"Feature\") · outline (the\n * case as written, a tone-coloured hairline on no fill) ·\n * code (the mono face, the case as written, normal tracking\n * — a version tag \"v2.4.0\", a commit \"a3f8c2d\", a path)\n * hue 1..5 — a categorical colour, read from --color-chart-N,\n * for KINDS of things (a DNS type, a platform, a content\n * type). Tone is for status; hue is for category, and a\n * tone given with it wins. With every variant: soft is the\n * case as written on a tint of the hue with a hue dot and\n * foreground text; label (and code) is filled with the hue\n * and black or white text, whichever reads; outline is a\n * hue hairline with foreground text. The text is never the\n * hue itself: a chart colour is not made to be read as type\n * Case is a variant, not its own prop: uppercase needs wide tracking and\n * sentence case needs none, so the two travel together, and a `case`\n * prop would have to be ignored by code. A soft primary lives on `soft`\n * because `label` primary is a solid fill apps already rely on. The\n * outline's ring is inset, so every variant is the same size in a row. */\nconst BADGE_LOOK = {\n label: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary text-primary-foreground\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n soft: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary/15 text-primary\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n outline: {\n neutral: \"bg-transparent text-foreground ring-1 ring-inset ring-border\",\n primary: \"bg-transparent text-primary ring-1 ring-inset ring-primary/50\",\n success: \"bg-transparent text-success ring-1 ring-inset ring-success/50\",\n warning: \"bg-transparent text-warning ring-1 ring-inset ring-warning/50\",\n danger: \"bg-transparent text-destructive ring-1 ring-inset ring-destructive/50\",\n info: \"bg-transparent text-info ring-1 ring-inset ring-info/50\",\n },\n};\nBADGE_LOOK.code = BADGE_LOOK.label;\n\nconst BADGE_FACE = {\n label: \"font-medium uppercase tracking-wide\",\n soft: \"font-medium\",\n outline: \"font-medium\",\n code: \"font-mono font-normal\",\n};\n\n// Categorical hues, literal so Tailwind sees every class. A filled hue's\n// text is black or white, picked from the fill's relative luminance (the\n// WCAG formula, on the clipped sRGB channels): what CSS contrast-color()\n// does, which no token can. A chart colour sits mid-lightness (chart-2…5\n// are clamped to OKLCH L 0.55–0.8, often past sRGB), where the theme's\n// light text fails on a light theme and its dark text on a dark primary.\n// Black below Y 0.179, white above: the better of the two is always\n// 4.58:1 or more. The soft tint is light (8%) so foreground text keeps\n// its contrast.\nconst HUE_LOOK = {\n label: {\n 1: \"bg-chart-1 text-[color:color(from_var(--color-chart-1)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 2: \"bg-chart-2 text-[color:color(from_var(--color-chart-2)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 3: \"bg-chart-3 text-[color:color(from_var(--color-chart-3)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 4: \"bg-chart-4 text-[color:color(from_var(--color-chart-4)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 5: \"bg-chart-5 text-[color:color(from_var(--color-chart-5)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n },\n soft: {\n 1: \"bg-chart-1/8 text-foreground\",\n 2: \"bg-chart-2/8 text-foreground\",\n 3: \"bg-chart-3/8 text-foreground\",\n 4: \"bg-chart-4/8 text-foreground\",\n 5: \"bg-chart-5/8 text-foreground\",\n },\n outline: {\n 1: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-1\",\n 2: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-2\",\n 3: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-3\",\n 4: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-4\",\n 5: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-5\",\n },\n};\nHUE_LOOK.code = HUE_LOOK.label;\nconst HUE_DOT = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nexport function Badge({ tone, hue, variant = \"label\", className, children }) {\n const v = BADGE_FACE[variant] ? variant : \"label\";\n const h = tone == null && HUE_DOT[hue] ? hue : null;\n const look = h ? HUE_LOOK[v][h] : BADGE_LOOK[v][tone ?? \"neutral\"] ?? BADGE_LOOK[v].neutral;\n return (\n <span className={cx(\"inline-flex items-center rounded-sm px-1.5 py-0.5 text-2xs\", h && v === \"soft\" && \"gap-1\", BADGE_FACE[v], look, className)}>\n {h && v === \"soft\" ? <span aria-hidden=\"true\" className={cx(\"size-1.5 shrink-0 rounded-full\", HUE_DOT[h])} /> : null}\n {children}\n </span>\n );\n}\n\n/** The small uppercase line above a heading. */\nexport function Kicker({ className, children }) {\n return <p className={cx(\"text-2xs font-medium uppercase tracking-wider text-muted-foreground\", className)}>{children}</p>;\n}\n\n/** What a surface shows when a real collection is empty AND there is no\n * fixture to show (a filter with no matches, a search with no results).\n * Says what would appear here and offers the one way to make it. */\nexport function Empty({ icon = \"search\", title, body, action, className }) {\n return (\n <div className={cx(\"grid place-items-center gap-2 rounded-lg border border-dashed border-border px-6 py-10 text-center\", className)}>\n <Icon name={icon} size={22} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium\">{title}</p>\n {body ? <p className=\"max-w-prose text-sm text-muted-foreground\">{body}</p> : null}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n );\n}\n"
193
+ },
194
+ {
195
+ "path": "templates/next/files/components/ui/page.jsx",
196
+ "type": "registry:ui",
197
+ "target": "@components/makersclaw/ui/page.jsx",
198
+ "content": "\"use client\";\n\nimport { cx } from \"./cx\";\n\n// PageLayout — a screen's main area with an optional side column, taking the\n// full height of the content it sits in. The MAIN is the column that scrolls.\n// The SIDE is the fixed one: it holds its place, top to bottom, while the\n// main scrolls, and it is short.\n//\n// Why (UI review X12, 2026-10-12): Project Roadmap's Today ended at 680px in\n// a 1240px window. The screen drew its own two-column grid inside the\n// scrolling content, so it was only as tall as its words: the side column's\n// divider stopped halfway down and the bottom half of the window was empty.\n// Every screen with a side drew that grid by hand. This is the one.\n// `npm run check` (rule 23, page) refuses a hand-drawn main + side with a\n// ruled <aside>.\n//\n// What goes in the side, and what never does (UI review X13, 2026-10-12:\n// Newsletter Engine's first run was a short intro on the left and a long\n// \"Name it\" form in the right column. \"this can only work if one column is\n// fixed, usually the 3rd column, and the middle one is scrolling\"): the side\n// is a summary, the next step, a small panel, a few facts, a short list. It\n// never needs a scroll of its own. A form, a paste box, a long list, a\n// shelf of examples belong in the MAIN, where the page scrolls: a step that\n// opens reveals its own card there. The side's own scroll is a last resort\n// for a window shorter than the side (the bar is thin to say so), never how\n// a long thing is meant to be read. `npm run check` (rule 27, sidelong)\n// refuses a side column that holds a form or a long list.\n//\n// It lives INSIDE an AppLayout (the shell or the screen's own): the layout\n// owns the window's height, this fills the content region it is given. Use\n// the AppLayout's `contentMode=\"fill\"` and give its content no padding\n// (omit `contentClassName`): the columns pad themselves, so the divider can\n// run to the edges. In contentMode scroll it fills the same.\n//\n// children the main area\n// side the side column, the FIXED one: short things (a summary, the\n// next step, a record's facts, a small panel). It stays put\n// while the main scrolls; never a long form or a long list.\n// Without it the page is one scrolling column\n// sideWidth px (304; 220 to 560)\n// sideAt md · lg (the default) · xl: the width of THIS page from\n// which the side sits beside the main (42 · 48 · 64 rem). It\n// is the page's own width, not the window's: an open agent\n// dock takes a quarter of the window, and the page must not\n// be squeezed by it. Narrower, the side goes under the main in\n// one scroll, nothing hidden behind a button\n// mainLabel the main area's name for a screen reader (\"Content\")\n// sideLabel the side's name (\"Details\")\n// padded default true: p-4 sm:p-6 on the main, p-4 on the side. false\n// for a page that draws its own edges\n// fill true: the main is a flex column, so a child with `flex-1`\n// (a list, a table) grows into the height instead of stopping\n// where its rows end. With the side beside it, or alone; with\n// the side under it (narrow) the page is one scroll and there\n// is no height to grow into\n// className, mainClassName, sideClassName spacing and token backgrounds\n// (bg-muted). Never a height or an overflow: the layout owns those\nconst px = (value, fallback, min, max) => {\n const n = Number(value);\n return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;\n};\n\n// Tailwind reads class names whole from the source, so every size is written\n// out. md is a page 42rem wide, lg 48rem, xl 64rem (the AppLayout's\n// inspectorIn=\"frame\" sizes).\nconst BESIDE = {\n md: {\n grid: \"@2xl:grid-cols-[minmax(0,1fr)_var(--page-side)] @2xl:grid-rows-[minmax(0,1fr)] @2xl:overflow-hidden\",\n column: \"@2xl:min-h-0 @2xl:overflow-y-auto @2xl:overscroll-contain\",\n side: \"@2xl:border-l @2xl:border-t-0 @2xl:[scrollbar-width:thin]\",\n },\n lg: {\n grid: \"@3xl:grid-cols-[minmax(0,1fr)_var(--page-side)] @3xl:grid-rows-[minmax(0,1fr)] @3xl:overflow-hidden\",\n column: \"@3xl:min-h-0 @3xl:overflow-y-auto @3xl:overscroll-contain\",\n side: \"@3xl:border-l @3xl:border-t-0 @3xl:[scrollbar-width:thin]\",\n },\n xl: {\n grid: \"@5xl:grid-cols-[minmax(0,1fr)_var(--page-side)] @5xl:grid-rows-[minmax(0,1fr)] @5xl:overflow-hidden\",\n column: \"@5xl:min-h-0 @5xl:overflow-y-auto @5xl:overscroll-contain\",\n side: \"@5xl:border-l @5xl:border-t-0 @5xl:[scrollbar-width:thin]\",\n },\n};\n\nconst REGION = \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\";\nconst LETS_GO = \"print:block print:h-auto print:min-h-0 print:overflow-visible\";\n\nexport function PageLayout({ children, side, sideWidth, sideAt = \"lg\", mainLabel = \"Content\", sideLabel = \"Details\", padded = true, fill = false, className, mainClassName, sideClassName }) {\n const has = side != null && side !== false;\n const at = BESIDE[sideAt] ?? BESIDE.lg;\n return (\n <div data-page-layout className={cx(\"@container flex min-h-0 min-w-0 flex-1 flex-col\", LETS_GO, className)}>\n <div\n data-page-scroll\n style={{ \"--page-side\": px(sideWidth, 304, 220, 560) }}\n className={cx(\n \"grid min-h-0 min-w-0 flex-1 grid-cols-[minmax(0,1fr)] content-start\",\n has ? cx(\"overflow-y-auto overscroll-contain\", at.grid) : \"grid-rows-[minmax(0,1fr)] overflow-hidden\",\n LETS_GO,\n )}\n >\n <div\n data-page-main\n role=\"region\"\n aria-label={mainLabel}\n tabIndex={0}\n className={cx(\"min-w-0\", REGION, has ? at.column : \"min-h-0 overflow-y-auto overscroll-contain\", fill && \"flex flex-col\", padded && \"p-4 sm:p-6\", LETS_GO, mainClassName)}\n >\n {children}\n </div>\n {has ? (\n <aside\n data-page-side\n aria-label={sideLabel}\n tabIndex={0}\n className={cx(\"min-w-0 border-t border-border print:hidden\", REGION, at.column, at.side, padded && \"p-4\", sideClassName)}\n >\n {side}\n </aside>\n ) : null}\n </div>\n </div>\n );\n}\n"
193
199
  },
194
200
  {
195
201
  "path": "templates/next/files/components/ui/pagination.jsx",
@@ -249,7 +255,7 @@
249
255
  "path": "templates/next/files/components/ui/stat.jsx",
250
256
  "type": "registry:ui",
251
257
  "target": "@components/makersclaw/ui/stat.jsx",
252
- "content": "import { Children } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Numbers at a glance: a Stat, the strip of them, the signed change beside\n// one, a status dot, a legend of dots, a meter against a limit.\n// Server-safe (no hooks), so a server surface can use them directly.\n// Every number is tabular (data-nums), so a column of them lines up and a\n// ticking value does not shake. Colour is the signal: tones are named, as\n// Badge does it, and status tones are for status only — never decoration.\n// A dot can take a categorical `hue` (1..5, the chart colours) instead, for\n// KINDS of things: a platform, a content type. Tone is status, hue is\n// category; a tone given with a hue wins.\n// Blocks use these and never draw their own.\n\n// The text colour of a value in each tone.\nconst TEXT_TONES = {\n neutral: \"text-foreground\",\n muted: \"text-muted-foreground\",\n primary: \"text-primary\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-destructive\",\n info: \"text-info\",\n};\n\n// The fill of a dot or a bar in each tone. neutral is a quiet grey for a\n// dot; a Meter's neutral bar is bg-primary, the same as Progress, and its\n// muted bar is the grey.\nconst FILL_TONES = {\n neutral: \"bg-muted-foreground\",\n muted: \"bg-muted-foreground\",\n primary: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n info: \"bg-info\",\n};\n\n// The fill of a dot in each categorical hue (--color-chart-N).\nconst HUE_FILLS = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nconst EN = \"en-US\"; // one locale on the server and the client: no hydration drift\nconst formatNumber = (n) => (typeof n === \"number\" ? n.toLocaleString(EN, { maximumFractionDigits: 2 }) : String(n ?? \"\"));\n\n/* ------------------------------------------------------------------------\n Delta\n ------------------------------------------------------------------------ */\n\nconst DELTA_SIZES = { sm: { text: \"text-xs\", icon: 12 }, md: { text: \"text-sm\", icon: 14 } };\n\n/**\n * A signed change: an arrow, the value with its sign, an optional suffix —\n * \"↑ +12.4%\". Up is good (success) and down is bad (danger) unless\n * `invert`, for \"lower is better\" (latency, churn, cost). Zero is neutral,\n * with a flat dash and no sign.\n *\n * value the change, a number; its sign sets the arrow and the tone\n * display what to show for the size of it instead of the number\n * (\"$35K\"); the sign is still added. Default the number, to at\n * most one decimal\n * suffix after the number: \"%\", \" pts\", \"ms\"\n * invert lower is better: down turns success, up turns danger\n * label a muted line after it: \"vs last week\"\n * size sm (text-xs) · md (text-sm)\n * variant text (bare) · badge (a tinted pill, for a table cell or a tile corner)\n * arrow default true; false for the bare \"+$35K\" of a dense row\n * hideValue the arrow and its tone alone (a trend in a tight cell);\n * the value is still what a screen reader hears. Always draws\n * the arrow, whatever `arrow` says\n */\nexport function Delta({ value, display, suffix = \"\", invert = false, label, size = \"sm\", variant = \"text\", arrow = true, hideValue = false, className }) {\n const n = typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n const sign = n > 0 ? 1 : n < 0 ? -1 : 0;\n const good = invert ? sign < 0 : sign > 0;\n const tone = sign === 0 ? \"neutral\" : good ? \"success\" : \"danger\";\n const s = DELTA_SIZES[size] ?? DELTA_SIZES.sm;\n const amount = display ?? Math.abs(n).toLocaleString(EN, { maximumFractionDigits: 1 });\n const signed = `${sign > 0 ? \"+\" : sign < 0 ? \"−\" : \"\"}${amount}${suffix}`;\n const look =\n variant === \"badge\"\n ? {\n neutral: \"bg-muted text-muted-foreground\",\n success: \"bg-success/15 text-success\",\n danger: \"bg-destructive/15 text-destructive\",\n }[tone]\n : { neutral: \"text-muted-foreground\", success: \"text-success\", danger: \"text-destructive\" }[tone];\n return (\n <span className={cx(\"inline-flex items-baseline gap-1.5\", s.text, className)}>\n <span\n data-nums\n className={cx(\n \"inline-flex items-center gap-0.5 whitespace-nowrap font-medium\",\n variant === \"badge\" && \"rounded-sm px-1.5 py-0.5\",\n look,\n )}\n >\n {arrow || hideValue ? <Icon name={sign > 0 ? \"arrow-up\" : sign < 0 ? \"arrow-down\" : \"minus\"} size={s.icon} className=\"self-center\" /> : null}\n {/* The sign is a real minus (U+2212) on screen; a reader hears the word. */}\n {hideValue ? null : <span aria-hidden=\"true\">{signed}</span>}\n <span className=\"sr-only\">{sign > 0 ? `up ${amount}${suffix}` : sign < 0 ? `down ${amount}${suffix}` : `no change`}</span>\n </span>\n {label ? <span className=\"text-muted-foreground\">{label}</span> : null}\n </span>\n );\n}\n\n/* ------------------------------------------------------------------------\n StatusDot and Legend\n ------------------------------------------------------------------------ */\n\nconst DOT_SIZES = { sm: \"size-1.5\", md: \"size-2\" };\n\n/**\n * A small round dot in a tone: a row's state, a service's health, a\n * legend key. The dot alone says nothing to a screen reader — the meaning\n * is in `label` (shown), or in `label` + `hideLabel` (read, not shown), or\n * in the text beside it.\n *\n * tone neutral · primary · success · warning · danger · info\n * hue 1..5 — a categorical colour instead (a kind of thing, not\n * a state); ignored when a tone is given\n * size sm (6px, in a dense row) · md (8px)\n * pulse a ring that breathes out, for something live right now\n * (deploying, recording); still under prefers-reduced-motion\n * label the words after it: \"Healthy\", \"3 alerts\"\n * hideLabel keep the label for readers only\n */\nexport function StatusDot({ tone, hue, size = \"md\", pulse = false, label, hideLabel = false, className }) {\n const fill = tone == null && HUE_FILLS[hue] ? HUE_FILLS[hue] : FILL_TONES[tone ?? \"neutral\"] ?? FILL_TONES.neutral;\n const dot = (\n <span aria-hidden=\"true\" className={cx(\"relative inline-flex shrink-0\", DOT_SIZES[size] ?? DOT_SIZES.md)}>\n {pulse ? <span className={cx(\"absolute inset-0 rounded-full opacity-60 animate-ping motion-reduce:hidden\", fill)} /> : null}\n <span className={cx(\"relative inline-flex size-full rounded-full\", fill)} />\n </span>\n );\n if (!label) return <span className={cx(\"inline-flex items-center\", className)}>{dot}</span>;\n return (\n <span className={cx(\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\", className)}>\n {dot}\n <span className={hideLabel ? \"sr-only\" : \"min-w-0 truncate\"}>{label}</span>\n </span>\n );\n}\n\n/**\n * A row of dot · count · label, wrapping when it runs out of room — \"● 2\n * overdue · ● 3 urgent\". The key under a chart, the summary in a card\n * header. A list, so a reader hears how many kinds there are.\n *\n * items [{ tone, label, count }] — count optional; an item with count 0\n * is still drawn (say it, or leave it out of items). `hue` (1..5)\n * in place of `tone` for a key of categories\n * size sm (text-xs, 6px dots) · md (text-sm, 8px dots)\n * label the list's name for readers: \"Tasks by state\"\n */\nexport function Legend({ items = [], size = \"md\", label, className }) {\n if (!items.length) return null;\n return (\n <ul\n aria-label={label}\n className={cx(\"flex flex-wrap items-center gap-x-3 gap-y-1\", size === \"sm\" ? \"text-xs\" : \"text-sm\", className)}\n >\n {items.map((it, i) => (\n <li key={it.key ?? `${it.label}-${i}`} className=\"inline-flex items-center gap-1.5 text-muted-foreground\">\n <StatusDot tone={it.tone} hue={it.hue} size={size === \"sm\" ? \"sm\" : \"md\"} />\n {it.count != null ? (\n <span data-nums className=\"font-medium text-foreground\">\n {formatNumber(it.count)}\n </span>\n ) : null}\n <span>{it.label}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/* ------------------------------------------------------------------------\n Stat and StatGroup\n ------------------------------------------------------------------------ */\n\nconst STAT_SIZES = {\n sm: { label: \"text-xs\", value: \"text-lg font-semibold leading-tight\", unit: \"text-xs\", icon: 14, delta: \"sm\" },\n md: { label: \"text-sm\", value: \"text-2xl font-semibold leading-tight tracking-tight\", unit: \"text-sm\", icon: 16, delta: \"sm\" },\n lg: { label: \"text-sm\", value: \"font-display text-4xl font-semibold leading-none tracking-tight\", unit: \"text-base\", icon: 18, delta: \"md\" },\n};\n\n/**\n * One number with its name: \"Hit rate 94.8%\". The value is the loudest\n * thing on it; everything else is muted.\n *\n * label what it counts: \"Monthly revenue\"\n * value already formatted — \"$1.37M\", \"42\", \"2.4/4\" (a string or a\n * number; a number gets thousands separators)\n * unit after the value, smaller and muted: \"ms\", \"%\", \"/ 10.3 TB\"\n * caption a muted line under it: \"Unit + integration tests\"\n * tone colours the value, for a threshold state (a miss rate over\n * budget is danger). neutral (default) · primary · success ·\n * warning · danger · info\n * dot a tone for a StatusDot before the label (the state of the\n * thing, when the value itself stays neutral)\n * icon an Icon name, muted, at the end of the label row\n * delta the change beside the value: a number (a percent: 12.4 →\n * \"↑ +12.4%\") or Delta's props ({ value, display, suffix, label })\n * invert lower is better, passed to the Delta\n * size sm (a dense strip) · md · lg (the one number a card is about)\n * align start · end (a stat pinned to the right of a header)\n */\nexport function Stat({ label, value, unit, caption, tone = \"neutral\", dot, icon, delta, invert, size = \"md\", align = \"start\", className }) {\n const s = STAT_SIZES[size] ?? STAT_SIZES.md;\n const d = delta == null ? null : typeof delta === \"number\" ? { value: delta, suffix: \"%\" } : delta;\n return (\n <div className={cx(\"min-w-0\", align === \"end\" && \"text-right\", className)}>\n {label || icon || dot ? (\n <div className={cx(\"flex items-center gap-1.5 text-muted-foreground\", s.label, align === \"end\" && \"justify-end\")}>\n {dot ? <StatusDot tone={dot} size=\"sm\" /> : null}\n {label ? <span className=\"min-w-0 truncate\">{label}</span> : null}\n {icon ? <Icon name={icon} size={s.icon} className={cx(\"shrink-0\", align !== \"end\" && \"ml-auto\")} /> : null}\n </div>\n ) : null}\n <div className={cx(\"mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5\", align === \"end\" && \"justify-end\")}>\n <span className=\"inline-flex min-w-0 items-baseline gap-1\">\n <span data-nums className={cx(s.value, TEXT_TONES[tone] ?? TEXT_TONES.neutral)}>\n {formatNumber(value)}\n </span>\n {unit ? (\n <span data-nums className={cx(\"text-muted-foreground\", s.unit)}>\n {unit}\n </span>\n ) : null}\n </span>\n {d ? <Delta invert={invert} size={s.delta} {...d} /> : null}\n </div>\n {caption ? <p className=\"mt-1 text-xs text-muted-foreground\">{caption}</p> : null}\n </div>\n );\n}\n\n// Literal class names, so Tailwind sees every one.\nconst COLS = { 1: \"sm:grid-cols-1\", 2: \"sm:grid-cols-2\", 3: \"sm:grid-cols-3\", 4: \"sm:grid-cols-4\" };\nconst SPAN_SM = { 1: \"sm:col-span-1\", 2: \"sm:col-span-2\", 3: \"sm:col-span-3\", 4: \"sm:col-span-4\" };\n\n/**\n * The strip of 2 to 4 Stats across the top of a card or a page. Two\n * columns on a phone, all of them from sm up; a last cell that would\n * leave a hole stretches to fill its row.\n *\n * variant card (default: bordered, a rule between every cell) ·\n * divided (no box, a vertical rule between cells — for a strip\n * already inside a Card) · bare (spacing only)\n * columns from sm up; default the number of Stats (at most 4). Six\n * stats in columns={3} make two rows\n * label the strip's name for readers: \"Cache summary\"\n * children the Stats\n *\n * Edge to edge inside a Card: variant=\"card\" with className=\"rounded-none\n * border-x-0\" and the Card's padding taken off (-mx-4).\n */\nexport function StatGroup({ variant = \"card\", columns, label, className, children }) {\n const kids = Children.toArray(children).filter(Boolean);\n const n = kids.length;\n if (!n) return null;\n const cols = Math.min(4, Math.max(1, columns ?? Math.min(n, 4)));\n const phoneCols = Math.min(2, n);\n const card = variant === \"card\";\n const divided = variant === \"divided\";\n return (\n <div\n role={label ? \"group\" : undefined}\n aria-label={label}\n className={cx(\n \"grid\",\n phoneCols === 1 ? \"grid-cols-1\" : \"grid-cols-2\",\n COLS[cols],\n card && \"gap-px overflow-hidden rounded-lg border border-border bg-border\",\n divided && \"gap-y-4\",\n variant === \"bare\" && \"gap-4\",\n className,\n )}\n >\n {kids.map((kid, i) => {\n const last = i === n - 1;\n // A last cell alone on its row spans what is left of it.\n const phoneSpan = last && n % phoneCols !== 0 ? \"col-span-2\" : \"\";\n const remSm = n % cols;\n const smSpan = last ? SPAN_SM[remSm ? cols - remSm + 1 : 1] : phoneSpan ? \"sm:col-span-1\" : \"\";\n // Divided: a rule on every cell that does not start a row.\n const rulePhone = i % phoneCols !== 0;\n const ruleSm = i % cols !== 0;\n const rule = divided\n ? cx(\n \"pr-4\",\n rulePhone && \"border-l border-border pl-4\",\n rulePhone && !ruleSm && \"sm:border-l-0 sm:pl-0\",\n !rulePhone && ruleSm && \"sm:border-l sm:border-border sm:pl-4\",\n )\n : \"\";\n return (\n <div key={kid.key ?? i} className={cx(\"min-w-0\", card && \"bg-card p-4\", rule, phoneSpan, smSpan)}>\n {kid}\n </div>\n );\n })}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------------\n Meter\n ------------------------------------------------------------------------ */\n\nconst METER_SIZES = { thin: { track: \"h-1\", mark: \"h-3\" }, md: { track: \"h-2\", mark: \"h-4\" } };\n\n/**\n * A quantity against a limit: storage used of a plan, spend of a budget,\n * bandwidth of a cap. Drawn like Progress, but it is a role=meter, not a\n * progressbar — Progress is how far a job has got, which only moves\n * forward and ends; a Meter is how full something is, which can fall and\n * can go OVER. Over the limit the bar stays full, turns danger, and a\n * reader hears \"over by …\".\n *\n * value the amount used, a number in the unit of `max`\n * max the limit; default 100\n * label the meter's name, shown above it and read to a screen reader\n * unit after the numbers: \"GB\", \"credits\"\n * format (n) → string for every number shown; default thousands\n * separators (\"$\" + n / 1000 + \"K\" for money)\n * valueText the right side of the header; default \"used / limit unit\";\n * null hides it (a row that shows its own numbers)\n * valueTone the colour of that text: muted (default) · neutral ·\n * primary · success · warning · danger · info · \"bar\" (the\n * bar's own tone). Over the limit it is danger regardless\n * target a mark on the bar at this amount (a goal, a budget line),\n * named in the footer and to a reader\n * targetMark default true; false keeps the target in the footer and for\n * a reader but draws no tick on the bar\n * direction against `target`, when no `tone` is given: higher (a goal —\n * success once reached, warning below it) · lower (a budget —\n * success at or under it, warning past it). Higher also keeps\n * a bar over the limit out of danger: past a quota is good\n * thresholds [{ at, tone }] — `at` is a percent of max; the bar takes the\n * tone of the highest one reached: [{ at: 80, tone: \"warning\" },\n * { at: 95, tone: \"danger\" }]\n * tone the bar below every threshold: neutral (bg-primary, like\n * Progress) · muted (grey, a plain share that signals\n * nothing) · success · warning · danger · info\n * caption a muted line under the bar, left of the target\n * size thin (4px, a list row) · md (8px); a ring: sm (40px, a\n * list row or a stat tile) · md (64px) · lg (96px)\n * shape bar (default) · ring — a circular gauge, the percent in its\n * middle, the label, value text, caption and target beside\n * it. Tone, thresholds, target (a tick across the ring),\n * direction, over-the-limit and what a reader hears are the\n * bar's\n * center a ring's middle instead of the percent: a string or a node\n * (\"4.2 GB\", an Icon); null leaves it empty\n */\nexport function Meter({\n value,\n max = 100,\n label,\n unit,\n format = formatNumber,\n valueText,\n valueTone = \"muted\",\n target,\n targetMark = true,\n direction,\n thresholds,\n tone,\n caption,\n size = \"md\",\n shape = \"bar\",\n center,\n className,\n ...rest\n}) {\n const v = typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n const limit = max > 0 ? max : 100;\n const pct = (v / limit) * 100;\n const over = v > limit;\n const overBad = over && direction !== \"higher\";\n const u = unit ? ` ${unit}` : \"\";\n const hasTarget = typeof target === \"number\" && Number.isFinite(target);\n\n // No tone given: the direction judges the value against the target.\n const met = direction === \"higher\" ? v >= target : v <= target;\n let barTone = tone ?? (hasTarget && (direction === \"higher\" || direction === \"lower\") ? (met ? \"success\" : \"warning\") : \"neutral\");\n for (const t of [...(thresholds ?? [])].sort((a, b) => a.at - b.at)) if (pct >= t.at) barTone = t.tone;\n if (overBad) barTone = \"danger\";\n const fill = barTone === \"neutral\" ? \"bg-primary\" : FILL_TONES[barTone] ?? \"bg-primary\";\n const valueClass = overBad\n ? \"font-medium text-destructive\"\n : valueTone === \"bar\"\n ? cx(\"font-medium\", barTone === \"neutral\" ? \"text-primary\" : TEXT_TONES[barTone] ?? \"text-primary\")\n : valueTone === \"muted\"\n ? \"text-muted-foreground\"\n : cx(\"font-medium\", TEXT_TONES[valueTone] ?? \"text-muted-foreground\");\n\n const targetPct = hasTarget ? Math.min(100, Math.max(0, (target / limit) * 100)) : 0;\n const shown = valueText === undefined ? `${format(v)} / ${format(limit)}${u}` : valueText;\n const said = [\n `${format(v)} of ${format(limit)}${u}`,\n over ? `over by ${format(v - limit)}${u}` : `${Math.round(pct)}%`,\n hasTarget ? `target ${format(target)}${u}` : null,\n ]\n .filter(Boolean)\n .join(\", \");\n const meterProps = {\n role: \"meter\",\n \"aria-label\": label,\n \"aria-valuemin\": 0,\n \"aria-valuemax\": limit,\n \"aria-valuenow\": Math.min(v, limit),\n \"aria-valuetext\": said,\n };\n\n if (shape === \"ring\") {\n return (\n <MeterRing\n {...{ pct, over, overBad, barTone, valueTone, valueClass, label, shown, caption, hasTarget, targetMark, targetPct, center, size, className, meterProps, rest }}\n targetText={hasTarget ? `Target ${format(target)}${u}` : null}\n />\n );\n }\n\n const s = METER_SIZES[size] ?? METER_SIZES.md;\n const footer = caption || (hasTarget && size === \"md\");\n\n return (\n <div className={cx(\"grid gap-1.5\", className)}>\n {label || shown ? (\n <div aria-hidden=\"true\" className=\"flex items-baseline justify-between gap-3 text-sm\">\n <span className=\"min-w-0 truncate font-medium text-foreground\">{label}</span>\n {shown ? (\n <span data-nums className={cx(\"shrink-0 text-xs\", valueClass)}>\n {shown}\n </span>\n ) : null}\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={limit}\n aria-valuenow={Math.min(v, limit)}\n aria-valuetext={said}\n className={cx(\"relative w-full overflow-hidden rounded-full bg-accent\", s.track)}\n {...rest}\n >\n <div\n className={cx(\"h-full rounded-full transition-[width] duration-300 ease-out motion-reduce:transition-none\", fill)}\n style={{ width: `${Math.min(100, Math.max(0, pct))}%` }}\n />\n </div>\n {hasTarget && targetMark ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute top-1/2 w-0.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground\", s.mark)}\n style={{ left: `${targetPct}%` }}\n />\n ) : null}\n </div>\n {footer ? (\n <div aria-hidden={caption ? undefined : \"true\"} className=\"flex items-baseline justify-between gap-3 text-xs text-muted-foreground\">\n <span className=\"min-w-0\">{caption}</span>\n {hasTarget ? (\n <span data-nums className=\"shrink-0\">\n Target {format(target)}\n {u}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n// The ring's stroke in each tone (neutral is bg-primary's stroke, as the bar).\nconst STROKE_TONES = {\n neutral: \"stroke-primary\",\n muted: \"stroke-muted-foreground\",\n primary: \"stroke-primary\",\n success: \"stroke-success\",\n warning: \"stroke-warning\",\n danger: \"stroke-destructive\",\n info: \"stroke-info\",\n};\n\n// px is the ring's box, w its stroke; text the middle's type.\nconst RING_SIZES = {\n sm: { px: 40, w: 4, text: \"text-2xs font-semibold\", side: \"text-sm\", gap: \"gap-2.5\" },\n md: { px: 64, w: 6, text: \"text-sm font-semibold\", side: \"text-sm\", gap: \"gap-3\" },\n lg: { px: 96, w: 8, text: \"text-xl font-semibold tracking-tight\", side: \"text-base\", gap: \"gap-4\" },\n};\nRING_SIZES.thin = RING_SIZES.sm;\n\n// Meter shape=\"ring\": the same numbers, drawn as an arc from twelve o'clock.\nfunction MeterRing({ pct, overBad, barTone, valueTone, valueClass, label, shown, caption, hasTarget, targetMark, targetPct, targetText, center, size, className, meterProps, rest }) {\n const g = RING_SIZES[size] ?? RING_SIZES.md;\n const c = g.px / 2;\n const r = c - g.w / 2;\n const len = 2 * Math.PI * r;\n const frac = Math.min(1, Math.max(0, pct / 100));\n const stroke = STROKE_TONES[barTone] ?? \"stroke-primary\";\n const middleClass = overBad\n ? \"text-destructive\"\n : valueTone === \"bar\"\n ? barTone === \"neutral\"\n ? \"text-primary\"\n : TEXT_TONES[barTone] ?? \"text-primary\"\n : valueTone === \"muted\"\n ? \"text-foreground\"\n : TEXT_TONES[valueTone] ?? \"text-foreground\";\n const middle = center === undefined ? `${Math.round(pct)}%` : center;\n // The target tick: a short radial line across the ring, at its angle.\n const a = (targetPct / 100) * 2 * Math.PI - Math.PI / 2;\n const tick = (d) => [c + Math.cos(a) * d, c + Math.sin(a) * d];\n const [x1, y1] = tick(r - g.w / 2 - 1.5);\n const [x2, y2] = tick(r + g.w / 2 + 1.5);\n const side = label || shown || caption || (hasTarget && size !== \"sm\");\n\n return (\n <div className={cx(\"flex min-w-0 items-center\", g.gap, className)}>\n <div {...meterProps} className=\"relative shrink-0\" style={{ width: g.px, height: g.px }} {...rest}>\n <svg aria-hidden=\"true\" width={g.px} height={g.px} viewBox={`0 0 ${g.px} ${g.px}`} className=\"block overflow-visible\">\n {/* raw: an SVG gauge — the kit has no smaller part to draw an arc. */}\n <circle cx={c} cy={c} r={r} fill=\"none\" strokeWidth={g.w} className=\"stroke-accent\" />\n {frac > 0 ? (\n <circle\n cx={c}\n cy={c}\n r={r}\n fill=\"none\"\n strokeWidth={g.w}\n strokeLinecap=\"round\"\n strokeDasharray={`${len * frac} ${len}`}\n transform={`rotate(-90 ${c} ${c})`}\n className={cx(\"transition-[stroke-dasharray] duration-300 ease-out motion-reduce:transition-none\", stroke)}\n />\n ) : null}\n {hasTarget && targetMark ? (\n <line x1={x1} y1={y1} x2={x2} y2={y2} strokeWidth={2} strokeLinecap=\"round\" className=\"stroke-foreground\" />\n ) : null}\n </svg>\n {middle !== null && middle !== false ? (\n <span aria-hidden=\"true\" data-nums className={cx(\"absolute inset-0 flex items-center justify-center leading-none\", g.text, middleClass)}>\n {middle}\n </span>\n ) : null}\n </div>\n {side ? (\n <div className={cx(\"grid min-w-0 gap-0.5\", g.side)}>\n {label ? <span aria-hidden=\"true\" className=\"min-w-0 truncate font-medium text-foreground\">{label}</span> : null}\n {shown ? (\n <span data-nums aria-hidden=\"true\" className={cx(\"text-xs\", valueClass)}>\n {shown}\n </span>\n ) : null}\n {caption || (hasTarget && size !== \"sm\") ? (\n <span className=\"text-xs text-muted-foreground\">\n {caption}\n {caption && hasTarget && size !== \"sm\" ? \" · \" : null}\n {hasTarget && size !== \"sm\" ? <span aria-hidden=\"true\" data-nums>{targetText}</span> : null}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n"
258
+ "content": "import { Children } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Numbers at a glance: a Stat, the strip of them, the signed change beside\n// one, a status dot, a legend of dots, a meter against a limit.\n// Server-safe (no hooks), so a server surface can use them directly.\n// Every number is tabular (data-nums), so a column of them lines up and a\n// ticking value does not shake. Colour is the signal: tones are named, as\n// Badge does it, and status tones are for status only — never decoration.\n// A dot can take a categorical `hue` (1..5, the chart colours) instead, for\n// KINDS of things: a platform, a content type. Tone is status, hue is\n// category; a tone given with a hue wins.\n// Blocks use these and never draw their own.\n\n// The text colour of a value in each tone.\nconst TEXT_TONES = {\n neutral: \"text-foreground\",\n muted: \"text-muted-foreground\",\n primary: \"text-primary\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-destructive\",\n info: \"text-info\",\n};\n\n// The fill of a dot or a bar in each tone. neutral is a quiet grey for a\n// dot; a Meter's neutral bar is bg-primary, the same as Progress, and its\n// muted bar is the grey.\nconst FILL_TONES = {\n neutral: \"bg-muted-foreground\",\n muted: \"bg-muted-foreground\",\n primary: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n info: \"bg-info\",\n};\n\n// The fill of a dot in each categorical hue (--color-chart-N).\nconst HUE_FILLS = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nconst EN = \"en-US\"; // one locale on the server and the client: no hydration drift\nconst formatNumber = (n) => (typeof n === \"number\" ? n.toLocaleString(EN, { maximumFractionDigits: 2 }) : String(n ?? \"\"));\n\n/* ------------------------------------------------------------------------\n Delta\n ------------------------------------------------------------------------ */\n\nconst DELTA_SIZES = { sm: { text: \"text-xs\", icon: 12 }, md: { text: \"text-sm\", icon: 14 } };\n\n/**\n * A signed change: an arrow, the value with its sign, an optional suffix —\n * \"↑ +12.4%\". Up is good (success) and down is bad (danger) unless\n * `invert`, for \"lower is better\" (latency, churn, cost). Zero is neutral,\n * with a flat dash and no sign.\n *\n * value the change, a number; its sign sets the arrow and the tone\n * display what to show for the size of it instead of the number\n * (\"$35K\"); the sign is still added. Default the number, to at\n * most one decimal\n * suffix after the number: \"%\", \" pts\", \"ms\"\n * invert lower is better: down turns success, up turns danger\n * label a muted line after it: \"vs last week\"\n * size sm (text-xs) · md (text-sm)\n * variant text (bare) · badge (a tinted pill, for a table cell or a tile corner)\n * arrow default true; false for the bare \"+$35K\" of a dense row\n * hideValue the arrow and its tone alone (a trend in a tight cell);\n * the value is still what a screen reader hears. Always draws\n * the arrow, whatever `arrow` says\n */\nexport function Delta({ value, display, suffix = \"\", invert = false, label, size = \"sm\", variant = \"text\", arrow = true, hideValue = false, className }) {\n const n = typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n const sign = n > 0 ? 1 : n < 0 ? -1 : 0;\n const good = invert ? sign < 0 : sign > 0;\n const tone = sign === 0 ? \"neutral\" : good ? \"success\" : \"danger\";\n const s = DELTA_SIZES[size] ?? DELTA_SIZES.sm;\n const amount = display ?? Math.abs(n).toLocaleString(EN, { maximumFractionDigits: 1 });\n const signed = `${sign > 0 ? \"+\" : sign < 0 ? \"−\" : \"\"}${amount}${suffix}`;\n const look =\n variant === \"badge\"\n ? {\n neutral: \"bg-muted text-muted-foreground\",\n success: \"bg-success/15 text-success\",\n danger: \"bg-destructive/15 text-destructive\",\n }[tone]\n : { neutral: \"text-muted-foreground\", success: \"text-success\", danger: \"text-destructive\" }[tone];\n return (\n <span className={cx(\"inline-flex items-baseline gap-1.5\", s.text, className)}>\n <span\n data-nums\n className={cx(\n \"inline-flex items-center gap-0.5 whitespace-nowrap font-medium\",\n variant === \"badge\" && \"rounded-sm px-1.5 py-0.5\",\n look,\n )}\n >\n {arrow || hideValue ? <Icon name={sign > 0 ? \"arrow-up\" : sign < 0 ? \"arrow-down\" : \"minus\"} size={s.icon} className=\"self-center\" /> : null}\n {/* The sign is a real minus (U+2212) on screen; a reader hears the word. */}\n {hideValue ? null : <span aria-hidden=\"true\">{signed}</span>}\n <span className=\"sr-only\">{sign > 0 ? `up ${amount}${suffix}` : sign < 0 ? `down ${amount}${suffix}` : `no change`}</span>\n </span>\n {label ? <span className=\"text-muted-foreground\">{label}</span> : null}\n </span>\n );\n}\n\n/* ------------------------------------------------------------------------\n StatusDot and Legend\n ------------------------------------------------------------------------ */\n\nconst DOT_SIZES = { sm: \"size-1.5\", md: \"size-2\" };\n\n/**\n * A small round dot in a tone: a row's state, a service's health, a\n * legend key. The dot alone says nothing to a screen reader — the meaning\n * is in `label` (shown), or in `label` + `hideLabel` (read, not shown), or\n * in the text beside it.\n *\n * tone neutral · primary · success · warning · danger · info\n * hue 1..5 — a categorical colour instead (a kind of thing, not\n * a state); ignored when a tone is given\n * size sm (6px, in a dense row) · md (8px)\n * pulse a ring that breathes out, for something live right now\n * (deploying, recording); still under prefers-reduced-motion\n * label the words after it: \"Healthy\", \"3 alerts\"\n * hideLabel keep the label for readers only\n */\nexport function StatusDot({ tone, hue, size = \"md\", pulse = false, label, hideLabel = false, className }) {\n const fill = tone == null && HUE_FILLS[hue] ? HUE_FILLS[hue] : FILL_TONES[tone ?? \"neutral\"] ?? FILL_TONES.neutral;\n const dot = (\n <span aria-hidden=\"true\" className={cx(\"relative inline-flex shrink-0\", DOT_SIZES[size] ?? DOT_SIZES.md)}>\n {pulse ? <span className={cx(\"absolute inset-0 rounded-full opacity-60 animate-ping motion-reduce:hidden\", fill)} /> : null}\n <span className={cx(\"relative inline-flex size-full rounded-full\", fill)} />\n </span>\n );\n if (!label) return <span className={cx(\"inline-flex items-center\", className)}>{dot}</span>;\n return (\n <span className={cx(\"inline-flex items-center gap-1.5 text-sm text-muted-foreground\", className)}>\n {dot}\n <span className={hideLabel ? \"sr-only\" : \"min-w-0 truncate\"}>{label}</span>\n </span>\n );\n}\n\n/**\n * A row of dot · count · label, wrapping when it runs out of room — \"● 2\n * overdue · ● 3 urgent\". The key under a chart, the summary in a card\n * header. A list, so a reader hears how many kinds there are.\n *\n * items [{ tone, label, count }] — count optional; an item with count 0\n * is still drawn (say it, or leave it out of items). `hue` (1..5)\n * in place of `tone` for a key of categories\n * size sm (text-xs, 6px dots) · md (text-sm, 8px dots)\n * label the list's name for readers: \"Tasks by state\"\n */\nexport function Legend({ items = [], size = \"md\", label, className }) {\n if (!items.length) return null;\n return (\n <ul\n aria-label={label}\n className={cx(\"flex flex-wrap items-center gap-x-3 gap-y-1\", size === \"sm\" ? \"text-xs\" : \"text-sm\", className)}\n >\n {items.map((it, i) => (\n <li key={it.key ?? `${it.label}-${i}`} className=\"inline-flex items-center gap-1.5 text-muted-foreground\">\n <StatusDot tone={it.tone} hue={it.hue} size={size === \"sm\" ? \"sm\" : \"md\"} />\n {it.count != null ? (\n <span data-nums className=\"font-medium text-foreground\">\n {formatNumber(it.count)}\n </span>\n ) : null}\n <span>{it.label}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/* ------------------------------------------------------------------------\n Stat and StatGroup\n ------------------------------------------------------------------------ */\n\nconst STAT_SIZES = {\n sm: { label: \"text-xs\", value: \"text-lg font-semibold leading-tight\", unit: \"text-xs\", icon: 14, delta: \"sm\" },\n md: { label: \"text-sm\", value: \"text-2xl font-semibold leading-tight tracking-tight\", unit: \"text-sm\", icon: 16, delta: \"sm\" },\n lg: { label: \"text-sm\", value: \"font-display text-4xl font-semibold leading-none tracking-tight\", unit: \"text-base\", icon: 18, delta: \"md\" },\n};\n\n/**\n * One number with its name: \"Hit rate 94.8%\". The value is the loudest\n * thing on it; everything else is muted.\n *\n * label what it counts: \"Monthly revenue\"\n * value already formatted — \"$1.37M\", \"42\", \"2.4/4\" (a string or a\n * number; a number gets thousands separators)\n * unit after the value, smaller and muted: \"ms\", \"%\", \"/ 10.3 TB\"\n * caption a muted line under it: \"Unit + integration tests\"\n * tone colours the value, for a threshold state (a miss rate over\n * budget is danger). neutral (default) · primary · success ·\n * warning · danger · info\n * dot a tone for a StatusDot before the label (the state of the\n * thing, when the value itself stays neutral)\n * icon an Icon name, muted, at the end of the label row\n * delta the change beside the value: a number (a percent: 12.4 →\n * \"↑ +12.4%\") or Delta's props ({ value, display, suffix, label })\n * invert lower is better, passed to the Delta\n * size sm (a dense strip) · md · lg (the one number a card is about)\n * align start · end (a stat pinned to the right of a header)\n */\nexport function Stat({ label, value, unit, caption, tone = \"neutral\", dot, icon, delta, invert, size = \"md\", align = \"start\", className }) {\n const s = STAT_SIZES[size] ?? STAT_SIZES.md;\n const d = delta == null ? null : typeof delta === \"number\" ? { value: delta, suffix: \"%\" } : delta;\n return (\n <div className={cx(\"min-w-0\", align === \"end\" && \"text-right\", className)}>\n {label || icon || dot ? (\n <div className={cx(\"flex items-center gap-1.5 text-muted-foreground\", s.label, align === \"end\" && \"justify-end\")}>\n {dot ? <StatusDot tone={dot} size=\"sm\" /> : null}\n {label ? <span className=\"min-w-0 truncate\">{label}</span> : null}\n {icon ? <Icon name={icon} size={s.icon} className={cx(\"shrink-0\", align !== \"end\" && \"ml-auto\")} /> : null}\n </div>\n ) : null}\n <div className={cx(\"mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5\", align === \"end\" && \"justify-end\")}>\n <span className=\"inline-flex min-w-0 items-baseline gap-1\">\n <span data-nums className={cx(s.value, TEXT_TONES[tone] ?? TEXT_TONES.neutral)}>\n {formatNumber(value)}\n </span>\n {unit ? (\n <span data-nums className={cx(\"text-muted-foreground\", s.unit)}>\n {unit}\n </span>\n ) : null}\n </span>\n {d ? <Delta invert={invert} size={s.delta} {...d} /> : null}\n </div>\n {caption ? <p className=\"mt-1 text-xs text-muted-foreground\">{caption}</p> : null}\n </div>\n );\n}\n\n// Literal class names, so Tailwind sees every one.\nconst COLS = { 1: \"sm:grid-cols-1\", 2: \"sm:grid-cols-2\", 3: \"sm:grid-cols-3\", 4: \"sm:grid-cols-4\" };\nconst SPAN_SM = { 1: \"sm:col-span-1\", 2: \"sm:col-span-2\", 3: \"sm:col-span-3\", 4: \"sm:col-span-4\" };\n\n/**\n * The strip of 2 to 4 Stats across the top of a card or a page. Two\n * columns on a phone, all of them from sm up; a last cell that would\n * leave a hole stretches to fill its row.\n *\n * variant card (default: bordered, a rule between every cell) ·\n * divided (no box, a vertical rule between cells — for a strip\n * already inside a Card) · bare (spacing only)\n * columns from sm up; default the number of Stats (at most 4). Six\n * stats in columns={3} make two rows\n * label the strip's name for readers: \"Cache summary\"\n * children the Stats\n *\n * Edge to edge inside a Card: variant=\"card\" with className=\"rounded-none\n * border-x-0\" and the Card's padding taken off (-mx-4).\n */\nexport function StatGroup({ variant = \"card\", columns, label, className, children }) {\n const kids = Children.toArray(children).filter(Boolean);\n const n = kids.length;\n if (!n) return null;\n const cols = Math.min(4, Math.max(1, columns ?? Math.min(n, 4)));\n const phoneCols = Math.min(2, n);\n const card = variant === \"card\";\n const divided = variant === \"divided\";\n return (\n <div\n role={label ? \"group\" : undefined}\n aria-label={label}\n className={cx(\n \"grid\",\n phoneCols === 1 ? \"grid-cols-1\" : \"grid-cols-2\",\n COLS[cols],\n card && \"gap-px overflow-hidden rounded-lg border border-border bg-border\",\n divided && \"gap-y-4\",\n variant === \"bare\" && \"gap-4\",\n className,\n )}\n >\n {kids.map((kid, i) => {\n const last = i === n - 1;\n // A last cell alone on its row spans what is left of it.\n const phoneSpan = last && n % phoneCols !== 0 ? \"col-span-2\" : \"\";\n const remSm = n % cols;\n const smSpan = last ? SPAN_SM[remSm ? cols - remSm + 1 : 1] : phoneSpan ? \"sm:col-span-1\" : \"\";\n // Divided: a rule on every cell that does not start a row.\n const rulePhone = i % phoneCols !== 0;\n const ruleSm = i % cols !== 0;\n const rule = divided\n ? cx(\n \"pr-4\",\n rulePhone && \"border-l border-border pl-4\",\n rulePhone && !ruleSm && \"sm:border-l-0 sm:pl-0\",\n !rulePhone && ruleSm && \"sm:border-l sm:border-border sm:pl-4\",\n )\n : \"\";\n return (\n <div key={kid.key ?? i} className={cx(\"min-w-0\", card && \"bg-card p-4\", rule, phoneSpan, smSpan)}>\n {kid}\n </div>\n );\n })}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------------\n Meter\n ------------------------------------------------------------------------ */\n\nconst METER_SIZES = { thin: { track: \"h-1\", mark: \"h-3\" }, md: { track: \"h-2\", mark: \"h-4\" } };\n\n/**\n * A quantity against a limit: storage used of a plan, spend of a budget,\n * bandwidth of a cap. Drawn like Progress, but it is a role=meter, not a\n * progressbar — Progress is how far a job has got, which only moves\n * forward and ends; a Meter is how full something is, which can fall and\n * can go OVER. Over the limit the bar stays full, turns danger, and a\n * reader hears \"over by …\".\n *\n * value the amount used, a number in the unit of `max`\n * max the limit; default 100\n * label the meter's name, shown above it and read to a screen reader\n * unit after the numbers: \"GB\", \"credits\"\n * format (n) → string for every number shown; default thousands\n * separators (\"$\" + n / 1000 + \"K\" for money)\n * valueText the right side of the header; default \"used / limit unit\";\n * null hides it (a row that shows its own numbers)\n * valueTone the colour of that text: muted (default) · neutral ·\n * primary · success · warning · danger · info · \"bar\" (the\n * bar's own tone). Over the limit it is danger regardless\n * target a mark on the bar at this amount (a goal, a budget line),\n * named in the footer and to a reader\n * targetMark default true; false keeps the target in the footer and for\n * a reader but draws no tick on the bar\n * direction against `target`, when no `tone` is given: higher (a goal —\n * success once reached, warning below it) · lower (a budget —\n * success at or under it, warning past it). Higher also keeps\n * a bar over the limit out of danger: past a quota is good\n * thresholds [{ at, tone }] — `at` is a percent of max; the bar takes the\n * tone of the highest one reached: [{ at: 80, tone: \"warning\" },\n * { at: 95, tone: \"danger\" }]\n * tone the bar below every threshold: neutral (bg-primary, like\n * Progress) · muted (grey, a plain share that signals\n * nothing) · success · warning · danger · info\n * caption a muted line under the bar, left of the target\n * size thin (4px, a list row) · md (8px); a ring: sm (40px, a\n * list row or a stat tile) · md (64px) · lg (96px)\n * shape bar (default) · ring — a circular gauge, the percent in its\n * middle, the label, value text, caption and target beside\n * it. Tone, thresholds, target (a tick across the ring),\n * direction, over-the-limit and what a reader hears are the\n * bar's\n * center a ring's middle instead of the percent: a string or a node\n * (\"4.2 GB\", an Icon); null leaves it empty\n * compact the bar alone (the ring alone, for shape=\"ring\"): no label,\n * no numbers, no caption, no target line, so it fits where\n * words spill: a top bar, a table cell, a card corner. The\n * label and the numbers are not lost: a screen reader hears\n * them as always, and the bar's tooltip (`title`) shows\n * \"label: used of limit\". Give it its width in className\n * (w-14, w-24); size thin reads best in a top bar. Without\n * compact a Meter draws its label as visible text, which\n * spills in a box under about 8rem (the Newsletter's \"15,000\n * cr left\" ran into Find, UI review X1; check rule 20,\n * meter, refuses it)\n */\nexport function Meter({\n value,\n max = 100,\n label,\n unit,\n format = formatNumber,\n valueText,\n valueTone = \"muted\",\n target,\n targetMark = true,\n direction,\n thresholds,\n tone,\n caption,\n size = \"md\",\n shape = \"bar\",\n center,\n compact = false,\n className,\n ...rest\n}) {\n const v = typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n const limit = max > 0 ? max : 100;\n const pct = (v / limit) * 100;\n const over = v > limit;\n const overBad = over && direction !== \"higher\";\n const u = unit ? ` ${unit}` : \"\";\n const hasTarget = typeof target === \"number\" && Number.isFinite(target);\n\n // No tone given: the direction judges the value against the target.\n const met = direction === \"higher\" ? v >= target : v <= target;\n let barTone = tone ?? (hasTarget && (direction === \"higher\" || direction === \"lower\") ? (met ? \"success\" : \"warning\") : \"neutral\");\n for (const t of [...(thresholds ?? [])].sort((a, b) => a.at - b.at)) if (pct >= t.at) barTone = t.tone;\n if (overBad) barTone = \"danger\";\n const fill = barTone === \"neutral\" ? \"bg-primary\" : FILL_TONES[barTone] ?? \"bg-primary\";\n const valueClass = overBad\n ? \"font-medium text-destructive\"\n : valueTone === \"bar\"\n ? cx(\"font-medium\", barTone === \"neutral\" ? \"text-primary\" : TEXT_TONES[barTone] ?? \"text-primary\")\n : valueTone === \"muted\"\n ? \"text-muted-foreground\"\n : cx(\"font-medium\", TEXT_TONES[valueTone] ?? \"text-muted-foreground\");\n\n const targetPct = hasTarget ? Math.min(100, Math.max(0, (target / limit) * 100)) : 0;\n const shown = valueText === undefined ? `${format(v)} / ${format(limit)}${u}` : valueText;\n const said = [\n `${format(v)} of ${format(limit)}${u}`,\n over ? `over by ${format(v - limit)}${u}` : `${Math.round(pct)}%`,\n hasTarget ? `target ${format(target)}${u}` : null,\n ]\n .filter(Boolean)\n .join(\", \");\n const meterProps = {\n role: \"meter\",\n \"aria-label\": label,\n \"aria-valuemin\": 0,\n \"aria-valuemax\": limit,\n \"aria-valuenow\": Math.min(v, limit),\n \"aria-valuetext\": said,\n };\n\n // The tooltip of a compact meter: the name, then the numbers a reader hears.\n const tip = compact ? (label ? `${label}: ${said}` : said) : undefined;\n\n if (shape === \"ring\") {\n return (\n <MeterRing\n {...{ pct, over, overBad, barTone, valueTone, valueClass, label, shown, caption, hasTarget, targetMark, targetPct, center, size, className, meterProps, rest, compact, tip }}\n targetText={hasTarget ? `Target ${format(target)}${u}` : null}\n />\n );\n }\n\n const s = METER_SIZES[size] ?? METER_SIZES.md;\n const footer = !compact && (caption || (hasTarget && size === \"md\"));\n\n return (\n <div className={cx(\"grid min-w-0 gap-1.5\", className)} title={tip}>\n {!compact && (label || shown) ? (\n <div aria-hidden=\"true\" className=\"flex items-baseline justify-between gap-3 text-sm\">\n <span className=\"min-w-0 truncate font-medium text-foreground\">{label}</span>\n {shown ? (\n <span data-nums className={cx(\"shrink-0 text-xs\", valueClass)}>\n {shown}\n </span>\n ) : null}\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={limit}\n aria-valuenow={Math.min(v, limit)}\n aria-valuetext={said}\n className={cx(\"relative w-full overflow-hidden rounded-full bg-accent\", s.track)}\n {...rest}\n >\n <div\n className={cx(\"h-full rounded-full transition-[width] duration-300 ease-out motion-reduce:transition-none\", fill)}\n style={{ width: `${Math.min(100, Math.max(0, pct))}%` }}\n />\n </div>\n {hasTarget && targetMark ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute top-1/2 w-0.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground\", s.mark)}\n style={{ left: `${targetPct}%` }}\n />\n ) : null}\n </div>\n {footer ? (\n <div aria-hidden={caption ? undefined : \"true\"} className=\"flex items-baseline justify-between gap-3 text-xs text-muted-foreground\">\n <span className=\"min-w-0\">{caption}</span>\n {hasTarget ? (\n <span data-nums className=\"shrink-0\">\n Target {format(target)}\n {u}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n// The ring's stroke in each tone (neutral is bg-primary's stroke, as the bar).\nconst STROKE_TONES = {\n neutral: \"stroke-primary\",\n muted: \"stroke-muted-foreground\",\n primary: \"stroke-primary\",\n success: \"stroke-success\",\n warning: \"stroke-warning\",\n danger: \"stroke-destructive\",\n info: \"stroke-info\",\n};\n\n// px is the ring's box, w its stroke; text the middle's type.\nconst RING_SIZES = {\n sm: { px: 40, w: 4, text: \"text-2xs font-semibold\", side: \"text-sm\", gap: \"gap-2.5\" },\n md: { px: 64, w: 6, text: \"text-sm font-semibold\", side: \"text-sm\", gap: \"gap-3\" },\n lg: { px: 96, w: 8, text: \"text-xl font-semibold tracking-tight\", side: \"text-base\", gap: \"gap-4\" },\n};\nRING_SIZES.thin = RING_SIZES.sm;\n\n// Meter shape=\"ring\": the same numbers, drawn as an arc from twelve o'clock.\nfunction MeterRing({ pct, overBad, barTone, valueTone, valueClass, label, shown, caption, hasTarget, targetMark, targetPct, targetText, center, size, className, meterProps, rest, compact, tip }) {\n const g = RING_SIZES[size] ?? RING_SIZES.md;\n const c = g.px / 2;\n const r = c - g.w / 2;\n const len = 2 * Math.PI * r;\n const frac = Math.min(1, Math.max(0, pct / 100));\n const stroke = STROKE_TONES[barTone] ?? \"stroke-primary\";\n const middleClass = overBad\n ? \"text-destructive\"\n : valueTone === \"bar\"\n ? barTone === \"neutral\"\n ? \"text-primary\"\n : TEXT_TONES[barTone] ?? \"text-primary\"\n : valueTone === \"muted\"\n ? \"text-foreground\"\n : TEXT_TONES[valueTone] ?? \"text-foreground\";\n const middle = center === undefined ? `${Math.round(pct)}%` : center;\n // The target tick: a short radial line across the ring, at its angle.\n const a = (targetPct / 100) * 2 * Math.PI - Math.PI / 2;\n const tick = (d) => [c + Math.cos(a) * d, c + Math.sin(a) * d];\n const [x1, y1] = tick(r - g.w / 2 - 1.5);\n const [x2, y2] = tick(r + g.w / 2 + 1.5);\n const side = !compact && (label || shown || caption || (hasTarget && size !== \"sm\"));\n\n return (\n <div className={cx(\"flex min-w-0 items-center\", g.gap, className)} title={tip}>\n <div {...meterProps} className=\"relative shrink-0\" style={{ width: g.px, height: g.px }} {...rest}>\n <svg aria-hidden=\"true\" width={g.px} height={g.px} viewBox={`0 0 ${g.px} ${g.px}`} className=\"block overflow-visible\">\n {/* raw: an SVG gauge — the kit has no smaller part to draw an arc. */}\n <circle cx={c} cy={c} r={r} fill=\"none\" strokeWidth={g.w} className=\"stroke-accent\" />\n {frac > 0 ? (\n <circle\n cx={c}\n cy={c}\n r={r}\n fill=\"none\"\n strokeWidth={g.w}\n strokeLinecap=\"round\"\n strokeDasharray={`${len * frac} ${len}`}\n transform={`rotate(-90 ${c} ${c})`}\n className={cx(\"transition-[stroke-dasharray] duration-300 ease-out motion-reduce:transition-none\", stroke)}\n />\n ) : null}\n {hasTarget && targetMark ? (\n <line x1={x1} y1={y1} x2={x2} y2={y2} strokeWidth={2} strokeLinecap=\"round\" className=\"stroke-foreground\" />\n ) : null}\n </svg>\n {middle !== null && middle !== false ? (\n <span aria-hidden=\"true\" data-nums className={cx(\"absolute inset-0 flex items-center justify-center leading-none\", g.text, middleClass)}>\n {middle}\n </span>\n ) : null}\n </div>\n {side ? (\n <div className={cx(\"grid min-w-0 gap-0.5\", g.side)}>\n {label ? <span aria-hidden=\"true\" className=\"min-w-0 truncate font-medium text-foreground\">{label}</span> : null}\n {shown ? (\n <span data-nums aria-hidden=\"true\" className={cx(\"text-xs\", valueClass)}>\n {shown}\n </span>\n ) : null}\n {caption || (hasTarget && size !== \"sm\") ? (\n <span className=\"text-xs text-muted-foreground\">\n {caption}\n {caption && hasTarget && size !== \"sm\" ? \" · \" : null}\n {hasTarget && size !== \"sm\" ? <span aria-hidden=\"true\" data-nums>{targetText}</span> : null}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n"
253
259
  },
254
260
  {
255
261
  "path": "templates/next/files/components/ui/steps.jsx",
@@ -279,7 +285,7 @@
279
285
  "path": "templates/next/files/components/ui/toast.jsx",
280
286
  "type": "registry:ui",
281
287
  "target": "@components/makersclaw/ui/toast.jsx",
282
- "content": "\"use client\";\n\nimport { isValidElement, useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\nimport { Spinner } from \"./feedback\";\n\n// Toast — a short line about something that just happened (\"Saved\",\n// \"Invite sent\"), which leaves on its own. Call it from any client code;\n// nothing needs mounting: the first toast mounts a host into the page's\n// top layer (a manual popover, above sheets and menus). app/layout.jsx is\n// fixed, so this is how an app gets toasts at all. Put <Toaster /> in a\n// surface only to choose where they stack.\n//\n// toast(\"Saved\") → id\n// toast.success / .error / .info / .warning(title, options)\n// toast.promise(promise, { loading, success, error })\n// success / error: a string, (value) → string, or\n// { title, description, action }; null dismisses. Returns the\n// promise.\n// toast.dismiss(id) one; no id = all\n//\n// options description the line under the title\n// action { label, onClick } — one button; the toast\n// closes after unless onClick calls\n// event.preventDefault()\n// duration ms, default 5000; Infinity stays until closed\n// id reuse one to update a toast in place\n//\n// <Toaster position visible />\n// position bottom-end (the default) · bottom-center ·\n// top-end · top-center. On a phone every position\n// is full width at its edge.\n// visible how many show at once (3); older ones wait behind\n//\n// Newest in front; the rest stack behind and fan out on hover or focus,\n// and every timer pauses while they do (and while the tab is hidden).\n// Close with the ×, Escape, or a sideways swipe. Screen readers hear each\n// toast once through a live region (assertive for .error). A toast is\n// never the only record of something that matters: an error the user\n// must act on is a Notice on the surface.\n\nconst DURATION = 5000;\nconst EXIT_MS = 200;\nconst GAP = 12;\nconst PEEK = 12;\nconst SWIPE = 80;\n\nconst TONES = {\n neutral: { box: \"border-border\", icon: null },\n success: { box: \"border-success/40\", icon: \"check\", color: \"text-success\" },\n info: { box: \"border-info/40\", icon: \"info\", color: \"text-info\" },\n warning: { box: \"border-warning/50\", icon: \"warning\", color: \"text-warning\" },\n danger: { box: \"border-destructive/50\", icon: \"warning\", color: \"text-destructive\" },\n loading: { box: \"border-border\", icon: null },\n};\n\n// Position → where the host sits. Class strings are written out whole:\n// Tailwind reads them literally.\nconst PLACES = {\n \"bottom-end\": \"top-auto bottom-0 left-0 right-0 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:left-auto sm:right-0\",\n \"bottom-center\": \"top-auto bottom-0 left-0 right-0 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:mx-auto\",\n \"top-end\": \"top-0 bottom-auto left-0 right-0 sm:left-auto sm:right-0\",\n \"top-center\": \"top-0 bottom-auto left-0 right-0 sm:mx-auto\",\n};\n\n// ---- The store: module-level, so toast() works from anywhere ----------\n\nconst EMPTY = { toasts: [], host: null };\nconst AUTO = { auto: true };\nlet state = EMPTY;\nlet seq = 0;\nlet auto = null;\nconst hosts = [];\nconst listeners = new Set();\n\nfunction set(next) {\n state = { ...state, ...next };\n for (const l of listeners) l();\n}\n\nfunction subscribe(l) {\n listeners.add(l);\n return () => listeners.delete(l);\n}\n\nconst reduced = () => typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n// No <Toaster /> on the page: lay one in <body>, in its own root.\nfunction mount() {\n if (typeof document === \"undefined\" || state.host || auto) return;\n const el = document.createElement(\"div\");\n el.setAttribute(\"data-toaster\", \"\");\n document.body.appendChild(el);\n auto = { el, root: createRoot(el) };\n set({ host: AUTO });\n auto.root.render(<Host token={AUTO} />);\n}\n\nfunction show(title, options, tone) {\n const { id: wanted, description, action, duration } = options ?? {};\n const id = wanted ?? `toast-${++seq}`;\n const was = state.toasts.find((t) => t.id === id);\n const item = {\n id,\n tone,\n title,\n description,\n action,\n duration: duration ?? (tone === \"loading\" ? Infinity : DURATION),\n version: (was?.version ?? 0) + 1,\n leaving: false,\n };\n set({ toasts: was ? state.toasts.map((t) => (t.id === id ? item : t)) : [item, ...state.toasts] });\n mount();\n return id;\n}\n\nfunction dismiss(id) {\n const hit = (t) => id === undefined || t.id === id;\n if (!state.toasts.some((t) => hit(t) && !t.leaving)) return;\n set({ toasts: state.toasts.map((t) => (hit(t) ? { ...t, leaving: true } : t)) });\n setTimeout(() => set({ toasts: state.toasts.filter((t) => !(hit(t) && t.leaving)) }), reduced() ? 0 : EXIT_MS);\n}\n\nexport function toast(title, options) {\n return show(title, options, \"neutral\");\n}\ntoast.success = (title, options) => show(title, options, \"success\");\ntoast.error = (title, options) => show(title, options, \"danger\");\ntoast.info = (title, options) => show(title, options, \"info\");\ntoast.warning = (title, options) => show(title, options, \"warning\");\ntoast.dismiss = dismiss;\ntoast.promise = (promise, { loading = \"Working…\", success, error } = {}) => {\n const p = typeof promise === \"function\" ? promise() : promise;\n const id = show(loading, {}, \"loading\");\n const settle = (message, value, tone) => {\n const m = typeof message === \"function\" ? message(value) : message;\n if (m == null || m === false) dismiss(id);\n else if (typeof m === \"object\" && !isValidElement(m)) show(m.title, { ...m, id }, tone);\n else show(m, { id }, tone);\n };\n Promise.resolve(p).then(\n (v) => settle(success ?? \"Done\", v, \"success\"),\n (e) => settle(error ?? \"Something went wrong\", e, \"danger\"),\n );\n return p;\n};\n\n// ---- The host ---------------------------------------------------------\n\n/** Where toasts stack. Optional — without one, the first toast mounts a\n * bottom-end host by itself. The newest <Toaster /> on the page wins. */\nexport function Toaster({ position = \"bottom-end\", visible = 3, className }) {\n const token = useRef(null);\n if (!token.current) token.current = {};\n useEffect(() => {\n const own = token.current;\n hosts.push(own);\n set({ host: own });\n if (auto) {\n const a = auto;\n auto = null;\n setTimeout(() => {\n a.root.unmount();\n a.el.remove();\n });\n }\n return () => {\n hosts.splice(hosts.indexOf(own), 1);\n set({ host: hosts[hosts.length - 1] ?? null });\n if (!state.host && state.toasts.length) mount();\n };\n }, []);\n return <Host token={token.current} position={position} visible={visible} className={className} />;\n}\n\nfunction Host({ token, position = \"bottom-end\", visible = 3, className }) {\n const s = useSyncExternalStore(subscribe, () => state, () => EMPTY);\n const pop = useRef(null);\n const said = useRef(new Set());\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [hidden, setHidden] = useState(false);\n const [heights, setHeights] = useState({});\n const [say, setSay] = useState({ polite: \"\", assertive: \"\" });\n const mine = s.host === token;\n const items = mine ? s.toasts : [];\n const live = items.filter((t) => !t.leaving);\n const newest = live[0]?.id;\n const top = position.startsWith(\"top\");\n\n // In the top layer while there is something to show. A modal <dialog>\n // opened since would sit above it: show again to rise over it.\n useLayoutEffect(() => {\n const p = pop.current;\n if (!p?.showPopover) return;\n const open = p.matches(\":popover-open\");\n if (items.length && !open) p.showPopover();\n else if (!items.length && open) p.hidePopover();\n else if (open && newest && document.querySelector(\"dialog:modal\") && !p.contains(document.activeElement)) {\n p.hidePopover();\n p.showPopover();\n }\n }, [items.length, newest]);\n\n // An emptied stack cannot report the pointer leaving it.\n useEffect(() => {\n if (items.length === 0) {\n setHovered(false);\n setFocused(false);\n }\n }, [items.length]);\n\n useEffect(() => {\n const on = () => setHidden(document.visibilityState === \"hidden\");\n document.addEventListener(\"visibilitychange\", on);\n return () => document.removeEventListener(\"visibilitychange\", on);\n }, []);\n\n // Each toast is announced once per version, a beat after the live\n // region exists (a region born with its text is not read).\n const timer = useRef(0);\n useEffect(() => () => clearTimeout(timer.current), []);\n useEffect(() => {\n const t = live.find((x) => !said.current.has(`${x.id}:${x.version}`));\n if (!t) return;\n said.current.add(`${t.id}:${t.version}`);\n const text = [t.title, t.description].filter((x) => typeof x === \"string\" || typeof x === \"number\").join(\". \");\n if (!text) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => setSay(t.tone === \"danger\" ? { polite: \"\", assertive: text } : { polite: text, assertive: \"\" }), 100);\n });\n\n const measure = useCallback((id, h) => setHeights((prev) => (prev[id] === h ? prev : { ...prev, [id]: h })), []);\n\n if (!mine) return null;\n\n const expanded = hovered || focused;\n const paused = expanded || hidden;\n const shown = live.slice(0, visible);\n let run = 0;\n const offsets = shown.map((t) => {\n const at = run;\n run += (heights[t.id] ?? 0) + GAP;\n return at;\n });\n const front = heights[shown[0]?.id] ?? 0;\n const height = expanded ? Math.max(0, run - GAP) : front + PEEK * Math.max(0, shown.length - 1);\n\n return (\n <>\n <div className=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {say.polite}\n </div>\n <div className=\"sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">\n {say.assertive}\n </div>\n <section\n ref={pop}\n popover=\"manual\"\n aria-label=\"Notifications\"\n onPointerEnter={() => setHovered(true)}\n onPointerLeave={() => setHovered(false)}\n onFocus={() => setFocused(true)}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setFocused(false);\n }}\n className={cx(\n \"pointer-events-none fixed z-50 m-0 h-auto w-full overflow-visible bg-transparent p-3 sm:w-96 sm:p-4\",\n PLACES[position] ?? PLACES[\"bottom-end\"],\n className,\n )}\n >\n <ol className=\"relative transition-[height] duration-300 ease-out motion-reduce:transition-none\" style={{ height }}>\n {items.map((t) => {\n const i = live.indexOf(t);\n return (\n <ToastItem\n key={t.id}\n t={t}\n index={i}\n top={top}\n beyond={i >= visible}\n offset={offsets[i] ?? 0}\n expanded={expanded}\n clip={!expanded && i > 0 ? front : undefined}\n paused={paused}\n onHeight={measure}\n />\n );\n })}\n </ol>\n </section>\n </>\n );\n}\n\nfunction ToastItem({ t, index, top, beyond, offset, expanded, clip, paused, onHeight }) {\n const inner = useRef(null);\n const left = useRef(t.duration);\n const ver = useRef(0);\n const drag = useRef(null);\n const last = useRef(\"\");\n const [dx, setDx] = useState(0);\n const [exit, setExit] = useState(0);\n const tone = TONES[t.tone] ?? TONES.neutral;\n\n // The timer: counts down only while nothing holds the stack open; a\n // new version (toast.promise settling, an update by id) starts it over.\n useEffect(() => {\n if (ver.current !== t.version) {\n ver.current = t.version;\n left.current = t.duration;\n }\n if (paused || t.leaving || !Number.isFinite(t.duration)) return;\n const start = Date.now();\n const h = setTimeout(() => dismiss(t.id), Math.max(0, left.current));\n return () => {\n clearTimeout(h);\n left.current -= Date.now() - start;\n };\n }, [paused, t.leaving, t.version, t.duration, t.id]);\n\n useLayoutEffect(() => {\n const el = inner.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => onHeight(t.id, el.offsetHeight));\n ro.observe(el);\n onHeight(t.id, el.offsetHeight);\n return () => ro.disconnect();\n }, [t.id, onHeight]);\n\n // Where it sits in the stack. A leaving toast keeps its last place\n // while it fades, so the others close the gap at once.\n const dir = top ? 1 : -1;\n if (!t.leaving) {\n const y = expanded ? offset : index * PEEK;\n const k = expanded ? 1 : 1 - Math.min(index, 3) * 0.05;\n last.current = `translateY(${dir * y}px) scale(${k})`;\n }\n\n function onPointerDown(e) {\n if (e.button !== 0 || e.target.closest(\"button, a, input, textarea, select\")) return;\n drag.current = { x: e.clientX, at: e.timeStamp };\n e.currentTarget.setPointerCapture(e.pointerId);\n }\n function onPointerMove(e) {\n if (drag.current) setDx(e.clientX - drag.current.x);\n }\n function onPointerUp(e) {\n const d = drag.current;\n drag.current = null;\n if (!d) return;\n const moved = e.clientX - d.x;\n const fast = Math.abs(moved) > 10 && Math.abs(moved) / Math.max(1, e.timeStamp - d.at) > 0.5;\n if (Math.abs(moved) > SWIPE || fast) {\n setExit(Math.sign(moved) || 1);\n dismiss(t.id);\n } else setDx(0);\n }\n\n const dragging = drag.current !== null && dx !== 0;\n const style = {\n zIndex: 1000 - Math.max(index, 0),\n transform: last.current,\n transformOrigin: top ? \"bottom center\" : \"top center\",\n height: clip,\n touchAction: \"pan-y\",\n };\n if (dragging) {\n style.translate = `${dx}px 0`;\n style.opacity = Math.max(0, 1 - Math.abs(dx) / 240);\n style.transition = \"none\";\n } else if (exit) style.translate = `${exit * 110}% 0`;\n\n return (\n <li\n style={style}\n inert={beyond || undefined}\n aria-hidden={beyond || undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={() => {\n drag.current = null;\n setDx(0);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") dismiss(t.id);\n }}\n className={cx(\n \"pointer-events-auto absolute inset-x-0 select-none\",\n \"transition-[opacity,translate,transform,height] duration-300 ease-out motion-reduce:transition-none\",\n top ? \"top-0 starting:-translate-y-full\" : \"bottom-0 starting:translate-y-full\",\n \"starting:opacity-0\",\n clip !== undefined && \"overflow-hidden rounded-lg\",\n (t.leaving || beyond) && \"opacity-0\",\n t.leaving && !exit && (top ? \"-translate-y-2\" : \"translate-y-2\"),\n beyond && \"pointer-events-none\",\n )}\n >\n <div\n ref={inner}\n className={cx(\n \"flex items-start gap-3 rounded-lg border bg-popover py-3 pl-4 pr-2 text-sm text-popover-foreground shadow-lg\",\n \"[&>*]:transition-opacity [&>*]:duration-200 motion-reduce:[&>*]:transition-none\",\n tone.box,\n clip !== undefined && \"[&>*]:opacity-0\",\n )}\n >\n {t.tone === \"loading\" ? (\n <Spinner size=\"sm\" label={null} className=\"mt-0.5 text-muted-foreground\" />\n ) : tone.icon ? (\n <Icon name={tone.icon} className={cx(\"mt-0.5 shrink-0\", tone.color)} />\n ) : null}\n <div className=\"min-w-0 flex-1 py-px\">\n <p className=\"font-medium leading-snug [overflow-wrap:anywhere]\">{t.title}</p>\n {t.description ? <p className=\"mt-0.5 text-muted-foreground [overflow-wrap:anywhere]\">{t.description}</p> : null}\n </div>\n {t.action ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"-my-1 shrink-0\"\n onClick={(e) => {\n t.action.onClick?.(e);\n if (!e.defaultPrevented) dismiss(t.id);\n }}\n >\n {t.action.label}\n </Button>\n ) : null}\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" className=\"-my-1.5 shrink-0\" onClick={() => dismiss(t.id)} />\n </div>\n </li>\n );\n}\n"
288
+ "content": "\"use client\";\n\nimport { isValidElement, useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\nimport { Spinner } from \"./feedback\";\n\n// Toast — a short line about something that just happened (\"Saved\",\n// \"Invite sent\"), which leaves on its own. Call it from any client code;\n// nothing needs mounting: the first toast mounts a host into the page's\n// top layer (a manual popover, above sheets and menus). app/layout.jsx is\n// fixed, so this is how an app gets toasts at all. Put <Toaster /> in a\n// surface only to choose where they stack.\n//\n// toast(\"Saved\") → id\n// toast.success / .error / .info / .warning(title, options)\n// toast.promise(promise, { loading, success, error })\n// success / error: a string, (value) → string, or\n// { title, description, action }; null dismisses. Returns the\n// promise.\n// toast.dismiss(id) one; no id = all\n//\n// options description the line under the title\n// action { label, onClick } — one button; the toast\n// closes after unless onClick calls\n// event.preventDefault()\n// duration ms, default 5000; Infinity stays until closed\n// id reuse one to update a toast in place\n//\n// <Toaster position visible />\n// position bottom-end (the default) · bottom-center ·\n// top-end · top-center. On a phone every position\n// is full width at its edge.\n// visible how many show at once (3); older ones wait behind\n//\n// Newest in front; the rest stack behind and fan out on hover or focus,\n// and every timer pauses while they do (and while the tab is hidden).\n// Close with the ×, Escape, or a sideways swipe (touch and pen; a mouse\n// selects the text instead, so an error can be copied). Screen readers hear each\n// toast once through a live region (assertive for .error). A toast is\n// never the only record of something that matters: an error the user\n// must act on is a Notice on the surface.\n\nconst DURATION = 5000;\nconst EXIT_MS = 200;\nconst GAP = 12;\nconst PEEK = 12;\nconst SWIPE = 80;\n\nconst TONES = {\n neutral: { box: \"border-border\", icon: null },\n success: { box: \"border-success/40\", icon: \"check\", color: \"text-success\" },\n info: { box: \"border-info/40\", icon: \"info\", color: \"text-info\" },\n warning: { box: \"border-warning/50\", icon: \"warning\", color: \"text-warning\" },\n danger: { box: \"border-destructive/50\", icon: \"warning\", color: \"text-destructive\" },\n loading: { box: \"border-border\", icon: null },\n};\n\n// Position → where the host sits. Class strings are written out whole:\n// Tailwind reads them literally.\nconst PLACES = {\n \"bottom-end\": \"top-auto bottom-0 left-0 right-0 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:left-auto sm:right-0\",\n \"bottom-center\": \"top-auto bottom-0 left-0 right-0 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:mx-auto\",\n \"top-end\": \"top-0 bottom-auto left-0 right-0 sm:left-auto sm:right-0\",\n \"top-center\": \"top-0 bottom-auto left-0 right-0 sm:mx-auto\",\n};\n\n// ---- The store: module-level, so toast() works from anywhere ----------\n\nconst EMPTY = { toasts: [], host: null };\nconst AUTO = { auto: true };\nlet state = EMPTY;\nlet seq = 0;\nlet auto = null;\nconst hosts = [];\nconst listeners = new Set();\n\nfunction set(next) {\n state = { ...state, ...next };\n for (const l of listeners) l();\n}\n\nfunction subscribe(l) {\n listeners.add(l);\n return () => listeners.delete(l);\n}\n\nconst reduced = () => typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n// No <Toaster /> on the page: lay one in <body>, in its own root.\nfunction mount() {\n if (typeof document === \"undefined\" || state.host || auto) return;\n const el = document.createElement(\"div\");\n el.setAttribute(\"data-toaster\", \"\");\n document.body.appendChild(el);\n auto = { el, root: createRoot(el) };\n set({ host: AUTO });\n auto.root.render(<Host token={AUTO} />);\n}\n\nfunction show(title, options, tone) {\n const { id: wanted, description, action, duration } = options ?? {};\n const id = wanted ?? `toast-${++seq}`;\n const was = state.toasts.find((t) => t.id === id);\n const item = {\n id,\n tone,\n title,\n description,\n action,\n duration: duration ?? (tone === \"loading\" ? Infinity : DURATION),\n version: (was?.version ?? 0) + 1,\n leaving: false,\n };\n set({ toasts: was ? state.toasts.map((t) => (t.id === id ? item : t)) : [item, ...state.toasts] });\n mount();\n return id;\n}\n\nfunction dismiss(id) {\n const hit = (t) => id === undefined || t.id === id;\n if (!state.toasts.some((t) => hit(t) && !t.leaving)) return;\n set({ toasts: state.toasts.map((t) => (hit(t) ? { ...t, leaving: true } : t)) });\n setTimeout(() => set({ toasts: state.toasts.filter((t) => !(hit(t) && t.leaving)) }), reduced() ? 0 : EXIT_MS);\n}\n\nexport function toast(title, options) {\n return show(title, options, \"neutral\");\n}\ntoast.success = (title, options) => show(title, options, \"success\");\ntoast.error = (title, options) => show(title, options, \"danger\");\ntoast.info = (title, options) => show(title, options, \"info\");\ntoast.warning = (title, options) => show(title, options, \"warning\");\ntoast.dismiss = dismiss;\ntoast.promise = (promise, { loading = \"Working…\", success, error } = {}) => {\n const p = typeof promise === \"function\" ? promise() : promise;\n const id = show(loading, {}, \"loading\");\n const settle = (message, value, tone) => {\n const m = typeof message === \"function\" ? message(value) : message;\n if (m == null || m === false) dismiss(id);\n else if (typeof m === \"object\" && !isValidElement(m)) show(m.title, { ...m, id }, tone);\n else show(m, { id }, tone);\n };\n Promise.resolve(p).then(\n (v) => settle(success ?? \"Done\", v, \"success\"),\n (e) => settle(error ?? \"Something went wrong\", e, \"danger\"),\n );\n return p;\n};\n\n// ---- The host ---------------------------------------------------------\n\n/** Where toasts stack. Optional — without one, the first toast mounts a\n * bottom-end host by itself. The newest <Toaster /> on the page wins. */\nexport function Toaster({ position = \"bottom-end\", visible = 3, className }) {\n const token = useRef(null);\n if (!token.current) token.current = {};\n useEffect(() => {\n const own = token.current;\n hosts.push(own);\n set({ host: own });\n if (auto) {\n const a = auto;\n auto = null;\n setTimeout(() => {\n a.root.unmount();\n a.el.remove();\n });\n }\n return () => {\n hosts.splice(hosts.indexOf(own), 1);\n set({ host: hosts[hosts.length - 1] ?? null });\n if (!state.host && state.toasts.length) mount();\n };\n }, []);\n return <Host token={token.current} position={position} visible={visible} className={className} />;\n}\n\nfunction Host({ token, position = \"bottom-end\", visible = 3, className }) {\n const s = useSyncExternalStore(subscribe, () => state, () => EMPTY);\n const pop = useRef(null);\n const said = useRef(new Set());\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [hidden, setHidden] = useState(false);\n const [heights, setHeights] = useState({});\n const [say, setSay] = useState({ polite: \"\", assertive: \"\" });\n const mine = s.host === token;\n const items = mine ? s.toasts : [];\n const live = items.filter((t) => !t.leaving);\n const newest = live[0]?.id;\n const top = position.startsWith(\"top\");\n\n // In the top layer while there is something to show. A modal <dialog>\n // opened since would sit above it: show again to rise over it.\n useLayoutEffect(() => {\n const p = pop.current;\n if (!p?.showPopover) return;\n const open = p.matches(\":popover-open\");\n if (items.length && !open) p.showPopover();\n else if (!items.length && open) p.hidePopover();\n else if (open && newest && document.querySelector(\"dialog:modal\") && !p.contains(document.activeElement)) {\n p.hidePopover();\n p.showPopover();\n }\n }, [items.length, newest]);\n\n // An emptied stack cannot report the pointer leaving it.\n useEffect(() => {\n if (items.length === 0) {\n setHovered(false);\n setFocused(false);\n }\n }, [items.length]);\n\n useEffect(() => {\n const on = () => setHidden(document.visibilityState === \"hidden\");\n document.addEventListener(\"visibilitychange\", on);\n return () => document.removeEventListener(\"visibilitychange\", on);\n }, []);\n\n // Each toast is announced once per version, a beat after the live\n // region exists (a region born with its text is not read).\n const timer = useRef(0);\n useEffect(() => () => clearTimeout(timer.current), []);\n useEffect(() => {\n const t = live.find((x) => !said.current.has(`${x.id}:${x.version}`));\n if (!t) return;\n said.current.add(`${t.id}:${t.version}`);\n const text = [t.title, t.description].filter((x) => typeof x === \"string\" || typeof x === \"number\").join(\". \");\n if (!text) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => setSay(t.tone === \"danger\" ? { polite: \"\", assertive: text } : { polite: text, assertive: \"\" }), 100);\n });\n\n const measure = useCallback((id, h) => setHeights((prev) => (prev[id] === h ? prev : { ...prev, [id]: h })), []);\n\n if (!mine) return null;\n\n const expanded = hovered || focused;\n const paused = expanded || hidden;\n const shown = live.slice(0, visible);\n let run = 0;\n const offsets = shown.map((t) => {\n const at = run;\n run += (heights[t.id] ?? 0) + GAP;\n return at;\n });\n const front = heights[shown[0]?.id] ?? 0;\n const height = expanded ? Math.max(0, run - GAP) : front + PEEK * Math.max(0, shown.length - 1);\n\n return (\n <>\n <div className=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {say.polite}\n </div>\n <div className=\"sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">\n {say.assertive}\n </div>\n <section\n ref={pop}\n popover=\"manual\"\n aria-label=\"Notifications\"\n onPointerEnter={() => setHovered(true)}\n onPointerLeave={() => setHovered(false)}\n onFocus={() => setFocused(true)}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setFocused(false);\n }}\n className={cx(\n \"pointer-events-none fixed z-50 m-0 h-auto w-full overflow-visible bg-transparent p-3 sm:w-96 sm:p-4\",\n PLACES[position] ?? PLACES[\"bottom-end\"],\n className,\n )}\n >\n <ol className=\"relative transition-[height] duration-300 ease-out motion-reduce:transition-none\" style={{ height }}>\n {items.map((t) => {\n const i = live.indexOf(t);\n return (\n <ToastItem\n key={t.id}\n t={t}\n index={i}\n top={top}\n beyond={i >= visible}\n offset={offsets[i] ?? 0}\n expanded={expanded}\n clip={!expanded && i > 0 ? front : undefined}\n paused={paused}\n onHeight={measure}\n />\n );\n })}\n </ol>\n </section>\n </>\n );\n}\n\nfunction ToastItem({ t, index, top, beyond, offset, expanded, clip, paused, onHeight }) {\n const inner = useRef(null);\n const left = useRef(t.duration);\n const ver = useRef(0);\n const drag = useRef(null);\n const last = useRef(\"\");\n const [dx, setDx] = useState(0);\n const [exit, setExit] = useState(0);\n const tone = TONES[t.tone] ?? TONES.neutral;\n\n // The timer: counts down only while nothing holds the stack open; a\n // new version (toast.promise settling, an update by id) starts it over.\n useEffect(() => {\n if (ver.current !== t.version) {\n ver.current = t.version;\n left.current = t.duration;\n }\n if (paused || t.leaving || !Number.isFinite(t.duration)) return;\n const start = Date.now();\n const h = setTimeout(() => dismiss(t.id), Math.max(0, left.current));\n return () => {\n clearTimeout(h);\n left.current -= Date.now() - start;\n };\n }, [paused, t.leaving, t.version, t.duration, t.id]);\n\n useLayoutEffect(() => {\n const el = inner.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => onHeight(t.id, el.offsetHeight));\n ro.observe(el);\n onHeight(t.id, el.offsetHeight);\n return () => ro.disconnect();\n }, [t.id, onHeight]);\n\n // Where it sits in the stack. A leaving toast keeps its last place\n // while it fades, so the others close the gap at once.\n const dir = top ? 1 : -1;\n if (!t.leaving) {\n const y = expanded ? offset : index * PEEK;\n const k = expanded ? 1 : 1 - Math.min(index, 3) * 0.05;\n last.current = `translateY(${dir * y}px) scale(${k})`;\n }\n\n function onPointerDown(e) {\n // A mouse selects the words (to copy an error); only touch and pen swipe.\n if (e.pointerType === \"mouse\" || e.button !== 0 || e.target.closest(\"button, a, input, textarea, select\")) return;\n drag.current = { x: e.clientX, at: e.timeStamp };\n e.currentTarget.setPointerCapture(e.pointerId);\n }\n function onPointerMove(e) {\n if (drag.current) setDx(e.clientX - drag.current.x);\n }\n function onPointerUp(e) {\n const d = drag.current;\n drag.current = null;\n if (!d) return;\n const moved = e.clientX - d.x;\n const fast = Math.abs(moved) > 10 && Math.abs(moved) / Math.max(1, e.timeStamp - d.at) > 0.5;\n if (Math.abs(moved) > SWIPE || fast) {\n setExit(Math.sign(moved) || 1);\n dismiss(t.id);\n } else setDx(0);\n }\n\n const dragging = drag.current !== null && dx !== 0;\n const style = {\n zIndex: 1000 - Math.max(index, 0),\n transform: last.current,\n transformOrigin: top ? \"bottom center\" : \"top center\",\n height: clip,\n touchAction: \"pan-y\",\n };\n if (dragging) {\n style.translate = `${dx}px 0`;\n style.opacity = Math.max(0, 1 - Math.abs(dx) / 240);\n style.transition = \"none\";\n } else if (exit) style.translate = `${exit * 110}% 0`;\n\n return (\n <li\n style={style}\n inert={beyond || undefined}\n aria-hidden={beyond || undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={() => {\n drag.current = null;\n setDx(0);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") dismiss(t.id);\n }}\n className={cx(\n \"pointer-events-auto absolute inset-x-0 select-text\",\n dragging && \"select-none\",\n \"transition-[opacity,translate,transform,height] duration-300 ease-out motion-reduce:transition-none\",\n top ? \"top-0 starting:-translate-y-full\" : \"bottom-0 starting:translate-y-full\",\n \"starting:opacity-0\",\n clip !== undefined && \"overflow-hidden rounded-lg\",\n (t.leaving || beyond) && \"opacity-0\",\n t.leaving && !exit && (top ? \"-translate-y-2\" : \"translate-y-2\"),\n beyond && \"pointer-events-none\",\n )}\n >\n <div\n ref={inner}\n className={cx(\n \"flex items-start gap-3 rounded-lg border bg-popover py-3 pl-4 pr-2 text-sm text-popover-foreground shadow-lg\",\n \"[&>*]:transition-opacity [&>*]:duration-200 motion-reduce:[&>*]:transition-none\",\n tone.box,\n clip !== undefined && \"[&>*]:opacity-0\",\n )}\n >\n {t.tone === \"loading\" ? (\n <Spinner size=\"sm\" label={null} className=\"mt-0.5 text-muted-foreground\" />\n ) : tone.icon ? (\n <Icon name={tone.icon} className={cx(\"mt-0.5 shrink-0\", tone.color)} />\n ) : null}\n <div className=\"min-w-0 flex-1 py-px\">\n <p className=\"font-medium leading-snug [overflow-wrap:anywhere]\">{t.title}</p>\n {t.description ? <p className=\"mt-0.5 text-muted-foreground [overflow-wrap:anywhere]\">{t.description}</p> : null}\n </div>\n {t.action ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"-my-1 shrink-0\"\n onClick={(e) => {\n t.action.onClick?.(e);\n if (!e.defaultPrevented) dismiss(t.id);\n }}\n >\n {t.action.label}\n </Button>\n ) : null}\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" className=\"-my-1.5 shrink-0\" onClick={() => dismiss(t.id)} />\n </div>\n </li>\n );\n}\n"
283
289
  },
284
290
  {
285
291
  "path": "templates/next/files/components/ui/toggle.jsx",