@kitn.ai/ui 0.25.1 → 0.26.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 +5 -7
- package/dist/components/attachment-types.d.ts +46 -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/chat-thread.d.ts +3 -41
- package/dist/components/code-block.d.ts +22 -0
- package/dist/components/conversation-item.d.ts +54 -1
- package/dist/components/conversation-list.d.ts +65 -0
- package/dist/components/form.d.ts +42 -0
- package/dist/components/message.d.ts +7 -0
- package/dist/components/toast.d.ts +2 -1
- 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/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
- package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
- package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
- package/dist/custom-elements.json +2931 -2572
- 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/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/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
- package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
- package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
- package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
- package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
- package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
- package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
- package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
- package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
- package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
- package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
- package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
- package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
- package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
- package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
- package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
- package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
- package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
- package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
- package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
- package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
- package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
- package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
- package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
- package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
- package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
- package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
- package/dist/elements/chunks/form-CQirdC4W.js +1 -0
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
- package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
- package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
- package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
- package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
- package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
- package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
- package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
- package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
- package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
- package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
- package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
- package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
- package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
- package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
- package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
- package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
- package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
- package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
- package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
- package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
- package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
- package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
- package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
- package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
- package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
- package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
- package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
- package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
- package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
- package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
- package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
- package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
- package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
- 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.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 +149 -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-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-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/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/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/slots.d.ts +35 -3
- 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 +232 -148
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5456 -4931
- package/dist/index.server.js +4142 -3669
- package/dist/kai-provider.es.js +40 -40
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +184 -69
- package/dist/llms/llms.txt +3 -3
- package/dist/mcp.es.js +2669 -546
- 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/react/index.d.ts +115 -266
- package/dist/react.js +247 -221
- package/dist/register-impl-DpE7icIb.js +293 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/tool-defs.d.ts +35 -0
- package/dist/schemas.js +403 -310
- package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
- package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
- package/dist/solid.d.ts +5 -0
- package/dist/solid.js +210 -203
- package/dist/solid.server.js +210 -203
- package/dist/state/index.d.ts +4 -0
- 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 +269 -169
- package/dist/types.d.ts +4 -2
- package/dist/ui/dock.d.ts +109 -0
- package/dist/ui/hover-card.d.ts +44 -0
- package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
- package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
- package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
- package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
- package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
- package/dist/variant-custom-L16JLLZy.js +1 -0
- package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
- package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
- package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
- 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/frameworks/react/index.tsx +132 -93
- package/llms-full.txt +184 -69
- package/llms.txt +3 -3
- package/package.json +27 -7
- 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 +275 -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 +61 -5
- package/src/agent-tooling/mcp/tools/reference.ts +440 -4
- 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/registry.ts +19 -7
- package/src/agent-tooling/route-emit.ts +26 -2
- package/src/components/attachment-types.ts +47 -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/chat-container.tsx +4 -1
- package/src/components/chat-thread.tsx +36 -7
- package/src/components/code-block.tsx +79 -3
- package/src/components/composer.tsx +8 -1
- package/src/components/conversation-item.tsx +114 -4
- package/src/components/conversation-list.tsx +212 -12
- package/src/components/embed.tsx +4 -0
- package/src/components/form.tsx +41 -14
- package/src/components/message.tsx +75 -5
- package/src/components/response-stream.tsx +13 -7
- package/src/components/screen.tsx +5 -0
- package/src/components/scroll-button.tsx +7 -0
- package/src/components/toast.tsx +11 -5
- 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 +6 -7
- package/src/elements/audio-visualizer.tsx +48 -4
- package/src/elements/chat-workspace.tsx +159 -287
- package/src/elements/chat.tsx +12 -9
- 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/conversation-item.tsx +80 -0
- package/src/elements/conversation-list.tsx +60 -11
- package/src/elements/default-input.tsx +5 -5
- 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 +204 -0
- package/src/elements/dropdown.tsx +123 -0
- package/src/elements/element-diagnostics.ts +392 -0
- package/src/elements/element-manifest.json +12 -0
- package/src/elements/element-meta.json +672 -439
- package/src/elements/element-nonscalar.json +170 -0
- package/src/elements/element-types.d.ts +232 -148
- package/src/elements/menu.tsx +13 -0
- package/src/elements/prompt-dock.tsx +2 -1
- package/src/elements/prompt-input.tsx +8 -8
- package/src/elements/register-impl.ts +46 -0
- package/src/elements/register.ts +28 -0
- package/src/elements/resizable.tsx +52 -25
- package/src/elements/scroll-button.tsx +5 -0
- package/src/elements/slots.ts +107 -12
- package/src/elements/styles.css +30 -6
- package/src/elements/thinking-bar.tsx +16 -7
- package/src/elements/toast.tsx +3 -0
- 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 +2 -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 +297 -6
- package/src/solid.ts +5 -0
- package/src/state/index.ts +12 -0
- 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/dialog.tsx +6 -0
- package/src/ui/dock.tsx +445 -0
- package/src/ui/dropdown.tsx +67 -23
- package/src/ui/hover-card.tsx +156 -6
- package/src/ui/input.tsx +29 -10
- package/src/ui/resizable.tsx +11 -0
- 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/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
- package/dist/elements/chunks/arrow-left-iJVnQmxA.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/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/circle-BG7VkPep.js +0 -1
- package/dist/elements/chunks/circle-check-GqvO2UBs.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/context-hKjNqiyQ.js +0 -1
- package/dist/elements/chunks/conversation-list-CsGJjSKG.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/dropdown-CmNna5lT.js +0 -1
- package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
- package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
- package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
- package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
- package/dist/elements/chunks/input-DrseEjhp.js +0 -1
- package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
- package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
- package/dist/elements/chunks/reasoning-BrxQfpfI.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/store-HIPGhNz9.js +0 -1
- package/dist/elements/chunks/textarea-DPfuhswM.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/x-BOhjBeOd.js +0 -1
- package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
- package/dist/register-impl-DGmGzrEM.js +0 -145
- package/dist/variant-custom-MkTNTycd.js +0 -1
- package/src/primitives/card-validate-generator.testlib.ts +0 -38
|
@@ -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/dialog.tsx
CHANGED
|
@@ -190,6 +190,12 @@ export function Dialog(props: DialogProps) {
|
|
|
190
190
|
tabindex={-1}
|
|
191
191
|
data-expanded={presence.state() === 'open' ? '' : undefined}
|
|
192
192
|
data-closed={presence.state() === 'closed' ? '' : undefined}
|
|
193
|
+
// `outline-none` is DELIBERATE here and must stay. This is a
|
|
194
|
+
// `tabindex={-1}` focus-trap container: it is focused
|
|
195
|
+
// programmatically on open to move keyboard and screen-reader
|
|
196
|
+
// context into the dialog, and is never a tab stop. Drawing a ring
|
|
197
|
+
// would put a blue box around the entire dialog every time it
|
|
198
|
+
// opened. The controls inside it carry their own focus rings.
|
|
193
199
|
class={cn(
|
|
194
200
|
'flex max-h-[85vh] w-full max-w-lg flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-xl outline-none',
|
|
195
201
|
'animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95 motion-reduce:animate-none',
|
package/src/ui/dock.tsx
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEffect, createSignal, createUniqueId, Show, untrack, type Accessor, type JSX,
|
|
3
|
+
} from 'solid-js';
|
|
4
|
+
import { MessageCircle, X } from 'lucide-solid';
|
|
5
|
+
|
|
6
|
+
/** Which corner the dock sits in. LOGICAL, so `-end` follows the writing direction
|
|
7
|
+
* and an RTL page docks on the left with no extra work (both reference
|
|
8
|
+
* implementations reached for `inset-inline-end` independently). */
|
|
9
|
+
export type DockPosition = 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
|
|
10
|
+
|
|
11
|
+
/** Where focus goes when the panel opens.
|
|
12
|
+
* - `content` (default) — `focus()` the first element assigned to the panel, falling
|
|
13
|
+
* back to the panel itself when that element cannot take focus. Content-AGNOSTIC:
|
|
14
|
+
* the dock never learns what it is holding.
|
|
15
|
+
* - `panel` — focus the `tabindex="-1"` panel container (what `kai-dialog` does).
|
|
16
|
+
* - `none` — never move focus. */
|
|
17
|
+
export type DockFocusOnOpen = 'content' | 'panel' | 'none';
|
|
18
|
+
|
|
19
|
+
/** Imperative open controller, handed to a parent (the `kai-dock` facade) via
|
|
20
|
+
* `controllerRef` so it can drive/observe open state with `wireDisclosure`. */
|
|
21
|
+
export interface DockController { open: Accessor<boolean>; setOpen: (v: boolean) => void }
|
|
22
|
+
|
|
23
|
+
export interface DockProps {
|
|
24
|
+
/** The panel body. ANY content — the dock never reads or types it. */
|
|
25
|
+
children?: JSX.Element;
|
|
26
|
+
/** Content inside the built-in button while CLOSED. Defaults to a chat glyph. */
|
|
27
|
+
launcher?: JSX.Element;
|
|
28
|
+
/** Content inside the button while OPEN. Defaults to a ✕; when only `launcher` is
|
|
29
|
+
* given, that content stays rather than morphing into a clashing built-in. */
|
|
30
|
+
launcherOpen?: JSX.Element;
|
|
31
|
+
/** The widget's NAME. Derives the panel's accessible name and both launcher names
|
|
32
|
+
* (`Open ${label}` / `Close ${label}`) — one string instead of three. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** i18n override for the launcher's name while closed. */
|
|
35
|
+
openLabel?: string;
|
|
36
|
+
/** i18n override for the launcher's name while open. */
|
|
37
|
+
closeLabel?: string;
|
|
38
|
+
/** Which corner. Logical, RTL-correct. Defaults to `bottom-end`. */
|
|
39
|
+
position?: DockPosition;
|
|
40
|
+
/** Render the unread dot. CONSUMER-OWNED: shown only while closed, and never
|
|
41
|
+
* written back by the dock (see the component doc). */
|
|
42
|
+
unread?: boolean;
|
|
43
|
+
/** Disable the launcher. */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Where focus lands on open. Defaults to `content`. */
|
|
46
|
+
focusOnOpen?: DockFocusOnOpen;
|
|
47
|
+
/** Controlled open state. When set, the component never changes it itself — drive it
|
|
48
|
+
* from `onOpenChange`. Omit for uncontrolled (internal) state. */
|
|
49
|
+
open?: boolean;
|
|
50
|
+
/** Initial open state when uncontrolled. Does NOT move focus (see the focus contract). */
|
|
51
|
+
defaultOpen?: boolean;
|
|
52
|
+
/** Fires whenever the dock wants to open or close (launcher / Escape / a method). */
|
|
53
|
+
onOpenChange?: (open: boolean) => void;
|
|
54
|
+
/** Receive the open controller once mounted. */
|
|
55
|
+
controllerRef?: (api: DockController) => void;
|
|
56
|
+
/** Receive the focusable panel node so a facade's `focus()` can target it. */
|
|
57
|
+
panelRef?: (el: HTMLElement) => void;
|
|
58
|
+
/** Receive the launcher button so a facade's `focus()` can target it. */
|
|
59
|
+
launcherRef?: (el: HTMLButtonElement) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the element `focusOnOpen: 'content'` should focus.
|
|
62
|
+
*
|
|
63
|
+
* A PROP, because the answer is "the first element assigned to the panel slot" and
|
|
64
|
+
* only the web-component facade can see slot assignments — a Solid caller passes
|
|
65
|
+
* its own ref instead. Returning nothing falls back to focusing the panel.
|
|
66
|
+
*/
|
|
67
|
+
contentTarget?: () => HTMLElement | null | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The built-in closed glyph. Exported so the facade can use it as its `launcher`
|
|
71
|
+
* slot's FALLBACK content — one definition, two users, rather than a second copy
|
|
72
|
+
* of the same icon choice living in the element layer. */
|
|
73
|
+
export function DockLauncherGlyph(): JSX.Element {
|
|
74
|
+
return <MessageCircle size={24} />;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The built-in open glyph. Exported for the same reason as {@link DockLauncherGlyph}. */
|
|
78
|
+
export function DockCloseGlyph(): JSX.Element {
|
|
79
|
+
return <X size={24} />;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The dock's own stylesheet, scoped to its shadow root.
|
|
84
|
+
*
|
|
85
|
+
* IN A `<style>` RATHER THAN INLINE, and that is a decision. `kai-prompt-dock` puts
|
|
86
|
+
* its tokenized chrome in an inline `style={{}}`, which resolves `var()` fallbacks
|
|
87
|
+
* and lets an outside override win — but an inline declaration also beats every rule
|
|
88
|
+
* in the cascade, and this element ships TWO rules that have to be able to win: the
|
|
89
|
+
* narrow-viewport full-bleed default and the reduced-motion rule. Both are media
|
|
90
|
+
* queries, which cannot be expressed inline at all. So the geometry lives here and
|
|
91
|
+
* only the open/closed state (visibility / opacity / pointer-events) is inline, where
|
|
92
|
+
* it belongs: that one must beat any consumer rule, and it is also the half a jsdom
|
|
93
|
+
* test can actually read.
|
|
94
|
+
*
|
|
95
|
+
* EVERY RULE IS SCOPED UNDER `[data-kai-dock]`, including the `[part=...]` ones. Inside
|
|
96
|
+
* a shadow root that scoping is redundant; in a plain Solid page it is the difference
|
|
97
|
+
* between styling this dock and restyling every `[part="panel"]` on the document.
|
|
98
|
+
*
|
|
99
|
+
* DOCUMENTED CONSTRAINT, which the element cannot fix from inside: put `<kai-dock>`
|
|
100
|
+
* as a body-level child. A `transform` / `filter` / `contain` ancestor makes
|
|
101
|
+
* `position: fixed` resolve against that ancestor instead of the viewport.
|
|
102
|
+
*/
|
|
103
|
+
const DOCK_CSS = `
|
|
104
|
+
:host { display: contents; }
|
|
105
|
+
|
|
106
|
+
[data-kai-dock] {
|
|
107
|
+
position: fixed;
|
|
108
|
+
z-index: var(--kai-dock-z, 1000);
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: var(--kai-dock-gap, 0.85rem);
|
|
111
|
+
/* Only a positioning frame: it must not swallow clicks on the page it floats
|
|
112
|
+
over. The launcher and the open panel opt back in. */
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Logical insets throughout, so an RTL page docks on the other side for free.
|
|
117
|
+
column-reverse for the bottom corners keeps the LAUNCHER FIRST IN DOM ORDER --
|
|
118
|
+
Tab reaches the control before the panel content -- while painting it below. */
|
|
119
|
+
[data-kai-dock][data-position^="bottom"] {
|
|
120
|
+
flex-direction: column-reverse;
|
|
121
|
+
inset-block-end: var(--kai-dock-inset, 1.5rem);
|
|
122
|
+
}
|
|
123
|
+
[data-kai-dock][data-position^="top"] {
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
inset-block-start: var(--kai-dock-inset, 1.5rem);
|
|
126
|
+
}
|
|
127
|
+
[data-kai-dock][data-position$="end"] {
|
|
128
|
+
inset-inline-end: var(--kai-dock-inset, 1.5rem);
|
|
129
|
+
align-items: flex-end;
|
|
130
|
+
}
|
|
131
|
+
[data-kai-dock][data-position$="start"] {
|
|
132
|
+
inset-inline-start: var(--kai-dock-inset, 1.5rem);
|
|
133
|
+
align-items: flex-start;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
[data-kai-dock] [part="launcher"] {
|
|
137
|
+
pointer-events: auto;
|
|
138
|
+
position: relative;
|
|
139
|
+
/* ABOVE the panel, always. At <=480px the panel goes fixed/inset:0 and it comes
|
|
140
|
+
after the launcher in DOM order, so at z-index:auto it painted OVER the launcher
|
|
141
|
+
and a touch user lost every pointer route to closing. Both reference
|
|
142
|
+
implementations stacked the launcher over the panel for the same reason. */
|
|
143
|
+
z-index: 1;
|
|
144
|
+
flex: none;
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
gap: 0.5rem;
|
|
149
|
+
/* The size token is the BLOCK size and the inline MINIMUM, not a fixed square. An
|
|
150
|
+
icon-only launcher is still a perfect disc (the icon plus padding is narrower
|
|
151
|
+
than the minimum), but a slotted TEXT label — the reference app's "Support"
|
|
152
|
+
pill — widens the button instead of overflowing it. */
|
|
153
|
+
inline-size: auto;
|
|
154
|
+
min-inline-size: var(--kai-dock-launcher-size, 56px);
|
|
155
|
+
block-size: var(--kai-dock-launcher-size, 56px);
|
|
156
|
+
padding-block: 0;
|
|
157
|
+
padding-inline: 0.9rem;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
border: 0;
|
|
160
|
+
border-radius: 9999px;
|
|
161
|
+
background: var(--color-primary);
|
|
162
|
+
color: var(--color-primary-foreground);
|
|
163
|
+
box-shadow: 0 10px 25px -6px rgb(0 0 0 / 0.35);
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition: transform 150ms ease, box-shadow 150ms ease;
|
|
166
|
+
}
|
|
167
|
+
[data-kai-dock] [part="launcher"]:hover:not(:disabled) { transform: translateY(-1px); }
|
|
168
|
+
[data-kai-dock] [part="launcher"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
|
|
169
|
+
[data-kai-dock] [part="launcher"]:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
170
|
+
|
|
171
|
+
[data-kai-dock] [part="badge"] {
|
|
172
|
+
position: absolute;
|
|
173
|
+
inset-block-start: 3px;
|
|
174
|
+
inset-inline-end: 3px;
|
|
175
|
+
inline-size: 0.75rem;
|
|
176
|
+
block-size: 0.75rem;
|
|
177
|
+
border-radius: 9999px;
|
|
178
|
+
background: var(--color-destructive);
|
|
179
|
+
box-shadow: 0 0 0 2px var(--color-primary);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
[data-kai-dock] [part="panel"] {
|
|
183
|
+
pointer-events: auto;
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
inline-size: var(--kai-dock-width, 380px);
|
|
188
|
+
block-size: var(--kai-dock-height, 600px);
|
|
189
|
+
max-inline-size: calc(100vw - 2 * var(--kai-dock-inset, 1.5rem));
|
|
190
|
+
max-block-size: calc(
|
|
191
|
+
100dvh - 2 * var(--kai-dock-inset, 1.5rem)
|
|
192
|
+
- var(--kai-dock-launcher-size, 56px) - var(--kai-dock-gap, 0.85rem)
|
|
193
|
+
);
|
|
194
|
+
border: 1px solid var(--color-border);
|
|
195
|
+
border-radius: var(--kai-dock-radius, 16px);
|
|
196
|
+
background: var(--color-background);
|
|
197
|
+
color: var(--color-foreground);
|
|
198
|
+
box-shadow: 0 20px 45px -12px rgb(0 0 0 / 0.35);
|
|
199
|
+
/* VISIBILITY IS SWITCHED, NEVER ANIMATED, and the zero duration is the whole point.
|
|
200
|
+
visibility 180ms here meant the COMPUTED value stayed hidden for about two
|
|
201
|
+
frames after opening while the inline style already said visible — and a
|
|
202
|
+
focus() into a hidden subtree is dropped silently, so focusOnOpen never landed
|
|
203
|
+
in a real browser (it worked under reduced motion, which is what made it look like
|
|
204
|
+
a focus bug rather than a paint one).
|
|
205
|
+
This rule is the CLOSING direction: hold visibility for the length of the fade so
|
|
206
|
+
the panel does not vanish mid-transition, then switch. The opening direction sets
|
|
207
|
+
the delay to 0s below, so opening is synchronous and focus has somewhere to go. */
|
|
208
|
+
transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
|
|
209
|
+
}
|
|
210
|
+
[data-kai-dock] [part="panel"][data-expanded] {
|
|
211
|
+
transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 0s;
|
|
212
|
+
}
|
|
213
|
+
/* The panel is a flex column and the slotted surface fills it -- the one layout
|
|
214
|
+
fact both reference implementations wrote by hand (flex:1; min-height:0). Both
|
|
215
|
+
spellings are here on purpose: ::slotted covers the shadow-DOM facade, > *
|
|
216
|
+
covers a plain Solid caller, and each is inert in the other's world (a <slot>
|
|
217
|
+
is display:contents, so flex properties do nothing to it). */
|
|
218
|
+
[data-kai-dock] [part="panel"] ::slotted(*) { flex: 1 1 auto; min-block-size: 0; }
|
|
219
|
+
[data-kai-dock] [part="panel"] > * { flex: 1 1 auto; min-block-size: 0; }
|
|
220
|
+
[data-kai-dock] [part="panel"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
|
|
221
|
+
[data-kai-dock][data-position^="bottom"] [part="panel"][data-closed] { transform: translateY(0.5rem); }
|
|
222
|
+
[data-kai-dock][data-position^="top"] [part="panel"][data-closed] { transform: translateY(-0.5rem); }
|
|
223
|
+
|
|
224
|
+
/* Narrow viewports take the panel full-bleed. A SHIPPED DEFAULT, not a prop: both
|
|
225
|
+
references wrote this rule independently, which makes it a HOW. The launcher stays
|
|
226
|
+
in its corner and ON TOP of the full-bleed panel — that is the z-index: 1 in the
|
|
227
|
+
launcher rule above, not anything in this block, and without it a phone user has no
|
|
228
|
+
pointer route to closing at all (Escape alone strands exactly the touch user). */
|
|
229
|
+
@media (max-width: 480px) {
|
|
230
|
+
[data-kai-dock] [part="panel"] {
|
|
231
|
+
position: fixed;
|
|
232
|
+
inset: 0;
|
|
233
|
+
inline-size: auto;
|
|
234
|
+
block-size: auto;
|
|
235
|
+
max-inline-size: none;
|
|
236
|
+
max-block-size: none;
|
|
237
|
+
border: 0;
|
|
238
|
+
border-radius: 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@media (prefers-reduced-motion: reduce) {
|
|
243
|
+
/* [data-expanded] is listed explicitly because the open-direction rule above is
|
|
244
|
+
MORE SPECIFIC than a bare [part="panel"], so without it an opening panel kept
|
|
245
|
+
animating opacity and transform for exactly the users who asked it not to. */
|
|
246
|
+
[data-kai-dock] [part="launcher"],
|
|
247
|
+
[data-kai-dock] [part="panel"],
|
|
248
|
+
[data-kai-dock] [part="panel"][data-expanded] { transition: none; }
|
|
249
|
+
[data-kai-dock] [part="panel"][data-closed] { transform: none; }
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The deepest active element, drilling through nested shadow roots.
|
|
255
|
+
*
|
|
256
|
+
* A local copy of the helper `ui/dialog.tsx` already carries. Deliberately not
|
|
257
|
+
* refactored into a shared module by this change: dialog is not this task's file, and
|
|
258
|
+
* a four-line tree walk duplicated once is cheaper than a cross-file move nobody
|
|
259
|
+
* asked for. If a third caller appears, hoist it then.
|
|
260
|
+
*/
|
|
261
|
+
function deepActiveElement(): HTMLElement | null {
|
|
262
|
+
let el = document.activeElement as HTMLElement | null;
|
|
263
|
+
while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement as HTMLElement;
|
|
264
|
+
return el;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Is `node` inside `root` in the FLATTENED tree?
|
|
269
|
+
*
|
|
270
|
+
* `root.contains(node)` is the wrong question here and would answer `false` for every
|
|
271
|
+
* real case: the panel's content is light-DOM nodes ASSIGNED to a slot, so it lives in
|
|
272
|
+
* the host's tree, not the panel's. This walks up through `assignedSlot` at each slot
|
|
273
|
+
* and through `host` at each shadow boundary, which is the tree focus actually moves in.
|
|
274
|
+
*/
|
|
275
|
+
function isWithin(root: Node, node: Node | null): boolean {
|
|
276
|
+
let cur: Node | null = node;
|
|
277
|
+
while (cur) {
|
|
278
|
+
if (cur === root) return true;
|
|
279
|
+
const slot = cur instanceof Element ? cur.assignedSlot : null;
|
|
280
|
+
if (slot) { cur = slot; continue; }
|
|
281
|
+
if (cur.parentNode) { cur = cur.parentNode; continue; }
|
|
282
|
+
cur = cur instanceof ShadowRoot ? cur.host : null;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Dock — a corner-docked launcher button with a panel that opens above it.
|
|
289
|
+
*
|
|
290
|
+
* The affordance behind a support widget: a circular button pinned to a corner of the
|
|
291
|
+
* viewport, and a floating panel holding whatever you slot into it. The panel is
|
|
292
|
+
* content-agnostic on purpose — a chat, a form, your own component — and the dock
|
|
293
|
+
* never reads, types or reaches into it.
|
|
294
|
+
*
|
|
295
|
+
* WHAT IT OWNS (all of it non-configurable, because these are facts about the medium
|
|
296
|
+
* rather than preferences):
|
|
297
|
+
*
|
|
298
|
+
* - **Hide semantics.** Closed means `visibility: hidden` + `opacity: 0` + `inert`,
|
|
299
|
+
* and the panel is NEVER unmounted. `display: none` would leave the panel with no
|
|
300
|
+
* layout box while closed, so anything inside that measures itself — a thread
|
|
301
|
+
* scroller, a `ResizeObserver` — measures zero and re-measures on every open. The
|
|
302
|
+
* cost of keeping the box is that a stream keeps rendering into a closed panel,
|
|
303
|
+
* which is precisely what makes an unread dot honest.
|
|
304
|
+
* - **Focus.** On open, focus moves per `focusOnOpen`; on close it returns to the
|
|
305
|
+
* launcher, always. `inert` is cleared and visibility restored BEFORE `focus()` runs
|
|
306
|
+
* — a `focus()` into an inert or hidden subtree is silently dropped, no error — via
|
|
307
|
+
* a `queueMicrotask`, the same shape `ui/dialog.tsx` uses. Mount moves no focus.
|
|
308
|
+
* - **Escape.** Closes only while the dock CONTAINS focus, and never stops the event.
|
|
309
|
+
* A background widget that ate every Escape on the page would break the host page's
|
|
310
|
+
* own menus; swallowing a key from a page you are a guest on is a decide-quietly move.
|
|
311
|
+
* - **No focus trap**, deliberately, unlike `kai-dialog`. The page staying usable is
|
|
312
|
+
* the whole point of "docked", which is also why the panel is `aria-modal="false"`.
|
|
313
|
+
*
|
|
314
|
+
* WHAT IT REFUSES: it never aborts a request, never persists its own open state, never
|
|
315
|
+
* decides what "unread" means, and never clears `unread` on open — writing over a
|
|
316
|
+
* consumer's prop is the trap `kai-chat.loading` fell into. Those are the app's calls;
|
|
317
|
+
* `open` + `onOpenChange` is the seam.
|
|
318
|
+
*
|
|
319
|
+
* Geometry is CSS custom properties rather than props: `--kai-dock-width` ·
|
|
320
|
+
* `--kai-dock-height` · `--kai-dock-inset` · `--kai-dock-gap` · `--kai-dock-radius` ·
|
|
321
|
+
* `--kai-dock-z` · `--kai-dock-launcher-size`.
|
|
322
|
+
*
|
|
323
|
+
* @see `elements/dock.tsx` for the `<kai-dock>` web-component facade.
|
|
324
|
+
*/
|
|
325
|
+
export function Dock(props: DockProps) {
|
|
326
|
+
const panelId = `kai-dock-panel-${createUniqueId()}`;
|
|
327
|
+
const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false);
|
|
328
|
+
|
|
329
|
+
const isControlled = () => props.open !== undefined;
|
|
330
|
+
const isOpen = () => (isControlled() ? !!props.open : internalOpen());
|
|
331
|
+
const setOpen = (v: boolean) => {
|
|
332
|
+
if (!isControlled()) setInternalOpen(v);
|
|
333
|
+
props.onOpenChange?.(v);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
props.controllerRef?.({ open: isOpen, setOpen });
|
|
337
|
+
|
|
338
|
+
let panel: HTMLElement | undefined;
|
|
339
|
+
let launcher: HTMLButtonElement | undefined;
|
|
340
|
+
|
|
341
|
+
const label = () => props.label ?? 'Chat';
|
|
342
|
+
const launcherName = () => (isOpen()
|
|
343
|
+
? props.closeLabel ?? `Close ${label()}`
|
|
344
|
+
: props.openLabel ?? `Open ${label()}`);
|
|
345
|
+
|
|
346
|
+
/** The focus half of an open, deferred so the state flip above has already landed. */
|
|
347
|
+
const focusOnOpen = () => {
|
|
348
|
+
const mode = props.focusOnOpen ?? 'content';
|
|
349
|
+
if (mode === 'none') return;
|
|
350
|
+
if (mode === 'content') {
|
|
351
|
+
const target = props.contentTarget?.();
|
|
352
|
+
target?.focus();
|
|
353
|
+
// Content-agnostic fallback: a plain <div> is not focusable, so `focus()` was a
|
|
354
|
+
// silent no-op and the panel takes it instead. Asking where focus ACTUALLY
|
|
355
|
+
// landed is the only way to tell, since the dock is not allowed to inspect what
|
|
356
|
+
// it is holding.
|
|
357
|
+
if (target && isWithin(panel!, deepActiveElement())) return;
|
|
358
|
+
}
|
|
359
|
+
panel?.focus();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// The focus choreography, and the ONE ordering that is a hard invariant.
|
|
363
|
+
//
|
|
364
|
+
// SEEDED WITH THE INITIAL OPEN VALUE, not `false`. `kai-dialog` seeds `false` on
|
|
365
|
+
// purpose so a `defaultOpen` modal DOES take focus at mount; a docked widget is the
|
|
366
|
+
// opposite — it is a guest on someone's page and must not steal focus on load, from
|
|
367
|
+
// `default-open` OR from `open`. Same mechanism, opposite default, and the
|
|
368
|
+
// difference is exactly the modal/non-modal split.
|
|
369
|
+
createEffect((wasOpen: boolean) => {
|
|
370
|
+
const open = isOpen();
|
|
371
|
+
if (open !== wasOpen) {
|
|
372
|
+
// The attributes and inline styles below are written by render effects, which
|
|
373
|
+
// Solid runs before this one; the microtask is belt-and-braces for a state
|
|
374
|
+
// change driven from outside a Solid batch, and gives a freshly-slotted element
|
|
375
|
+
// a turn to upgrade before we call focus() on it.
|
|
376
|
+
if (open) queueMicrotask(focusOnOpen);
|
|
377
|
+
else queueMicrotask(() => launcher?.focus());
|
|
378
|
+
}
|
|
379
|
+
return open;
|
|
380
|
+
}, untrack(() => props.open ?? props.defaultOpen ?? false));
|
|
381
|
+
|
|
382
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
383
|
+
if (e.key !== 'Escape' || !isOpen()) return;
|
|
384
|
+
// D-C. This listener is ON the dock rather than on `document`, so the only
|
|
385
|
+
// keydowns it can ever see are ones whose target is inside it — which IS the
|
|
386
|
+
// condition "the dock contains focus", with no ambiguity and no second check. It
|
|
387
|
+
// also composes: an overlay inside the panel that handles its own Escape and
|
|
388
|
+
// stops the event correctly prevents the dock from closing underneath it.
|
|
389
|
+
// No stopPropagation, no preventDefault: the host page keeps its key.
|
|
390
|
+
setOpen(false);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<>
|
|
395
|
+
<style>{DOCK_CSS}</style>
|
|
396
|
+
<div data-kai-dock data-position={props.position ?? 'bottom-end'} onKeyDown={onKeyDown}>
|
|
397
|
+
<button
|
|
398
|
+
ref={(el) => { launcher = el; props.launcherRef?.(el); }}
|
|
399
|
+
part="launcher"
|
|
400
|
+
type="button"
|
|
401
|
+
aria-expanded={isOpen() ? 'true' : 'false'}
|
|
402
|
+
aria-controls={panelId}
|
|
403
|
+
// The NAME is what changes for assistive tech; the glyph morph is decorative.
|
|
404
|
+
aria-label={launcherName()}
|
|
405
|
+
disabled={props.disabled || undefined}
|
|
406
|
+
onClick={() => setOpen(!isOpen())}
|
|
407
|
+
>
|
|
408
|
+
<Show when={props.unread && !isOpen()}>
|
|
409
|
+
<span part="badge" aria-hidden="true" />
|
|
410
|
+
</Show>
|
|
411
|
+
<span aria-hidden="true" style={{ display: 'inline-flex' }}>
|
|
412
|
+
<Show when={isOpen()} fallback={props.launcher ?? <DockLauncherGlyph />}>
|
|
413
|
+
{props.launcherOpen ?? props.launcher ?? <DockCloseGlyph />}
|
|
414
|
+
</Show>
|
|
415
|
+
</span>
|
|
416
|
+
</button>
|
|
417
|
+
<div
|
|
418
|
+
ref={(el) => { panel = el; props.panelRef?.(el); }}
|
|
419
|
+
part="panel"
|
|
420
|
+
id={panelId}
|
|
421
|
+
role="dialog"
|
|
422
|
+
// Non-modal, and saying so is load-bearing given there is no focus trap:
|
|
423
|
+
// claiming modality without trapping is worse than claiming neither.
|
|
424
|
+
aria-modal="false"
|
|
425
|
+
aria-label={label()}
|
|
426
|
+
tabindex={-1}
|
|
427
|
+
data-expanded={isOpen() ? '' : undefined}
|
|
428
|
+
data-closed={isOpen() ? undefined : ''}
|
|
429
|
+
// `inert` is the half that actually stops Tab: a visibility:hidden panel is
|
|
430
|
+
// already skipped by most AT, but a focusable control inside it is not.
|
|
431
|
+
bool:inert={!isOpen()}
|
|
432
|
+
// INLINE, unlike the geometry: the closed state must beat any consumer rule,
|
|
433
|
+
// and it is the half a test can read without a layout engine.
|
|
434
|
+
style={{
|
|
435
|
+
visibility: isOpen() ? 'visible' : 'hidden',
|
|
436
|
+
opacity: isOpen() ? '1' : '0',
|
|
437
|
+
'pointer-events': isOpen() ? 'auto' : 'none',
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
{props.children}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</>
|
|
444
|
+
);
|
|
445
|
+
}
|