@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.
- package/LICENSE +21 -0
- package/README.md +1125 -2
- package/dist/blocks/app-bar.d.ts +43 -0
- package/dist/blocks/app-bar.js +244 -0
- package/dist/blocks/app-status-bar.d.ts +93 -0
- package/dist/blocks/app-status-bar.js +42 -0
- package/dist/blocks/async-state.d.ts +21 -0
- package/dist/blocks/async-state.js +8 -0
- package/dist/blocks/card-grid.d.ts +11 -0
- package/dist/blocks/card-grid.js +21 -0
- package/dist/blocks/command-menu.d.ts +26 -0
- package/dist/blocks/command-menu.js +70 -0
- package/dist/blocks/composer.d.ts +23 -0
- package/dist/blocks/composer.js +13 -0
- package/dist/blocks/empty-state.d.ts +59 -0
- package/dist/blocks/empty-state.js +35 -0
- package/dist/blocks/faq-chat.d.ts +62 -0
- package/dist/blocks/faq-chat.js +77 -0
- package/dist/blocks/game-share-card.d.ts +17 -0
- package/dist/blocks/game-share-card.js +9 -0
- package/dist/blocks/launcher-grid.d.ts +50 -0
- package/dist/blocks/launcher-grid.js +11 -0
- package/dist/blocks/listing-detail.d.ts +23 -0
- package/dist/blocks/listing-detail.js +12 -0
- package/dist/blocks/loading-state.d.ts +5 -0
- package/dist/blocks/loading-state.js +8 -0
- package/dist/blocks/mosaic.d.ts +29 -0
- package/dist/blocks/mosaic.js +19 -0
- package/dist/blocks/permission-grid.d.ts +45 -0
- package/dist/blocks/permission-grid.js +14 -0
- package/dist/blocks/pricing-card.d.ts +53 -0
- package/dist/blocks/pricing-card.js +12 -0
- package/dist/blocks/release-tag.d.ts +20 -0
- package/dist/blocks/release-tag.js +10 -0
- package/dist/blocks/resource-select.d.ts +22 -0
- package/dist/blocks/resource-select.js +16 -0
- package/dist/blocks/result-state.d.ts +15 -0
- package/dist/blocks/result-state.js +8 -0
- package/dist/blocks/section-card.d.ts +31 -0
- package/dist/blocks/section-card.js +7 -0
- package/dist/blocks/section-heading.d.ts +13 -0
- package/dist/blocks/section-heading.js +11 -0
- package/dist/blocks/spec-sheet.d.ts +125 -0
- package/dist/blocks/spec-sheet.js +47 -0
- package/dist/blocks/status-tray.d.ts +83 -0
- package/dist/blocks/status-tray.js +79 -0
- package/dist/blocks/upload-queue.d.ts +48 -0
- package/dist/blocks/upload-queue.js +24 -0
- package/dist/components/action-row.d.ts +16 -0
- package/dist/components/action-row.js +11 -0
- package/dist/components/activity-line.d.ts +33 -0
- package/dist/components/activity-line.js +69 -0
- package/dist/components/alert.d.ts +17 -0
- package/dist/components/alert.js +17 -0
- package/dist/components/badge.d.ts +19 -0
- package/dist/components/badge.js +24 -0
- package/dist/components/button.d.ts +55 -0
- package/dist/components/button.js +230 -0
- package/dist/components/card.d.ts +61 -0
- package/dist/components/card.js +48 -0
- package/dist/components/chat-bubble.d.ts +34 -0
- package/dist/components/chat-bubble.js +10 -0
- package/dist/components/checkbox-bead.d.ts +6 -0
- package/dist/components/checkbox-bead.js +67 -0
- package/dist/components/checkbox.d.ts +21 -0
- package/dist/components/checkbox.js +46 -0
- package/dist/components/code-input.d.ts +35 -0
- package/dist/components/code-input.js +126 -0
- package/dist/components/combobox.d.ts +86 -0
- package/dist/components/combobox.js +218 -0
- package/dist/components/confirm-action.d.ts +23 -0
- package/dist/components/confirm-action.js +64 -0
- package/dist/components/copy-field.d.ts +53 -0
- package/dist/components/copy-field.js +49 -0
- package/dist/components/count-badge.d.ts +15 -0
- package/dist/components/count-badge.js +9 -0
- package/dist/components/cover-pattern.d.ts +19 -0
- package/dist/components/cover-pattern.js +74 -0
- package/dist/components/data-list.d.ts +25 -0
- package/dist/components/data-list.js +16 -0
- package/dist/components/dialog.d.ts +39 -0
- package/dist/components/dialog.js +73 -0
- package/dist/components/diff-view.d.ts +14 -0
- package/dist/components/diff-view.js +13 -0
- package/dist/components/disclosure.d.ts +29 -0
- package/dist/components/disclosure.js +19 -0
- package/dist/components/dropdown-menu.d.ts +49 -0
- package/dist/components/dropdown-menu.js +98 -0
- package/dist/components/field-guide.d.ts +41 -0
- package/dist/components/field-guide.js +18 -0
- package/dist/components/field.d.ts +164 -0
- package/dist/components/field.js +228 -0
- package/dist/components/game-feedback.d.ts +84 -0
- package/dist/components/game-feedback.js +48 -0
- package/dist/components/game-hud.d.ts +126 -0
- package/dist/components/game-hud.js +136 -0
- package/dist/components/generation-frame.d.ts +36 -0
- package/dist/components/generation-frame.js +97 -0
- package/dist/components/gooey.d.ts +32 -0
- package/dist/components/gooey.js +25 -0
- package/dist/components/hold-button.d.ts +26 -0
- package/dist/components/hold-button.js +115 -0
- package/dist/components/icon-button.d.ts +24 -0
- package/dist/components/icon-button.js +18 -0
- package/dist/components/icon-checkbox.d.ts +20 -0
- package/dist/components/icon-checkbox.js +9 -0
- package/dist/components/icon-choice-mark.d.ts +6 -0
- package/dist/components/icon-choice-mark.js +5 -0
- package/dist/components/icon-lamp.d.ts +42 -0
- package/dist/components/icon-lamp.js +15 -0
- package/dist/components/icon-radio.d.ts +16 -0
- package/dist/components/icon-radio.js +9 -0
- package/dist/components/inline-code.d.ts +3 -0
- package/dist/components/inline-code.js +7 -0
- package/dist/components/input-group.d.ts +9 -0
- package/dist/components/input-group.js +15 -0
- package/dist/components/input.d.ts +15 -0
- package/dist/components/input.js +15 -0
- package/dist/components/item-grid.d.ts +110 -0
- package/dist/components/item-grid.js +136 -0
- package/dist/components/keycap.d.ts +80 -0
- package/dist/components/keycap.js +18 -0
- package/dist/components/label.d.ts +13 -0
- package/dist/components/label.js +7 -0
- package/dist/components/listbox.d.ts +50 -0
- package/dist/components/listbox.js +134 -0
- package/dist/components/loader-blobs.d.ts +5 -0
- package/dist/components/loader-blobs.js +128 -0
- package/dist/components/loader.d.ts +21 -0
- package/dist/components/loader.js +25 -0
- package/dist/components/markdown.d.ts +23 -0
- package/dist/components/markdown.js +70 -0
- package/dist/components/minimap-labels.d.ts +39 -0
- package/dist/components/minimap-labels.js +113 -0
- package/dist/components/monogram.d.ts +32 -0
- package/dist/components/monogram.js +24 -0
- package/dist/components/motion-visibility.d.ts +2 -0
- package/dist/components/motion-visibility.js +25 -0
- package/dist/components/navigation-rail.d.ts +11 -0
- package/dist/components/navigation-rail.js +55 -0
- package/dist/components/overlay-root.d.ts +16 -0
- package/dist/components/overlay-root.js +24 -0
- package/dist/components/pixel-number.d.ts +50 -0
- package/dist/components/pixel-number.js +113 -0
- package/dist/components/popover.d.ts +31 -0
- package/dist/components/popover.js +24 -0
- package/dist/components/presence.d.ts +59 -0
- package/dist/components/presence.js +232 -0
- package/dist/components/price-tag.d.ts +11 -0
- package/dist/components/price-tag.js +6 -0
- package/dist/components/progress.d.ts +25 -0
- package/dist/components/progress.js +24 -0
- package/dist/components/prose.d.ts +14 -0
- package/dist/components/prose.js +10 -0
- package/dist/components/quota-meter.d.ts +24 -0
- package/dist/components/quota-meter.js +11 -0
- package/dist/components/radio-bead.d.ts +4 -0
- package/dist/components/radio-bead.js +53 -0
- package/dist/components/radio-group.d.ts +54 -0
- package/dist/components/radio-group.js +64 -0
- package/dist/components/resize-handle.d.ts +23 -0
- package/dist/components/resize-handle.js +31 -0
- package/dist/components/rough-drawing.d.ts +42 -0
- package/dist/components/rough-drawing.js +231 -0
- package/dist/components/rough.d.ts +68 -0
- package/dist/components/rough.js +33 -0
- package/dist/components/scroll-area.d.ts +12 -0
- package/dist/components/scroll-area.js +96 -0
- package/dist/components/scroll-motion.d.ts +12 -0
- package/dist/components/scroll-motion.js +31 -0
- package/dist/components/scroll-to-bottom-blob-state.d.ts +40 -0
- package/dist/components/scroll-to-bottom-blob-state.js +109 -0
- package/dist/components/scroll-to-bottom-blob.d.ts +16 -0
- package/dist/components/scroll-to-bottom-blob.js +174 -0
- package/dist/components/select.d.ts +9 -0
- package/dist/components/select.js +12 -0
- package/dist/components/selectable.d.ts +13 -0
- package/dist/components/selectable.js +45 -0
- package/dist/components/send-action.d.ts +33 -0
- package/dist/components/send-action.js +96 -0
- package/dist/components/separator.d.ts +11 -0
- package/dist/components/separator.js +7 -0
- package/dist/components/skeleton.d.ts +5 -0
- package/dist/components/skeleton.js +7 -0
- package/dist/components/slider.d.ts +18 -0
- package/dist/components/slider.js +31 -0
- package/dist/components/stat.d.ts +17 -0
- package/dist/components/stat.js +7 -0
- package/dist/components/status-bead.d.ts +9 -0
- package/dist/components/status-bead.js +107 -0
- package/dist/components/status-mark.d.ts +12 -0
- package/dist/components/status-mark.js +177 -0
- package/dist/components/status.d.ts +38 -0
- package/dist/components/status.js +28 -0
- package/dist/components/store-card.d.ts +21 -0
- package/dist/components/store-card.js +10 -0
- package/dist/components/surface.d.ts +89 -0
- package/dist/components/surface.js +31 -0
- package/dist/components/switch-bead.d.ts +4 -0
- package/dist/components/switch-bead.js +62 -0
- package/dist/components/switch.d.ts +11 -0
- package/dist/components/switch.js +36 -0
- package/dist/components/tabs.d.ts +55 -0
- package/dist/components/tabs.js +81 -0
- package/dist/components/technonomicon.d.ts +192 -0
- package/dist/components/technonomicon.js +121 -0
- package/dist/components/textarea.d.ts +9 -0
- package/dist/components/textarea.js +11 -0
- package/dist/components/toaster.d.ts +4 -0
- package/dist/components/toaster.js +13 -0
- package/dist/components/tool-call-chip.d.ts +22 -0
- package/dist/components/tool-call-chip.js +100 -0
- package/dist/components/tooltip.d.ts +41 -0
- package/dist/components/tooltip.js +169 -0
- package/dist/components/trading-card.d.ts +60 -0
- package/dist/components/trading-card.js +13 -0
- package/dist/components/undo-action.d.ts +52 -0
- package/dist/components/undo-action.js +100 -0
- package/dist/components/undo-toast.d.ts +21 -0
- package/dist/components/undo-toast.js +58 -0
- package/dist/components/visually-hidden.d.ts +9 -0
- package/dist/components/visually-hidden.js +11 -0
- package/dist/components/window.d.ts +65 -0
- package/dist/components/window.js +31 -0
- package/dist/components/wizard.d.ts +17 -0
- package/dist/components/wizard.js +19 -0
- package/dist/enhance/cigarette-card.d.ts +8 -0
- package/dist/enhance/cigarette-card.js +33 -0
- package/dist/enhance/sketch.d.ts +16 -0
- package/dist/enhance/sketch.js +130 -0
- package/dist/form-error.d.ts +7 -0
- package/dist/form-error.js +14 -0
- package/dist/form-fields.d.ts +15 -0
- package/dist/form-fields.js +250 -0
- package/dist/form-schema.d.ts +166 -0
- package/dist/form-schema.js +398 -0
- package/dist/form.d.ts +30 -0
- package/dist/form.js +20 -0
- package/dist/lib/cn.d.ts +8 -0
- package/dist/lib/cn.js +12 -0
- package/dist/lib/format-money.d.ts +2 -0
- package/dist/lib/format-money.js +22 -0
- package/dist/lib/glide.d.ts +12 -0
- package/dist/lib/glide.js +143 -0
- package/dist/lib/ime.d.ts +3 -0
- package/dist/lib/ime.js +8 -0
- package/dist/lib/motion-spring.d.ts +27 -0
- package/dist/lib/motion-spring.js +36 -0
- package/dist/lib/motion.d.ts +51 -0
- package/dist/lib/motion.js +161 -0
- package/dist/lib/reorder-motion.d.ts +13 -0
- package/dist/lib/reorder-motion.js +201 -0
- package/dist/lib/seed-hash.d.ts +6 -0
- package/dist/lib/seed-hash.js +13 -0
- package/dist/lib/tanstack-highlight.d.ts +4 -0
- package/dist/lib/tanstack-highlight.js +41 -0
- package/dist/schema-form.d.ts +120 -0
- package/dist/schema-form.js +129 -0
- package/dist/styles.css +18814 -0
- package/docs/api/README.md +134 -0
- package/docs/api/blocks/app-bar.md +227 -0
- package/docs/api/blocks/app-status-bar.md +257 -0
- package/docs/api/blocks/async-state.md +53 -0
- package/docs/api/blocks/card-grid.md +31 -0
- package/docs/api/blocks/command-menu.md +89 -0
- package/docs/api/blocks/composer.md +40 -0
- package/docs/api/blocks/empty-state.md +97 -0
- package/docs/api/blocks/faq-chat.md +147 -0
- package/docs/api/blocks/game-share-card.md +40 -0
- package/docs/api/blocks/launcher-grid.md +86 -0
- package/docs/api/blocks/listing-detail.md +51 -0
- package/docs/api/blocks/loading-state.md +21 -0
- package/docs/api/blocks/mosaic.md +94 -0
- package/docs/api/blocks/permission-grid.md +79 -0
- package/docs/api/blocks/pricing-card.md +90 -0
- package/docs/api/blocks/release-tag.md +62 -0
- package/docs/api/blocks/resource-select.md +126 -0
- package/docs/api/blocks/result-state.md +42 -0
- package/docs/api/blocks/section-card.md +42 -0
- package/docs/api/blocks/section-heading.md +61 -0
- package/docs/api/blocks/spec-sheet.md +400 -0
- package/docs/api/blocks/status-tray.md +141 -0
- package/docs/api/blocks/upload-queue.md +110 -0
- package/docs/api/components/action-row.md +51 -0
- package/docs/api/components/activity-line.md +103 -0
- package/docs/api/components/alert.md +60 -0
- package/docs/api/components/badge.md +77 -0
- package/docs/api/components/button.md +94 -0
- package/docs/api/components/card.md +165 -0
- package/docs/api/components/chat-bubble.md +66 -0
- package/docs/api/components/checkbox-bead.md +20 -0
- package/docs/api/components/checkbox.md +42 -0
- package/docs/api/components/code-input.md +61 -0
- package/docs/api/components/combobox.md +303 -0
- package/docs/api/components/confirm-action.md +58 -0
- package/docs/api/components/copy-field.md +106 -0
- package/docs/api/components/count-badge.md +39 -0
- package/docs/api/components/cover-pattern.md +43 -0
- package/docs/api/components/data-list.md +80 -0
- package/docs/api/components/dialog.md +235 -0
- package/docs/api/components/diff-view.md +59 -0
- package/docs/api/components/disclosure.md +71 -0
- package/docs/api/components/dropdown-menu.md +269 -0
- package/docs/api/components/field-guide.md +174 -0
- package/docs/api/components/field.md +252 -0
- package/docs/api/components/game-feedback.md +157 -0
- package/docs/api/components/game-hud.md +359 -0
- package/docs/api/components/generation-frame.md +59 -0
- package/docs/api/components/hold-button.md +44 -0
- package/docs/api/components/icon-button.md +56 -0
- package/docs/api/components/icon-checkbox.md +42 -0
- package/docs/api/components/icon-choice-mark.md +31 -0
- package/docs/api/components/icon-lamp.md +92 -0
- package/docs/api/components/icon-radio.md +42 -0
- package/docs/api/components/inline-code.md +21 -0
- package/docs/api/components/input-group.md +53 -0
- package/docs/api/components/input.md +54 -0
- package/docs/api/components/item-grid.md +262 -0
- package/docs/api/components/keycap.md +102 -0
- package/docs/api/components/label.md +40 -0
- package/docs/api/components/listbox.md +131 -0
- package/docs/api/components/loader-blobs.md +9 -0
- package/docs/api/components/loader.md +51 -0
- package/docs/api/components/markdown.md +91 -0
- package/docs/api/components/minimap-labels.md +84 -0
- package/docs/api/components/monogram.md +96 -0
- package/docs/api/components/motion-visibility.md +9 -0
- package/docs/api/components/navigation-rail.md +37 -0
- package/docs/api/components/overlay-root.md +92 -0
- package/docs/api/components/pixel-number.md +120 -0
- package/docs/api/components/popover.md +80 -0
- package/docs/api/components/presence.md +106 -0
- package/docs/api/components/price-tag.md +48 -0
- package/docs/api/components/progress.md +90 -0
- package/docs/api/components/prose.md +48 -0
- package/docs/api/components/quota-meter.md +47 -0
- package/docs/api/components/radio-bead.md +9 -0
- package/docs/api/components/radio-group.md +126 -0
- package/docs/api/components/resize-handle.md +46 -0
- package/docs/api/components/rough-drawing.md +122 -0
- package/docs/api/components/rough.md +66 -0
- package/docs/api/components/scroll-area.md +22 -0
- package/docs/api/components/scroll-motion.md +9 -0
- package/docs/api/components/scroll-to-bottom-blob-state.md +162 -0
- package/docs/api/components/scroll-to-bottom-blob.md +73 -0
- package/docs/api/components/select.md +39 -0
- package/docs/api/components/send-action.md +58 -0
- package/docs/api/components/separator.md +38 -0
- package/docs/api/components/skeleton.md +31 -0
- package/docs/api/components/slider.md +70 -0
- package/docs/api/components/stat.md +42 -0
- package/docs/api/components/status-bead.md +19 -0
- package/docs/api/components/status-mark.md +19 -0
- package/docs/api/components/status.md +105 -0
- package/docs/api/components/store-card.md +53 -0
- package/docs/api/components/surface.md +161 -0
- package/docs/api/components/switch-bead.md +9 -0
- package/docs/api/components/switch.md +40 -0
- package/docs/api/components/tabs.md +138 -0
- package/docs/api/components/technonomicon.md +773 -0
- package/docs/api/components/textarea.md +40 -0
- package/docs/api/components/toaster.md +29 -0
- package/docs/api/components/tool-call-chip.md +68 -0
- package/docs/api/components/tooltip.md +113 -0
- package/docs/api/components/trading-card.md +95 -0
- package/docs/api/components/undo-action.md +117 -0
- package/docs/api/components/undo-toast.md +49 -0
- package/docs/api/components/visually-hidden.md +37 -0
- package/docs/api/components/window.md +291 -0
- package/docs/api/components/wizard.md +65 -0
- package/docs/api/enhance/cigarette-card.md +52 -0
- package/docs/api/enhance/sketch.md +42 -0
- package/docs/api/form.md +1623 -0
- package/docs/api/lib/cn.md +46 -0
- package/docs/api/lib/format-money.md +30 -0
- package/docs/api/lib/glide.md +36 -0
- package/docs/api/lib/ime.md +29 -0
- package/docs/api/lib/motion-spring.md +91 -0
- package/docs/api/lib/motion.md +212 -0
- package/docs/api/lib/reorder-motion.md +46 -0
- package/docs/api/lib/seed-hash.md +31 -0
- package/docs/api/lib/tanstack-highlight.md +40 -0
- package/package.json +597 -3
- package/src/app-status-bar.css +158 -0
- package/src/fonts.css +1 -0
- package/src/status-tray.css +282 -0
- package/src/styles.css +10177 -0
package/README.md
CHANGED
|
@@ -1,3 +1,1126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Caps (`@fungi.computer/caps`)
|
|
2
2
|
|
|
3
|
-
|
|
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`.
|