@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Only the pressed-button card turns over; a preview card is a link. */
|
|
2
|
+
const CARD = "button.caps-cigarette-card";
|
|
3
|
+
/** Show one face of a server-rendered CigaretteCard. */
|
|
4
|
+
export function setCigaretteCardFlipped(card, flipped) {
|
|
5
|
+
card.setAttribute("aria-pressed", String(flipped));
|
|
6
|
+
card.dataset.flipped = String(flipped);
|
|
7
|
+
card
|
|
8
|
+
.querySelector(".caps-cigarette-card-front")
|
|
9
|
+
?.setAttribute("aria-hidden", String(flipped));
|
|
10
|
+
card
|
|
11
|
+
.querySelector(".caps-cigarette-card-back")
|
|
12
|
+
?.setAttribute("aria-hidden", String(!flipped));
|
|
13
|
+
}
|
|
14
|
+
function onClick(event) {
|
|
15
|
+
// A hydrated CigaretteCard handles its own click and marks it prevented.
|
|
16
|
+
if (event.defaultPrevented || !(event.target instanceof Element))
|
|
17
|
+
return;
|
|
18
|
+
const card = event.target.closest(CARD);
|
|
19
|
+
if (!card)
|
|
20
|
+
return;
|
|
21
|
+
setCigaretteCardFlipped(card, card.getAttribute("aria-pressed") !== "true");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Flip server-rendered CigaretteCards on click, tap, Enter or Space. The card
|
|
25
|
+
* is a native button, so the keyboard reaches it as a click; this only
|
|
26
|
+
* toggles the pressed state. Returns a cleanup that removes the listener.
|
|
27
|
+
*/
|
|
28
|
+
export function enhanceCigaretteCards(root = document) {
|
|
29
|
+
root.addEventListener("click", onClick);
|
|
30
|
+
return () => {
|
|
31
|
+
root.removeEventListener("click", onClick);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-in pencil layer for server-rendered field-guide pages.
|
|
3
|
+
*
|
|
4
|
+
* Markup contract, all plain attributes so any host can use it without React:
|
|
5
|
+
* - `data-sketch-root` marks a positioned host; its overlay is drawn inside it.
|
|
6
|
+
* - `data-sketch="circle" | "underline"` marks inline text to annotate.
|
|
7
|
+
* - `data-sketch-leader-x` / `data-sketch-leader-y` (fractions of the target
|
|
8
|
+
* box) on a note draw a leader from the note to the `data-sketch-target`
|
|
9
|
+
* inside the same `data-sketch-item`. The layout decides whether leaders
|
|
10
|
+
* apply by setting `--caps-sketch-leader: on` on the note.
|
|
11
|
+
*
|
|
12
|
+
* Strokes use `currentColor`, so a theme switch recolors them without a
|
|
13
|
+
* redraw. The host receives `data-sketch-drawn` once annotations exist, which
|
|
14
|
+
* lets Caps hide the static CSS fallbacks they replace.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sketch(root?: ParentNode): () => void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import roughImport from "roughjs";
|
|
2
|
+
function resolveRough(entry) {
|
|
3
|
+
return "svg" in entry ? entry : entry.default;
|
|
4
|
+
}
|
|
5
|
+
const rough = resolveRough(roughImport);
|
|
6
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
7
|
+
/**
|
|
8
|
+
* The opt-in pencil layer for server-rendered field-guide pages.
|
|
9
|
+
*
|
|
10
|
+
* Markup contract, all plain attributes so any host can use it without React:
|
|
11
|
+
* - `data-sketch-root` marks a positioned host; its overlay is drawn inside it.
|
|
12
|
+
* - `data-sketch="circle" | "underline"` marks inline text to annotate.
|
|
13
|
+
* - `data-sketch-leader-x` / `data-sketch-leader-y` (fractions of the target
|
|
14
|
+
* box) on a note draw a leader from the note to the `data-sketch-target`
|
|
15
|
+
* inside the same `data-sketch-item`. The layout decides whether leaders
|
|
16
|
+
* apply by setting `--caps-sketch-leader: on` on the note.
|
|
17
|
+
*
|
|
18
|
+
* Strokes use `currentColor`, so a theme switch recolors them without a
|
|
19
|
+
* redraw. The host receives `data-sketch-drawn` once annotations exist, which
|
|
20
|
+
* lets Caps hide the static CSS fallbacks they replace.
|
|
21
|
+
*/
|
|
22
|
+
export function sketch(root = document) {
|
|
23
|
+
const hosts = Array.from(root.querySelectorAll("[data-sketch-root]"));
|
|
24
|
+
const draw = () => {
|
|
25
|
+
for (const [index, host] of hosts.entries())
|
|
26
|
+
drawHost(host, index);
|
|
27
|
+
};
|
|
28
|
+
draw();
|
|
29
|
+
// Older engines and jsdom lack ResizeObserver; they draw once.
|
|
30
|
+
const observer = "ResizeObserver" in globalThis ? new ResizeObserver(draw) : undefined;
|
|
31
|
+
for (const host of hosts)
|
|
32
|
+
observer?.observe(host);
|
|
33
|
+
// Web fonts move annotated text once they load; jsdom has no font set.
|
|
34
|
+
if ("fonts" in document)
|
|
35
|
+
void document.fonts.ready.then(draw);
|
|
36
|
+
return () => {
|
|
37
|
+
observer?.disconnect();
|
|
38
|
+
for (const host of hosts) {
|
|
39
|
+
host.querySelector(":scope > .caps-sketch-overlay")?.remove();
|
|
40
|
+
delete host.dataset.sketchDrawn;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function drawHost(host, index) {
|
|
45
|
+
host.querySelector(":scope > .caps-sketch-overlay")?.remove();
|
|
46
|
+
const bounds = host.getBoundingClientRect();
|
|
47
|
+
if (!bounds.width || !bounds.height)
|
|
48
|
+
return;
|
|
49
|
+
const overlay = document.createElementNS(SVG, "svg");
|
|
50
|
+
overlay.setAttribute("viewBox", `0 0 ${String(bounds.width)} ${String(bounds.height)}`);
|
|
51
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
52
|
+
overlay.setAttribute("focusable", "false");
|
|
53
|
+
overlay.classList.add("caps-sketch-overlay");
|
|
54
|
+
const generator = rough.svg(overlay);
|
|
55
|
+
for (const [markIndex, mark] of host
|
|
56
|
+
.querySelectorAll("[data-sketch]")
|
|
57
|
+
.entries()) {
|
|
58
|
+
const annotation = drawMark(generator, bounds, mark, {
|
|
59
|
+
roughness: 1.1,
|
|
60
|
+
stroke: "currentColor",
|
|
61
|
+
strokeWidth: 1.3,
|
|
62
|
+
seed: 2601 + index * 10 + markIndex,
|
|
63
|
+
});
|
|
64
|
+
if (annotation)
|
|
65
|
+
overlay.append(annotation);
|
|
66
|
+
}
|
|
67
|
+
for (const [noteIndex, note] of host
|
|
68
|
+
.querySelectorAll("[data-sketch-leader-x]")
|
|
69
|
+
.entries()) {
|
|
70
|
+
const leader = drawLeader(generator, bounds, note, {
|
|
71
|
+
roughness: 1.2,
|
|
72
|
+
stroke: "currentColor",
|
|
73
|
+
strokeWidth: 1.2,
|
|
74
|
+
seed: 3101 + index * 10 + noteIndex,
|
|
75
|
+
});
|
|
76
|
+
if (leader)
|
|
77
|
+
overlay.append(leader);
|
|
78
|
+
}
|
|
79
|
+
host.append(overlay);
|
|
80
|
+
host.dataset.sketchDrawn = "";
|
|
81
|
+
}
|
|
82
|
+
function drawMark(generator, host, mark, options) {
|
|
83
|
+
const box = mark.getBoundingClientRect();
|
|
84
|
+
if (!box.width || !box.height)
|
|
85
|
+
return undefined;
|
|
86
|
+
const x = box.left - host.left;
|
|
87
|
+
const y = box.top - host.top;
|
|
88
|
+
switch (mark.dataset.sketch) {
|
|
89
|
+
case "circle":
|
|
90
|
+
return generator.ellipse(x + box.width / 2, y + box.height / 2, box.width + 9, box.height + 7, options);
|
|
91
|
+
case "underline":
|
|
92
|
+
return generator.line(x, y + box.height + 3, x + box.width, y + box.height + 3, options);
|
|
93
|
+
default:
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function drawLeader(generator, host, note, options) {
|
|
98
|
+
if (getComputedStyle(note).getPropertyValue("--caps-sketch-leader").trim() !==
|
|
99
|
+
"on")
|
|
100
|
+
return undefined;
|
|
101
|
+
const target = note
|
|
102
|
+
.closest("[data-sketch-item]")
|
|
103
|
+
?.querySelector("[data-sketch-target]");
|
|
104
|
+
const fx = Number(note.dataset.sketchLeaderX);
|
|
105
|
+
const fy = Number(note.dataset.sketchLeaderY);
|
|
106
|
+
if (!target || !Number.isFinite(fx) || !Number.isFinite(fy))
|
|
107
|
+
return undefined;
|
|
108
|
+
const targetBox = target.getBoundingClientRect();
|
|
109
|
+
const noteBox = note.getBoundingClientRect();
|
|
110
|
+
const endX = targetBox.left - host.left + targetBox.width * fx;
|
|
111
|
+
const endY = targetBox.top - host.top + targetBox.height * fy;
|
|
112
|
+
const startX = Math.min(Math.max(endX, noteBox.left - host.left + 18), noteBox.right - host.left - 18);
|
|
113
|
+
const startY = noteBox.top - host.top - 8;
|
|
114
|
+
const length = Math.hypot(endX - startX, endY - startY);
|
|
115
|
+
if (length < 12)
|
|
116
|
+
return undefined;
|
|
117
|
+
const group = document.createElementNS(SVG, "g");
|
|
118
|
+
const bend = Math.min(16, length / 10);
|
|
119
|
+
group.append(generator.curve([
|
|
120
|
+
[startX, startY],
|
|
121
|
+
[(startX + endX) / 2 + bend, (startY + endY) / 2],
|
|
122
|
+
[endX, endY],
|
|
123
|
+
], options));
|
|
124
|
+
const angle = Math.atan2(endY - startY, endX - startX);
|
|
125
|
+
for (const side of [-1, 1]) {
|
|
126
|
+
const wing = angle + Math.PI + side * 0.45;
|
|
127
|
+
group.append(generator.line(endX, endY, endX + Math.cos(wing) * 11, endY + Math.sin(wing) * 11, options));
|
|
128
|
+
}
|
|
129
|
+
return group;
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** First readable TanStack string error or Standard Schema issue. */
|
|
3
|
+
export declare function fieldError(meta: {
|
|
4
|
+
errors: readonly unknown[];
|
|
5
|
+
}): string | undefined;
|
|
6
|
+
/** Effect's maintained Standard Schema adapter, including nested issue paths. */
|
|
7
|
+
export declare const effectValidator: typeof Schema.toStandardSchemaV1;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
const readStringError = Schema.decodeUnknownOption(Schema.String);
|
|
3
|
+
const readIssueError = Schema.decodeUnknownOption(Schema.Struct({ message: Schema.String }));
|
|
4
|
+
/** First readable TanStack string error or Standard Schema issue. */
|
|
5
|
+
export function fieldError(meta) {
|
|
6
|
+
for (const error of meta.errors) {
|
|
7
|
+
const message = Option.getOrUndefined(Option.orElse(Option.map(readIssueError(error), (issue) => issue.message), () => readStringError(error)));
|
|
8
|
+
if (message !== undefined && message.length > 0)
|
|
9
|
+
return message;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Effect's maintained Standard Schema adapter, including nested issue paths. */
|
|
14
|
+
export const effectValidator = Schema.toStandardSchemaV1;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FormValues } from "./form-schema.js";
|
|
2
|
+
import type { SchemaFormModel } from "./schema-form.js";
|
|
3
|
+
/** Props accepted by FormFields. */
|
|
4
|
+
export type FormFieldsProps<V extends FormValues> = {
|
|
5
|
+
model: SchemaFormModel<V>;
|
|
6
|
+
/** Render only these fields, in this order, without section headings. */
|
|
7
|
+
names?: readonly (keyof V & string)[];
|
|
8
|
+
/** Disable every field, such as while an uncertain request is retried. */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Render a schema form's fields from its schema: every section in order, or
|
|
13
|
+
* only `names` (for placing fields in Screen cells or custom layouts).
|
|
14
|
+
*/
|
|
15
|
+
export declare function FormFields<V extends FormValues>({ model, names, disabled, }: FormFieldsProps<V>): (import("react/jsx-runtime").JSX.Element | null)[];
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useField, useSelector, } from "@tanstack/react-form";
|
|
3
|
+
import { Option, Schema } from "effect";
|
|
4
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
|
+
import { Button } from "./components/button.js";
|
|
7
|
+
import { UploadQueue } from "./blocks/upload-queue.js";
|
|
8
|
+
import { Checkbox } from "./components/checkbox.js";
|
|
9
|
+
import { Combobox, ComboboxTrigger } from "./components/combobox.js";
|
|
10
|
+
import { Field, FieldSet, FormSection, NumberField, } from "./components/field.js";
|
|
11
|
+
import { IconCheckbox } from "./components/icon-checkbox.js";
|
|
12
|
+
import { IconRadio } from "./components/icon-radio.js";
|
|
13
|
+
import { Input } from "./components/input.js";
|
|
14
|
+
import { InputGroup, InputGroupAddon } from "./components/input-group.js";
|
|
15
|
+
import { RadioGroup, RadioGroupCard, RadioGroupItem, } from "./components/radio-group.js";
|
|
16
|
+
import { Select } from "./components/select.js";
|
|
17
|
+
import { Switch } from "./components/switch.js";
|
|
18
|
+
import { Textarea } from "./components/textarea.js";
|
|
19
|
+
import { VisuallyHidden } from "./components/visually-hidden.js";
|
|
20
|
+
import { fieldError } from "./form-error.js";
|
|
21
|
+
// TanStack hands field values back untyped; each control reads its own kind.
|
|
22
|
+
const readText = Schema.decodeUnknownOption(Schema.String);
|
|
23
|
+
const readFlag = Schema.decodeUnknownOption(Schema.Boolean);
|
|
24
|
+
const readAmount = Schema.decodeUnknownOption(Schema.NullOr(Schema.Finite));
|
|
25
|
+
const readTexts = Schema.decodeUnknownOption(Schema.Array(Schema.String));
|
|
26
|
+
const filesSchema = Schema.Array(Schema.instanceOf(File));
|
|
27
|
+
const readFiles = Schema.decodeUnknownOption(filesSchema);
|
|
28
|
+
const inputSchema = Schema.Union([
|
|
29
|
+
Schema.String,
|
|
30
|
+
Schema.Finite,
|
|
31
|
+
Schema.Boolean,
|
|
32
|
+
Schema.Null,
|
|
33
|
+
Schema.Undefined,
|
|
34
|
+
Schema.Array(Schema.String),
|
|
35
|
+
filesSchema,
|
|
36
|
+
]);
|
|
37
|
+
const readInput = Schema.decodeUnknownOption(inputSchema);
|
|
38
|
+
const text = (field) => Option.getOrElse(readText(field.state.value), () => "");
|
|
39
|
+
const flag = (field) => Option.getOrElse(readFlag(field.state.value), () => false);
|
|
40
|
+
const amount = (field) => Option.getOrElse(readAmount(field.state.value), () => null);
|
|
41
|
+
const texts = (field) => Option.getOrElse(readTexts(field.state.value), () => []);
|
|
42
|
+
const files = (field) => Option.getOrElse(readFiles(field.state.value), () => []);
|
|
43
|
+
const input = (field) => Option.getOrElse(readInput(field.state.value), () => null);
|
|
44
|
+
function SecretInput({ field, spec, locked, ...control }) {
|
|
45
|
+
const [shown, setShown] = useState(false);
|
|
46
|
+
return (_jsxs(InputGroup, { children: [_jsx(Input, { ...control, name: spec.name, disabled: locked, type: shown ? "text" : "password", autoComplete: "off", spellCheck: false, className: "caps-field-secret", placeholder: spec.placeholder, readOnly: spec.readOnly, value: text(field), onBlur: field.handleBlur, onChange: (event) => {
|
|
47
|
+
field.handleChange(event.currentTarget.value);
|
|
48
|
+
} }), _jsx(Button, { type: "button", variant: "outline", size: "icon", "aria-label": shown ? "Hide" : "Show", "aria-pressed": shown, disabled: locked, onClick: () => {
|
|
49
|
+
setShown((current) => !current);
|
|
50
|
+
}, children: shown ? _jsx(EyeOff, { "aria-hidden": "true" }) : _jsx(Eye, { "aria-hidden": "true" }) })] }));
|
|
51
|
+
}
|
|
52
|
+
const inputTypes = { text: "text", email: "email", url: "url", tel: "tel" };
|
|
53
|
+
/** The picker supplies File objects; queue rows use only their metadata. */
|
|
54
|
+
function FileInput({ spec, field, locked, ...control }) {
|
|
55
|
+
const selected = files(field);
|
|
56
|
+
const disabled = locked || spec.readOnly === true;
|
|
57
|
+
const picker = useRef(null);
|
|
58
|
+
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx(VisuallyHidden, { asChild: true, children: _jsx("input", { ...control, ref: picker, type: "file", tabIndex: -1, required: control.required === true && selected.length === 0, name: spec.name, accept: spec.accept, multiple: spec.multiple, disabled: disabled, onBlur: field.handleBlur, onChange: (event) => {
|
|
59
|
+
field.handleChange(Array.from(event.currentTarget.files ?? []));
|
|
60
|
+
// The queue owns the selection display, including resets and removal.
|
|
61
|
+
// Clear the native picker so choosing the same file fires another change.
|
|
62
|
+
event.currentTarget.value = "";
|
|
63
|
+
} }) }), _jsx(Button, { type: "button", variant: "outline", "aria-label": `Choose files for ${spec.label}`, "aria-describedby": control["aria-describedby"], "aria-invalid": control["aria-invalid"], disabled: disabled, onBlur: field.handleBlur, onClick: () => {
|
|
64
|
+
picker.current?.click();
|
|
65
|
+
}, children: "Choose files" }), _jsx(UploadQueue, { items: selected.map((file, index) => ({
|
|
66
|
+
id: String(index),
|
|
67
|
+
name: file.name,
|
|
68
|
+
preview: { kind: "file" },
|
|
69
|
+
progress: 0,
|
|
70
|
+
progressLabel: `${String(file.size)} bytes`,
|
|
71
|
+
statusLabel: "Selected",
|
|
72
|
+
actions: disabled
|
|
73
|
+
? {}
|
|
74
|
+
: {
|
|
75
|
+
remove: {
|
|
76
|
+
label: "Remove",
|
|
77
|
+
accessibleLabel: `Remove ${file.name}`,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})), onRemove: (id) => {
|
|
81
|
+
field.handleChange(selected.filter((_, index) => String(index) !== id));
|
|
82
|
+
}, onCancel: () => undefined, onRetry: () => undefined })] }));
|
|
83
|
+
}
|
|
84
|
+
/** One control, wired by the Field around it (Field clones these props in). */
|
|
85
|
+
function SpecControl({ spec, field, locked, ...control }) {
|
|
86
|
+
const setText = (value) => {
|
|
87
|
+
field.handleChange(value);
|
|
88
|
+
};
|
|
89
|
+
const shared = {
|
|
90
|
+
...control,
|
|
91
|
+
name: spec.name,
|
|
92
|
+
disabled: locked,
|
|
93
|
+
onBlur: field.handleBlur,
|
|
94
|
+
};
|
|
95
|
+
switch (spec.kind) {
|
|
96
|
+
case "text":
|
|
97
|
+
case "email":
|
|
98
|
+
case "url":
|
|
99
|
+
case "tel":
|
|
100
|
+
return (_jsx(Input, { ...shared, type: inputTypes[spec.kind], inputMode: spec.kind === "text" ? undefined : spec.kind, autoComplete: spec.autoComplete, placeholder: spec.placeholder, maxLength: spec.maxLength, readOnly: spec.readOnly, value: text(field), onChange: (event) => {
|
|
101
|
+
setText(event.currentTarget.value);
|
|
102
|
+
} }));
|
|
103
|
+
case "textarea":
|
|
104
|
+
return (_jsx(Textarea, { ...shared, rows: spec.rows ?? 3, placeholder: spec.placeholder, maxLength: spec.maxLength, readOnly: spec.readOnly, value: text(field), onChange: (event) => {
|
|
105
|
+
setText(event.currentTarget.value);
|
|
106
|
+
} }));
|
|
107
|
+
case "secret":
|
|
108
|
+
return (_jsx(SecretInput, { ...control, field: field, spec: spec, locked: locked }));
|
|
109
|
+
case "money":
|
|
110
|
+
return (_jsxs(InputGroup, { children: [_jsx(InputGroupAddon, { className: "caps-field-currency", children: spec.currency ?? "USD" }), _jsx(Input, { ...shared, inputMode: "decimal", autoComplete: "off", className: "caps-field-amount", placeholder: spec.placeholder ?? "0.00", readOnly: spec.readOnly, value: text(field), onChange: (event) => {
|
|
111
|
+
setText(event.currentTarget.value);
|
|
112
|
+
} })] }));
|
|
113
|
+
case "date":
|
|
114
|
+
return (_jsx(Input, { ...shared, type: "date", readOnly: spec.readOnly, value: text(field), onChange: (event) => {
|
|
115
|
+
setText(event.currentTarget.value);
|
|
116
|
+
} }));
|
|
117
|
+
case "number":
|
|
118
|
+
return (_jsx(NumberField, { ...shared, ...(spec.min === undefined ? {} : { min: spec.min }), ...(spec.max === undefined ? {} : { max: spec.max }), ...(spec.step === undefined ? {} : { step: spec.step }), ...(spec.unit === undefined ? {} : { unit: spec.unit }), ...(spec.wrap === undefined ? {} : { wrap: spec.wrap }), disabled: locked || spec.readOnly === true, decrementLabel: `Decrease ${spec.label}`, incrementLabel: `Increase ${spec.label}`, value: amount(field), onValueChange: (value) => {
|
|
119
|
+
field.handleChange(value);
|
|
120
|
+
} }));
|
|
121
|
+
case "select":
|
|
122
|
+
return (_jsxs(Select, { ...shared, value: text(field), onChange: (event) => {
|
|
123
|
+
setText(event.currentTarget.value);
|
|
124
|
+
}, children: [spec.placeholder === undefined ? null : (_jsx("option", { value: "", disabled: true, children: spec.placeholder })), spec.options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
|
|
125
|
+
case "combobox":
|
|
126
|
+
return (_jsx(Combobox, { id: control.id ?? spec.name, "aria-describedby": control["aria-describedby"], "aria-invalid": control["aria-invalid"], "aria-required": control["aria-required"], items: spec.options, getKey: (option) => option.value, getLabel: (option) => option.label, isDisabled: (option) => option.disabled || locked || spec.readOnly === true, value: text(field), ...(spec.placeholder === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: { placeholder: spec.placeholder }), trigger: _jsx(ComboboxTrigger, { name: spec.name, disabled: locked || spec.readOnly === true, onBlur: field.handleBlur, children: spec.options.find((option) => option.value === text(field))
|
|
129
|
+
?.label ??
|
|
130
|
+
spec.placeholder ??
|
|
131
|
+
"Choose…" }), inputProps: {
|
|
132
|
+
"aria-label": `Search ${spec.label}`,
|
|
133
|
+
disabled: locked || spec.readOnly === true,
|
|
134
|
+
}, onValueChange: (value) => {
|
|
135
|
+
setText(value ?? "");
|
|
136
|
+
field.handleBlur();
|
|
137
|
+
} }));
|
|
138
|
+
case "file":
|
|
139
|
+
return (_jsx(FileInput, { ...control, spec: spec, field: field, locked: locked }));
|
|
140
|
+
case "checkbox":
|
|
141
|
+
return (_jsx(Checkbox, { ...shared, disabled: locked || spec.readOnly === true, checked: flag(field), onChange: (event) => {
|
|
142
|
+
field.handleChange(event.currentTarget.checked);
|
|
143
|
+
} }));
|
|
144
|
+
case "switch":
|
|
145
|
+
return (_jsx(Switch, { ...shared, disabled: locked || spec.readOnly === true, checked: flag(field), onCheckedChange: (checked) => {
|
|
146
|
+
field.handleChange(checked);
|
|
147
|
+
} }));
|
|
148
|
+
case "radio":
|
|
149
|
+
case "icon-radio":
|
|
150
|
+
case "checkboxes":
|
|
151
|
+
case "icon-checkboxes":
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const groupKinds = new Set([
|
|
156
|
+
"radio",
|
|
157
|
+
"icon-radio",
|
|
158
|
+
"checkboxes",
|
|
159
|
+
"icon-checkboxes",
|
|
160
|
+
]);
|
|
161
|
+
/** Choice groups: a legend names the group; each option names its input. */
|
|
162
|
+
function SpecGroup({ spec, field, locked, error, }) {
|
|
163
|
+
const disabled = locked || spec.readOnly === true;
|
|
164
|
+
const legend = spec.label;
|
|
165
|
+
if (spec.kind === "checkboxes" || spec.kind === "icon-checkboxes") {
|
|
166
|
+
const selected = texts(field);
|
|
167
|
+
const toggle = (value, on) => {
|
|
168
|
+
field.handleChange(on
|
|
169
|
+
? [...selected, value]
|
|
170
|
+
: selected.filter((current) => current !== value));
|
|
171
|
+
};
|
|
172
|
+
return (_jsx(FieldSet, { legend: legend, description: spec.help, disabled: disabled, error: error ?? null, children: spec.kind === "icon-checkboxes" ? (_jsx("div", { className: "caps-field-icon-choices", children: spec.options.map((option) => (_jsx(IconCheckbox, { name: spec.name, value: option.value, label: option.label, ...(option.caption === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { caption: option.caption }), icon: option.icon, disabled: option.disabled, checked: selected.includes(option.value), onBlur: field.handleBlur, onChange: (event) => {
|
|
175
|
+
toggle(option.value, event.currentTarget.checked);
|
|
176
|
+
} }, option.value))) })) : (spec.options.map((option) => (_jsx(Field, { label: option.label, description: option.description, orientation: "inline", children: _jsx(Checkbox, { name: spec.name, value: option.value, disabled: option.disabled, checked: selected.includes(option.value), onBlur: field.handleBlur, onChange: (event) => {
|
|
177
|
+
toggle(option.value, event.currentTarget.checked);
|
|
178
|
+
} }) }, option.value)))) }));
|
|
179
|
+
}
|
|
180
|
+
const cards = spec.options.some((option) => option.description !== undefined || option.meta !== undefined);
|
|
181
|
+
return (_jsx(FieldSet, { legend: legend, description: spec.help, disabled: disabled, error: error ?? null, children: _jsx(RadioGroup, { name: spec.name, "aria-label": legend, value: text(field), onValueChange: (next) => {
|
|
182
|
+
field.handleChange(next);
|
|
183
|
+
field.handleBlur();
|
|
184
|
+
},
|
|
185
|
+
// Icon keys and choice cards sit in a wrapping row.
|
|
186
|
+
orientation: spec.kind === "icon-radio" || cards ? "horizontal" : "vertical", children: spec.options.map((option) => spec.kind === "icon-radio" ? (_jsx(IconRadio, { value: option.value, label: option.label, ...(option.caption === undefined
|
|
187
|
+
? {}
|
|
188
|
+
: { caption: option.caption }), icon: option.icon, disabled: option.disabled }, option.value)) : cards ? (_jsx(RadioGroupCard, { value: option.value, title: option.label, description: option.description, meta: option.meta, disabled: option.disabled }, option.value)) : (_jsx(Field, { label: option.label, orientation: "inline", children: _jsx(RadioGroupItem, { value: option.value, disabled: option.disabled }) }, option.value))) }) }));
|
|
189
|
+
}
|
|
190
|
+
/** The `render` bailout: the caller's control with the built-in wiring. */
|
|
191
|
+
function RenderedControl({ spec, field, locked, error, render, ...control }) {
|
|
192
|
+
return render({
|
|
193
|
+
name: spec.name,
|
|
194
|
+
label: spec.label,
|
|
195
|
+
help: spec.help,
|
|
196
|
+
value: input(field),
|
|
197
|
+
onChange: (value) => {
|
|
198
|
+
field.handleChange(value);
|
|
199
|
+
},
|
|
200
|
+
onBlur: field.handleBlur,
|
|
201
|
+
error,
|
|
202
|
+
disabled: locked,
|
|
203
|
+
// Field clones the id and descriptions in, so its label names the control.
|
|
204
|
+
id: control.id ?? spec.name,
|
|
205
|
+
"aria-describedby": control["aria-describedby"],
|
|
206
|
+
"aria-invalid": error !== undefined,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function SchemaField({ form, spec, locked, }) {
|
|
210
|
+
const field = useField({ form, name: spec.name });
|
|
211
|
+
const submitted = useSelector(form.store, (state) => state.submissionAttempts > 0);
|
|
212
|
+
const error = field.state.meta.isBlurred || submitted
|
|
213
|
+
? fieldError(field.state.meta)
|
|
214
|
+
: undefined;
|
|
215
|
+
if (spec.render === undefined && groupKinds.has(spec.kind))
|
|
216
|
+
return (_jsx(SpecGroup, { spec: spec, field: field, locked: locked, error: error }));
|
|
217
|
+
const orientation = spec.kind === "checkbox"
|
|
218
|
+
? "inline"
|
|
219
|
+
: (spec.layout ?? (spec.kind === "switch" ? "row" : "stacked")) === "row"
|
|
220
|
+
? "horizontal"
|
|
221
|
+
: "vertical";
|
|
222
|
+
return (_jsx(Field, { label: spec.label, description: spec.help, icon: spec.icon, orientation: orientation, error: error, ...(spec.required === undefined ? {} : { required: spec.required }), ...(spec.optional === undefined ? {} : { optional: spec.optional }), children: spec.render === undefined ? (_jsx(SpecControl, { spec: spec, field: field, locked: locked })) : (_jsx(RenderedControl, { spec: spec, field: field, locked: locked, error: error, render: spec.render })) }));
|
|
223
|
+
}
|
|
224
|
+
const readValues = Schema.decodeUnknownOption(Schema.Record(Schema.String, inputSchema));
|
|
225
|
+
function Visible({ form, spec, locked, }) {
|
|
226
|
+
const when = spec.when;
|
|
227
|
+
const shown = useSelector(form.store, (state) => when === undefined
|
|
228
|
+
? true
|
|
229
|
+
: when(Option.getOrElse(readValues(state.values), () => ({}))));
|
|
230
|
+
if (!shown)
|
|
231
|
+
return null;
|
|
232
|
+
return _jsx(SchemaField, { form: form, spec: spec, locked: locked });
|
|
233
|
+
}
|
|
234
|
+
function SectionFields({ form, section, locked, }) {
|
|
235
|
+
const fields = section.fields.map((spec) => (_jsx(Visible, { form: form, spec: spec, locked: locked }, spec.name)));
|
|
236
|
+
return section.title === undefined ? (fields) : (_jsx(FormSection, { title: section.title, description: section.description, children: fields }));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Render a schema form's fields from its schema: every section in order, or
|
|
240
|
+
* only `names` (for placing fields in Screen cells or custom layouts).
|
|
241
|
+
*/
|
|
242
|
+
export function FormFields({ model, names, disabled = false, }) {
|
|
243
|
+
const form = model.form;
|
|
244
|
+
if (names !== undefined)
|
|
245
|
+
return names.map((name) => {
|
|
246
|
+
const spec = model.description.fields.find((field) => field.name === name);
|
|
247
|
+
return spec === undefined ? null : (_jsx(Visible, { form: form, spec: spec, locked: disabled }, name));
|
|
248
|
+
});
|
|
249
|
+
return model.description.sections.map((section) => (_jsx(SectionFields, { form: form, section: section, locked: disabled }, section.id ?? "")));
|
|
250
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@tanstack/react-form";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
/**
|
|
6
|
+
* The raw input one field holds: text, a number (or empty), a flag, or a set
|
|
7
|
+
* of chosen values or files. The schema turns it into the domain value at submit.
|
|
8
|
+
*/
|
|
9
|
+
export type FieldInput = string | number | boolean | null | undefined | readonly string[] | readonly File[];
|
|
10
|
+
/** The raw values a schema form holds: one entry per field name. */
|
|
11
|
+
export type FormValues = Readonly<Record<string, FieldInput>>;
|
|
12
|
+
/** Every control a schema field can render as. */
|
|
13
|
+
export type FieldKind = "text" | "email" | "url" | "tel" | "textarea" | "secret" | "money" | "date" | "number" | "select" | "combobox" | "file" | "radio" | "icon-radio" | "checkbox" | "switch" | "checkboxes" | "icon-checkboxes";
|
|
14
|
+
/** One choice's presentation, keyed by its value in `options`. */
|
|
15
|
+
export type OptionAnnotation = {
|
|
16
|
+
readonly label?: string;
|
|
17
|
+
/** A sentence under the label; radio options with one render as cards. */
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
/** A short readout on a radio card's trailing edge, such as a price. */
|
|
20
|
+
readonly meta?: string;
|
|
21
|
+
/** A Sigil or App icon: choices with icons render as icon keys. */
|
|
22
|
+
readonly icon?: ReactNode;
|
|
23
|
+
/** The short legend on an icon key; `label` stays the accessible name. */
|
|
24
|
+
readonly caption?: string;
|
|
25
|
+
readonly disabled?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** What a `render` annotation receives: the same wiring every built-in kind gets. */
|
|
28
|
+
export type FieldRenderProps<T extends FieldInput = FieldInput> = {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly help: ReactNode;
|
|
32
|
+
readonly value: T;
|
|
33
|
+
/** Set the field's raw value; validation runs as it would for a built-in kind. */
|
|
34
|
+
onChange(value: T): void;
|
|
35
|
+
/** Mark the field visited, so its error shows. */
|
|
36
|
+
readonly onBlur: () => void;
|
|
37
|
+
/** The visible error, after the person leaves the field or submits. */
|
|
38
|
+
readonly error: string | undefined;
|
|
39
|
+
readonly disabled: boolean;
|
|
40
|
+
/** Put these on the control so the Field's label, help and error name it. */
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly "aria-describedby": string | undefined;
|
|
43
|
+
readonly "aria-invalid": boolean;
|
|
44
|
+
};
|
|
45
|
+
/** Presentation for one schema field. The schema stays the single source. */
|
|
46
|
+
export type FieldAnnotation<T extends FieldInput = FieldInput> = {
|
|
47
|
+
/** The visible label and accessible name; defaults to the humanized key. */
|
|
48
|
+
readonly label?: string;
|
|
49
|
+
readonly help?: string;
|
|
50
|
+
/** Overrides the kind inferred from the schema type. */
|
|
51
|
+
readonly kind?: FieldKind;
|
|
52
|
+
readonly placeholder?: string;
|
|
53
|
+
/** The `sections` key this field sits in; unsectioned fields come first. */
|
|
54
|
+
readonly section?: string;
|
|
55
|
+
/** Sort position within its section; fields keep schema order otherwise. */
|
|
56
|
+
readonly order?: number;
|
|
57
|
+
/** `row` puts the control opposite its label (a settings row). */
|
|
58
|
+
readonly layout?: "stacked" | "row";
|
|
59
|
+
readonly required?: boolean;
|
|
60
|
+
/** Marks an optional field in a mostly required form. */
|
|
61
|
+
readonly optional?: boolean;
|
|
62
|
+
/** A decorative Sigil or App icon before the label. */
|
|
63
|
+
readonly icon?: ReactNode;
|
|
64
|
+
/** Labels, help and icons for literal choices, keyed by value. */
|
|
65
|
+
readonly options?: Readonly<Record<string, string | OptionAnnotation>>;
|
|
66
|
+
readonly currency?: string;
|
|
67
|
+
readonly unit?: string;
|
|
68
|
+
readonly min?: number;
|
|
69
|
+
readonly max?: number;
|
|
70
|
+
readonly step?: number;
|
|
71
|
+
readonly wrap?: boolean;
|
|
72
|
+
readonly rows?: number;
|
|
73
|
+
readonly maxLength?: number;
|
|
74
|
+
readonly autoComplete?: string;
|
|
75
|
+
/** Native file-picker filter; the schema owns file-type validation. */
|
|
76
|
+
readonly accept?: string;
|
|
77
|
+
readonly multiple?: boolean;
|
|
78
|
+
/** Per-file byte limit, enforced by the field's schema check. */
|
|
79
|
+
readonly maxBytes?: number;
|
|
80
|
+
readonly readOnly?: boolean;
|
|
81
|
+
/** Show the field only while this holds for the current raw values. */
|
|
82
|
+
when?(values: FormValues): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The bailout: draw this field's control yourself. Caps still owns the
|
|
85
|
+
* label, help, error, layout, validation and save bar.
|
|
86
|
+
*/
|
|
87
|
+
render?(field: FieldRenderProps<T>): ReactNode;
|
|
88
|
+
};
|
|
89
|
+
/** A titled group of fields, declared on the struct. */
|
|
90
|
+
export type SectionAnnotation = {
|
|
91
|
+
readonly title: string;
|
|
92
|
+
readonly description?: string;
|
|
93
|
+
};
|
|
94
|
+
/** Holds a field annotation inside schema annotations (Effect or Zod). */
|
|
95
|
+
export declare class FieldMeta {
|
|
96
|
+
readonly annotation: FieldAnnotation;
|
|
97
|
+
constructor(annotation: FieldAnnotation);
|
|
98
|
+
}
|
|
99
|
+
/** Holds the section list inside a struct's annotations. */
|
|
100
|
+
export declare class SectionsMeta {
|
|
101
|
+
readonly sections: Readonly<Record<string, SectionAnnotation>>;
|
|
102
|
+
constructor(sections: Readonly<Record<string, SectionAnnotation>>);
|
|
103
|
+
}
|
|
104
|
+
declare const formKey = "caps/form";
|
|
105
|
+
/** Schema annotations for one field: Effect `.annotate()` or Zod `.meta()`. */
|
|
106
|
+
export type FieldAnnotations = Record<`caps/form:${number}`, FieldMeta> & {
|
|
107
|
+
title?: string;
|
|
108
|
+
description?: string;
|
|
109
|
+
};
|
|
110
|
+
/** Schema annotations for a struct's sections. */
|
|
111
|
+
export type SectionAnnotations = {
|
|
112
|
+
readonly [formKey]: SectionsMeta;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Annotate a schema field for forms. Works with Effect
|
|
116
|
+
* (`Schema.String.annotate(field({ … }))`) and Zod (`z.string().meta(field({ … }))`).
|
|
117
|
+
* `label` and `help` also become the schema's `title` and `description`.
|
|
118
|
+
*/
|
|
119
|
+
export declare function field<T extends FieldInput = FieldInput>(annotation: FieldAnnotation<T>): FieldAnnotations;
|
|
120
|
+
/** Annotate a struct with its sections, in display order. */
|
|
121
|
+
export declare function sections(list: Readonly<Record<string, SectionAnnotation>>): SectionAnnotations;
|
|
122
|
+
/** A key or token: masked, never autofilled, with a reveal key. */
|
|
123
|
+
export declare const Secret: Schema.brand<Schema.String, "Secret">;
|
|
124
|
+
/** One choice, resolved for rendering. */
|
|
125
|
+
export type ResolvedOption = {
|
|
126
|
+
readonly value: string;
|
|
127
|
+
readonly label: string;
|
|
128
|
+
readonly description: string | undefined;
|
|
129
|
+
readonly meta: string | undefined;
|
|
130
|
+
readonly icon: ReactNode;
|
|
131
|
+
readonly caption: string | undefined;
|
|
132
|
+
readonly disabled: boolean;
|
|
133
|
+
};
|
|
134
|
+
/** One field, resolved from the schema for rendering. */
|
|
135
|
+
export type ResolvedField = Omit<FieldAnnotation, "options" | "kind" | "help"> & {
|
|
136
|
+
readonly help: string | undefined;
|
|
137
|
+
readonly name: string;
|
|
138
|
+
readonly label: string;
|
|
139
|
+
readonly kind: FieldKind;
|
|
140
|
+
readonly options: readonly ResolvedOption[];
|
|
141
|
+
readonly optionalInSchema: boolean;
|
|
142
|
+
readonly index: number;
|
|
143
|
+
};
|
|
144
|
+
/** A section and its fields, in display order. */
|
|
145
|
+
export type ResolvedSection = {
|
|
146
|
+
readonly id: string | null;
|
|
147
|
+
readonly title: string | undefined;
|
|
148
|
+
readonly description: string | undefined;
|
|
149
|
+
readonly fields: readonly ResolvedField[];
|
|
150
|
+
};
|
|
151
|
+
/** Everything a form needs from its schema. */
|
|
152
|
+
export type FormDescription = {
|
|
153
|
+
readonly sections: readonly ResolvedSection[];
|
|
154
|
+
readonly fields: readonly ResolvedField[];
|
|
155
|
+
readonly defaults: FormValues;
|
|
156
|
+
};
|
|
157
|
+
/** Read a form's fields, sections and empty defaults from its schema. */
|
|
158
|
+
export declare function describeSchema(schema: Schema.Top | z.ZodType): FormDescription;
|
|
159
|
+
/**
|
|
160
|
+
* A form schema: an Effect `Schema.Struct` (the house path) or a Zod object.
|
|
161
|
+
* `V` is the raw input the controls hold; `Out` is what the submit receives.
|
|
162
|
+
*/
|
|
163
|
+
export type FormSchema<V extends FormValues = FormValues, Out = unknown> = Schema.Codec<Out, V> | z.ZodType<Out, V>;
|
|
164
|
+
/** The schema as a Standard Schema validator for TanStack Form. */
|
|
165
|
+
export declare function standardSchema<V extends FormValues, Out>(schema: FormSchema<V, Out>): StandardSchemaV1<V, Out>;
|
|
166
|
+
export {};
|