@kitn.ai/ui 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/bin/mcp.js +16 -3
- package/bin/route.js +23 -0
- package/bin/route.test.js +58 -0
- package/dist/{elements/chunks/Icon-b5A8hlHs.js → Icon-BnFKqqtY.js} +1 -1
- package/dist/action-icons-CBskfBMz.js +1 -0
- package/dist/arrow-down-Br0qtpsz.js +1 -0
- package/dist/arrow-left-Dj7xgE12.js +1 -0
- package/dist/artifact-CvXEm1lS.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-C43ZegEq.js +1 -0
- package/dist/audio-visualizer-B-79y1Tj.js +1 -0
- package/dist/badge-BpUbq8c0.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-sn17Ip9v.js +1 -0
- package/dist/{elements/chunks/button-B9okqG73.js → button-BH6rN0U-.js} +1 -1
- package/dist/card-renderer-Cb1gQmth.js +1 -0
- package/dist/card-routing-DifQgS7n.js +1 -0
- package/dist/check-DPeeMUYx.js +1 -0
- package/dist/checkbox-Dc9Yov_B.js +1 -0
- package/dist/checkbox-group-B1kDLLYk.js +1 -0
- package/dist/chevron-down-CHSWk7ZT.js +1 -0
- package/dist/chevron-right-B7l_fMgx.js +1 -0
- package/dist/choice-card-BT3c1S8g.js +1 -0
- package/dist/circle-Bb0yOpqM.js +1 -0
- package/dist/circle-check-BGZpCiz7.js +1 -0
- package/dist/circle-x-wobWkoSx.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/code-block-CUQ5mtv0.js +1 -0
- package/dist/collapsible-CPJbf6FM.js +1 -0
- package/dist/components/attachment-types.d.ts +53 -1
- package/dist/components/attachments.d.ts +31 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
- package/dist/components/audio-visualizer/index.d.ts +15 -1
- package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
- package/dist/components/card-renderer.d.ts +13 -0
- package/dist/components/card.d.ts +4 -0
- package/dist/components/chat-thread.d.ts +49 -42
- package/dist/components/code-block.d.ts +22 -0
- package/dist/components/conversation-item.d.ts +66 -1
- package/dist/components/conversation-list.d.ts +77 -0
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +89 -0
- package/dist/components/message.d.ts +29 -0
- package/dist/components/reasoning.d.ts +6 -0
- package/dist/components/scroll-button.d.ts +11 -0
- package/dist/components/thread.d.ts +5 -0
- package/dist/components/toast.d.ts +2 -1
- package/dist/components/tool-types.d.ts +9 -0
- package/dist/components/voice-input.d.ts +8 -0
- package/dist/components/voice-output.d.ts +12 -1
- package/dist/components/workspace-shell.d.ts +87 -0
- package/dist/composer-CjngLzal.js +106 -0
- package/dist/confirm-card-DCx_A4bU.js +1 -0
- package/dist/construct-cli.es.js +1169 -0
- package/dist/context-BY8V-Nlp.js +1 -0
- package/dist/controllable-D43BKmtX.js +1 -0
- package/dist/conversation-list-D4lLU_yS.js +1 -0
- package/dist/copy-B6DO4PqB.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-CUzOlaMS.js → create-tween-BPzaefTp.js} +1 -1
- package/dist/{create-tween-B5Y4Im8G.js → create-tween-CnMHviKB.js} +1 -1
- package/dist/{create-tween-4z1XYLkR.js → create-tween-D1aiGZZh.js} +1 -1
- package/dist/{elements/chunks/create-tween-CUrM6D_o.js → create-tween-DUSQUKpr.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +3468 -1927
- package/dist/default-input-CucQEOqH.js +1 -0
- package/dist/define-CGGX-7Vg.js +1 -0
- package/dist/define.d.ts +4 -0
- package/dist/define.js +510 -0
- package/dist/define.server.d.ts +5 -0
- package/dist/define.server.js +506 -0
- package/dist/diagnostics/hook.d.ts +81 -0
- package/dist/diagnostics/index.d.ts +7 -0
- package/dist/diagnostics/report-request.d.ts +61 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +209 -0
- package/dist/disclosure-CbLtajUQ.js +1 -0
- package/dist/dropdown-BwhViJFd.js +1 -0
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkbox-group.d.ts +1 -0
- package/dist/elements/checkbox-group.js +1 -0
- package/dist/elements/checkbox.d.ts +1 -0
- package/dist/elements/checkbox.js +1 -0
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-item.d.ts +1 -0
- package/dist/elements/conversation-item.js +1 -0
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +3 -3
- package/dist/elements/define-entry.d.ts +12 -0
- package/dist/elements/define.d.ts +27 -0
- package/dist/elements/diagnostic-events.d.ts +74 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.d.ts +1 -0
- package/dist/elements/dock.js +243 -0
- package/dist/elements/dropdown.d.ts +1 -0
- package/dist/elements/dropdown.js +1 -0
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-data-types.d.ts +37 -0
- package/dist/elements/element-diagnostics.d.ts +81 -0
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-grid.d.ts +1 -0
- package/dist/elements/pane-grid.js +1 -0
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +1 -0
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/register.d.ts +2 -0
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +3 -3
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/select.d.ts +1 -0
- package/dist/elements/select.js +1 -0
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +1 -0
- package/dist/elements/slots.d.ts +36 -4
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +589 -167
- package/dist/{elements/chunks/ellipsis-CVxqfsDZ.js → ellipsis-DFr4Vqr3.js} +1 -1
- package/dist/embed-5s_vU4J0.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-DG2sRdQL.js → external-link-u9d85ORQ.js} +1 -1
- package/dist/file-text-CX7_x42o.js +1 -0
- package/dist/{elements/chunks/file-tree-yGWNK0Js.js → file-tree-D3kBjXI5.js} +1 -1
- package/dist/folder-V-YdIL8f.js +1 -0
- package/dist/form-BRNUMCN2.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-DaeiYrTA.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-BpixdZNq.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8473 -6470
- package/dist/index.server.js +6495 -4958
- package/dist/info-8mFsIjS3.js +1 -0
- package/dist/input-d1Yuu-hz.js +1 -0
- package/dist/javascript-C25yR2R2.js +1 -6
- package/dist/json-DxJze_jm.js +1 -6
- package/dist/kai-provider.es.js +40 -40
- package/dist/kai.es.js +1 -1
- package/dist/kbd-961zMPFB.js +1 -0
- package/dist/link-DlZZBGaR.js +1 -0
- package/dist/{elements/chunks/link-preview-Cf5KOP-i.js → link-preview-DIIbV2Sd.js} +1 -1
- package/dist/loader-YQszHFaW.js +1 -0
- package/dist/{elements/chunks/markdown-DbozyYqa.js → markdown-CVk1MPsj.js} +1 -1
- package/dist/mcp.es.js +3873 -544
- package/dist/message-CwKVEdZg.js +1 -0
- package/dist/message-DS8lzHc1.js +1 -0
- package/dist/message-circle-Xf8Q-vZp.js +1 -0
- package/dist/message-square-Cxnwgkns.js +1 -0
- package/dist/{elements/chunks/minimize-2-SegJ_oku.js → minimize-2-UEZmP2QT.js} +1 -1
- package/dist/{elements/chunks/model-switcher-D7JrFFHp.js → model-switcher-Kk1jEQ5t.js} +1 -1
- package/dist/{elements/chunks/overlay-CV7z2YuX.js → overlay-C1od6LOz.js} +1 -1
- package/dist/panel-right-ryET0yyn.js +1 -0
- package/dist/paperclip-BDNoZFJP.js +1 -0
- package/dist/play-OFIvYXUQ.js +1 -0
- package/dist/primitives/card-data-types.d.ts +49 -0
- package/dist/primitives/card-routing.d.ts +1 -33
- package/dist/primitives/card-validate.d.ts +2 -2
- package/dist/primitives/chat-config.d.ts +29 -2
- package/dist/primitives/field-mask.d.ts +96 -0
- package/dist/primitives/field-semantics.d.ts +36 -0
- package/dist/primitives/input-mask.d.ts +46 -0
- package/dist/primitives/url-scheme-policy.d.ts +34 -0
- package/dist/primitives/use-resize-observer.d.ts +23 -0
- package/dist/primitives/use-speech-recognition.d.ts +8 -0
- package/dist/primitives/use-text-stream.d.ts +5 -0
- package/dist/progress-bar-BvYX0aoy.js +1 -0
- package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
- package/dist/radio-DcL23cNJ.js +1 -0
- package/dist/react/index.d.ts +306 -283
- package/dist/react.js +309 -247
- package/dist/reasoning-Cackq4Rh.js +1 -0
- package/dist/register-impl-C7rENvPK.js +424 -0
- package/dist/{elements/chunks/resizable-Dpzjlvp4.js → resizable-Cp9HUYih.js} +1 -1
- package/dist/rotate-ccw-0DLRDHH5.js +1 -0
- package/dist/rotate-cw-DOB9IGcU.js +1 -0
- package/dist/schemas/form.schema.json +21 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/tool-defs.d.ts +45 -0
- package/dist/schemas.js +398 -305
- package/dist/scroll-area-emVmWP4K.js +1 -0
- package/dist/scroll-button-CeP_gv1x.js +1 -0
- package/dist/select-CWmu00tS.js +1 -0
- package/dist/{elements/chunks/separator-DH2r_ZSZ.js → separator-CE6vly6z.js} +1 -1
- package/dist/{elements/chunks/settings-BFMOd6yB.js → settings-CE-78SvZ.js} +1 -1
- package/dist/{elements/chunks/settings-group-B3VZKIcz.js → settings-group-ZFWNxFQR.js} +1 -1
- package/dist/{elements/chunks/skeleton-N8BuWYmf.js → skeleton-BebdP4mf.js} +1 -1
- package/dist/slider-BAKRYUlz.js +1 -0
- package/dist/slots-DaNjX0rc.js +1 -0
- package/dist/{solid-BMuDmFo_.js → solid-BE5ui6sy.js} +10672 -8116
- package/dist/{solid-CGYNdLRR.js → solid-DFhsce3O.js} +8137 -6056
- package/dist/solid.d.ts +15 -0
- package/dist/solid.js +248 -235
- package/dist/solid.server.js +248 -235
- package/dist/source-B0FlguSx.js +1 -0
- package/dist/star-CBMZvLuN.js +1 -0
- package/dist/state/index.d.ts +5 -1
- package/dist/state/mock.d.ts +32 -2
- package/dist/state/persistence.d.ts +51 -0
- package/dist/state/stream.d.ts +50 -0
- package/dist/state/threads.d.ts +71 -0
- package/dist/state.js +297 -172
- package/dist/store-7eoadydQ.js +1 -0
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-CViARZCy.js +1 -0
- package/dist/tasks-card-MjCcOhR_.js +1 -0
- package/dist/text-shimmer-DKlwRF2G.js +1 -0
- package/dist/textarea-3ay2Me2-.js +1 -0
- package/dist/theme.tokens.css +82 -13
- package/dist/{elements/chunks/thumbs-up-DQe0hvkZ.js → thumbs-up-DQcu2HMO.js} +1 -1
- package/dist/toast-store-C6yKOhbk.js +1 -0
- package/dist/{elements/chunks/tool-C1YtxwPu.js → tool-BCYNlfKy.js} +1 -1
- package/dist/{elements/chunks/tooltip-BqwnFWhn.js → tooltip-05zI4hjs.js} +1 -1
- package/dist/trash-2-BR1RxGky.js +1 -0
- package/dist/{elements/chunks/triangle-alert-B6or7F5u.js → triangle-alert-DHohDOCy.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- package/dist/types.d.ts +4 -2
- package/dist/typescript-RycA9KXf.js +1 -6
- package/dist/ui/checkbox-group.d.ts +83 -0
- package/dist/ui/checkbox.d.ts +40 -0
- package/dist/ui/dock.d.ts +131 -0
- package/dist/ui/hover-card.d.ts +44 -0
- package/dist/ui/icon.d.ts +3 -0
- package/dist/ui/input.d.ts +48 -2
- package/dist/ui/radio.d.ts +85 -0
- package/dist/ui/select.d.ts +71 -0
- package/dist/ui/slider.d.ts +65 -0
- package/dist/ui/switch.d.ts +9 -2
- package/dist/upload-BbGW6A_u.js +1 -0
- package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
- package/dist/use-card-resolution-CJW4grFa.js +1 -0
- package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-BUOVgPOB.js} +27 -27
- package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CUsUJvFI.js} +28 -28
- package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CW2i4bfr.js} +2 -2
- package/dist/{elements/chunks/variant-aurora-QXTZPyST.js → variant-aurora-xiMPty8f.js} +2 -2
- package/dist/variant-custom-BQtOnmeO.js +1 -0
- package/dist/{variant-custom-BhS9wIpU.js → variant-custom-CAm9L-E5.js} +65 -65
- package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-CAnzzppW.js} +40 -40
- package/dist/variant-custom-D91HjAKz.js +1 -0
- package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-Ca1Ls4_y.js} +21 -21
- package/dist/{variant-wave-CbVsmgwT.js → variant-wave-CxX39DKd.js} +25 -25
- package/dist/{variant-wave-BwzrHD1s.js → variant-wave-DYyzIIZv.js} +2 -2
- package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-uY5crU_K.js} +2 -2
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/wire/chunk.d.ts +47 -0
- package/dist/wire/diagnostics.d.ts +575 -0
- package/dist/wire/encode-probe.d.ts +70 -0
- package/dist/wire/encode.d.ts +24 -0
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/sse.d.ts +10 -2
- package/dist/wire.js +917 -480
- package/dist/x-C5m1JFgi.js +1 -0
- package/frameworks/react/index.tsx +325 -108
- package/llms-full.txt +1354 -82
- package/llms.txt +6 -3
- package/package.json +39 -6
- package/src/agent-tooling/archetypes.ts +41 -9
- package/src/agent-tooling/catalog/README.md +676 -0
- package/src/agent-tooling/catalog/catalog-types.ts +231 -0
- package/src/agent-tooling/catalog/fabrications.ts +96 -0
- package/src/agent-tooling/catalog/invariants.ts +284 -0
- package/src/agent-tooling/catalog/labs-titles.ts +114 -0
- package/src/agent-tooling/catalog/scenarios.ts +87 -0
- package/src/agent-tooling/catalog/surfaces.ts +347 -0
- package/src/agent-tooling/construct/cli-entry.ts +8 -0
- package/src/agent-tooling/construct/cli.ts +131 -0
- package/src/agent-tooling/construct/codegen.ts +1353 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +250 -0
- package/src/agent-tooling/construct/dev.ts +148 -0
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +23 -0
- package/src/agent-tooling/construct/schema.ts +371 -0
- package/src/agent-tooling/integrations/anthropic.ts +7 -1
- package/src/agent-tooling/integrations/cloudflare.ts +43 -2
- package/src/agent-tooling/integrations/langgraph.ts +7 -1
- package/src/agent-tooling/integrations/mastra.ts +7 -1
- package/src/agent-tooling/integrations/mock.ts +92 -17
- package/src/agent-tooling/integrations/ollama.ts +7 -1
- package/src/agent-tooling/integrations/openai.ts +7 -1
- package/src/agent-tooling/integrations/openrouter.ts +7 -1
- package/src/agent-tooling/integrations/pi.ts +56 -1
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
- package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
- package/src/agent-tooling/mcp/manifest.ts +100 -1
- package/src/agent-tooling/mcp/server.ts +63 -6
- package/src/agent-tooling/mcp/tools/construct.ts +130 -0
- package/src/agent-tooling/mcp/tools/debug.ts +126 -4
- package/src/agent-tooling/mcp/tools/reference.ts +542 -5
- package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
- package/src/agent-tooling/mcp/tools/theme.ts +146 -30
- package/src/agent-tooling/mcp/validate-args.ts +141 -0
- package/src/agent-tooling/recipes/composed-thread.ts +714 -0
- package/src/agent-tooling/recipes/index.ts +22 -0
- package/src/agent-tooling/recipes/types.ts +32 -0
- package/src/agent-tooling/registry.ts +24 -7
- package/src/agent-tooling/route-emit.ts +26 -2
- package/src/components/attachment-types.ts +54 -1
- package/src/components/attachments.tsx +169 -22
- package/src/components/audio-visualizer/fit-scale.ts +112 -0
- package/src/components/audio-visualizer/index.tsx +80 -3
- package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
- package/src/components/audio-visualizer/variant-bar.tsx +38 -6
- package/src/components/audio-visualizer/variant-custom.tsx +8 -2
- package/src/components/audio-visualizer/variant-grid.tsx +16 -11
- package/src/components/audio-visualizer/variant-radial.tsx +15 -10
- package/src/components/audio-visualizer/variant-wave.tsx +7 -2
- package/src/components/card-renderer.tsx +39 -8
- package/src/components/card.tsx +10 -2
- package/src/components/chat-container.tsx +4 -1
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +105 -13
- package/src/components/choice-card.tsx +102 -37
- package/src/components/coachmark.tsx +1 -1
- package/src/components/code-block.tsx +81 -5
- package/src/components/composer.tsx +89 -8
- package/src/components/confirm-card.tsx +20 -4
- package/src/components/conversation-item.tsx +138 -4
- package/src/components/conversation-list.tsx +230 -15
- package/src/components/embed.tsx +4 -0
- package/src/components/empty.tsx +14 -3
- package/src/components/file-tree.tsx +5 -1
- package/src/components/form-widgets.tsx +257 -195
- package/src/components/form.tsx +336 -21
- package/src/components/loader.tsx +12 -5
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +142 -12
- package/src/components/prompt-input.tsx +7 -1
- package/src/components/prompt-suggestion.tsx +4 -1
- package/src/components/reasoning.tsx +30 -3
- package/src/components/response-compare.tsx +2 -2
- package/src/components/response-stream.tsx +13 -7
- package/src/components/screen.tsx +5 -0
- package/src/components/scroll-button.tsx +63 -4
- package/src/components/source.tsx +6 -1
- package/src/components/tasks-card.tsx +58 -16
- package/src/components/thread.tsx +10 -1
- package/src/components/toast.tsx +11 -5
- package/src/components/tool-types.ts +9 -0
- package/src/components/voice-input.tsx +35 -3
- package/src/components/voice-output.tsx +46 -6
- package/src/components/workspace-shell.tsx +285 -0
- package/src/diagnostics/hook.ts +352 -0
- package/src/diagnostics/index.ts +82 -0
- package/src/diagnostics/report-request.ts +296 -0
- package/src/elements/attachments.tsx +9 -8
- package/src/elements/audio-visualizer.tsx +48 -4
- package/src/elements/chat-workspace.tsx +159 -287
- package/src/elements/chat.tsx +31 -10
- package/src/elements/checkbox-group.tsx +164 -0
- package/src/elements/checkbox.tsx +134 -0
- package/src/elements/code-block.tsx +19 -1
- package/src/elements/command.tsx +4 -1
- package/src/elements/compiled.css +1 -1
- package/src/elements/composer.tsx +12 -1
- package/src/elements/conversation-item.tsx +120 -0
- package/src/elements/conversation-list.tsx +60 -11
- package/src/elements/default-input.tsx +5 -5
- package/src/elements/define-entry.ts +12 -0
- package/src/elements/define.tsx +168 -8
- package/src/elements/diagnostic-events.ts +114 -0
- package/src/elements/dialog.tsx +47 -0
- package/src/elements/disclosure.ts +34 -12
- package/src/elements/dock.tsx +215 -0
- package/src/elements/dropdown.tsx +123 -0
- package/src/elements/element-data-types.ts +40 -0
- package/src/elements/element-diagnostics.ts +392 -0
- package/src/elements/element-manifest.json +36 -0
- package/src/elements/element-meta.json +1483 -560
- package/src/elements/element-nonscalar.json +185 -0
- package/src/elements/element-types.d.ts +589 -167
- package/src/elements/feedback-bar.tsx +5 -0
- package/src/elements/icon-names.json +29 -0
- package/src/elements/input.tsx +312 -23
- package/src/elements/menu.tsx +13 -0
- package/src/elements/message.tsx +2 -0
- package/src/elements/pane-grid.tsx +111 -0
- package/src/elements/prompt-dock.tsx +2 -1
- package/src/elements/prompt-input.tsx +16 -10
- package/src/elements/radio-group.tsx +129 -0
- package/src/elements/register-impl.ts +52 -0
- package/src/elements/register.ts +28 -0
- package/src/elements/resizable.tsx +52 -25
- package/src/elements/scroll-button.tsx +59 -6
- package/src/elements/select.tsx +165 -0
- package/src/elements/slider.tsx +171 -0
- package/src/elements/slots.ts +108 -13
- package/src/elements/styles.css +119 -9
- package/src/elements/thinking-bar.tsx +16 -7
- package/src/elements/thread.tsx +2 -0
- package/src/elements/toast.tsx +17 -1
- package/src/elements/tool.tsx +26 -5
- package/src/elements/voice-input.tsx +11 -2
- package/src/elements/voice-output.tsx +16 -3
- package/src/index.ts +4 -1
- package/src/primitives/card-data-types.ts +49 -0
- package/src/primitives/card-host.tsx +4 -2
- package/src/primitives/card-routing.ts +7 -56
- package/src/primitives/card-schemas/form.schema.json +21 -1
- package/src/primitives/card-validate.ts +10 -2
- package/src/primitives/chat-config.tsx +33 -6
- package/src/primitives/field-mask.ts +256 -0
- package/src/primitives/field-semantics.ts +115 -0
- package/src/primitives/input-mask.ts +853 -0
- package/src/primitives/toast-store.ts +66 -4
- package/src/primitives/url-scheme-policy.ts +70 -0
- package/src/primitives/use-resize-observer.ts +33 -0
- package/src/primitives/use-speech-recognition.ts +12 -1
- package/src/primitives/use-stick-to-bottom.ts +34 -2
- package/src/primitives/use-text-stream.ts +33 -2
- package/src/remote/provider-runtime.ts +6 -3
- package/src/schemas/index.ts +1 -0
- package/src/schemas/tool-defs.ts +307 -6
- package/src/solid.ts +15 -0
- package/src/state/index.ts +13 -1
- package/src/state/mock.ts +92 -6
- package/src/state/persistence.ts +180 -0
- package/src/state/stream.ts +71 -4
- package/src/state/threads.ts +142 -0
- package/src/types.ts +4 -2
- package/src/ui/agent-card.tsx +2 -2
- package/src/ui/avatar.tsx +1 -1
- package/src/ui/checkbox-group.tsx +153 -0
- package/src/ui/checkbox.tsx +63 -0
- package/src/ui/dialog.tsx +6 -0
- package/src/ui/dock.tsx +584 -0
- package/src/ui/dropdown.tsx +67 -23
- package/src/ui/hover-card.tsx +156 -6
- package/src/ui/icon.tsx +53 -0
- package/src/ui/input.tsx +275 -16
- package/src/ui/kbd.tsx +1 -1
- package/src/ui/nav.tsx +1 -1
- package/src/ui/pane-group.tsx +2 -2
- package/src/ui/radio.tsx +150 -0
- package/src/ui/resizable.tsx +11 -0
- package/src/ui/select.tsx +168 -0
- package/src/ui/slider.tsx +178 -0
- package/src/ui/switch.tsx +49 -18
- package/src/utils/cn.ts +11 -5
- package/src/wire/chunk.ts +47 -0
- package/src/wire/consume.ts +185 -26
- package/src/wire/diagnostics.ts +727 -0
- package/src/wire/encode-probe.ts +214 -0
- package/src/wire/encode.ts +267 -14
- package/src/wire/formats/anthropic.ts +8 -1
- package/src/wire/formats/openai.ts +5 -0
- package/src/wire/index.ts +22 -0
- package/src/wire/read.ts +250 -6
- package/src/wire/sse.ts +16 -3
- package/theme.css +84 -13
- package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
- package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
- package/dist/elements/chunks/artifact-BSuPYFHY.js +0 -1
- package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
- package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
- package/dist/elements/chunks/badge-TIMiJFKc.js +0 -1
- package/dist/elements/chunks/bash-InADTalH.js +0 -1
- package/dist/elements/chunks/card-renderer-CSoTL_e1.js +0 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
- package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
- package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
- package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
- package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
- package/dist/elements/chunks/choice-card-CGMc3kek.js +0 -1
- package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
- package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
- package/dist/elements/chunks/circle-x-mhH7pOK2.js +0 -1
- package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
- package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
- package/dist/elements/chunks/composer-CZZmtYOT.js +0 -69
- package/dist/elements/chunks/confirm-card-BWjf2-xo.js +0 -1
- package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
- package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
- package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
- package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
- package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
- package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
- package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
- package/dist/elements/chunks/download-DnCyWFJE.js +0 -1
- package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
- package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
- package/dist/elements/chunks/file-text-D0J9F6M7.js +0 -1
- package/dist/elements/chunks/folder-NV1pi_ud.js +0 -1
- package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
- package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
- package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
- package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
- package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
- package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
- package/dist/elements/chunks/info-C8r5Q6cc.js +0 -1
- package/dist/elements/chunks/input-DrseEjhp.js +0 -1
- package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
- package/dist/elements/chunks/json-DxJze_jm.js +0 -1
- package/dist/elements/chunks/kbd-OGJDUT-O.js +0 -1
- package/dist/elements/chunks/link-BdsfLB7b.js +0 -1
- package/dist/elements/chunks/loader-B5QzK_8_.js +0 -1
- package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
- package/dist/elements/chunks/message-DtXKPqiR.js +0 -1
- package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
- package/dist/elements/chunks/paperclip-Bg0ITU3b.js +0 -1
- package/dist/elements/chunks/progress-bar-D1_rzt6N.js +0 -1
- package/dist/elements/chunks/prompt-suggestion-D5Sm8xF1.js +0 -1
- package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
- package/dist/elements/chunks/rotate-cw-DzVDJOA1.js +0 -1
- package/dist/elements/chunks/scroll-area-93A1bbeG.js +0 -1
- package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
- package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
- package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
- package/dist/elements/chunks/source-w9V0NnBn.js +0 -1
- package/dist/elements/chunks/star-DHvACxtm.js +0 -1
- package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
- package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
- package/dist/elements/chunks/tasks-card-lPxoodlA.js +0 -1
- package/dist/elements/chunks/text-shimmer-BY_rI3s5.js +0 -1
- package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
- package/dist/elements/chunks/toast-store-VUZZBhzH.js +0 -1
- package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
- package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
- package/dist/elements/chunks/use-card-resolution-DJg32jGH.js +0 -1
- package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
- package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
- package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
- package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
- package/dist/llms/llms-full.txt +0 -2834
- package/dist/llms/llms.txt +0 -156
- package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
- package/dist/register-impl-DGmGzrEM.js +0 -145
- package/dist/ui/stat.d.ts +0 -16
- package/dist/variant-custom-MkTNTycd.js +0 -1
- package/src/primitives/card-validate-generator.testlib.ts +0 -38
- package/src/ui/stat.tsx +0 -41
- /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
- /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/state/persistence.ts — the mechanical halves of the persistence story.
|
|
2
|
+
//
|
|
3
|
+
// The boundary (spec §1, CLAUDE.md): persistence POLICY — what is stored,
|
|
4
|
+
// where, for how long, under what quota — is the app's. What lives here is
|
|
5
|
+
// only the mechanics every such app re-derives by hand: validating a stored
|
|
6
|
+
// thread back into `ChatMessage[]` (F-18), and the debounce/flush shape that
|
|
7
|
+
// keeps a per-token stream from hitting storage per token. No storage call,
|
|
8
|
+
// no fetch, no DOM; the save fn, the delay, and the reaction to a drop are
|
|
9
|
+
// all the consumer's.
|
|
10
|
+
import type { ChatMessage, FeedbackVote, MessagePart } from '../elements/chat-types';
|
|
11
|
+
|
|
12
|
+
/** One record the parse refused, and why. Deciding loudly is the caller's
|
|
13
|
+
* verb (warn, reject the whole record, count it); this is the information
|
|
14
|
+
* that a quiet version would withhold. */
|
|
15
|
+
export interface DroppedStored {
|
|
16
|
+
at: 'thread' | 'message' | 'part';
|
|
17
|
+
reason: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParsedThread {
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
/** Empty on a clean parse. Never silently shorter output than input. */
|
|
24
|
+
dropped: DroppedStored[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
28
|
+
return typeof v === 'object' && v !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One payload check per `MessagePart` variant. The key set is DERIVED from the
|
|
33
|
+
* union in `src/elements/chat-types.ts` at compile time: `satisfies
|
|
34
|
+
* Record<MessagePart['type'], …>` makes a missing key (a 7th variant landed)
|
|
35
|
+
* and a stale key (a variant renamed away) each a type error in THIS file —
|
|
36
|
+
* the same union `lint:silent-drops` and `verify:scaffold` read, so the lists
|
|
37
|
+
* cannot disagree. The behavioral cross-check lives in ./persistence.test.ts,
|
|
38
|
+
* which re-reads the union by a deliberately different method.
|
|
39
|
+
*
|
|
40
|
+
* These are shape checks, not a trust boundary for rendering: a rehydrated
|
|
41
|
+
* part is model output twice over (it was model output before it was
|
|
42
|
+
* hand-editable storage), and it stays safe to draw because the elements are
|
|
43
|
+
* what draw it. What this buys is that a truncated or half-written record
|
|
44
|
+
* reaches the element as a well-formed thread minus the broken piece, instead
|
|
45
|
+
* of as `parts: undefined` taking the whole app down on first render.
|
|
46
|
+
*/
|
|
47
|
+
const PART_VALIDATORS = {
|
|
48
|
+
text: (v) => typeof v.text === 'string',
|
|
49
|
+
reasoning: (v) => typeof v.text === 'string',
|
|
50
|
+
tool: (v) => isRecord(v.tool),
|
|
51
|
+
card: (v) => isRecord(v.envelope) && typeof v.envelope.type === 'string' && typeof v.envelope.id === 'string',
|
|
52
|
+
source: (v) => isRecord(v.source),
|
|
53
|
+
file: (v) => isRecord(v.attachment),
|
|
54
|
+
} satisfies Record<MessagePart['type'], (v: Record<string, unknown>) => boolean>;
|
|
55
|
+
|
|
56
|
+
function parsePart(v: unknown, dropped: DroppedStored[]): MessagePart | null {
|
|
57
|
+
if (!isRecord(v) || typeof v.type !== 'string') {
|
|
58
|
+
dropped.push({ at: 'part', reason: 'not a part record', value: v });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const validate = (PART_VALIDATORS as Record<string, (v: Record<string, unknown>) => boolean>)[v.type];
|
|
62
|
+
if (!validate) {
|
|
63
|
+
// An unknown `type` is dropped rather than passed on: every renderer and
|
|
64
|
+
// encoder switches on it and has no branch for a made-up one.
|
|
65
|
+
dropped.push({ at: 'part', reason: `unknown part type "${v.type}"`, value: v });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!validate(v)) {
|
|
69
|
+
dropped.push({ at: 'part', reason: `missing or invalid payload for "${v.type}" part`, value: v });
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Verbatim, not rebuilt: optional fields the variants carry (`raw`,
|
|
73
|
+
// `signature`, `index`, …) are load-bearing on the wire re-encode, so the
|
|
74
|
+
// validated record passes through whole.
|
|
75
|
+
return v as unknown as MessagePart;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseMessage(v: unknown, dropped: DroppedStored[]): ChatMessage | null {
|
|
79
|
+
if (!isRecord(v) || typeof v.id !== 'string' || (v.role !== 'user' && v.role !== 'assistant') || !Array.isArray(v.parts)) {
|
|
80
|
+
dropped.push({ at: 'message', reason: 'not a message record (id/role/parts)', value: v });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const parts = v.parts
|
|
84
|
+
.map((p) => parsePart(p, dropped))
|
|
85
|
+
.filter((p): p is MessagePart => p !== null);
|
|
86
|
+
if (parts.length === 0) {
|
|
87
|
+
// A message whose every part was unreadable is an empty bubble, and the
|
|
88
|
+
// per-part entries above already say what was lost.
|
|
89
|
+
dropped.push({ at: 'message', reason: 'no readable parts survived', value: v });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const message: ChatMessage = { id: v.id, role: v.role, parts };
|
|
93
|
+
if (v.feedback === 'like' || v.feedback === 'dislike') message.feedback = v.feedback as FeedbackVote;
|
|
94
|
+
if (isRecord(v.avatar)) message.avatar = v.avatar;
|
|
95
|
+
if (Array.isArray(v.actions)) message.actions = v.actions as ChatMessage['actions'];
|
|
96
|
+
return message;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a stored thread — the JSON-parsed value of one thread's messages
|
|
101
|
+
* array — back into `ChatMessage[]`. Never throws: unreadable records are
|
|
102
|
+
* DROPPED and reported in `dropped`, so a truncated write loses one record,
|
|
103
|
+
* not every conversation. What to DO about a drop (warn, discard the whole
|
|
104
|
+
* record, telemetry) is the caller's policy.
|
|
105
|
+
*/
|
|
106
|
+
export function parseStoredThread(value: unknown): ParsedThread {
|
|
107
|
+
const dropped: DroppedStored[] = [];
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
return { messages: [], dropped: [{ at: 'thread', reason: 'stored thread is not an array', value }] };
|
|
110
|
+
}
|
|
111
|
+
const messages = value
|
|
112
|
+
.map((m) => parseMessage(m, dropped))
|
|
113
|
+
.filter((m): m is ChatMessage => m !== null);
|
|
114
|
+
return { messages, dropped };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SaveSchedulerOptions {
|
|
118
|
+
/** The quiet period before a scheduled snapshot is written. This is policy
|
|
119
|
+
* and therefore REQUIRED — the kit ships no default debounce. */
|
|
120
|
+
delayMs: number;
|
|
121
|
+
/** The scheduler the seam runs on. Defaults to the global timers; inject
|
|
122
|
+
* your own for tests or a non-timer scheduler. */
|
|
123
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
124
|
+
clearTimeout?: (handle: unknown) => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SaveScheduler<T> {
|
|
128
|
+
/** Remember `snapshot` as the latest state and (re)start the quiet-period
|
|
129
|
+
* timer. Call it on every state change; only the last snapshot inside the
|
|
130
|
+
* quiet period is written. */
|
|
131
|
+
schedule(snapshot: T): void;
|
|
132
|
+
/** Write the pending snapshot NOW and disarm the timer — the
|
|
133
|
+
* `beforeunload`/`visibilitychange` story, so a tab closed mid-stream still
|
|
134
|
+
* persists. No-op when nothing is pending: the last write already happened. */
|
|
135
|
+
flush(): void;
|
|
136
|
+
/** Drop the pending write unsaved (e.g. unmount-without-persist). */
|
|
137
|
+
cancel(): void;
|
|
138
|
+
pending(): boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The debounce/flush seam: a streaming reply lands a state update per token,
|
|
143
|
+
* and without this shape so would the consumer's storage backend. The kit owns
|
|
144
|
+
* only the timer dance; `save` (WHAT and WHERE, including its own try/catch
|
|
145
|
+
* policy for quota errors) and `delayMs` (HOW LONG) are the consumer's.
|
|
146
|
+
*/
|
|
147
|
+
export function createSaveScheduler<T>(save: (snapshot: T) => void, opts: SaveSchedulerOptions): SaveScheduler<T> {
|
|
148
|
+
const setTimer = opts.setTimeout ?? ((fn: () => void, ms: number) => setTimeout(fn, ms));
|
|
149
|
+
const clearTimer = opts.clearTimeout ?? ((handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>));
|
|
150
|
+
|
|
151
|
+
let latest: T | undefined;
|
|
152
|
+
let armed = false;
|
|
153
|
+
let handle: unknown;
|
|
154
|
+
|
|
155
|
+
const disarm = () => {
|
|
156
|
+
if (!armed) return;
|
|
157
|
+
clearTimer(handle);
|
|
158
|
+
armed = false;
|
|
159
|
+
};
|
|
160
|
+
const write = () => {
|
|
161
|
+
armed = false;
|
|
162
|
+
save(latest as T);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
schedule(snapshot) {
|
|
167
|
+
latest = snapshot;
|
|
168
|
+
disarm();
|
|
169
|
+
handle = setTimer(write, opts.delayMs);
|
|
170
|
+
armed = true;
|
|
171
|
+
},
|
|
172
|
+
flush() {
|
|
173
|
+
if (!armed) return;
|
|
174
|
+
disarm();
|
|
175
|
+
save(latest as T);
|
|
176
|
+
},
|
|
177
|
+
cancel: disarm,
|
|
178
|
+
pending: () => armed,
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/state/stream.ts
CHANGED
|
@@ -116,6 +116,56 @@ export interface AssistantStream {
|
|
|
116
116
|
* assignability already holds. Measurements and the trade: boundary point 2. */
|
|
117
117
|
addFile(attachment: AttachmentData): AssistantStream;
|
|
118
118
|
done(): void;
|
|
119
|
+
/** Settles the turn as FAILED and puts `reason` where the reader can find it.
|
|
120
|
+
*
|
|
121
|
+
* WHERE THE REASON LANDS, in order:
|
|
122
|
+
* 1. every tool part that has NOT produced a result flips to `output-error`
|
|
123
|
+
* with `errorText: reason`, so no panel spins forever. A tool ALREADY in
|
|
124
|
+
* `output-error` with its own non-empty `errorText` is left exactly as it
|
|
125
|
+
* is: "search index offline" is the answer to what went wrong and
|
|
126
|
+
* "Connection lost." is the generic outer symptom, so overwriting the
|
|
127
|
+
* specific with the generic loses the only actionable thing on the
|
|
128
|
+
* message. It still counts as carrying the failure. A tool in
|
|
129
|
+
* `output-error` with NO text does get filled in — an error panel with
|
|
130
|
+
* nothing in it says no more than a blank bubble does.
|
|
131
|
+
* 2. if no part was able to carry it, the reason is APPENDED as its own text
|
|
132
|
+
* part.
|
|
133
|
+
*
|
|
134
|
+
* Never both — a turn does not report the same failure twice.
|
|
135
|
+
*
|
|
136
|
+
* "Where the reader can FIND it" is deliberate, and rule 1 and rule 2 are not
|
|
137
|
+
* equally loud. Rule 2 is text in the thread: read without any interaction.
|
|
138
|
+
* Rule 1 is a tool panel, which renders COLLAPSED — the header shows an error
|
|
139
|
+
* icon and a badge, so the failure is unmissable, but the `errorText` itself
|
|
140
|
+
* is one click away. That is the right default for a failed tool inside an
|
|
141
|
+
* otherwise readable answer, and it is why rule 2 exists rather than always
|
|
142
|
+
* stamping the panel and calling it reported.
|
|
143
|
+
*
|
|
144
|
+
* Rule 2 is the whole point and it used to be missing. `abort` only ever did
|
|
145
|
+
* rule 1, so a TEXT-ONLY turn — every turn of a text-only support widget —
|
|
146
|
+
* had nothing to stamp: the string was discarded and a failed request
|
|
147
|
+
* rendered an EMPTY assistant bubble, while the consumer that passed a
|
|
148
|
+
* perfectly good sentence believed it had reported the failure. That is the
|
|
149
|
+
* repo's "decide loudly" rule broken on the one path where being quiet is
|
|
150
|
+
* worst, and it shipped because the discard is invisible from the call site.
|
|
151
|
+
*
|
|
152
|
+
* It is a NEW part, never a merge onto the trailing text: gluing "Connection
|
|
153
|
+
* lost." onto the model's half-finished sentence reads as the model saying
|
|
154
|
+
* it. Text that already streamed stays exactly where it is.
|
|
155
|
+
*
|
|
156
|
+
* The reason is TRIMMED, and `abort()` with no reason — or one that is empty
|
|
157
|
+
* or all whitespace — appends nothing: there is no reason to discard, and the
|
|
158
|
+
* kit will not invent copy the consumer did not write. Rule 1 still runs,
|
|
159
|
+
* with `errorText: undefined`. Whitespace is not a pedantic case here: the
|
|
160
|
+
* scaffold hands over `err.message`, and an `Error` is free to carry `''`,
|
|
161
|
+
* which would otherwise render an INVISIBLE text part — a blank bubble that
|
|
162
|
+
* also claims to have said something.
|
|
163
|
+
*
|
|
164
|
+
* Settled is settled: after `done()` or a first `abort()`, this is a no-op
|
|
165
|
+
* like every other mutator, so the reason cannot be appended twice.
|
|
166
|
+
*
|
|
167
|
+
* The reason is CONSUMER-facing text and is rendered as markdown like any
|
|
168
|
+
* other text part. Pass a sentence a visitor can read, not a stack trace. */
|
|
119
169
|
abort(reason?: string): void;
|
|
120
170
|
}
|
|
121
171
|
|
|
@@ -151,10 +201,27 @@ export function createAssistantStream(
|
|
|
151
201
|
addFile(attachment) { mutate((p) => [...p, { type: 'file', attachment }]); return stream; },
|
|
152
202
|
done() { settled = true; },
|
|
153
203
|
abort(reason) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
// Whitespace-only is the same nothing as empty. `err.message` on a thrown
|
|
205
|
+
// Error is free to be either, and appending it would render an INVISIBLE
|
|
206
|
+
// text part -- a blank bubble that also claims to have said something.
|
|
207
|
+
const text = reason?.trim() || undefined;
|
|
208
|
+
mutate((p) => {
|
|
209
|
+
let carried = false;
|
|
210
|
+
const next = p.map((part) => {
|
|
211
|
+
if (part.type !== 'tool' || part.tool.state === 'output-available') return part;
|
|
212
|
+
// Already showing its own, more specific failure: keep it, and count
|
|
213
|
+
// it as carrying. See the note above `abort`.
|
|
214
|
+
if (part.tool.state === 'output-error' && part.tool.errorText?.trim()) {
|
|
215
|
+
carried = true;
|
|
216
|
+
return part;
|
|
217
|
+
}
|
|
218
|
+
carried = true;
|
|
219
|
+
return { ...part, tool: { ...part.tool, state: 'output-error' as const, errorText: text } };
|
|
220
|
+
});
|
|
221
|
+
// The reason has to land SOMEWHERE. See the note above `abort`.
|
|
222
|
+
if (carried || !text) return next;
|
|
223
|
+
return [...next, { type: 'text', text }];
|
|
224
|
+
});
|
|
158
225
|
settled = true;
|
|
159
226
|
},
|
|
160
227
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/state/threads.ts — the thread-switching fold.
|
|
2
|
+
//
|
|
3
|
+
// The identity category from the workspace glue inventory (spec §1/§3b): an
|
|
4
|
+
// id-bound `SetMessages`, per-thread loading state, the abort map, and
|
|
5
|
+
// delete-under-stream safety. Pure folds + callbacks per the state charter —
|
|
6
|
+
// no fetch, no DOM, no storage; the only state a helper here owns is the
|
|
7
|
+
// irreducible kind (which turns are in flight), never the consumer's records.
|
|
8
|
+
import type { ChatMessage } from '../elements/chat-types';
|
|
9
|
+
import type { SetMessages } from './stream';
|
|
10
|
+
|
|
11
|
+
/** The minimal thread record the fold needs. The consumer's own record type
|
|
12
|
+
* (title, timestamps, whatever its storage keeps) extends this — those fields
|
|
13
|
+
* are policy and stay outside the kit. */
|
|
14
|
+
export interface ThreadLike {
|
|
15
|
+
id: string;
|
|
16
|
+
messages: ChatMessage[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The functional-updater setter over the whole thread LIST — the same
|
|
20
|
+
* React-setState shape `SetMessages` is, one level up. */
|
|
21
|
+
export type SetThreads<T extends ThreadLike> = (updater: (prev: T[]) => T[]) => void;
|
|
22
|
+
|
|
23
|
+
export interface BindThreadOptions<T extends ThreadLike> {
|
|
24
|
+
/** Stamp the changed record (e.g. `updatedAt`). Runs on a hit only. Kept a
|
|
25
|
+
* callback because WHAT gets stamped — and with whose clock — is the
|
|
26
|
+
* consumer's record shape, not ours. */
|
|
27
|
+
touch?: (thread: T) => T;
|
|
28
|
+
/** A delta arrived for a thread that is no longer in the list (deleted while
|
|
29
|
+
* its reply was streaming). The delta is dropped either way — dropping is
|
|
30
|
+
* the only correct fold — but the drop is reported, not silent. */
|
|
31
|
+
onDrop?: (threadId: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pure fold: apply `updater` to the messages of thread `id` within a list.
|
|
36
|
+
*
|
|
37
|
+
* reactivity-two-halves (CLAUDE.md): the hit gets a NEW containing array
|
|
38
|
+
* (which notifies) and a NEW record object (which makes the change visible);
|
|
39
|
+
* every other record keeps its reference. A MISS returns `threads` itself —
|
|
40
|
+
* the same reference — so a late delta for a deleted thread re-renders
|
|
41
|
+
* nothing and resurrects nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function updateThreadMessages<T extends ThreadLike>(
|
|
44
|
+
threads: T[],
|
|
45
|
+
id: string,
|
|
46
|
+
updater: (prev: ChatMessage[]) => ChatMessage[],
|
|
47
|
+
opts: BindThreadOptions<T> = {},
|
|
48
|
+
): T[] {
|
|
49
|
+
let hit = false;
|
|
50
|
+
const next = threads.map((t) => {
|
|
51
|
+
if (t.id !== id) return t;
|
|
52
|
+
hit = true;
|
|
53
|
+
const updated = { ...t, messages: updater(t.messages) };
|
|
54
|
+
return opts.touch ? opts.touch(updated) : updated;
|
|
55
|
+
});
|
|
56
|
+
if (!hit) {
|
|
57
|
+
opts.onDrop?.(id);
|
|
58
|
+
return threads;
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A `SetMessages` bound to ONE thread. Capture the id when the user hits send
|
|
65
|
+
* and hand the result to `createAssistantStream`: the reply then lands in the
|
|
66
|
+
* thread that was open at send time, never whichever one is open when the
|
|
67
|
+
* tokens arrive — which is what makes switching away mid-reply (and coming
|
|
68
|
+
* back to a finished answer) safe.
|
|
69
|
+
*/
|
|
70
|
+
export function bindThreadMessages<T extends ThreadLike>(
|
|
71
|
+
setThreads: SetThreads<T>,
|
|
72
|
+
id: string,
|
|
73
|
+
opts: BindThreadOptions<T> = {},
|
|
74
|
+
): SetMessages {
|
|
75
|
+
return (updater) => {
|
|
76
|
+
setThreads((prev) => updateThreadMessages(prev, id, updater, opts));
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The in-flight-turn bookkeeping for a multi-thread app: which threads are
|
|
81
|
+
* streaming, and the AbortController for each. See `createThreadSessions`. */
|
|
82
|
+
export interface ThreadSessions {
|
|
83
|
+
/** Register a new in-flight turn. One turn per thread: a prior in-flight
|
|
84
|
+
* turn for the SAME thread is aborted first. Pass the returned controller's
|
|
85
|
+
* `signal` to your fetch — the fetch line itself stays yours. */
|
|
86
|
+
begin(threadId: string): AbortController;
|
|
87
|
+
/** The turn settled (done or failed) — call from your `finally`. `controller`
|
|
88
|
+
* is required so a slow old turn's cleanup cannot clear a newer turn that
|
|
89
|
+
* has since begun on the same thread: a stale controller is a no-op. */
|
|
90
|
+
end(threadId: string, controller: AbortController): void;
|
|
91
|
+
/** Cancel the in-flight turn (the delete-under-stream story): aborts the
|
|
92
|
+
* controller and clears the streaming state. Returns whether anything was
|
|
93
|
+
* actually in flight. Pair it with the id-bound sink above, which drops any
|
|
94
|
+
* deltas that were already past the abort. */
|
|
95
|
+
abort(threadId: string): boolean;
|
|
96
|
+
isStreaming(threadId: string): boolean;
|
|
97
|
+
/** A fresh array snapshot each call — safe to hand straight to a setState. */
|
|
98
|
+
streamingIds(): string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create the session tracker. `onChange` fires with a fresh `streamingIds()`
|
|
103
|
+
* snapshot after every transition — mirror it into your framework's state to
|
|
104
|
+
* derive `loading` (`sessions.isStreaming(activeId)` is the per-thread flag a
|
|
105
|
+
* single boolean cannot be, because a stream keeps running when you switch
|
|
106
|
+
* away).
|
|
107
|
+
*
|
|
108
|
+
* This is the one stateful object in the module, because an abort map is
|
|
109
|
+
* irreducibly stateful; it still does no I/O and owns no timer.
|
|
110
|
+
*/
|
|
111
|
+
export function createThreadSessions(onChange?: (streamingIds: string[]) => void): ThreadSessions {
|
|
112
|
+
const inFlight = new Map<string, AbortController>();
|
|
113
|
+
const snapshot = () => Array.from(inFlight.keys());
|
|
114
|
+
const notify = () => onChange?.(snapshot());
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
begin(threadId) {
|
|
118
|
+
inFlight.get(threadId)?.abort();
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
inFlight.set(threadId, controller);
|
|
121
|
+
notify();
|
|
122
|
+
return controller;
|
|
123
|
+
},
|
|
124
|
+
end(threadId, controller) {
|
|
125
|
+
if (inFlight.get(threadId) !== controller) return;
|
|
126
|
+
inFlight.delete(threadId);
|
|
127
|
+
notify();
|
|
128
|
+
},
|
|
129
|
+
abort(threadId) {
|
|
130
|
+
const controller = inFlight.get(threadId);
|
|
131
|
+
if (!controller) return false;
|
|
132
|
+
controller.abort();
|
|
133
|
+
inFlight.delete(threadId);
|
|
134
|
+
notify();
|
|
135
|
+
return true;
|
|
136
|
+
},
|
|
137
|
+
isStreaming(threadId) {
|
|
138
|
+
return inFlight.has(threadId);
|
|
139
|
+
},
|
|
140
|
+
streamingIds: snapshot,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -31,9 +31,11 @@ export interface ConversationSummary {
|
|
|
31
31
|
id: string;
|
|
32
32
|
title: string;
|
|
33
33
|
groupId?: string;
|
|
34
|
-
scope
|
|
34
|
+
/** Only meaningful to scope-aware consumers; the list never reads it. */
|
|
35
|
+
scope?: ConversationScope;
|
|
35
36
|
messageCount: number;
|
|
36
|
-
|
|
37
|
+
/** Fallback for the auto relative time when updatedAt is absent. */
|
|
38
|
+
lastMessageAt?: string;
|
|
37
39
|
updatedAt: string;
|
|
38
40
|
/** Right-aligned trailing text on the row — a count, status, or "days ago".
|
|
39
41
|
* When omitted, a short relative time is auto-derived from `updatedAt`
|
package/src/ui/agent-card.tsx
CHANGED
|
@@ -156,7 +156,7 @@ export function AgentCard(props: AgentCardProps) {
|
|
|
156
156
|
/>
|
|
157
157
|
</span>
|
|
158
158
|
<Show when={local.status.label}>
|
|
159
|
-
<span class={cn('text-
|
|
159
|
+
<span class={cn('text-caption font-medium leading-none', TONE_TEXT[tone()])}>
|
|
160
160
|
{local.status.label}
|
|
161
161
|
</span>
|
|
162
162
|
</Show>
|
|
@@ -185,7 +185,7 @@ export function AgentCard(props: AgentCardProps) {
|
|
|
185
185
|
|
|
186
186
|
<span class="flex shrink-0 items-center gap-2">
|
|
187
187
|
<Show when={local.needsAttention}>
|
|
188
|
-
<span class="inline-flex items-center gap-1 rounded-full bg-tool-amber/15 px-2 py-0.5 text-
|
|
188
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-tool-amber/15 px-2 py-0.5 text-caption font-medium leading-none text-tool-amber">
|
|
189
189
|
<Bell class="size-3" aria-hidden="true" />
|
|
190
190
|
Needs you
|
|
191
191
|
</span>
|
package/src/ui/avatar.tsx
CHANGED
|
@@ -8,7 +8,7 @@ export interface AvatarProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
|
8
8
|
size?: 'sm' | 'md' | 'lg';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const sizeClasses = { sm: 'h-6 w-6 text-
|
|
11
|
+
const sizeClasses = { sm: 'h-6 w-6 text-micro', md: 'h-8 w-8 text-xs', lg: 'h-10 w-10 text-sm' };
|
|
12
12
|
|
|
13
13
|
export function Avatar(props: AvatarProps) {
|
|
14
14
|
const [local, rest] = splitProps(props, ['src', 'alt', 'fallback', 'size', 'class']);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type JSX, For, Show, splitProps } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Checkbox } from './checkbox';
|
|
4
|
+
|
|
5
|
+
/** One choice in a {@link CheckboxGroup}. */
|
|
6
|
+
export interface CheckboxOption<T = string> {
|
|
7
|
+
/** The value this row selects. Compared to the group's `value` by identity. */
|
|
8
|
+
value: T;
|
|
9
|
+
/** The row's visible label. */
|
|
10
|
+
label: JSX.Element;
|
|
11
|
+
/** Optional second line under the label. */
|
|
12
|
+
description?: JSX.Element;
|
|
13
|
+
/** Disable this row alone (the group's `disabled` disables all of them). */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CheckboxGroupProps<T = string>
|
|
18
|
+
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'> {
|
|
19
|
+
/**
|
|
20
|
+
* The choices, in display order. Rendered in full: the kit never truncates,
|
|
21
|
+
* re-orders or de-duplicates a consumer's option list.
|
|
22
|
+
*/
|
|
23
|
+
options: readonly CheckboxOption<T>[];
|
|
24
|
+
/** The selected values. Each option is checked when its `value` is in here (by identity). */
|
|
25
|
+
value?: readonly T[];
|
|
26
|
+
/**
|
|
27
|
+
* The shared form-control name every box in the group carries, so a native form
|
|
28
|
+
* submits the whole selection under one key and `FormData.getAll(name)` reads it back.
|
|
29
|
+
*
|
|
30
|
+
* UNLIKE {@link RadioGroup}, this has no default. A radio group needs a shared `name`
|
|
31
|
+
* for the browser to make the set exclusive and arrow-navigable, so one is generated
|
|
32
|
+
* when none is given; checkboxes are independent controls and need nothing to behave
|
|
33
|
+
* correctly. Generating a name here would submit the selection under a random key,
|
|
34
|
+
* which is worse than submitting nothing.
|
|
35
|
+
*/
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Disable every row. */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Accessible name for the group. Rendered as `aria-label` on the `group`. */
|
|
40
|
+
label?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Fires on every toggle with the NEXT selection, plus the option that moved and
|
|
43
|
+
* which way it went. The next selection is a fresh array — the group holds no state
|
|
44
|
+
* of its own, so `value` is yours to own.
|
|
45
|
+
*/
|
|
46
|
+
onChange?: (value: T[], option: CheckboxOption<T>, checked: boolean) => void;
|
|
47
|
+
/** Fires when a box loses focus — the commit point for a form field. */
|
|
48
|
+
onOptionBlur?: () => void;
|
|
49
|
+
/** Extra classes for each row. */
|
|
50
|
+
itemClass?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Presentation slot. Replaces the default label/description column with whatever
|
|
53
|
+
* you return, so a row can carry media, a badge or a price without a second
|
|
54
|
+
* checkbox component existing. The control, the row chrome and the group semantics
|
|
55
|
+
* stay ours.
|
|
56
|
+
*/
|
|
57
|
+
children?: (option: CheckboxOption<T>, state: { checked: boolean; disabled: boolean }) => JSX.Element;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A vertical set of checkbox rows in a bordered, divided list — the kit's standard
|
|
62
|
+
* "pick any number" control, and {@link RadioGroup}'s sibling: same options shape,
|
|
63
|
+
* same row chrome, same presentation slot, multi-value instead of single.
|
|
64
|
+
*
|
|
65
|
+
* Every row is a real `<input type="checkbox">` inside a `<label>`, so clicking
|
|
66
|
+
* anywhere on the row toggles it and the keyboard behaviour (a tab stop per box,
|
|
67
|
+
* Space to toggle) is the browser's rather than a reimplementation. The wrapper is
|
|
68
|
+
* `role="group"`, not `role="listbox"`: the boxes are independent controls and
|
|
69
|
+
* nothing here overrides what they already announce.
|
|
70
|
+
*
|
|
71
|
+
* Everything not listed in `CheckboxGroupProps` is forwarded to the group element, so
|
|
72
|
+
* `id`, `aria-labelledby`, any other `aria-*` and any `data-*` hook land where a form
|
|
73
|
+
* expects them. No validation is applied — "at least one" is your application's rule,
|
|
74
|
+
* not the kit's.
|
|
75
|
+
*
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <CheckboxGroup
|
|
78
|
+
* label="Environments"
|
|
79
|
+
* name="env"
|
|
80
|
+
* options={[{ value: 'prod', label: 'Production' }, { value: 'staging', label: 'Staging' }]}
|
|
81
|
+
* value={envs()}
|
|
82
|
+
* onChange={setEnvs}
|
|
83
|
+
* />
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function CheckboxGroup<T = string>(props: CheckboxGroupProps<T>): JSX.Element {
|
|
87
|
+
const [local, rest] = splitProps(props, [
|
|
88
|
+
'options', 'value', 'name', 'disabled', 'label', 'onChange', 'onOptionBlur', 'itemClass', 'children', 'class',
|
|
89
|
+
]);
|
|
90
|
+
const selected = (): readonly T[] => local.value ?? [];
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
{...rest}
|
|
94
|
+
role="group"
|
|
95
|
+
aria-label={local.label}
|
|
96
|
+
class={cn('divide-y divide-border overflow-hidden rounded-lg border border-border', local.class)}
|
|
97
|
+
>
|
|
98
|
+
<For each={local.options}>
|
|
99
|
+
{(opt) => {
|
|
100
|
+
const checked = () => selected().includes(opt.value);
|
|
101
|
+
const rowDisabled = () => !!(local.disabled || opt.disabled);
|
|
102
|
+
return (
|
|
103
|
+
<label
|
|
104
|
+
class={cn(
|
|
105
|
+
'flex cursor-pointer items-center gap-3 px-3 py-2.5 text-sm transition-colors',
|
|
106
|
+
opt.description !== undefined && 'items-start',
|
|
107
|
+
checked()
|
|
108
|
+
? 'bg-accent font-medium text-accent-foreground'
|
|
109
|
+
: 'text-foreground hover:bg-muted/50',
|
|
110
|
+
local.itemClass,
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<Checkbox
|
|
114
|
+
name={local.name}
|
|
115
|
+
value={String(opt.value)}
|
|
116
|
+
checked={checked()}
|
|
117
|
+
disabled={rowDisabled()}
|
|
118
|
+
class={opt.description === undefined ? undefined : 'mt-0.5'}
|
|
119
|
+
onChange={(e) => {
|
|
120
|
+
const on = e.currentTarget.checked;
|
|
121
|
+
const now = selected();
|
|
122
|
+
// Filter-or-append rather than a toggle on the previous array, so the
|
|
123
|
+
// emitted selection follows the CONTROL's state. They diverge when the
|
|
124
|
+
// caller re-renders mid-interaction, and following the box is what a
|
|
125
|
+
// native form would report.
|
|
126
|
+
const next = on ? [...now.filter((v) => v !== opt.value), opt.value] : now.filter((v) => v !== opt.value);
|
|
127
|
+
local.onChange?.(next, opt, on);
|
|
128
|
+
}}
|
|
129
|
+
onBlur={() => local.onOptionBlur?.()}
|
|
130
|
+
/>
|
|
131
|
+
<Show
|
|
132
|
+
when={local.children}
|
|
133
|
+
fallback={
|
|
134
|
+
// No description, a bare <span> — byte for byte what the form's
|
|
135
|
+
// hand-rolled checkbox list rendered before this component existed.
|
|
136
|
+
// The two-line column only appears when there is a second line.
|
|
137
|
+
<Show when={opt.description !== undefined} fallback={<span>{opt.label}</span>}>
|
|
138
|
+
<span class="flex flex-col gap-0.5">
|
|
139
|
+
<span>{opt.label}</span>
|
|
140
|
+
<span class="text-xs font-normal text-muted-foreground">{opt.description}</span>
|
|
141
|
+
</span>
|
|
142
|
+
</Show>
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
{(render) => render()(opt, { checked: checked(), disabled: rowDisabled() })}
|
|
146
|
+
</Show>
|
|
147
|
+
</label>
|
|
148
|
+
);
|
|
149
|
+
}}
|
|
150
|
+
</For>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type JSX, splitProps, createEffect } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface CheckboxProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'type' | 'children'> {
|
|
5
|
+
/**
|
|
6
|
+
* The mixed / partially-checked state ("some of the things below are ticked").
|
|
7
|
+
*
|
|
8
|
+
* `indeterminate` is a DOM PROPERTY with no HTML attribute, so it cannot be set
|
|
9
|
+
* from markup — every caller that wanted it used to reach for a `ref` plus a
|
|
10
|
+
* `createEffect`. That effect now lives in here, once, and callers pass a prop.
|
|
11
|
+
*
|
|
12
|
+
* Note that the property is purely visual plus an accessibility hint: a checkbox
|
|
13
|
+
* whose `indeterminate` is true still reports `checked === false` and submits
|
|
14
|
+
* accordingly. A native indeterminate checkbox already announces as "mixed", so
|
|
15
|
+
* this component does not stamp `aria-checked="mixed"` on top of it — pass one
|
|
16
|
+
* yourself if you also want the attribute in the DOM.
|
|
17
|
+
*/
|
|
18
|
+
indeterminate?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A checkbox. A REAL `<input type="checkbox">` behind `appearance: none`, styled by
|
|
23
|
+
* the kit's `.kai-checkbox` rule, never a `<div>` wearing `role="checkbox"`.
|
|
24
|
+
*
|
|
25
|
+
* That is the whole design: the native control brings keyboard operation (Space),
|
|
26
|
+
* the `:focus-visible` ring, form participation via `name` / `value`, and correct
|
|
27
|
+
* screen-reader announcement — all of it for free and all of it correct. Every
|
|
28
|
+
* accessibility defect this kit's control audit found was in a control that had
|
|
29
|
+
* replaced the native element with something hand-rolled.
|
|
30
|
+
*
|
|
31
|
+
* Everything not listed in `CheckboxProps` is forwarded to the input, so `id`,
|
|
32
|
+
* `name`, `value`, `required`, `disabled`, `aria-*` and any `data-*` hook behave
|
|
33
|
+
* exactly as they do on a plain `<input>`.
|
|
34
|
+
*
|
|
35
|
+
* Validation is deliberately absent. `required` is passed through to the native
|
|
36
|
+
* attribute and nothing else — whether an unchecked box is an error is the
|
|
37
|
+
* consuming application's call, not the kit's.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Checkbox name="notify" checked={on()} onChange={(e) => setOn(e.currentTarget.checked)} />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function Checkbox(props: CheckboxProps): JSX.Element {
|
|
44
|
+
const [local, rest] = splitProps(props, ['class', 'indeterminate', 'ref']);
|
|
45
|
+
return (
|
|
46
|
+
<input
|
|
47
|
+
{...rest}
|
|
48
|
+
type="checkbox"
|
|
49
|
+
class={cn('kai-checkbox', local.class)}
|
|
50
|
+
ref={(el) => {
|
|
51
|
+
// `ref` is split out and re-applied by hand because this component needs the
|
|
52
|
+
// element for `indeterminate` as well. Solid always hands a component a ref
|
|
53
|
+
// FUNCTION (a variable ref compiles into one), so the typeof guard is for the
|
|
54
|
+
// hand-written object case only.
|
|
55
|
+
const forward = local.ref;
|
|
56
|
+
if (typeof forward === 'function') (forward as (el: HTMLInputElement) => void)(el);
|
|
57
|
+
createEffect(() => {
|
|
58
|
+
el.indeterminate = !!local.indeterminate;
|
|
59
|
+
});
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|