@fungi.computer/caps 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (387) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1125 -2
  3. package/dist/blocks/app-bar.d.ts +43 -0
  4. package/dist/blocks/app-bar.js +244 -0
  5. package/dist/blocks/app-status-bar.d.ts +93 -0
  6. package/dist/blocks/app-status-bar.js +42 -0
  7. package/dist/blocks/async-state.d.ts +21 -0
  8. package/dist/blocks/async-state.js +8 -0
  9. package/dist/blocks/card-grid.d.ts +11 -0
  10. package/dist/blocks/card-grid.js +21 -0
  11. package/dist/blocks/command-menu.d.ts +26 -0
  12. package/dist/blocks/command-menu.js +70 -0
  13. package/dist/blocks/composer.d.ts +23 -0
  14. package/dist/blocks/composer.js +13 -0
  15. package/dist/blocks/empty-state.d.ts +59 -0
  16. package/dist/blocks/empty-state.js +35 -0
  17. package/dist/blocks/faq-chat.d.ts +62 -0
  18. package/dist/blocks/faq-chat.js +77 -0
  19. package/dist/blocks/game-share-card.d.ts +17 -0
  20. package/dist/blocks/game-share-card.js +9 -0
  21. package/dist/blocks/launcher-grid.d.ts +50 -0
  22. package/dist/blocks/launcher-grid.js +11 -0
  23. package/dist/blocks/listing-detail.d.ts +23 -0
  24. package/dist/blocks/listing-detail.js +12 -0
  25. package/dist/blocks/loading-state.d.ts +5 -0
  26. package/dist/blocks/loading-state.js +8 -0
  27. package/dist/blocks/mosaic.d.ts +29 -0
  28. package/dist/blocks/mosaic.js +19 -0
  29. package/dist/blocks/permission-grid.d.ts +45 -0
  30. package/dist/blocks/permission-grid.js +14 -0
  31. package/dist/blocks/pricing-card.d.ts +53 -0
  32. package/dist/blocks/pricing-card.js +12 -0
  33. package/dist/blocks/release-tag.d.ts +20 -0
  34. package/dist/blocks/release-tag.js +10 -0
  35. package/dist/blocks/resource-select.d.ts +22 -0
  36. package/dist/blocks/resource-select.js +16 -0
  37. package/dist/blocks/result-state.d.ts +15 -0
  38. package/dist/blocks/result-state.js +8 -0
  39. package/dist/blocks/section-card.d.ts +31 -0
  40. package/dist/blocks/section-card.js +7 -0
  41. package/dist/blocks/section-heading.d.ts +13 -0
  42. package/dist/blocks/section-heading.js +11 -0
  43. package/dist/blocks/spec-sheet.d.ts +125 -0
  44. package/dist/blocks/spec-sheet.js +47 -0
  45. package/dist/blocks/status-tray.d.ts +83 -0
  46. package/dist/blocks/status-tray.js +79 -0
  47. package/dist/blocks/upload-queue.d.ts +48 -0
  48. package/dist/blocks/upload-queue.js +24 -0
  49. package/dist/components/action-row.d.ts +16 -0
  50. package/dist/components/action-row.js +11 -0
  51. package/dist/components/activity-line.d.ts +33 -0
  52. package/dist/components/activity-line.js +69 -0
  53. package/dist/components/alert.d.ts +17 -0
  54. package/dist/components/alert.js +17 -0
  55. package/dist/components/badge.d.ts +19 -0
  56. package/dist/components/badge.js +24 -0
  57. package/dist/components/button.d.ts +55 -0
  58. package/dist/components/button.js +230 -0
  59. package/dist/components/card.d.ts +61 -0
  60. package/dist/components/card.js +48 -0
  61. package/dist/components/chat-bubble.d.ts +34 -0
  62. package/dist/components/chat-bubble.js +10 -0
  63. package/dist/components/checkbox-bead.d.ts +6 -0
  64. package/dist/components/checkbox-bead.js +67 -0
  65. package/dist/components/checkbox.d.ts +21 -0
  66. package/dist/components/checkbox.js +46 -0
  67. package/dist/components/code-input.d.ts +35 -0
  68. package/dist/components/code-input.js +126 -0
  69. package/dist/components/combobox.d.ts +86 -0
  70. package/dist/components/combobox.js +218 -0
  71. package/dist/components/confirm-action.d.ts +23 -0
  72. package/dist/components/confirm-action.js +64 -0
  73. package/dist/components/copy-field.d.ts +53 -0
  74. package/dist/components/copy-field.js +49 -0
  75. package/dist/components/count-badge.d.ts +15 -0
  76. package/dist/components/count-badge.js +9 -0
  77. package/dist/components/cover-pattern.d.ts +19 -0
  78. package/dist/components/cover-pattern.js +74 -0
  79. package/dist/components/data-list.d.ts +25 -0
  80. package/dist/components/data-list.js +16 -0
  81. package/dist/components/dialog.d.ts +39 -0
  82. package/dist/components/dialog.js +73 -0
  83. package/dist/components/diff-view.d.ts +14 -0
  84. package/dist/components/diff-view.js +13 -0
  85. package/dist/components/disclosure.d.ts +29 -0
  86. package/dist/components/disclosure.js +19 -0
  87. package/dist/components/dropdown-menu.d.ts +49 -0
  88. package/dist/components/dropdown-menu.js +98 -0
  89. package/dist/components/field-guide.d.ts +41 -0
  90. package/dist/components/field-guide.js +18 -0
  91. package/dist/components/field.d.ts +164 -0
  92. package/dist/components/field.js +228 -0
  93. package/dist/components/game-feedback.d.ts +84 -0
  94. package/dist/components/game-feedback.js +48 -0
  95. package/dist/components/game-hud.d.ts +126 -0
  96. package/dist/components/game-hud.js +136 -0
  97. package/dist/components/generation-frame.d.ts +36 -0
  98. package/dist/components/generation-frame.js +97 -0
  99. package/dist/components/gooey.d.ts +32 -0
  100. package/dist/components/gooey.js +25 -0
  101. package/dist/components/hold-button.d.ts +26 -0
  102. package/dist/components/hold-button.js +115 -0
  103. package/dist/components/icon-button.d.ts +24 -0
  104. package/dist/components/icon-button.js +18 -0
  105. package/dist/components/icon-checkbox.d.ts +20 -0
  106. package/dist/components/icon-checkbox.js +9 -0
  107. package/dist/components/icon-choice-mark.d.ts +6 -0
  108. package/dist/components/icon-choice-mark.js +5 -0
  109. package/dist/components/icon-lamp.d.ts +42 -0
  110. package/dist/components/icon-lamp.js +15 -0
  111. package/dist/components/icon-radio.d.ts +16 -0
  112. package/dist/components/icon-radio.js +9 -0
  113. package/dist/components/inline-code.d.ts +3 -0
  114. package/dist/components/inline-code.js +7 -0
  115. package/dist/components/input-group.d.ts +9 -0
  116. package/dist/components/input-group.js +15 -0
  117. package/dist/components/input.d.ts +15 -0
  118. package/dist/components/input.js +15 -0
  119. package/dist/components/item-grid.d.ts +110 -0
  120. package/dist/components/item-grid.js +136 -0
  121. package/dist/components/keycap.d.ts +80 -0
  122. package/dist/components/keycap.js +18 -0
  123. package/dist/components/label.d.ts +13 -0
  124. package/dist/components/label.js +7 -0
  125. package/dist/components/listbox.d.ts +50 -0
  126. package/dist/components/listbox.js +134 -0
  127. package/dist/components/loader-blobs.d.ts +5 -0
  128. package/dist/components/loader-blobs.js +128 -0
  129. package/dist/components/loader.d.ts +21 -0
  130. package/dist/components/loader.js +25 -0
  131. package/dist/components/markdown.d.ts +23 -0
  132. package/dist/components/markdown.js +70 -0
  133. package/dist/components/minimap-labels.d.ts +39 -0
  134. package/dist/components/minimap-labels.js +113 -0
  135. package/dist/components/monogram.d.ts +32 -0
  136. package/dist/components/monogram.js +24 -0
  137. package/dist/components/motion-visibility.d.ts +2 -0
  138. package/dist/components/motion-visibility.js +25 -0
  139. package/dist/components/navigation-rail.d.ts +11 -0
  140. package/dist/components/navigation-rail.js +55 -0
  141. package/dist/components/overlay-root.d.ts +16 -0
  142. package/dist/components/overlay-root.js +24 -0
  143. package/dist/components/pixel-number.d.ts +50 -0
  144. package/dist/components/pixel-number.js +113 -0
  145. package/dist/components/popover.d.ts +31 -0
  146. package/dist/components/popover.js +24 -0
  147. package/dist/components/presence.d.ts +59 -0
  148. package/dist/components/presence.js +232 -0
  149. package/dist/components/price-tag.d.ts +11 -0
  150. package/dist/components/price-tag.js +6 -0
  151. package/dist/components/progress.d.ts +25 -0
  152. package/dist/components/progress.js +24 -0
  153. package/dist/components/prose.d.ts +14 -0
  154. package/dist/components/prose.js +10 -0
  155. package/dist/components/quota-meter.d.ts +24 -0
  156. package/dist/components/quota-meter.js +11 -0
  157. package/dist/components/radio-bead.d.ts +4 -0
  158. package/dist/components/radio-bead.js +53 -0
  159. package/dist/components/radio-group.d.ts +54 -0
  160. package/dist/components/radio-group.js +64 -0
  161. package/dist/components/resize-handle.d.ts +23 -0
  162. package/dist/components/resize-handle.js +31 -0
  163. package/dist/components/rough-drawing.d.ts +42 -0
  164. package/dist/components/rough-drawing.js +231 -0
  165. package/dist/components/rough.d.ts +68 -0
  166. package/dist/components/rough.js +33 -0
  167. package/dist/components/scroll-area.d.ts +12 -0
  168. package/dist/components/scroll-area.js +96 -0
  169. package/dist/components/scroll-motion.d.ts +12 -0
  170. package/dist/components/scroll-motion.js +31 -0
  171. package/dist/components/scroll-to-bottom-blob-state.d.ts +40 -0
  172. package/dist/components/scroll-to-bottom-blob-state.js +109 -0
  173. package/dist/components/scroll-to-bottom-blob.d.ts +16 -0
  174. package/dist/components/scroll-to-bottom-blob.js +174 -0
  175. package/dist/components/select.d.ts +9 -0
  176. package/dist/components/select.js +12 -0
  177. package/dist/components/selectable.d.ts +13 -0
  178. package/dist/components/selectable.js +45 -0
  179. package/dist/components/send-action.d.ts +33 -0
  180. package/dist/components/send-action.js +96 -0
  181. package/dist/components/separator.d.ts +11 -0
  182. package/dist/components/separator.js +7 -0
  183. package/dist/components/skeleton.d.ts +5 -0
  184. package/dist/components/skeleton.js +7 -0
  185. package/dist/components/slider.d.ts +18 -0
  186. package/dist/components/slider.js +31 -0
  187. package/dist/components/stat.d.ts +17 -0
  188. package/dist/components/stat.js +7 -0
  189. package/dist/components/status-bead.d.ts +9 -0
  190. package/dist/components/status-bead.js +107 -0
  191. package/dist/components/status-mark.d.ts +12 -0
  192. package/dist/components/status-mark.js +177 -0
  193. package/dist/components/status.d.ts +38 -0
  194. package/dist/components/status.js +28 -0
  195. package/dist/components/store-card.d.ts +21 -0
  196. package/dist/components/store-card.js +10 -0
  197. package/dist/components/surface.d.ts +89 -0
  198. package/dist/components/surface.js +31 -0
  199. package/dist/components/switch-bead.d.ts +4 -0
  200. package/dist/components/switch-bead.js +62 -0
  201. package/dist/components/switch.d.ts +11 -0
  202. package/dist/components/switch.js +36 -0
  203. package/dist/components/tabs.d.ts +55 -0
  204. package/dist/components/tabs.js +81 -0
  205. package/dist/components/technonomicon.d.ts +192 -0
  206. package/dist/components/technonomicon.js +121 -0
  207. package/dist/components/textarea.d.ts +9 -0
  208. package/dist/components/textarea.js +11 -0
  209. package/dist/components/toaster.d.ts +4 -0
  210. package/dist/components/toaster.js +13 -0
  211. package/dist/components/tool-call-chip.d.ts +22 -0
  212. package/dist/components/tool-call-chip.js +100 -0
  213. package/dist/components/tooltip.d.ts +41 -0
  214. package/dist/components/tooltip.js +169 -0
  215. package/dist/components/trading-card.d.ts +60 -0
  216. package/dist/components/trading-card.js +13 -0
  217. package/dist/components/undo-action.d.ts +52 -0
  218. package/dist/components/undo-action.js +100 -0
  219. package/dist/components/undo-toast.d.ts +21 -0
  220. package/dist/components/undo-toast.js +58 -0
  221. package/dist/components/visually-hidden.d.ts +9 -0
  222. package/dist/components/visually-hidden.js +11 -0
  223. package/dist/components/window.d.ts +65 -0
  224. package/dist/components/window.js +31 -0
  225. package/dist/components/wizard.d.ts +17 -0
  226. package/dist/components/wizard.js +19 -0
  227. package/dist/enhance/cigarette-card.d.ts +8 -0
  228. package/dist/enhance/cigarette-card.js +33 -0
  229. package/dist/enhance/sketch.d.ts +16 -0
  230. package/dist/enhance/sketch.js +130 -0
  231. package/dist/form-error.d.ts +7 -0
  232. package/dist/form-error.js +14 -0
  233. package/dist/form-fields.d.ts +15 -0
  234. package/dist/form-fields.js +250 -0
  235. package/dist/form-schema.d.ts +166 -0
  236. package/dist/form-schema.js +398 -0
  237. package/dist/form.d.ts +30 -0
  238. package/dist/form.js +20 -0
  239. package/dist/lib/cn.d.ts +8 -0
  240. package/dist/lib/cn.js +12 -0
  241. package/dist/lib/format-money.d.ts +2 -0
  242. package/dist/lib/format-money.js +22 -0
  243. package/dist/lib/glide.d.ts +12 -0
  244. package/dist/lib/glide.js +143 -0
  245. package/dist/lib/ime.d.ts +3 -0
  246. package/dist/lib/ime.js +8 -0
  247. package/dist/lib/motion-spring.d.ts +27 -0
  248. package/dist/lib/motion-spring.js +36 -0
  249. package/dist/lib/motion.d.ts +51 -0
  250. package/dist/lib/motion.js +161 -0
  251. package/dist/lib/reorder-motion.d.ts +13 -0
  252. package/dist/lib/reorder-motion.js +201 -0
  253. package/dist/lib/seed-hash.d.ts +6 -0
  254. package/dist/lib/seed-hash.js +13 -0
  255. package/dist/lib/tanstack-highlight.d.ts +4 -0
  256. package/dist/lib/tanstack-highlight.js +41 -0
  257. package/dist/schema-form.d.ts +120 -0
  258. package/dist/schema-form.js +129 -0
  259. package/dist/styles.css +18814 -0
  260. package/docs/api/README.md +134 -0
  261. package/docs/api/blocks/app-bar.md +227 -0
  262. package/docs/api/blocks/app-status-bar.md +257 -0
  263. package/docs/api/blocks/async-state.md +53 -0
  264. package/docs/api/blocks/card-grid.md +31 -0
  265. package/docs/api/blocks/command-menu.md +89 -0
  266. package/docs/api/blocks/composer.md +40 -0
  267. package/docs/api/blocks/empty-state.md +97 -0
  268. package/docs/api/blocks/faq-chat.md +147 -0
  269. package/docs/api/blocks/game-share-card.md +40 -0
  270. package/docs/api/blocks/launcher-grid.md +86 -0
  271. package/docs/api/blocks/listing-detail.md +51 -0
  272. package/docs/api/blocks/loading-state.md +21 -0
  273. package/docs/api/blocks/mosaic.md +94 -0
  274. package/docs/api/blocks/permission-grid.md +79 -0
  275. package/docs/api/blocks/pricing-card.md +90 -0
  276. package/docs/api/blocks/release-tag.md +62 -0
  277. package/docs/api/blocks/resource-select.md +126 -0
  278. package/docs/api/blocks/result-state.md +42 -0
  279. package/docs/api/blocks/section-card.md +42 -0
  280. package/docs/api/blocks/section-heading.md +61 -0
  281. package/docs/api/blocks/spec-sheet.md +400 -0
  282. package/docs/api/blocks/status-tray.md +141 -0
  283. package/docs/api/blocks/upload-queue.md +110 -0
  284. package/docs/api/components/action-row.md +51 -0
  285. package/docs/api/components/activity-line.md +103 -0
  286. package/docs/api/components/alert.md +60 -0
  287. package/docs/api/components/badge.md +77 -0
  288. package/docs/api/components/button.md +94 -0
  289. package/docs/api/components/card.md +165 -0
  290. package/docs/api/components/chat-bubble.md +66 -0
  291. package/docs/api/components/checkbox-bead.md +20 -0
  292. package/docs/api/components/checkbox.md +42 -0
  293. package/docs/api/components/code-input.md +61 -0
  294. package/docs/api/components/combobox.md +303 -0
  295. package/docs/api/components/confirm-action.md +58 -0
  296. package/docs/api/components/copy-field.md +106 -0
  297. package/docs/api/components/count-badge.md +39 -0
  298. package/docs/api/components/cover-pattern.md +43 -0
  299. package/docs/api/components/data-list.md +80 -0
  300. package/docs/api/components/dialog.md +235 -0
  301. package/docs/api/components/diff-view.md +59 -0
  302. package/docs/api/components/disclosure.md +71 -0
  303. package/docs/api/components/dropdown-menu.md +269 -0
  304. package/docs/api/components/field-guide.md +174 -0
  305. package/docs/api/components/field.md +252 -0
  306. package/docs/api/components/game-feedback.md +157 -0
  307. package/docs/api/components/game-hud.md +359 -0
  308. package/docs/api/components/generation-frame.md +59 -0
  309. package/docs/api/components/hold-button.md +44 -0
  310. package/docs/api/components/icon-button.md +56 -0
  311. package/docs/api/components/icon-checkbox.md +42 -0
  312. package/docs/api/components/icon-choice-mark.md +31 -0
  313. package/docs/api/components/icon-lamp.md +92 -0
  314. package/docs/api/components/icon-radio.md +42 -0
  315. package/docs/api/components/inline-code.md +21 -0
  316. package/docs/api/components/input-group.md +53 -0
  317. package/docs/api/components/input.md +54 -0
  318. package/docs/api/components/item-grid.md +262 -0
  319. package/docs/api/components/keycap.md +102 -0
  320. package/docs/api/components/label.md +40 -0
  321. package/docs/api/components/listbox.md +131 -0
  322. package/docs/api/components/loader-blobs.md +9 -0
  323. package/docs/api/components/loader.md +51 -0
  324. package/docs/api/components/markdown.md +91 -0
  325. package/docs/api/components/minimap-labels.md +84 -0
  326. package/docs/api/components/monogram.md +96 -0
  327. package/docs/api/components/motion-visibility.md +9 -0
  328. package/docs/api/components/navigation-rail.md +37 -0
  329. package/docs/api/components/overlay-root.md +92 -0
  330. package/docs/api/components/pixel-number.md +120 -0
  331. package/docs/api/components/popover.md +80 -0
  332. package/docs/api/components/presence.md +106 -0
  333. package/docs/api/components/price-tag.md +48 -0
  334. package/docs/api/components/progress.md +90 -0
  335. package/docs/api/components/prose.md +48 -0
  336. package/docs/api/components/quota-meter.md +47 -0
  337. package/docs/api/components/radio-bead.md +9 -0
  338. package/docs/api/components/radio-group.md +126 -0
  339. package/docs/api/components/resize-handle.md +46 -0
  340. package/docs/api/components/rough-drawing.md +122 -0
  341. package/docs/api/components/rough.md +66 -0
  342. package/docs/api/components/scroll-area.md +22 -0
  343. package/docs/api/components/scroll-motion.md +9 -0
  344. package/docs/api/components/scroll-to-bottom-blob-state.md +162 -0
  345. package/docs/api/components/scroll-to-bottom-blob.md +73 -0
  346. package/docs/api/components/select.md +39 -0
  347. package/docs/api/components/send-action.md +58 -0
  348. package/docs/api/components/separator.md +38 -0
  349. package/docs/api/components/skeleton.md +31 -0
  350. package/docs/api/components/slider.md +70 -0
  351. package/docs/api/components/stat.md +42 -0
  352. package/docs/api/components/status-bead.md +19 -0
  353. package/docs/api/components/status-mark.md +19 -0
  354. package/docs/api/components/status.md +105 -0
  355. package/docs/api/components/store-card.md +53 -0
  356. package/docs/api/components/surface.md +161 -0
  357. package/docs/api/components/switch-bead.md +9 -0
  358. package/docs/api/components/switch.md +40 -0
  359. package/docs/api/components/tabs.md +138 -0
  360. package/docs/api/components/technonomicon.md +773 -0
  361. package/docs/api/components/textarea.md +40 -0
  362. package/docs/api/components/toaster.md +29 -0
  363. package/docs/api/components/tool-call-chip.md +68 -0
  364. package/docs/api/components/tooltip.md +113 -0
  365. package/docs/api/components/trading-card.md +95 -0
  366. package/docs/api/components/undo-action.md +117 -0
  367. package/docs/api/components/undo-toast.md +49 -0
  368. package/docs/api/components/visually-hidden.md +37 -0
  369. package/docs/api/components/window.md +291 -0
  370. package/docs/api/components/wizard.md +65 -0
  371. package/docs/api/enhance/cigarette-card.md +52 -0
  372. package/docs/api/enhance/sketch.md +42 -0
  373. package/docs/api/form.md +1623 -0
  374. package/docs/api/lib/cn.md +46 -0
  375. package/docs/api/lib/format-money.md +30 -0
  376. package/docs/api/lib/glide.md +36 -0
  377. package/docs/api/lib/ime.md +29 -0
  378. package/docs/api/lib/motion-spring.md +91 -0
  379. package/docs/api/lib/motion.md +212 -0
  380. package/docs/api/lib/reorder-motion.md +46 -0
  381. package/docs/api/lib/seed-hash.md +31 -0
  382. package/docs/api/lib/tanstack-highlight.md +40 -0
  383. package/package.json +597 -3
  384. package/src/app-status-bar.css +158 -0
  385. package/src/fonts.css +1 -0
  386. package/src/status-tray.css +282 -0
  387. package/src/styles.css +10177 -0
package/README.md CHANGED
@@ -1,3 +1,1126 @@
1
- # Temporary Holding Version
1
+ # Caps (`@fungi.computer/caps`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Caps is a React component library: accessible primitives such as buttons,
4
+ fields, dialogs, menus and comboboxes, plus presentation blocks such as a chat
5
+ composer, an app bar, window frames, status trays, game HUD pieces and editorial
6
+ print furniture. It is built on daisyUI and themed by
7
+ [Stipe](../stipe/README.md), so every component picks up its look from the
8
+ active `data-theme` instead of from component props.
9
+
10
+ Caps renders and handles interaction. It does not fetch, navigate, check out,
11
+ pick a model or know your data model. Your app owns state, effects, copy, routes
12
+ and policy, and passes them in as props, slots and callbacks.
13
+
14
+ ## Install or set up
15
+
16
+ ```sh
17
+ npm install @fungi.computer/caps react react-dom
18
+ ```
19
+
20
+ Caps works with React 18 or 19 and ships as ESM. It brings
21
+ `@fungi.computer/stipe` along as a dependency.
22
+
23
+ There is no root import. Import each component from its own subpath, for example
24
+ `@fungi.computer/caps/components/button` or
25
+ `@fungi.computer/caps/blocks/composer`. The `exports` map in `package.json` is
26
+ the complete list.
27
+
28
+ Pick one stylesheet route:
29
+
30
+ - `@fungi.computer/caps/styles.css` is precompiled CSS with Stipe's themes and
31
+ every class Caps uses. Import it once in your app entry. You do not need
32
+ Tailwind for this route.
33
+ - `@fungi.computer/caps/tailwind.css` is the Tailwind v4 source, for hosts that
34
+ compile Tailwind themselves and want Caps classes and their own utilities in
35
+ one build. Import it from your main stylesheet and point Tailwind at the
36
+ compiled components, for example
37
+ `@source "../node_modules/@fungi.computer/caps/dist";`.
38
+
39
+ With an asset bundler such as Vite, also import `@fungi.computer/caps/fonts.css`
40
+ to load the bundled Nunito, Maple Mono and display fonts.
41
+
42
+ ## Quick start
43
+
44
+ A small notes form with a Caps button, the Composer block and a status bead.
45
+ Mount `NoteComposer` from any React root.
46
+
47
+ ```tsx
48
+ import { useState, type FormEvent } from "react";
49
+ import { Button } from "@fungi.computer/caps/components/button";
50
+ import { Status } from "@fungi.computer/caps/components/status";
51
+ import { Composer } from "@fungi.computer/caps/blocks/composer";
52
+ import "@fungi.computer/caps/styles.css";
53
+
54
+ export function NoteComposer() {
55
+ const [draft, setDraft] = useState("");
56
+ const [notes, setNotes] = useState<string[]>([]);
57
+
58
+ function onSubmit(event: FormEvent<HTMLFormElement>) {
59
+ event.preventDefault();
60
+ const note = draft.trim();
61
+ if (!note) return;
62
+ setNotes((current) => [...current, note]);
63
+ setDraft("");
64
+ }
65
+
66
+ return (
67
+ <form onSubmit={onSubmit} className="flex flex-col gap-md p-lg">
68
+ <Status
69
+ label={`${notes.length} notes`}
70
+ tone={notes.length > 0 ? "success" : "neutral"}
71
+ />
72
+ <Composer
73
+ input={
74
+ <input
75
+ className="input join-item w-full"
76
+ value={draft}
77
+ onChange={(event) => setDraft(event.target.value)}
78
+ aria-label="Note"
79
+ />
80
+ }
81
+ action={
82
+ <Button type="submit" size="sm" disabled={!draft.trim()}>
83
+ Add
84
+ </Button>
85
+ }
86
+ />
87
+ <ul>
88
+ {notes.map((note, index) => (
89
+ <li key={index}>{note}</li>
90
+ ))}
91
+ </ul>
92
+ </form>
93
+ );
94
+ }
95
+ ```
96
+
97
+ Set a theme on the page root, for example `<html data-theme="mocha">`. Without
98
+ one, the page follows the reader's light or dark setting.
99
+
100
+ ## Core concepts
101
+
102
+ ### Hosts own behaviour
103
+
104
+ Caps owns rendering plus the interaction, controlled or uncontrolled state,
105
+ focus, keyboard handling, teardown and accessibility that belong to a UI
106
+ primitive. Your app owns product data and state, authority, routes, persistence,
107
+ fetching, effects, commands, policy and copy. Blocks receive props, slots and
108
+ callbacks. They never fetch.
109
+
110
+ Components keep their native contracts. `Button` forwards native props and its
111
+ ref, adds `variant`, `size` and `loading`, and merges your `className` last.
112
+ `Checkbox`, `Switch` and `Progress` keep their native elements.
113
+
114
+ ### Explicit subpaths
115
+
116
+ Every component, block, helper and stylesheet has its own subpath. There is no
117
+ barrel, alias or hidden provider, so you import exactly what you render and
118
+ nothing else ends up in your bundle.
119
+
120
+ ### Overlay destinations
121
+
122
+ Menus, Combobox popups, dialogs, tooltips, notices and the AppBar dial portal
123
+ into the document body by default. A shell can provide `OverlayRoot` from
124
+ `@fungi.computer/caps/components/overlay-root` with `containers` for `popover`,
125
+ `dialog`, `toast` and `tooltip`. The shell owns their layout and paint order;
126
+ Caps owns interaction and chooses the semantic destination. A null destination
127
+ waits for the shell root to mount, including during server rendering. Base UI
128
+ menu, Combobox and dialog portals still accept explicit container overrides.
129
+
130
+ In body mode, isolate the App root so page controls cannot paint over portaled
131
+ dialogs. Agar's `mountApp` does this. Dialogs and menus rank by portal order;
132
+ tooltips and toasts use their Stipe layers.
133
+
134
+ A portaled menu opened from a dialog inside Tourtois is not covered by this
135
+ body-mode policy: its isolated popover layer ranks below the dialog layer. No
136
+ current consumer uses that combination. Such a consumer needs a Caps
137
+ portal-into-dialog composition, with its collision boundary kept separate from
138
+ its portal container.
139
+
140
+ A shell can also supply `subscribeDismiss` to `OverlayRoot`. When the host
141
+ reports blur or child-surface focus, menus, Combobox popups, App dials and
142
+ tooltips close together. Dialogs and notices stay mounted. The subscription
143
+ retires with the component; standalone components retain their ordinary
144
+ dismissal behavior.
145
+
146
+ ### Themes and CSS
147
+
148
+ Stipe ships a curated theme list, including Daisy `light` and `dark`. Choose a
149
+ palette or finish with `data-theme`; hosts can add their own presets. `latte`,
150
+ `mocha`, `field-guide`, `field-guide-night`, `embed`, `embed-dark`, `phosphor`,
151
+ `codec`, `pirate-radio` and `pirate-radio-night` are Stipe's maintained presets.
152
+
153
+ ```html
154
+ <html data-theme="mocha">
155
+ ...
156
+ </html>
157
+ ```
158
+
159
+ Your CSS can add a preset or local treatment by overriding daisyUI or Stipe
160
+ variables. Keep it CSS-only:
161
+
162
+ ```css
163
+ [data-theme="workbench"] {
164
+ --color-primary: #8b5cf6;
165
+ --color-primary-content: #ffffff;
166
+ --stipe-surface: var(--color-base-200);
167
+ --stipe-fg-muted: #4c4f69;
168
+ }
169
+ ```
170
+
171
+ Card variants describe structure and interaction (`surface`, `outline`,
172
+ `interactive`, `game`, `fit`). Use `featured` to emphasize an item or
173
+ `state="empty"` for absent content. The nested Stipe `vishnu` finish supplies
174
+ Vishnu's gradient faces, stage frames, hard shadows and hover motion while
175
+ retaining the host palette. An ordinary surface card inside that finish keeps
176
+ its normal face, so play controls remain independent of the stage.
177
+
178
+ `@fungi.computer/caps/blocks/game-share-card` composes a published game's
179
+ `heading`, optional `eyebrow` and `cover`, body children, and a primary `action`
180
+ element. The host supplies the link, cover alt text, data and copy behavior; the
181
+ block owns their layout. Set `data-theme="vishnu"` on the block for the Vishnu
182
+ finish.
183
+
184
+ Caps reads its material from the theme vocabulary below. Choose the look with
185
+ `data-theme`. Do not layer component CSS over Caps.
186
+
187
+ | Token | Caps material role |
188
+ | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | daisyUI `--depth` | Scales molded highlights, bevels, recesses, and elevation shadows. `1` preserves the molded latte/mocha material. `0` removes dimensional effects for flat linework. |
190
+ | daisyUI `--border` | Structural border and linework thickness. |
191
+ | daisyUI `--radius-field`, `--radius-selector`, `--radius-box` | Caps control, selection, and container corner geometry. |
192
+ | daisyUI `--color-base-*`, `--color-base-content`, `--color-neutral`, semantic colors | Surface faces, ink, neutral controls, and state accents. |
193
+ | Stipe `--stipe-linework` | Structural line color. The default follows `--color-base-300`, while field-guide uses ink. |
194
+ | Stipe `--card-paper`, `--card-rule`, `--card-ink`, `--card-rule-width` | Collector card faces, ruled borders, text, and frame weight. |
195
+ | Stipe `--card-frame`, `--card-frame-ink`, `--card-accent`, `--card-window`, `--card-window-ink`, `--card-texture`, `--card-shadow` | Card frame band and its ink, the accent rule and series mark, the art window stock and its ink, paper grain, and the single drop shadow. |
196
+ | Stipe `--font-display` | Fraunces, the self-hosted display serif for headlines, decks, card titles and flavor text. |
197
+ | Stipe `--font-nameplate` | Grenze Gotisch, the self-hosted blackletter for a Masthead's nameplate. |
198
+ | Stipe `--newsprint-column-rule`, `--marginalia-ink` | Issue column separators and notes in the margin. |
199
+ | Stipe `--growth-spore`, `--growth-mycelium`, `--growth-fruiting` | Growth-stage badge colors. |
200
+
201
+ Caps adds no parallel material vocabulary, because daisyUI already supplies
202
+ depth, border weight, radii and semantic colors. `field-guide` sets
203
+ `--depth: 0`, thin borders, small radii, cream paper colors and muted naturalist
204
+ accents. `field-guide-night` keeps the same material on dark blue-green paper
205
+ with cream ink.
206
+
207
+ `Screen` supplies display material and keeps the inherited interface font. Use
208
+ `font-mono` on machine values, identifiers and code, not on a whole screen. Use
209
+ opaque semantic tokens for text hierarchy and ordinary surfaces. Keep opacity
210
+ for real compositing such as scrims, overlays and visual effects.
211
+
212
+ Filled and outline buttons read finish tokens: `--caps-button-border-width`
213
+ (length), `--caps-button-weight` (font weight), `--caps-button-press` (active
214
+ travel), `--caps-button-drop` and `--caps-button-drop-active` (complete shadow
215
+ lists), and `--caps-button-cut` (a CSS clip path, `none` keeps the uncut
216
+ silhouette). A disabled filled button reads `--caps-button-disabled-opacity`,
217
+ `--caps-button-disabled-face`, `--caps-button-disabled-ink`,
218
+ `--caps-button-disabled-edge`, and `--caps-button-disabled-drop`. Unset tokens
219
+ keep the default rendering. Roles still choose the colour. Joined input actions
220
+ keep their shared seam.
221
+
222
+ ### Class merging
223
+
224
+ `@fungi.computer/caps/lib/cn` merges classes with `cn@0.2.6` and Stipe's
225
+ [named spacing scale](https://stipe.shiit.app/concepts/#named-spacing). It
226
+ accepts conditional strings, arrays and objects and keeps caller-last Tailwind
227
+ overrides, so `cn("text-xs px-xs py-sm", "px-md")` gives
228
+ `"text-xs py-sm px-md"`. The helper keeps typography and spacing names apart.
229
+
230
+ ### Forms
231
+
232
+ Every product form is built from the Caps form layer; Apps never hand-roll
233
+ labels, help text, errors, spacing or action rows. All parts live in
234
+ `@fungi.computer/caps/components/field` (plus `RadioGroupCard` in
235
+ `components/radio-group`). Validation stays with the caller (TanStack Form or
236
+ local state); Caps only presents it.
237
+
238
+ | Part | Job |
239
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `Form` | The `<form>` with the field rhythm (16px between fields). `busy` disables every control through one fieldset and sets `aria-busy`. |
241
+ | `FormSection` | A titled group (`title`, `description`, `level`). Consecutive sections sit 32px apart with one quiet rule. |
242
+ | `Field` | Label, then help, then control, then error. Wires `id`, `htmlFor`, `aria-describedby` (keeping the caller's own ids), `aria-invalid` and `required`. `orientation="horizontal"` puts a switch opposite its copy (settings rows); `orientation="inline"` puts a checkbox before its copy. `icon` leads the copy with a decorative Sigil or App icon (hidden from assistive tech). A horizontal Field bounds a Select to 16rem, and at 480px and under drops a Select, Input or Textarea under its label. `readout` names a row whose child is a read-only Status rather than a control. |
243
+ | `FieldSet` | A legend-named group of related choices (a list of inline checkbox `Field`s). `disabled` disables every choice. Use it, not a hand-rolled `<fieldset><legend>`. |
244
+ | `Label` | `required` adds the asterisk; `optional` adds a quiet mono "Optional" readout. Mark whichever is rarer in the form. |
245
+ | `FieldError` | The announced, collapsing error line. `Field` and `FormFooter` already render one. |
246
+ | `FormFooter` | Exactly one primary `Button`, last; every other action is `ghost` or `outline`. `error` shows the submit error above the actions, `status` seats a quiet readout (a `Status` LED plus "Saved") opposite them, `layout="stretch"` stacks full-width actions for single-column cards, and `divider={false}` drops the rule on short inline forms. Under 24rem the actions share the row equally. |
247
+ | `NumberField` | A spinbutton: a native `input type="number"` between − and + keys (which stay on touch), with the value as a mono readout and an optional `unit`. `value`/`onValueChange` (committed values are clamped to `min`/`max`), `step`, `wrap`. Arrows step, Page Up/Down move ten steps, Home/End jump to min/max; typing commits on blur or Enter. Field wires label, description and error to the input. Stable parts (`caps-number-field`, `-decrement`, `-readout`, `-input`, `-unit`, `-increment`, plus `data-at-min`/`data-at-max`/`data-disabled`) let a context such as a Screen redraw it as a dial. |
248
+ | `RadioGroupCard` | A radio choice that needs a sentence or a readout (`title`, `description`, `meta`). Selection is ink, never the accent. |
249
+
250
+ `IconCheckbox` (`components/icon-checkbox`) and `IconRadio`
251
+ (`components/icon-radio`, inside `RadioGroup`) accept an `icon` slot, a required
252
+ full `label`, and an optional short `caption`. Their native inputs retain refs,
253
+ Field descriptions/errors, keyboard selection, form submission and reset.
254
+ Selection shows the icon in colour and adds an ink lamp; off is a grey
255
+ silhouette. Caps does not depend on an icon library. Use the standard LIST, READ
256
+ (book-eye), WRITE (pencil), and RUN Sigils for Computer rights.
257
+
258
+ Rules:
259
+
260
+ - Controls share the field height (Daisy `--size-field`: 40px by default), so
261
+ inputs, selects and the buttons beside them line up.
262
+ - Show a field's error inline once the person has left it or submitted; show
263
+ server and network failures in `FormFooter error`.
264
+ - While submitting, set `Form busy` and the primary `Button loading`; never swap
265
+ its label for "Please wait…".
266
+
267
+ ```tsx
268
+ <Form aria-label="Computer settings" busy={saving} onSubmit={submit}>
269
+ <FormSection title="Computer" description="How your Team sees it.">
270
+ <Field label="Name" required error={nameError}>
271
+ <Input value={name} onChange={onName} />
272
+ </Field>
273
+ <Field label="Notes" optional>
274
+ <Textarea rows={3} />
275
+ </Field>
276
+ </FormSection>
277
+ <FormFooter
278
+ error={saveError}
279
+ status={
280
+ saved ? <Status tone="success" size="compact" label="Saved" /> : null
281
+ }
282
+ >
283
+ <Button type="reset" variant="ghost">
284
+ Cancel
285
+ </Button>
286
+ <Button type="submit" loading={saving}>
287
+ Save changes
288
+ </Button>
289
+ </FormFooter>
290
+ </Form>
291
+ ```
292
+
293
+ ### TanStack Form bindings
294
+
295
+ The opt-in [`@fungi.computer/caps/form`](docs/api/form.md) subpath uses
296
+ `@tanstack/react-form` (1.33.2 in this workspace) and Effect 4 as optional
297
+ peers. Install both when using this subpath. The presentational components keep
298
+ their existing API.
299
+
300
+ `FormField` accepts TanStack's field options (`form`, typed nested `name`,
301
+ `validators`, listeners) plus Caps `label`, `description`, `required`,
302
+ `optional`, `orientation` and `className`. Its child receives the native field
303
+ API: bind the control's value, change and blur handlers there. Caps connects the
304
+ label, description and first readable error to that control. Errors appear after
305
+ blur or a submission attempt; TanStack retains validation timing.
306
+
307
+ `fieldError(meta)` reads the first nonempty string or Standard Schema issue
308
+ message, returning `undefined` when there is none. `effectValidator(schema)`
309
+ uses Effect's maintained `Schema.toStandardSchemaV1`, preserving nested issue
310
+ paths and decoded values. TanStack validates with that schema; submission values
311
+ remain TanStack's input values, so decode separately when saving transformed
312
+ data.
313
+
314
+ `SubmitFooter` subscribes to `canSubmit` and `isSubmitting`, renders a native
315
+ submit button, and keeps it disabled and busy until the submit promise settles.
316
+ It accepts `FormFooter` presentation props, a `label`, an additional `disabled`
317
+ condition and `buttonProps` for appearance. Keep mutation errors in its `error`
318
+ prop and await the actual mutation in `onSubmit`.
319
+
320
+ ```tsx
321
+ import { useForm } from "@tanstack/react-form";
322
+ import { Schema } from "effect";
323
+ import { Form } from "@fungi.computer/caps/components/field";
324
+ import { Input } from "@fungi.computer/caps/components/input";
325
+ import {
326
+ FormField,
327
+ SubmitFooter,
328
+ effectValidator,
329
+ } from "@fungi.computer/caps/form";
330
+
331
+ function Settings({ save }: { save: (name: string) => Promise<void> }) {
332
+ const validator = effectValidator(
333
+ Schema.Struct({ name: Schema.String.check(Schema.isMinLength(1)) }),
334
+ );
335
+ const form = useForm({
336
+ defaultValues: { name: "" },
337
+ validators: { onBlur: validator, onSubmit: validator },
338
+ onSubmit: async ({ value }) => save(value.name),
339
+ });
340
+ return (
341
+ <form.Subscribe selector={(state) => state.isSubmitting}>
342
+ {(busy) => (
343
+ <Form
344
+ busy={busy}
345
+ noValidate
346
+ onSubmit={(event) => {
347
+ event.preventDefault();
348
+ if (!form.state.isSubmitting) void form.handleSubmit();
349
+ }}
350
+ >
351
+ <FormField form={form} name="name" label="Name" required>
352
+ {(field) => (
353
+ <Input
354
+ name={field.name}
355
+ value={field.state.value}
356
+ onBlur={field.handleBlur}
357
+ onChange={(event) =>
358
+ field.handleChange(event.currentTarget.value)
359
+ }
360
+ />
361
+ )}
362
+ </FormField>
363
+ <SubmitFooter form={form} label="Save" />
364
+ </Form>
365
+ )}
366
+ </form.Subscribe>
367
+ );
368
+ }
369
+ ```
370
+
371
+ Workbench: `forms--form-anatomy`, `forms--number-field`, `forms--form-states`,
372
+ `forms--choice-cards`, `forms--form-footers`, `forms--field`, and
373
+ `forms--settings-rows` (settings as compact `DataList` rows of icon Fields).
374
+
375
+ ### Schema forms
376
+
377
+ Forms are schema-first. The schema is the single source: it validates the raw
378
+ input, parses what the submit receives, and its annotations give each field's
379
+ label, kind, help, placeholder, options, order and section.
380
+ `<SchemaForm schema={…} />` renders the whole form; there is no field list.
381
+
382
+ ```tsx
383
+ import { Schema } from "effect";
384
+ import { SchemaForm, Secret, field, sections } from "@fungi.computer/caps/form";
385
+
386
+ const computer = Schema.Struct({
387
+ name: Schema.Trim.check(Schema.isMinLength(1)).annotate(
388
+ field({ required: true, section: "identity" }),
389
+ ),
390
+ token: Secret.annotate(field({ label: "Deploy token", section: "access" })),
391
+ region: Schema.Literals(["eu", "us"]).annotate(
392
+ field({ options: { eu: "Europe (Helsinki)", us: "United States" } }),
393
+ ),
394
+ internet: Schema.Boolean.annotate(field({ label: "Internet access" })),
395
+ }).annotate(
396
+ sections({ identity: { title: "Identity" }, access: { title: "Access" } }),
397
+ );
398
+
399
+ <SchemaForm
400
+ schema={computer}
401
+ defaultValues={{ name: "", token: "", region: "eu", internet: true }}
402
+ onSubmit={async (value) =>
403
+ (await save(value))
404
+ ? undefined
405
+ : { kind: "refused", fields: { name: "Taken." } }
406
+ }
407
+ discardLabel="Discard"
408
+ aria-label="Computer"
409
+ />;
410
+ ```
411
+
412
+ - **Schemas:** an Effect `Schema.Struct` (house path; annotate with
413
+ `.annotate(field({ … }))`) or a Zod object (annotate with
414
+ `.meta(field({ … }))`). The form holds the schema's encoded (raw) values;
415
+ `onSubmit` gets only the parsed value and returns nothing (saved),
416
+ `{ kind: "saved", values?, message? }` or
417
+ `{ kind: "refused", error?, fields? }`. Field refusals land on their fields
418
+ and clear when the field changes.
419
+ - **Kinds are inferred, annotations win:** String → text; Number (or
420
+ `NullOr(Number)`) → number; Boolean → switch; a literal union → radio (≤ 3
421
+ options, or options with a `description`/`meta` → choice cards), select
422
+ (4–12), or searchable `combobox` (more than 12); options with an `icon` → icon
423
+ keys; an array of literals → checkboxes (icon keys with icons); `Secret` →
424
+ secret. `kind` overrides: `email`, `url`, `tel`, `textarea`, `secret`, `money`
425
+ (`currency`), `date`, `select`, `combobox`, `radio`, `checkbox`, `switch`,
426
+ `file`, and the icon variants. `combobox` uses literal choices or annotated
427
+ `options` and keeps a string value.
428
+ - **Files:** annotate `Schema.Array(Schema.instanceOf(File))` (or
429
+ `z.array(z.file())`) with `kind: "file"`, `accept`, `multiple` and `maxBytes`.
430
+ The raw value is `readonly File[]`; a Caps “Choose files” button opens the
431
+ visually hidden, accessible native picker and feeds UploadQueue rows. The
432
+ schema enforces size (Effect `.check()` or Zod `.max()` on each file), using
433
+ the same byte limit as `maxBytes`; `accept` is a picker hint. Caps reads
434
+ metadata only; the submit handler receives the File objects.
435
+ - **Annotations:** `label` (else the humanized key), `help`, `placeholder`,
436
+ `section` + `order`, `layout: "row"` (settings row), `required`/`optional`,
437
+ `icon`, `options` (labels, descriptions, meta, icons, captions), number
438
+ `min`/`max`/`step`/`unit`, `readOnly`, `when(values)` for conditional fields.
439
+ `sections({ … })` on the struct titles the groups, in order.
440
+ - **The bailout is `render`:** `field<number>({ render: (control) => … })` draws
441
+ the control yourself and gets `value`, `onChange`, `onBlur`, `error`,
442
+ `disabled`, `id` and `aria-*`; Caps still owns the label, help, error,
443
+ validation and save bar. Precedence: `render` > `kind` > inferred kind.
444
+ - **Page state, not schema:** `SchemaForm` takes `submitLabel`, `discardLabel`,
445
+ `allowClean`, `disabled` (lock every field), `footer` (replace the SaveBar;
446
+ `null` drops it) and `children` (a custom layout: place fields with
447
+ `<FormFields model names={["…"]} />`). Use `useSchemaForm(options)` and pass
448
+ `model` when the page needs `useSchemaFormStatus(model)` (dirty, submitting)
449
+ for a leave guard.
450
+
451
+ `FormField` / `SubmitFooter` remain the hand-wired path for forms that are not a
452
+ struct.
453
+
454
+ **Controls on a screen.** Inside a `Screen`/`Display` the same controls take the
455
+ screen context (tokens only, no new props): labels become tiny mono legends,
456
+ Switch/Checkbox/Slider go flat and calm, icon keys get a hairline and faint ink
457
+ tint so their colour Sigil is the only colour, and `NumberField` becomes an
458
+ encoder dial between hairline − / + keys. Lay an instrument out with
459
+ `ScreenHeader`, `ScreenCells` and `ScreenCell` (`components/surface`), and show
460
+ read-only rights with `IconLamp` / `IconLamps` (`components/icon-lamp`). Keep
461
+ the Save key under the glass. Workbench: `forms--declared-form`,
462
+ `forms--declared-form-states`, `forms--screen-instrument`, `forms--icon-lamps`.
463
+
464
+ ### Destructive confirmations
465
+
466
+ `ConfirmAction` (`components/confirm-action`) owns confirmation, pending and
467
+ failure feedback. Use `mode="hold"` with `label`, `confirmLabel` (the spoken
468
+ hold instruction), and `onConfirm: () => Promise<void>`; use `mode="type"` with
469
+ a nonempty `phrase` and a destructive `confirmLabel`. Type mode opens a Dialog;
470
+ only an exact match enables submission. Escape or Cancel clears the phrase and
471
+ returns focus to the trigger. Pending actions cannot be dismissed or submitted
472
+ again. Resolve to finish (and close the Dialog); reject to keep it available for
473
+ retry. Optional `error` supplies safe failure copy; rejection details are never
474
+ rendered. The host still owns the request, idempotency and resource removal.
475
+ Workbench: `forms--confirm-actions` (choose Complete or Refuse).
476
+
477
+ ### Button finish tokens
478
+
479
+ Filled and outline buttons inherit `--caps-button-border-width` (length),
480
+ `--caps-button-weight` (font weight), `--caps-button-press` (active travel),
481
+ `--caps-button-drop` and `--caps-button-drop-active` (complete shadow lists),
482
+ and `--caps-button-cut` (a CSS clip path; `none` keeps the uncut silhouette). A
483
+ disabled filled button also reads `--caps-button-disabled-opacity`,
484
+ `--caps-button-disabled-face`, `--caps-button-disabled-ink`,
485
+ `--caps-button-disabled-edge`, and `--caps-button-disabled-drop`, so a theme can
486
+ show a flat disabled plate instead of a dimmed key. Unset tokens keep the
487
+ existing button rendering and native state behaviour. Roles still choose the
488
+ colour; a nested Stipe finish chooses these material values. Joined input
489
+ actions retain their stationary shared seam.
490
+
491
+ ### Dialogs and menus
492
+
493
+ `Dialog` and `DropdownMenu` use Base UI's event details, focus management and
494
+ open state. Their styled parts accept Base UI's `render` composition and
495
+ state-based `className` callbacks. You can place the portal inside a window and
496
+ choose focus targets. When the container mounts with the overlay, pass the
497
+ mounted element through a React callback ref. Passing `null` keeps the portal
498
+ waiting for that element.
499
+
500
+ `DialogContent` is the convenient portal, backdrop and popup composition. It
501
+ keeps child state across closes by default. Its backdrop follows Base UI's full
502
+ modal mode. `modal={false}` and `modal="trap-focus"` have no backdrop. For a
503
+ different composition, use `DialogPortal`, `DialogBackdrop` and `DialogPopup`
504
+ directly. The raw portal keeps Base UI's default mount lifetime. You can retain
505
+ closing content with `details.preventUnmountOnClose()` and finish its animation
506
+ with the native `actionsRef.current.unmount()`.
507
+
508
+ `DropdownMenuContent` exposes placement and portal-container options. Use
509
+ `DropdownMenuPortal`, `DropdownMenuPositioner` and `DropdownMenuPopup` directly
510
+ for custom anchors or collision boundaries. Both overlays support
511
+ `interactionOwner="host"`: your app receives Escape and owns focus restoration.
512
+ Local overlays use Base UI's normal closing behaviour.
513
+
514
+ CSS inherits through the DOM, not through React portals. For a nested theme,
515
+ render popups into an unclipped container inside that theme.
516
+
517
+ ### Selection contracts
518
+
519
+ Use `rootRef` to receive the root div of a generic `Combobox` or `Listbox`.
520
+ `Combobox` also exposes `inputRef` and `triggerRef` for its input and popup
521
+ trigger.
522
+
523
+ Import `ComboboxProps<T>` from `@fungi.computer/caps/components/combobox` and
524
+ `ResourceSelectProps` from `@fungi.computer/caps/blocks/resource-select`. Use
525
+ `Extract` or `Exclude` to select a mode, and indexed access to declare reusable
526
+ callbacks or native trigger metadata. The public props keep the inline or popup,
527
+ controlled or uncontrolled, and single or multiple constraints, so you need no
528
+ private aliases. The [selection contract notes](docs/selection-contract.md) have
529
+ worked examples.
530
+
531
+ ### Windows
532
+
533
+ `WindowFrame`, `WindowHeader`, `WindowActions` and `WindowBody` draw a rounded
534
+ application window. Caps owns presentation, not a window manager. Your app
535
+ supplies positioning, active and floating state, drag-handle events and
536
+ lifecycle commands, and owns tiling, floating geometry and layout focus.
537
+ `GameWindowContent` adds padded reading rhythm for a game's headings, readouts
538
+ and action keys inside `WindowBody`.
539
+
540
+ Only the callbacks you pass produce action buttons. Omit `handleProps` for a
541
+ static title. Otherwise the title is a native button, separate from the actions.
542
+ Name the frame with `aria-label` or `aria-labelledby`. A window is a non-modal
543
+ section, not a dialog or focus trap. `WindowFrame data-theme="paper"` draws it
544
+ in an ink line with a hard offset shadow, for windows over a game world.
545
+
546
+ ### Editorial and print components
547
+
548
+ Some components serve long-form, magazine-style pages. They read Stipe's card,
549
+ marginalia, growth and type tokens, so they look right in `field-guide`,
550
+ `field-guide-night` and the other editorial themes.
551
+
552
+ - `@fungi.computer/caps/components/field-guide`: `Keycap` (a native keyboard
553
+ key), `StatusLed` (a static labelled light), `CartridgeLabel` (a ruled
554
+ cartridge face) and `SpecimenLabel` (plate annotation, catalogue metadata,
555
+ title and summary).
556
+ - `@fungi.computer/caps/components/technonomicon`: `CigaretteCard`, `Masthead`,
557
+ `SectionBar`, `Folio`, `IssueMasthead`, `Spread` and related editorial pieces.
558
+
559
+ `CigaretteCard` is a framed collector card at the playing-card ratio 63:88. Its
560
+ content scales and clamps to fit instead of stretching the card: a display-serif
561
+ title and type line, a hexagonal series mark, an inset art window, a stat plate
562
+ on an accent rule, a flavor line and a collector line with an optional set
563
+ symbol. The reverse keeps the same frame around a ruled measurement ledger with
564
+ sources. `finish="full-art"` runs the drawing under paper panels inside an
565
+ accent border. A `null` stat value prints "not measured". Caps never invents a
566
+ number. Give a card `href` to make it a preview: a link to the card's own page
567
+ that shows only its face. The card on its own page is a pressed button that
568
+ turns over.
569
+
570
+ `Masthead` sets a nameplate in `--font-nameplate` blackletter with an optional
571
+ printed `mark`, a `tagline`, an `edition` line between a hairline and a double
572
+ rule, and two ears (`startEar`, `endEar`) that flank the nameplate on wide pages
573
+ and sit under it on phones. `size="front"` prints the large front-page head and
574
+ `size="running"` the smaller inner-page head. `SectionBar` is its section
575
+ navigation: small capitals between rules, the `current` section printed in
576
+ reverse, an optional letter `marker`, a `shortcut` announced through
577
+ `aria-keyshortcuts`, and `tools` at the end. It scrolls sideways on narrow pages
578
+ instead of wrapping. `Folio` is the running foot. `IssueMasthead` names its
579
+ `publication` in the kicker. `Spread layout="lead"` opens its first teaser
580
+ paragraph on a drop cap.
581
+
582
+ Server-rendered pages without React hydration can opt in to two small browser
583
+ enhancements. `enhanceCigaretteCards()` from
584
+ `@fungi.computer/caps/enhance/cigarette-card` makes click, tap, Enter and Space
585
+ toggle a card's `aria-pressed`, and ignores clicks a hydrated card already
586
+ handled. `sketch()` from `@fungi.computer/caps/enhance/sketch` draws rough.js
587
+ pencil marks inside every `[data-sketch-root]`. Mark inline text with
588
+ `data-sketch="circle"` or `data-sketch="underline"`, and a note with
589
+ `data-sketch-leader-x/y` to draw a leader to the `data-sketch-target` inside the
590
+ same `data-sketch-item`. The layout turns leaders on with
591
+ `--caps-sketch-leader: on`. Strokes use `currentColor`, so theme switches need
592
+ no redraw. `SpecimenLabel sketch` marks its plate and detail, and its ruled CSS
593
+ marks stay until the pencil layer draws.
594
+
595
+ Static sites (for example Astro) can use plain-HTML class contracts from the
596
+ stylesheet without React:
597
+
598
+ - Journal: `caps-meta` (catalogue line), `caps-entry-row` (contents row,
599
+ `data-thumb="none"` without a plate), `caps-toc`, `caps-backlinks`,
600
+ `caps-tool-call`, `caps-collage` (specimen sketchbook with `--art-*` and
601
+ `--note-*` placement variables), `caps-note-teaser`, `caps-inset`
602
+ (`data-align="end"` floats a card beside prose), `caps-nav-link`,
603
+ `caps-shortcuts`, `caps-skip-link` and `caps-pencil-reveal`.
604
+ - Print: `caps-headline` (`data-size="banner|lead|story|brief"`), `caps-deck`,
605
+ `caps-byline`, `caps-jump` (the "continued on" line), `caps-story-head`,
606
+ `caps-section-head` (`data-size="front"` for a section front), `caps-edition`
607
+ (children `data-edition-slot="lead|aside|wide"`, the aside ruled beside the
608
+ lead), `caps-column-grid` (one ruled column per topic, `--caps-columns`),
609
+ `caps-column-head`, `caps-column-empty`, `caps-brief`, `caps-box`,
610
+ `caps-numeral`, `caps-print-prose` (display headings, pull-quote blockquotes,
611
+ an ornamental break and, with `data-dropcap`, a drop cap) and
612
+ `caps-live-portrait` (a live avatar traced by a host SVG filter whose
613
+ `caps-live-portrait-ink` flood reads `--card-ink`).
614
+ `caps-note-teaser[data-size="lead"]` sets a teaser as a front-page lead.
615
+ - Page turns: keyframes `caps-page-out`, `caps-page-in`, `caps-page-back-out`
616
+ and `caps-page-back-in` are for your view transitions. Drop them under reduced
617
+ motion.
618
+
619
+ ## API reference
620
+
621
+ The generated [API reference](docs/api/README.md) documents every subpath:
622
+ `components/*`, `blocks/*`, `molecules/*`, `enhance/*`, `lib/*` and the opt-in
623
+ [`form` bindings](docs/api/form.md), with each component's props.
624
+
625
+ ## Examples
626
+
627
+ ### Button
628
+
629
+ ```tsx
630
+ import { useRef } from "react";
631
+ import { Button } from "@fungi.computer/caps/components/button";
632
+
633
+ export function SaveButton({
634
+ pending,
635
+ onSave,
636
+ }: {
637
+ pending: boolean;
638
+ onSave: () => void;
639
+ }) {
640
+ const ref = useRef<HTMLButtonElement>(null);
641
+
642
+ return (
643
+ <Button
644
+ ref={ref}
645
+ type="submit"
646
+ aria-label="Save changes"
647
+ disabled={pending}
648
+ loading={pending}
649
+ variant="outline"
650
+ size="sm"
651
+ className="transition-none rounded-full"
652
+ onClick={onSave}
653
+ >
654
+ Save
655
+ </Button>
656
+ );
657
+ }
658
+ ```
659
+
660
+ The caller's `transition-none` replaces the base transition utility, and
661
+ `rounded-full` adds a local treatment. daisyUI's `btn-outline` and color classes
662
+ compose, so color belongs in `variant` or an extra daisyUI class.
663
+
664
+ ### Composer with an async submit
665
+
666
+ `Composer` is a layout block. Its input and action are your controls, and so are
667
+ their labels, disabled state, keyboard behaviour and effects. An avatar or other
668
+ leading content goes in the `leading` slot as any React node.
669
+
670
+ ```tsx
671
+ import { useState, type FormEvent } from "react";
672
+ import { Button } from "@fungi.computer/caps/components/button";
673
+ import { Composer } from "@fungi.computer/caps/blocks/composer";
674
+
675
+ export function PromptComposer({
676
+ submit,
677
+ }: {
678
+ submit: (value: string) => Promise<void>;
679
+ }) {
680
+ const [value, setValue] = useState("");
681
+ const [pending, setPending] = useState(false);
682
+
683
+ async function onSubmit(event: FormEvent) {
684
+ event.preventDefault();
685
+ if (!value.trim() || pending) return;
686
+ setPending(true);
687
+ try {
688
+ await submit(value);
689
+ setValue("");
690
+ } finally {
691
+ setPending(false);
692
+ }
693
+ }
694
+
695
+ return (
696
+ <form onSubmit={onSubmit}>
697
+ <Composer
698
+ input={
699
+ <input
700
+ className="input join-item w-full"
701
+ value={value}
702
+ onChange={(event) => setValue(event.target.value)}
703
+ aria-label="Prompt"
704
+ />
705
+ }
706
+ action={
707
+ <Button
708
+ type="submit"
709
+ size="sm"
710
+ disabled={pending || !value.trim()}
711
+ loading={pending}
712
+ >
713
+ Send
714
+ </Button>
715
+ }
716
+ />
717
+ </form>
718
+ );
719
+ }
720
+ ```
721
+
722
+ `submit` is your effect. It can call an API, enqueue work or do something local.
723
+ Caps only joins the controls.
724
+
725
+ ### Window
726
+
727
+ ```tsx
728
+ import type { ReactNode } from "react";
729
+ import {
730
+ WindowActions,
731
+ WindowBody,
732
+ WindowFrame,
733
+ WindowHeader,
734
+ } from "@fungi.computer/caps/components/window";
735
+
736
+ export function NotesWindow({
737
+ active,
738
+ close,
739
+ children,
740
+ }: {
741
+ active: boolean;
742
+ close: () => void;
743
+ children: ReactNode;
744
+ }) {
745
+ return (
746
+ <WindowFrame aria-label="Notes" active={active}>
747
+ <WindowHeader
748
+ title="Notes"
749
+ actions={<WindowActions title="Notes" onClose={close} />}
750
+ />
751
+ <WindowBody>{children}</WindowBody>
752
+ </WindowFrame>
753
+ );
754
+ }
755
+ ```
756
+
757
+ ### App bar
758
+
759
+ `AppBar` keeps running apps visible to the left of an anchored launcher.
760
+ Clicking the launcher reveals available apps to the right and an optional
761
+ primary action above. You own app identity, icons, selected state and callbacks.
762
+ Caps owns disclosure, Anime.js springs, overflow and keyboard focus.
763
+
764
+ ```tsx
765
+ import { AppBar } from "@fungi.computer/caps/blocks/app-bar";
766
+
767
+ export function Dock({
768
+ openWindow,
769
+ openSettings,
770
+ createChat,
771
+ }: {
772
+ openWindow: (id: string) => void;
773
+ openSettings: () => void;
774
+ createChat: () => void;
775
+ }) {
776
+ return (
777
+ <AppBar
778
+ windows={[
779
+ {
780
+ id: "editor",
781
+ label: "Editor",
782
+ active: true,
783
+ onSelect: () => openWindow("editor"),
784
+ },
785
+ ]}
786
+ launchers={[
787
+ { id: "settings", label: "Settings", onSelect: openSettings },
788
+ ]}
789
+ launcher={{ label: "Open apps" }}
790
+ primaryAction={{
791
+ label: "New chat",
792
+ icon: <span aria-hidden="true">+</span>,
793
+ onSelect: createChat,
794
+ }}
795
+ />
796
+ );
797
+ }
798
+ ```
799
+
800
+ Icons are React nodes. Narrow screens keep the same two scrollable trays. Escape
801
+ closes the disclosure and returns focus to the launcher. Reduced motion settles
802
+ immediately. Available-app and primary-action buttons do not claim toggle state.
803
+ `alwaysExpanded` keeps the disclosure open for dock layouts.
804
+
805
+ ### Status beads and dashboard controls
806
+
807
+ `Status` is a gooey SVG bead. Set `glyph` (`dot`, `pill` or `bang`), `tone`, and
808
+ optional `motion` (`none`, `pulse` or `bounce`). Shape changes animate from the
809
+ current geometry with Anime.js, and reduced motion settles immediately. You map
810
+ your state to these props. An optional `label` renders beside the decorative
811
+ bead.
812
+
813
+ ```tsx
814
+ import { Status } from "@fungi.computer/caps/components/status";
815
+
816
+ export function InboxBead({
817
+ unread,
818
+ selected,
819
+ }: {
820
+ unread: boolean;
821
+ selected: boolean;
822
+ }) {
823
+ return (
824
+ <>
825
+ <Status
826
+ glyph={unread ? "bang" : selected ? "pill" : "dot"}
827
+ tone="primary"
828
+ />
829
+ <Status label="Working" tone="success" motion="pulse" />
830
+ </>
831
+ );
832
+ }
833
+ ```
834
+
835
+ `IconButton` defaults to the Dashboard's ghost, circular, extra-small control.
836
+ It keeps its grid size on phones and touch screens; an invisible hit area
837
+ reaches `--caps-touch-target` instead. A disabled ghost button stays faceless
838
+ and dims to `--caps-disabled-ink`. Disabled filled buttons retain their color,
839
+ material and shape at reduced opacity, including invalid and pending form
840
+ submits. `Checkbox`, `Switch`, and `Progress` keep their native elements and
841
+ existing animations. `ScrollToBottomBlob` stays mounted beside a supplied
842
+ scroller and owns its visibility: show beyond 48px from the end, hide below 8px.
843
+ It defaults to forward DOM order (`direction="reverse"` supports
844
+ column-reverse). The host owns `onJump` and the appended-message `unread` count.
845
+ `focusAfterJump` optionally returns keyboard focus to the composer; otherwise
846
+ the scroller receives focus. Motion reads Stipe's spring-duration, bounce,
847
+ stretch and fast tokens. Reduced motion uses short opacity fades with no moving
848
+ beads or stretch. The Feedback story provides real scroll, unread-arrival and
849
+ jump scenarios.
850
+
851
+ ### Status tray
852
+
853
+ `@fungi.computer/caps/blocks/status-tray` exports `StatusTray`,
854
+ `StatusTrayList`, `StatusTrayRow` and `StatusTrayNote`. You supply readouts and
855
+ content, own `openId` and handle actions. One tray opens at a time. Escape and
856
+ the close control return focus to its chip. Outside pointer presses close it
857
+ without stealing focus. Disabled items never open, and zero counts stay hidden.
858
+
859
+ ```tsx
860
+ import { useState } from "react";
861
+ import { StatusTray } from "@fungi.computer/caps/blocks/status-tray";
862
+
863
+ export function PortsTray({ ports }: { ports: readonly string[] }) {
864
+ const [openId, setOpenId] = useState<string | null>(null);
865
+ return (
866
+ <StatusTray
867
+ openId={openId}
868
+ onOpenChange={setOpenId}
869
+ items={[
870
+ {
871
+ id: "ports",
872
+ label: "Ports",
873
+ count: ports.length,
874
+ tone: "success",
875
+ content: (
876
+ <ul>
877
+ {ports.map((port) => (
878
+ <li key={port}>{port}</li>
879
+ ))}
880
+ </ul>
881
+ ),
882
+ },
883
+ ]}
884
+ />
885
+ );
886
+ }
887
+ ```
888
+
889
+ An open tray is a calm popover list, with nothing boxed:
890
+
891
+ - `StatusTrayRow` is one line: the App's own `icon` (a Sigil, a Lucide glyph or
892
+ a `Status`), the `primary` title, a muted `secondary` detail that truncates,
893
+ then `actions`.
894
+ - Actions are quiet text buttons: `<Button variant="ghost" size="xs">`. A
895
+ destructive one such as Stop uses `variant="ghost-destructive"`, which turns
896
+ red on hover or focus. Give a row one action where you can.
897
+ - Precede a `StatusTrayList` with a `DataListHeading` for a small group label.
898
+ - `StatusTrayNote` is the empty state. It reads "All caught up." unless you pass
899
+ other text. For an unavailable feed, give it a `title`; unavailable is not a
900
+ zero count.
901
+ - The tray's close control is a plain `IconButton`. The host owns capability
902
+ checks.
903
+
904
+ `leading` and `trailing` seat adjacent controls, and an item's `after` places
905
+ controls right after its chip in DOM and keyboard order. `align` chooses the
906
+ panel edge; `placement="screen"` uses flat screen-native readouts instead of
907
+ housing chips. The block has no product data, persistence or appearance variant.
908
+
909
+ Themes can set these tokens:
910
+
911
+ - `--caps-status-tray-chip-*` and `--caps-status-tray-count-bg`
912
+ - `--caps-status-tray-title-{case,tracking}` and `--caps-status-tray-width`
913
+ - `--caps-status-tray-key-{face,tint,height}`, for KeycapButtons beside the
914
+ chips
915
+ - `--caps-status-tray-row-{height,hover,wrap}` and
916
+ `--caps-status-tray-icon-size`
917
+ - `--caps-status-tray-chips-display`
918
+ - `--caps-status-tray-note-tracking` and `--caps-status-tray-muted`
919
+ - `--caps-status-tray-screen-{bg,fg,bg-active,fg-active}`
920
+
921
+ Defaults use existing Stipe tokens. The Ladle Status tray stories show the
922
+ maintained themes and a nested finish using the same props.
923
+
924
+ ### Game pieces
925
+
926
+ Game UI reads the enclosing theme's `--card-ink` and `--card-paper`. The nested
927
+ `paper` finish supplies window linework, hard shadows, and control depth while
928
+ retaining that palette.
929
+
930
+ ```tsx
931
+ import { KeycapButton } from "@fungi.computer/caps/components/keycap";
932
+ import { GameMeter } from "@fungi.computer/caps/components/progress";
933
+ import { PixelNumber } from "@fungi.computer/caps/components/pixel-number";
934
+ import {
935
+ ItemGrid,
936
+ type ItemGridCell,
937
+ type ItemGridGhost,
938
+ type ItemGridItem,
939
+ } from "@fungi.computer/caps/components/item-grid";
940
+
941
+ export function Hud({
942
+ building,
943
+ toggleBuild,
944
+ items,
945
+ ghost,
946
+ pickUpOrDrop,
947
+ previewDrop,
948
+ }: {
949
+ building: boolean;
950
+ toggleBuild: () => void;
951
+ items: readonly ItemGridItem[];
952
+ ghost: ItemGridGhost | null;
953
+ pickUpOrDrop: (cell: ItemGridCell) => void;
954
+ previewDrop: (cell: ItemGridCell | null) => void;
955
+ }) {
956
+ return (
957
+ <>
958
+ <KeycapButton
959
+ label="Build"
960
+ shortcut="B"
961
+ pressed={building}
962
+ onClick={toggleBuild}
963
+ />
964
+ <GameMeter label="Hull" value={7} max={10} tone="success" />
965
+ <PixelNumber text="08:00" size="lg" tone="primary" />
966
+ <PixelNumber value={3} signed outline tone="success" />
967
+ <ItemGrid
968
+ label="Backpack"
969
+ columns={8}
970
+ rows={6}
971
+ items={items}
972
+ ghost={ghost}
973
+ onCellActivate={pickUpOrDrop}
974
+ onCellHover={previewDrop}
975
+ />
976
+ </>
977
+ );
978
+ }
979
+ ```
980
+
981
+ - `Keycap` is a native `kbd`. `KeycapButton` is a native button whose `shortcut`
982
+ only labels the key. Passing `pressed` makes it a toggle with a lamp and
983
+ `aria-pressed`. Your app owns the key binding. `tone` is a role: `primary` and
984
+ `accent` tint the face from the theme colour, `inverse` is the dark key, and
985
+ `destructive` stays neutral until hovered or focused. `label={null}` makes an
986
+ icon-only key that needs an `aria-label`. The molded lip, sheen and press
987
+ travel scale with `--depth`. Themes set
988
+ `--caps-keycap-{face,ink,edge,tint,width,min-width,height,justify,padding,font-size,font-weight}`.
989
+ - `GameMeter` pairs a labelled native progress bar with the current and maximum
990
+ values. The game supplies both values and the tone.
991
+ - `PixelNumber` draws a 3×5 face as SVG at integer scales and keeps the plain
992
+ text for assistive tech. `outline` rings the lit pixels in the ink line so the
993
+ number reads over any world.
994
+ - `ItemGrid` is presentation. Cells carry the keyboard and pointer interface
995
+ (arrows, Home and End, Enter and Space), and items and the drop ghost are
996
+ drawn from your facts. It never moves, merges or validates an item. A refused
997
+ ghost shows your reason under the tray and describes the grid with it.
998
+ `ItemSlot` is one paper-doll pocket.
999
+
1000
+ Game HUD pieces inherit their material and controls from a surrounding
1001
+ `data-theme="paper"` boundary. Nest it inside `field-guide` or
1002
+ `field-guide-night`, then optionally nest an `ember` or `tide` tint. Cards and
1003
+ plates do not reskin descendant Buttons. A complete nested palette can choose
1004
+ its own control finish.
1005
+
1006
+ ### Editorial pieces
1007
+
1008
+ ```tsx
1009
+ import { Keycap, StatusLed } from "@fungi.computer/caps/components/field-guide";
1010
+ import {
1011
+ Folio,
1012
+ Masthead,
1013
+ SectionBar,
1014
+ } from "@fungi.computer/caps/components/technonomicon";
1015
+
1016
+ export function FrontPage() {
1017
+ return (
1018
+ <>
1019
+ <Masthead
1020
+ href="/"
1021
+ wordmark="The Spore Print"
1022
+ tagline="Notes from a very strange forest."
1023
+ edition={["Saturday, September 26, 2026", "Issue I"]}
1024
+ sections={
1025
+ <SectionBar
1026
+ aria-label="Sections"
1027
+ sections={[{ href: "/", label: "Front Page", current: true }]}
1028
+ />
1029
+ }
1030
+ />
1031
+ <p>
1032
+ Press <Keycap>J</Keycap> for the next story.{" "}
1033
+ <StatusLed active label="Online" />
1034
+ </p>
1035
+ <Folio items={["The Spore Print", "Page A1"]} />
1036
+ </>
1037
+ );
1038
+ }
1039
+ ```
1040
+
1041
+ On a server-rendered page, turn on card flips and pencil marks once in the
1042
+ browser:
1043
+
1044
+ ```ts
1045
+ import { enhanceCigaretteCards } from "@fungi.computer/caps/enhance/cigarette-card";
1046
+ import { sketch } from "@fungi.computer/caps/enhance/sketch";
1047
+
1048
+ enhanceCigaretteCards();
1049
+ sketch();
1050
+ ```
1051
+
1052
+ ## Limits and errors
1053
+
1054
+ - Caps has no root export. Importing `@fungi.computer/caps` itself fails. Use
1055
+ the subpaths.
1056
+ - Caps is not a widget SDK and has no data layer. It does not implement product
1057
+ behaviour, publish a registry or deploy anything.
1058
+ - `PixelNumber` throws on a non-finite number or a character it cannot draw.
1059
+ - `ItemGrid` throws when item or ghost geometry does not fit the grid.
1060
+ - `CigaretteCard` prints "not measured" for a `null` stat and never makes up a
1061
+ value.
1062
+ - Popups render into the document body by default and take its theme. Pass a
1063
+ portal container inside a nested theme to keep that theme.
1064
+ - Load Stipe's CSS once, through one of the two Caps stylesheet routes. Do not
1065
+ import Tailwind a second time or copy Stipe's token sheet into your app.
1066
+ - The editable-source registry in `registry/` is generated inside this
1067
+ repository and has no hosted endpoint.
1068
+
1069
+ ## Contribute and license
1070
+
1071
+ When you change Caps:
1072
+
1073
+ - Inspect the existing source, tests and host before editing.
1074
+ - Preserve native semantics, refs, keyboard behaviour, visible focus and ARIA.
1075
+ - Primitives and blocks receive props, slots and callbacks. They do not fetch.
1076
+ - Keep product vocabulary, models, routes, defaults and side effects in hosts.
1077
+ - Use explicit imports. Do not add a root barrel, alias, shim or deprecated
1078
+ export.
1079
+ - Add or update tests and an example for every new public seam.
1080
+
1081
+ The tests cover native props and refs, caller-last classes, labelled fields,
1082
+ controlled values, focus and keyboard behaviour, disabled options, Escape,
1083
+ tooltip teardown, SSR, state blocks, upload-row presentation and gooey-filter
1084
+ identity. From the monorepo root, run:
1085
+
1086
+ ```sh
1087
+ pnpm --dir packages/caps run lint
1088
+ pnpm --dir packages/caps run typecheck
1089
+ pnpm --dir packages/caps run test
1090
+ pnpm --dir packages/caps run build
1091
+ pnpm --dir packages/caps run docs
1092
+ pnpm check:package -- @fungi.computer/caps
1093
+ ```
1094
+
1095
+ `pack`, `smoke`, the package-level `stranger` and
1096
+ `pnpm verify:package -- @fungi.computer/caps` are release checks. Skip them
1097
+ during ordinary development.
1098
+
1099
+ The component workbench is a Ladle site. Run `pnpm workbench:serve` from the
1100
+ monorepo root to browse every component in each maintained theme, including
1101
+ interactive Status, window, overlay, game-piece and status-tray stories.
1102
+
1103
+ The editable-source registry copies Caps source into a consumer instead of
1104
+ importing the runtime package. To add a registry item, author the source under
1105
+ `src/` with tests and an example, then add its metadata to `registry-source/`
1106
+ (files, dependencies, registry dependencies and the Stipe CSS import). Add its
1107
+ maintained story ID to `registry-source/workbench-coverage.json` and its
1108
+ introduction to `workbench/guide.ts`. Generate the payloads and coverage, then
1109
+ run the checks:
1110
+
1111
+ ```sh
1112
+ pnpm --filter @fungi.computer/caps registry:generate
1113
+ pnpm --filter @fungi.computer/caps registry:check
1114
+ pnpm --filter @fungi.computer/caps registry:stranger
1115
+ ```
1116
+
1117
+ `registry/`, `registry.json` and `workbench/coverage.json` are generated by
1118
+ `registry:generate`. Do not edit them by hand. The ordinary package test checks
1119
+ coverage freshness and registry inventory equality before any workbench build.
1120
+ The registry stranger check installs every generated item into an isolated
1121
+ fixture, checks local imports and dependencies, type-checks it and builds it. It
1122
+ proves that a consumer can own the copied source without a Caps runtime import.
1123
+ Caps depends on the real `@fungi.computer/stipe` package. Copied styles or an
1124
+ alias are not a substitute.
1125
+
1126
+ Caps is MIT licensed, as declared in its `package.json`.