@kitn.ai/ui 0.28.0 → 0.30.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 +11 -7
- package/dist/{Icon-DObhsRtm.js → Icon-BzBET3E-.js} +1 -1
- package/dist/{action-icons-CcgNN2oT.js → action-icons-DaW2pU6m.js} +1 -1
- package/dist/agent-tooling/construct/public.d.ts +15 -0
- package/dist/agent-tooling/construct/schema.d.ts +111 -0
- package/dist/arrow-down-D783iMJj.js +1 -0
- package/dist/arrow-left-0VLM5kyg.js +1 -0
- package/dist/artifact-DGjzHYUT.js +1 -0
- package/dist/{attachments-Qr4kg-2b.js → attachments-YvXHlg88.js} +1 -1
- package/dist/{audio-visualizer-DToYdfSc.js → audio-visualizer-9lz5IHsL.js} +1 -1
- package/dist/{badge-CdEu_jn2.js → badge-Dh_vp8A5.js} +1 -1
- package/dist/{bell-DQAAOpDz.js → bell-CT7_WUwS.js} +1 -1
- package/dist/{button-DkuOnGLy.js → button-C_3te6fm.js} +1 -1
- package/dist/{card-renderer-Jx70m6_l.js → card-renderer-DZVaGH2e.js} +1 -1
- package/dist/check-CiEOVnEN.js +1 -0
- package/dist/{checkbox-D7iRB3nL.js → checkbox-Bu2T2e0U.js} +1 -1
- package/dist/{checkbox-group-B2_rpoPM.js → checkbox-group-BiryEXPQ.js} +1 -1
- package/dist/chevron-down-CSobuZRG.js +1 -0
- package/dist/chevron-right-DR9ZAP-2.js +1 -0
- package/dist/{choice-card-m0BYqqOK.js → choice-card-CM6IjkmL.js} +1 -1
- package/dist/circle-BC66EYF7.js +1 -0
- package/dist/circle-check-BqmahW0O.js +1 -0
- package/dist/{circle-x-BPG0iPst.js → circle-x-Bk-dA5Wf.js} +1 -1
- package/dist/{code-block-ClMLKb7C.js → code-block-B26t-Q7q.js} +1 -1
- package/dist/{collapsible-B4DlWicL.js → collapsible-Bhc636Cf.js} +1 -1
- package/dist/components/builder-composer-triggers.d.ts +62 -0
- package/dist/components/builder-layout.d.ts +54 -0
- package/dist/components/builder-message-actions.d.ts +72 -0
- package/dist/components/builder-panel.d.ts +189 -0
- package/dist/components/builder-preview.d.ts +11 -0
- package/dist/components/builder-shell-controls.d.ts +69 -0
- package/dist/components/builder-skeleton.d.ts +58 -0
- package/dist/components/builder-start.d.ts +75 -0
- package/dist/components/builder-workspace-variants.d.ts +23 -0
- package/dist/components/captions.d.ts +66 -0
- package/dist/{composer-sQ0HcvyG.js → composer-DMI85B6j.js} +1 -1
- package/dist/{confirm-card-rQWDQsnC.js → confirm-card-B0YIAwXp.js} +1 -1
- package/dist/construct-cli.es.js +3 -3
- package/dist/construct.d.ts +5 -0
- package/dist/construct.js +327 -0
- package/dist/{context-0kg4ZX1J.js → context-DWgfhqWp.js} +1 -1
- package/dist/controllable-BagRpyHv.js +1 -0
- package/dist/{conversation-item-BxwQLaFJ.js → conversation-item-BsOsVGmT.js} +1 -1
- package/dist/{conversation-list-BCfGN1JU.js → conversation-list-aX3Vtc7L.js} +1 -1
- package/dist/{copy-n9u9rqpi.js → copy-BmEwsNhJ.js} +1 -1
- package/dist/core-C8fzo39E.js +1 -12
- package/dist/{create-tween-uPkArjIG.js → create-tween-CNOAU3Fx.js} +1 -1
- package/dist/{create-tween-C-q-JCM-.js → create-tween-WfqiENls.js} +1 -1
- package/dist/{create-tween-CRrJXxBd.js → create-tween-wlFtW9Kh.js} +1 -1
- package/dist/{create-tween-DZ3RRDHD.js → create-tween-zt79ba32.js} +1 -1
- package/dist/{default-input-CaMrJkgH.js → default-input-DMV5Eaze.js} +1 -1
- package/dist/define-CI0y3eE4.js +1 -0
- package/dist/define.js +40 -40
- package/dist/define.server.js +45 -45
- package/dist/{disclosure-WUImBbDv.js → disclosure-NnACBxsC.js} +1 -1
- package/dist/{dropdown-wZVeJOMd.js → dropdown-DT7G0RGR.js} +1 -1
- 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/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.js +1 -1
- package/dist/elements/checkbox.js +1 -1
- 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.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.js +1 -1
- package/dist/elements/dropdown.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- 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.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/radio-group.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +1 -1
- 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.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/slider.js +1 -1
- 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/{ellipsis-CbOpJnU3.js → ellipsis-6pJ7SxYT.js} +1 -1
- package/dist/{embed-CrfjkbUG.js → embed-BMje-nxj.js} +1 -1
- package/dist/engine-javascript-C1x7zo1_.js +1 -141
- package/dist/{external-link-ByXAwBBQ.js → external-link-Beh9DKeR.js} +1 -1
- package/dist/{file-text-DMib2qwt.js → file-text-D1GPfZ8Q.js} +1 -1
- package/dist/{file-tree-CJqM2aP7.js → file-tree-Dwj2lqg-.js} +1 -1
- package/dist/{folder-_BAb929C.js → folder-b6D8x1DY.js} +1 -1
- package/dist/{form-DSZgZQZk.js → form-DWgdxDXu.js} +1 -1
- package/dist/{hover-card-BQbul9eq.js → hover-card-CWRkqm40.js} +1 -1
- package/dist/{icon-CdcfRhYI.js → icon-CL0jdhsf.js} +1 -1
- package/dist/index.js +1741 -1725
- package/dist/index.server.js +1582 -1566
- package/dist/{info-DsFjGuI_.js → info-0cOUherQ.js} +1 -1
- package/dist/{input-DKbmkMos.js → input-BH6Ru91Q.js} +1 -1
- package/dist/kai.es.js +1 -1
- package/dist/{kbd-PvdGhX4s.js → kbd-DRAnVtNX.js} +1 -1
- package/dist/{link-zF8PzWfF.js → link-Dlr0vssB.js} +1 -1
- package/dist/{link-preview-DbM1866N.js → link-preview-DODgRyV9.js} +1 -1
- package/dist/loader-KvJB6NrT.js +1 -0
- package/dist/{markdown-LMvmAyBk.js → markdown-DX9tuJVj.js} +1 -1
- package/dist/mcp.es.js +9 -5
- package/dist/{message-BYZECyek.js → message-BqQ7Ot5q.js} +1 -1
- package/dist/{message-BSsgvHvf.js → message-DkaGfBd8.js} +1 -1
- package/dist/message-circle-BPpp5QKv.js +1 -0
- package/dist/message-square-BLbsd9BJ.js +1 -0
- package/dist/{minimize-2-BQRoBV1V.js → minimize-2-CyOHJwcN.js} +1 -1
- package/dist/{model-switcher-CMpCOYcB.js → model-switcher-0hrKDiK2.js} +1 -1
- package/dist/{overlay-Cb-MkyDa.js → overlay-DBMFXie6.js} +1 -1
- package/dist/{panel-right-i8warFxT.js → panel-right-t8SWJL6e.js} +1 -1
- package/dist/{paperclip-ByY3UVO6.js → paperclip-CdzuOjbU.js} +1 -1
- package/dist/play-BXy1u7rW.js +1 -0
- package/dist/primitives/use-auto-resize.d.ts +11 -0
- package/dist/{progress-bar-jVAHvMUf.js → progress-bar-CiBtxMWT.js} +1 -1
- package/dist/{prompt-suggestion-CD-qtNgC.js → prompt-suggestion-I_DHP21i.js} +1 -1
- package/dist/{radio-BsoBMhkF.js → radio-C_p3n39l.js} +1 -1
- package/dist/{reasoning-4kBPtlTx.js → reasoning-BDwy_tIe.js} +1 -1
- package/dist/register-impl-DvEnhIxx.js +424 -0
- package/dist/{resizable-uPmY9VY-.js → resizable-SYC-fStM.js} +1 -1
- package/dist/{rotate-ccw-B2xn93py.js → rotate-ccw-B1nwmwuY.js} +1 -1
- package/dist/{rotate-cw-CMVLoCyH.js → rotate-cw-CLAnBcm1.js} +1 -1
- package/dist/{scroll-area-BqLUo8ZZ.js → scroll-area-zRyXoLhI.js} +1 -1
- package/dist/{scroll-button-DslXiJyL.js → scroll-button-jzrbYCG9.js} +1 -1
- package/dist/{select-2cLRtPGT.js → select-DM-0nEe0.js} +1 -1
- package/dist/{separator-BWQtNJcM.js → separator-BToOOoOt.js} +1 -1
- package/dist/{settings-BBR63jLg.js → settings-PaGo_S4U.js} +1 -1
- package/dist/{settings-group-B2ciy-Iw.js → settings-group-BFXGIfXq.js} +1 -1
- package/dist/{skeleton-CkHI-HgC.js → skeleton-DYpmFnnm.js} +1 -1
- package/dist/{slider-DDR9U-i0.js → slider-ebutAmkQ.js} +1 -1
- package/dist/{solid-DruVOXnf.js → solid-15v_3WVL.js} +3563 -3412
- package/dist/{solid-BzCFdL25.js → solid-5P-otgdi.js} +4502 -4296
- package/dist/solid.d.ts +6 -0
- package/dist/solid.js +259 -254
- package/dist/solid.server.js +259 -254
- package/dist/{source-GEnFF528.js → source-c1gpRl9h.js} +1 -1
- package/dist/{star-CilHq8GP.js → star-pcV_3LqF.js} +1 -1
- package/dist/{store-Ciyk6eZG.js → store-CSgbFFCg.js} +1 -1
- package/dist/{switch-KkeQIbuH.js → switch-Bh2OdLV2.js} +1 -1
- package/dist/{tasks-card-BrN4bggJ.js → tasks-card-CbqQAn2S.js} +1 -1
- package/dist/text-shimmer-BSzgEnDH.js +1 -0
- package/dist/textarea-DrYNt_0z.js +1 -0
- package/dist/theme.tokens.css +22 -21
- package/dist/{thumbs-up-CsT9Wzb9.js → thumbs-up-BL3-NenK.js} +1 -1
- package/dist/{toast-store-tgK6MuMv.js → toast-store-DkOlYczt.js} +1 -1
- package/dist/{tool-DzUjIBT8.js → tool-CeIWUVUe.js} +1 -1
- package/dist/{tooltip-Bkkg_4t0.js → tooltip-GA-nmg06.js} +1 -1
- package/dist/{trash-2-jBKxjGGQ.js → trash-2-CT66QWlm.js} +1 -1
- package/dist/{triangle-alert-DlQ68ppf.js → triangle-alert-CeHLWxhD.js} +1 -1
- package/dist/ui/color-field.d.ts +48 -0
- package/dist/ui/textarea.d.ts +4 -0
- package/dist/ui/toggle-chip.d.ts +40 -0
- package/dist/{upload-TcAHXv9W.js → upload-9Csb0F0J.js} +1 -1
- package/dist/{use-card-resolution-DaZqAdWf.js → use-card-resolution-Zg0a08Z-.js} +1 -1
- package/dist/{variant-aurora-DbW_OWL2.js → variant-aurora-4VBQHIKV.js} +1 -1
- package/dist/{variant-aurora-CNmfHhcx.js → variant-aurora-DBeCAQNk.js} +2 -2
- package/dist/{variant-aurora-D6GsdTPA.js → variant-aurora-FzHVXHby.js} +2 -2
- package/dist/{variant-aurora-BhLuYxJR.js → variant-aurora-LE3fqxgw.js} +1 -1
- package/dist/{variant-custom-BvYHR62b.js → variant-custom-AFRGcOBS.js} +1 -1
- package/dist/{variant-custom-DV2x254s.js → variant-custom-BQkNuPpE.js} +2 -2
- package/dist/{variant-custom-BRvaNHSi.js → variant-custom-DI5R0rln.js} +1 -1
- package/dist/{variant-custom-DMTrEO5w.js → variant-custom-WWnrrRHS.js} +2 -2
- package/dist/{variant-wave-CyBueXj9.js → variant-wave-DUCbH5oI.js} +2 -2
- package/dist/{variant-wave-BNMGrDfj.js → variant-wave-DuQV7DhQ.js} +2 -2
- package/dist/{variant-wave-BpDd8t3Z.js → variant-wave-nSI2sfII.js} +1 -1
- package/dist/{variant-wave-sp-qwsdI.js → variant-wave-wTP5v_jU.js} +1 -1
- package/dist/x-eiO9V6J6.js +1 -0
- package/{src/elements/styles.css → kit-base.css} +27 -44
- package/package.json +32 -17
- package/solid.css +33 -0
- package/theme.css +39 -23
- package/bin/route.test.js +0 -58
- package/dist/arrow-down-CjRU3JgV.js +0 -1
- package/dist/arrow-left-Bnpb1UEW.js +0 -1
- package/dist/artifact-Bwt7OgcR.js +0 -1
- package/dist/check-njkVdZu2.js +0 -1
- package/dist/chevron-down-B72s6PXR.js +0 -1
- package/dist/chevron-right-BatuHzUm.js +0 -1
- package/dist/circle-check-B62pODB3.js +0 -1
- package/dist/circle-zx0ByzY9.js +0 -1
- package/dist/controllable-CnGpMF6r.js +0 -1
- package/dist/define-By-uL588.js +0 -1
- package/dist/loader-BM4CEz4G.js +0 -1
- package/dist/message-circle-DruwObfX.js +0 -1
- package/dist/message-square-BG6q6mlm.js +0 -1
- package/dist/play-BvKKq3fX.js +0 -1
- package/dist/register-impl-B1hLkrI8.js +0 -424
- package/dist/text-shimmer-Dcj1MHuq.js +0 -1
- package/dist/textarea-Dpvbngbg.js +0 -1
- package/dist/x-Z5UntkIJ.js +0 -1
- package/src/agent-tooling/README.md +0 -192
- package/src/agent-tooling/archetypes.ts +0 -205
- package/src/agent-tooling/catalog/README.md +0 -676
- package/src/agent-tooling/catalog/catalog-types.ts +0 -231
- package/src/agent-tooling/catalog/fabrications.ts +0 -96
- package/src/agent-tooling/catalog/invariants.ts +0 -284
- package/src/agent-tooling/catalog/labs-titles.ts +0 -114
- package/src/agent-tooling/catalog/scenarios.ts +0 -87
- package/src/agent-tooling/catalog/surfaces.ts +0 -347
- package/src/agent-tooling/construct/cli-entry.ts +0 -8
- package/src/agent-tooling/construct/cli.ts +0 -147
- package/src/agent-tooling/construct/codegen.ts +0 -1635
- package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
- package/src/agent-tooling/construct/dev.ts +0 -148
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
- package/src/agent-tooling/construct/schema.ts +0 -452
- package/src/agent-tooling/integrations/anthropic.ts +0 -442
- package/src/agent-tooling/integrations/cloudflare.ts +0 -218
- package/src/agent-tooling/integrations/langgraph.ts +0 -133
- package/src/agent-tooling/integrations/mastra.ts +0 -182
- package/src/agent-tooling/integrations/mock.ts +0 -148
- package/src/agent-tooling/integrations/ollama.ts +0 -97
- package/src/agent-tooling/integrations/openai.ts +0 -95
- package/src/agent-tooling/integrations/openrouter.ts +0 -82
- package/src/agent-tooling/integrations/pi.ts +0 -176
- package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
- package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
- package/src/agent-tooling/mcp/manifest.ts +0 -422
- package/src/agent-tooling/mcp/server.ts +0 -116
- package/src/agent-tooling/mcp/stdio.ts +0 -18
- package/src/agent-tooling/mcp/tools/construct.ts +0 -130
- package/src/agent-tooling/mcp/tools/debug.ts +0 -621
- package/src/agent-tooling/mcp/tools/reference.ts +0 -980
- package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
- package/src/agent-tooling/mcp/tools/theme.ts +0 -456
- package/src/agent-tooling/mcp/tools/types.ts +0 -14
- package/src/agent-tooling/mcp/validate-args.ts +0 -141
- package/src/agent-tooling/recipes/composed-thread.ts +0 -714
- package/src/agent-tooling/recipes/index.ts +0 -22
- package/src/agent-tooling/recipes/types.ts +0 -32
- package/src/agent-tooling/registry.ts +0 -132
- package/src/agent-tooling/route-emit.ts +0 -305
- package/src/agent-tooling/types.ts +0 -390
- package/src/components/artifact-card.tsx +0 -120
- package/src/components/artifact.tsx +0 -914
- package/src/components/attachment-types.ts +0 -66
- package/src/components/attachments.tsx +0 -536
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
- package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
- package/src/components/audio-visualizer/fit-scale.ts +0 -112
- package/src/components/audio-visualizer/index.tsx +0 -528
- package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
- package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
- package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
- package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
- package/src/components/audio-visualizer/sizes.ts +0 -54
- package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
- package/src/components/audio-visualizer/variant-bar.tsx +0 -175
- package/src/components/audio-visualizer/variant-custom.tsx +0 -344
- package/src/components/audio-visualizer/variant-grid.tsx +0 -175
- package/src/components/audio-visualizer/variant-radial.tsx +0 -163
- package/src/components/audio-visualizer/variant-wave.tsx +0 -130
- package/src/components/audio-visualizer/wave.glsl.ts +0 -130
- package/src/components/card-fallback.tsx +0 -60
- package/src/components/card-renderer.tsx +0 -220
- package/src/components/card.tsx +0 -212
- package/src/components/chain-of-thought.tsx +0 -348
- package/src/components/chat-container.tsx +0 -89
- package/src/components/chat-scope-picker.tsx +0 -57
- package/src/components/chat-thread.tsx +0 -1014
- package/src/components/checkpoint.tsx +0 -88
- package/src/components/choice-card.tsx +0 -743
- package/src/components/coachmark.tsx +0 -249
- package/src/components/code-block.tsx +0 -181
- package/src/components/composer-dom.ts +0 -159
- package/src/components/composer-highlight.ts +0 -242
- package/src/components/composer-history.ts +0 -82
- package/src/components/composer.tsx +0 -1075
- package/src/components/confirm-card.tsx +0 -402
- package/src/components/context.tsx +0 -377
- package/src/components/conversation-item.tsx +0 -241
- package/src/components/conversation-list.tsx +0 -429
- package/src/components/conversation-panel.tsx +0 -118
- package/src/components/dismissed-stub.tsx +0 -80
- package/src/components/embed.tsx +0 -200
- package/src/components/empty.tsx +0 -177
- package/src/components/feedback-bar.tsx +0 -166
- package/src/components/file-tree.tsx +0 -530
- package/src/components/file-upload.tsx +0 -167
- package/src/components/form-widgets.tsx +0 -525
- package/src/components/form.tsx +0 -1296
- package/src/components/home-panel.tsx +0 -159
- package/src/components/image.tsx +0 -65
- package/src/components/link-preview.tsx +0 -195
- package/src/components/loader.tsx +0 -367
- package/src/components/markdown.tsx +0 -162
- package/src/components/message-skills.tsx +0 -40
- package/src/components/message.tsx +0 -765
- package/src/components/model-switcher.tsx +0 -132
- package/src/components/prompt-input.tsx +0 -188
- package/src/components/prompt-suggestion.tsx +0 -150
- package/src/components/reasoning.tsx +0 -293
- package/src/components/response-compare-types.ts +0 -152
- package/src/components/response-compare.tsx +0 -455
- package/src/components/response-stream.tsx +0 -109
- package/src/components/screen.tsx +0 -181
- package/src/components/scroll-button.tsx +0 -93
- package/src/components/source.tsx +0 -192
- package/src/components/tasks-card.tsx +0 -801
- package/src/components/text-shimmer.tsx +0 -37
- package/src/components/thinking-bar.tsx +0 -50
- package/src/components/thread.tsx +0 -228
- package/src/components/toast.tsx +0 -503
- package/src/components/tool-classify.ts +0 -26
- package/src/components/tool-types.ts +0 -49
- package/src/components/tool.tsx +0 -190
- package/src/components/use-card-resolution.ts +0 -44
- package/src/components/voice-input.tsx +0 -222
- package/src/components/voice-output.tsx +0 -242
- package/src/components/widget-tab-bar.tsx +0 -73
- package/src/components/workspace-shell.tsx +0 -285
- package/src/diagnostics/hook.ts +0 -352
- package/src/diagnostics/index.ts +0 -82
- package/src/diagnostics/report-request.ts +0 -296
- package/src/elements/agent-card.tsx +0 -67
- package/src/elements/artifact.tsx +0 -194
- package/src/elements/attachments.tsx +0 -140
- package/src/elements/audio-visualizer.tsx +0 -227
- package/src/elements/autoloader.ts +0 -89
- package/src/elements/avatar.tsx +0 -33
- package/src/elements/badge.tsx +0 -32
- package/src/elements/button.tsx +0 -149
- package/src/elements/card.tsx +0 -133
- package/src/elements/cards.tsx +0 -266
- package/src/elements/chain-of-thought.tsx +0 -141
- package/src/elements/chat-scope-picker.tsx +0 -65
- package/src/elements/chat-types.ts +0 -102
- package/src/elements/chat-workspace.tsx +0 -188
- package/src/elements/chat.tsx +0 -304
- package/src/elements/checkbox-group.tsx +0 -164
- package/src/elements/checkbox.tsx +0 -134
- package/src/elements/checkpoint.tsx +0 -47
- package/src/elements/choice.tsx +0 -100
- package/src/elements/coachmark.tsx +0 -90
- package/src/elements/code-block.tsx +0 -60
- package/src/elements/command.tsx +0 -197
- package/src/elements/compare.tsx +0 -111
- package/src/elements/compiled.css +0 -2
- package/src/elements/composer.tsx +0 -162
- package/src/elements/confirm-card.tsx +0 -76
- package/src/elements/context-meter.tsx +0 -107
- package/src/elements/conversation-item.tsx +0 -120
- package/src/elements/conversation-list.tsx +0 -203
- package/src/elements/css.ts +0 -5
- package/src/elements/default-input.tsx +0 -392
- package/src/elements/define-entry.ts +0 -12
- package/src/elements/define.tsx +0 -563
- package/src/elements/diagnostic-events.ts +0 -114
- package/src/elements/dialog.tsx +0 -145
- package/src/elements/disclosure.ts +0 -95
- package/src/elements/dock.tsx +0 -215
- package/src/elements/dropdown.tsx +0 -123
- package/src/elements/editable-label.tsx +0 -140
- package/src/elements/element-data-types.ts +0 -152
- package/src/elements/element-diagnostics.ts +0 -392
- package/src/elements/element-manifest.json +0 -354
- package/src/elements/element-nonscalar.json +0 -187
- package/src/elements/element-types.d.ts +0 -4353
- package/src/elements/embed.tsx +0 -35
- package/src/elements/empty.tsx +0 -29
- package/src/elements/feedback-bar.tsx +0 -66
- package/src/elements/file-tree.tsx +0 -62
- package/src/elements/file-upload.tsx +0 -44
- package/src/elements/form.tsx +0 -94
- package/src/elements/hover-card.tsx +0 -80
- package/src/elements/icon.tsx +0 -41
- package/src/elements/image.tsx +0 -32
- package/src/elements/input.tsx +0 -492
- package/src/elements/kbd.tsx +0 -45
- package/src/elements/link-preview.tsx +0 -34
- package/src/elements/loader.tsx +0 -25
- package/src/elements/markdown.tsx +0 -38
- package/src/elements/menu.tsx +0 -241
- package/src/elements/message-skills.tsx +0 -83
- package/src/elements/message.tsx +0 -361
- package/src/elements/model-switcher.tsx +0 -121
- package/src/elements/nav.tsx +0 -87
- package/src/elements/notice.tsx +0 -53
- package/src/elements/pane-grid.tsx +0 -111
- package/src/elements/pane-group.tsx +0 -119
- package/src/elements/pane.tsx +0 -129
- package/src/elements/popover.tsx +0 -81
- package/src/elements/progress-bar.tsx +0 -39
- package/src/elements/prompt-dock.tsx +0 -82
- package/src/elements/prompt-input.tsx +0 -240
- package/src/elements/prompt-suggestions.tsx +0 -113
- package/src/elements/radio-group.tsx +0 -129
- package/src/elements/reasoning.tsx +0 -71
- package/src/elements/register-impl.ts +0 -148
- package/src/elements/register.ts +0 -76
- package/src/elements/remote.tsx +0 -225
- package/src/elements/resizable.globals.d.ts +0 -36
- package/src/elements/resizable.tsx +0 -704
- package/src/elements/response-stream.tsx +0 -40
- package/src/elements/screen.tsx +0 -90
- package/src/elements/scroll-area.tsx +0 -33
- package/src/elements/scroll-button.tsx +0 -179
- package/src/elements/search.tsx +0 -185
- package/src/elements/segmented.tsx +0 -129
- package/src/elements/select.tsx +0 -165
- package/src/elements/separator.tsx +0 -36
- package/src/elements/setting-item.tsx +0 -50
- package/src/elements/settings-group.tsx +0 -37
- package/src/elements/skeleton.tsx +0 -45
- package/src/elements/slider.tsx +0 -171
- package/src/elements/slot-text.ts +0 -73
- package/src/elements/slots.ts +0 -787
- package/src/elements/source.tsx +0 -126
- package/src/elements/status.tsx +0 -42
- package/src/elements/switch.tsx +0 -118
- package/src/elements/tabs.tsx +0 -95
- package/src/elements/tasks.tsx +0 -103
- package/src/elements/text-shimmer.tsx +0 -28
- package/src/elements/thinking-bar.tsx +0 -43
- package/src/elements/thread.tsx +0 -139
- package/src/elements/toast.tsx +0 -106
- package/src/elements/tool.tsx +0 -78
- package/src/elements/tooltip.tsx +0 -85
- package/src/elements/validate-messages.ts +0 -76
- package/src/elements/voice-input.tsx +0 -100
- package/src/elements/voice-output.tsx +0 -82
- package/src/index.ts +0 -430
- package/src/primitives/audio-bands.ts +0 -174
- package/src/primitives/card-component-types.ts +0 -63
- package/src/primitives/card-contract.ts +0 -80
- package/src/primitives/card-data-types.ts +0 -361
- package/src/primitives/card-host.tsx +0 -37
- package/src/primitives/card-recovery.ts +0 -160
- package/src/primitives/card-registry.tsx +0 -78
- package/src/primitives/card-resolution.ts +0 -36
- package/src/primitives/card-routing.ts +0 -85
- package/src/primitives/card-schemas/artifact.schema.json +0 -92
- package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
- package/src/primitives/card-schemas/card-event.schema.json +0 -12
- package/src/primitives/card-schemas/choice.schema.json +0 -75
- package/src/primitives/card-schemas/confirm.schema.json +0 -65
- package/src/primitives/card-schemas/embed.schema.json +0 -63
- package/src/primitives/card-schemas/form.result.schema.json +0 -7
- package/src/primitives/card-schemas/form.schema.json +0 -53
- package/src/primitives/card-schemas/link.schema.json +0 -54
- package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
- package/src/primitives/card-schemas/tasks.schema.json +0 -83
- package/src/primitives/card-tags.ts +0 -61
- package/src/primitives/card-validate-cards.ts +0 -255
- package/src/primitives/card-validate-schemas.ts +0 -58
- package/src/primitives/card-validate.ts +0 -131
- package/src/primitives/chat-config.tsx +0 -103
- package/src/primitives/composer-model.ts +0 -49
- package/src/primitives/composer-triggers.ts +0 -35
- package/src/primitives/controllable.ts +0 -21
- package/src/primitives/conversation-store.ts +0 -248
- package/src/primitives/create-kai-chat.ts +0 -66
- package/src/primitives/create-tween.ts +0 -266
- package/src/primitives/embed-providers.ts +0 -254
- package/src/primitives/field-mask.ts +0 -256
- package/src/primitives/field-semantics.ts +0 -115
- package/src/primitives/highlighter.ts +0 -157
- package/src/primitives/input-mask.ts +0 -853
- package/src/primitives/link-preview.ts +0 -87
- package/src/primitives/message-feedback.ts +0 -124
- package/src/primitives/pdf-preview.ts +0 -121
- package/src/primitives/toast-store.ts +0 -338
- package/src/primitives/url-scheme-policy.ts +0 -70
- package/src/primitives/use-audio-analysis.ts +0 -340
- package/src/primitives/use-auto-resize.ts +0 -31
- package/src/primitives/use-resize-observer.ts +0 -45
- package/src/primitives/use-sequencer.ts +0 -72
- package/src/primitives/use-speech-recognition.ts +0 -146
- package/src/primitives/use-stick-to-bottom.ts +0 -75
- package/src/primitives/use-text-stream.ts +0 -143
- package/src/primitives/use-voice-recorder.ts +0 -62
- package/src/primitives/visualizer-sequences.ts +0 -350
- package/src/remote/host-embed.ts +0 -345
- package/src/remote/index.ts +0 -2
- package/src/remote/origin.ts +0 -30
- package/src/remote/provider-runtime.ts +0 -262
- package/src/remote/provider.ts +0 -2
- package/src/remote/validate.ts +0 -22
- package/src/remote/version.ts +0 -12
- package/src/remote/wire.ts +0 -48
- package/src/schemas/from-tool-call.ts +0 -151
- package/src/schemas/index.ts +0 -341
- package/src/schemas/provider-subsets.ts +0 -538
- package/src/schemas/registry.ts +0 -355
- package/src/schemas/tool-defs.ts +0 -793
- package/src/solid.ts +0 -173
- package/src/state/index.ts +0 -44
- package/src/state/messages.ts +0 -53
- package/src/state/mock.ts +0 -297
- package/src/state/parts.ts +0 -295
- package/src/state/persistence.ts +0 -180
- package/src/state/stream.ts +0 -246
- package/src/state/suggestions.ts +0 -9
- package/src/state/threads.ts +0 -142
- package/src/test-utils/fake-clock.ts +0 -88
- package/src/types.ts +0 -95
- package/src/ui/action-icons.ts +0 -53
- package/src/ui/agent-card.tsx +0 -218
- package/src/ui/avatar.tsx +0 -23
- package/src/ui/badge.tsx +0 -21
- package/src/ui/button.tsx +0 -49
- package/src/ui/card.tsx +0 -245
- package/src/ui/checkbox-group.tsx +0 -153
- package/src/ui/checkbox.tsx +0 -63
- package/src/ui/collapsible.tsx +0 -163
- package/src/ui/command.tsx +0 -103
- package/src/ui/dialog.tsx +0 -223
- package/src/ui/dock.tsx +0 -630
- package/src/ui/dropdown.tsx +0 -636
- package/src/ui/editable-label.tsx +0 -112
- package/src/ui/hover-card.tsx +0 -345
- package/src/ui/icon.tsx +0 -167
- package/src/ui/input.tsx +0 -391
- package/src/ui/kbd.tsx +0 -118
- package/src/ui/nav.tsx +0 -282
- package/src/ui/notice.tsx +0 -84
- package/src/ui/overlay.tsx +0 -277
- package/src/ui/pane-grid.tsx +0 -117
- package/src/ui/pane-group.tsx +0 -291
- package/src/ui/pane.tsx +0 -193
- package/src/ui/popover.tsx +0 -106
- package/src/ui/progress-bar.tsx +0 -83
- package/src/ui/prompt-dock.tsx +0 -145
- package/src/ui/radio.tsx +0 -150
- package/src/ui/resizable.tsx +0 -693
- package/src/ui/scroll-area.tsx +0 -26
- package/src/ui/segmented.tsx +0 -109
- package/src/ui/select.tsx +0 -168
- package/src/ui/separator.tsx +0 -10
- package/src/ui/settings-group.tsx +0 -67
- package/src/ui/skeleton.tsx +0 -73
- package/src/ui/slider.tsx +0 -178
- package/src/ui/status.tsx +0 -48
- package/src/ui/switch.tsx +0 -119
- package/src/ui/tabs.tsx +0 -168
- package/src/ui/textarea.tsx +0 -21
- package/src/ui/tooltip.tsx +0 -118
- package/src/utils/cn.ts +0 -30
- package/src/wire/chunk.ts +0 -248
- package/src/wire/consume.ts +0 -599
- package/src/wire/diagnostics.ts +0 -727
- package/src/wire/encode-probe.ts +0 -214
- package/src/wire/encode.ts +0 -947
- package/src/wire/files.ts +0 -342
- package/src/wire/formats/anthropic.ts +0 -274
- package/src/wire/formats/openai.ts +0 -228
- package/src/wire/index.ts +0 -95
- package/src/wire/media-types.ts +0 -344
- package/src/wire/read.ts +0 -396
- package/src/wire/sink-helpers.ts +0 -50
- package/src/wire/sse.ts +0 -153
|
@@ -1,853 +0,0 @@
|
|
|
1
|
-
// src/primitives/input-mask.ts
|
|
2
|
-
// Tier 2, the stateful half: one `HTMLInputElement` driven through the pure format engine
|
|
3
|
-
// in `field-mask.ts`. Framework-agnostic -- no Solid, no DOM beyond the one input and its
|
|
4
|
-
// document. Spec: docs/superpowers/specs/2026-08-24-form-field-formats-design.md
|
|
5
|
-
// (§2 tier 2, §3, and the §5 improvement list, which is binding here).
|
|
6
|
-
//
|
|
7
|
-
// THE FORMATTED TEXT IS `input.value` (spec §2). There is no overlay and no ghost layer,
|
|
8
|
-
// so the caret, selection, find-in-page and every mobile affordance are the browser's own.
|
|
9
|
-
// What this module adds on top is: interception, normalization, a caret that never rests
|
|
10
|
-
// inside a literal run, its own undo stack (a programmatic `.value` write destroys the
|
|
11
|
-
// native one), and a stated clipboard policy.
|
|
12
|
-
//
|
|
13
|
-
// Four rules hold this file together; each is one of the §5 improvements and each is the
|
|
14
|
-
// reason some obvious-looking shortcut is not taken:
|
|
15
|
-
// 1. `commit` is the ONLY writer of `el.value`, the selection, the undo stacks and the
|
|
16
|
-
// callbacks (§5.5). The reference this was derived from repeated that sequence in five
|
|
17
|
-
// places and they had already drifted apart.
|
|
18
|
-
// 2. `beforeinput` is the interception point WHERE IT IS CANCELABLE; where it is not, the
|
|
19
|
-
// longest-common-prefix/suffix diff in `input` reconciles whatever the browser did
|
|
20
|
-
// (§5.1). Both paths end in `applyEdit` -> `commit`, so there is one edit semantics.
|
|
21
|
-
// 3. Between `compositionstart` and `compositionend` this module does NOTHING: no cancel,
|
|
22
|
-
// no `.value` write, no caret move, no clamp (§5.2). Cancelling mid-composition breaks
|
|
23
|
-
// the composition outright, and Android word suggestion is far more common in these
|
|
24
|
-
// fields than CJK input.
|
|
25
|
-
// 4. `.value` is never shadowed with `Object.defineProperty` (§5.8). The canonical value
|
|
26
|
-
// is read through `getCanonicalValue()`; the element facade will publish it with
|
|
27
|
-
// `setFormValue()`.
|
|
28
|
-
//
|
|
29
|
-
// ONE KNOWN IMPRECISION, recorded rather than papered over. An undo entry's selection is
|
|
30
|
-
// read from the element at commit time. On the `beforeinput` path that is exactly right --
|
|
31
|
-
// the event was canceled, so the caret has not moved yet. On the `input` diff fallback the
|
|
32
|
-
// browser has ALREADY moved it, so the entry stores the post-edit caret against the
|
|
33
|
-
// pre-edit text: undoing a browser-driven edit restores the correct text with a caret that
|
|
34
|
-
// is merely plausible. Recovering the true one means caching the selection from
|
|
35
|
-
// `selectionchange` and trusting that it fires before `input`, which is browser-timing
|
|
36
|
-
// dependent and unverifiable in jsdom -- a guess dressed as a fix. The caret is clamped, so
|
|
37
|
-
// it is never out of range. Task 6 can measure the real ordering and decide.
|
|
38
|
-
import {
|
|
39
|
-
compileMask,
|
|
40
|
-
formatForDisplay,
|
|
41
|
-
formatRaw,
|
|
42
|
-
formattedToRawIndex,
|
|
43
|
-
normalizeToRaw,
|
|
44
|
-
rawFromFormatted,
|
|
45
|
-
rawToFormattedIndex,
|
|
46
|
-
type CaseMode,
|
|
47
|
-
type MaskPattern,
|
|
48
|
-
type RejectReason,
|
|
49
|
-
} from './field-mask';
|
|
50
|
-
import { canonicalize, type FieldSemanticType } from './field-semantics';
|
|
51
|
-
|
|
52
|
-
/** What a copy or cut puts on the clipboard. A stated policy the consumer selects, NOT a
|
|
53
|
-
* consequence of `obscure`: copying bullets is not a security control -- the
|
|
54
|
-
* value is in the page -- and whether a card number may be copied at all is an app-layer
|
|
55
|
-
* decision (CLAUDE.md: the kit decides HOW, the app decides WHETHER). */
|
|
56
|
-
export type CopyPolicy = 'formatted' | 'canonical' | 'obscured' | 'blocked';
|
|
57
|
-
|
|
58
|
-
/** Why an edit or a re-configuration refused, or partly refused, the content it was given.
|
|
59
|
-
*
|
|
60
|
-
* A superset of `field-mask.ts`'s `RejectReason`, widened HERE rather than there because
|
|
61
|
-
* the extra member is not an engine verdict: the engine's three describe one edit failing
|
|
62
|
-
* against one pattern, while `format-change-clipped` describes the PATTERN moving out from
|
|
63
|
-
* under a value that was already accepted. Widening keeps every consumer that narrows on
|
|
64
|
-
* the engine's three compiling unchanged. */
|
|
65
|
-
export type InputMaskRejectReason = RejectReason | 'format-change-clipped';
|
|
66
|
-
|
|
67
|
-
export interface InputMaskOptions {
|
|
68
|
-
format: string;
|
|
69
|
-
guide?: string;
|
|
70
|
-
semantic?: FieldSemanticType;
|
|
71
|
-
caseMode?: CaseMode;
|
|
72
|
-
copyPolicy?: CopyPolicy;
|
|
73
|
-
/** Tier 3. Wired -- it selects the default copy policy -- but does NOT yet transform the
|
|
74
|
-
* display; that lands with the obscured rendering in tier 3 (task 10). */
|
|
75
|
-
obscure?: boolean;
|
|
76
|
-
initialValue?: string;
|
|
77
|
-
onInput?: (detail: { canonical: string; formatted: string }) => void;
|
|
78
|
-
onReject?: (detail: { reason: InputMaskRejectReason; data: string }) => void;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface InputMask {
|
|
82
|
-
/** Accepts the canonical form or the formatted form; both normalize to the same value. */
|
|
83
|
-
setValue(value: string): void;
|
|
84
|
-
getRawValue(): string;
|
|
85
|
-
getCanonicalValue(): string;
|
|
86
|
-
getFormattedValue(): string;
|
|
87
|
-
setObscure(on: boolean): void;
|
|
88
|
-
/** Re-compiles and preserves the value, re-fitted to the new pattern. */
|
|
89
|
-
update(next: Partial<InputMaskOptions>): void;
|
|
90
|
-
detach(): void;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Undo history is capped (spec §5.6). A long-lived field otherwise grows an unbounded
|
|
94
|
-
* array; dropping from the bottom keeps the recent history, which is the useful end. */
|
|
95
|
-
const UNDO_LIMIT = 200;
|
|
96
|
-
|
|
97
|
-
/** The obscured glyph, U+2022. Tier 3's display uses the same one. */
|
|
98
|
-
const BULLET = '•';
|
|
99
|
-
|
|
100
|
-
/** One undo entry: the state to restore, INCLUDING the selection the user had when the
|
|
101
|
-
* edit that superseded it began (spec §5.6). Restoring text without the caret is what
|
|
102
|
-
* makes a custom undo stack feel broken.
|
|
103
|
-
*
|
|
104
|
-
* `pattern` is a fourth field beyond the three the spec names, and it is what lets the
|
|
105
|
-
* history SURVIVE an `update()`. A formatted string can only be decoded by the pattern
|
|
106
|
-
* that produced it -- `V-123` under `V-***` is raw `123`, and under `#####` it is raw `123`
|
|
107
|
-
* read from different positions -- so an entry restored under a pattern that did not write
|
|
108
|
-
* it is garbage. Carrying the pattern lets `restore` decode with the right one and then
|
|
109
|
-
* re-fit; without it the only safe thing to do on a format change is throw the whole stack
|
|
110
|
-
* away, which is itself a silent drop of the user's history. */
|
|
111
|
-
interface UndoEntry {
|
|
112
|
-
readonly formatted: string;
|
|
113
|
-
readonly pattern: MaskPattern;
|
|
114
|
-
readonly selStart: number;
|
|
115
|
-
readonly selEnd: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** How a commit affects the undo history.
|
|
119
|
-
* - `run` an insertion that may coalesce into the run already open.
|
|
120
|
-
* - `break` an edit that ends the run and always gets its own entry.
|
|
121
|
-
* - `none` a restore or a re-render: no history, and the redo stack survives. */
|
|
122
|
-
type UndoMode = 'break' | 'run' | 'none';
|
|
123
|
-
|
|
124
|
-
/** `inputType`s that insert. `insertText` is the one that coalesces into a typing run;
|
|
125
|
-
* the rest are bulk edits and each gets its own undo entry. */
|
|
126
|
-
const INSERT_TYPES = new Set([
|
|
127
|
-
'insertText',
|
|
128
|
-
'insertFromPaste',
|
|
129
|
-
'insertFromPasteAsQuotation',
|
|
130
|
-
'insertFromDrop',
|
|
131
|
-
'insertReplacementText',
|
|
132
|
-
'insertFromYank',
|
|
133
|
-
'insertTranspose',
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
/** `inputType`s that delete. `deleteByCut` is DELIBERATELY absent: the `cut` listener
|
|
137
|
-
* cancels the cut and performs the deletion itself, so this event never arrives for a
|
|
138
|
-
* cut -- and if it did, the range would already be gone and a second delete would eat a
|
|
139
|
-
* character the user never selected. */
|
|
140
|
-
const DELETE_TYPES = new Set([
|
|
141
|
-
'deleteContent',
|
|
142
|
-
'deleteContentBackward',
|
|
143
|
-
'deleteContentForward',
|
|
144
|
-
'deleteWordBackward',
|
|
145
|
-
'deleteWordForward',
|
|
146
|
-
'deleteSoftLineBackward',
|
|
147
|
-
'deleteSoftLineForward',
|
|
148
|
-
'deleteHardLineBackward',
|
|
149
|
-
'deleteHardLineForward',
|
|
150
|
-
'deleteByDrag',
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
/** Caret-moving keys break an open typing run (spec §5.6). Pointer-driven moves break it
|
|
154
|
-
* through the `mousedown` listener. */
|
|
155
|
-
const NAV_KEYS = new Set([
|
|
156
|
-
'ArrowLeft',
|
|
157
|
-
'ArrowRight',
|
|
158
|
-
'ArrowUp',
|
|
159
|
-
'ArrowDown',
|
|
160
|
-
'Home',
|
|
161
|
-
'End',
|
|
162
|
-
'PageUp',
|
|
163
|
-
'PageDown',
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
export function createInputMask(el: HTMLInputElement, options: InputMaskOptions): InputMask {
|
|
167
|
-
let opts: InputMaskOptions = { ...options };
|
|
168
|
-
let pattern: MaskPattern = compileMask(opts.format, opts.guide);
|
|
169
|
-
let hasGuide = opts.guide !== undefined;
|
|
170
|
-
let caseMode: CaseMode = opts.caseMode ?? 'preserve';
|
|
171
|
-
let semantic: FieldSemanticType = opts.semantic ?? 'custom';
|
|
172
|
-
let obscure = opts.obscure ?? false;
|
|
173
|
-
|
|
174
|
-
/** The formatted text, authoritative and equal to `el.value` outside a composition. */
|
|
175
|
-
let formatted = '';
|
|
176
|
-
/** The pattern that produced `formatted`. Diverges from `pattern` for exactly the span of
|
|
177
|
-
* an `update()`, which is why an undo entry pushed during one must be stamped with THIS
|
|
178
|
-
* and not with the pattern that has already been swapped in. */
|
|
179
|
-
let formattedPattern: MaskPattern = pattern;
|
|
180
|
-
/** The fill-position characters, authoritative. Kept beside `formatted` rather than
|
|
181
|
-
* re-read from it on every access: a consumer guide whose characters happen to satisfy
|
|
182
|
-
* their own position's class (`@@@` guided `abc`) would otherwise read back as content. */
|
|
183
|
-
let raw = '';
|
|
184
|
-
|
|
185
|
-
const undoStack: UndoEntry[] = [];
|
|
186
|
-
const redoStack: UndoEntry[] = [];
|
|
187
|
-
let runOpen = false;
|
|
188
|
-
|
|
189
|
-
let composing = false;
|
|
190
|
-
/** Set while THIS module writes the selection, so the `selectionchange` clamp does not
|
|
191
|
-
* react to its own work. Not a timer and not a race: it is set and cleared around one
|
|
192
|
-
* synchronous call. */
|
|
193
|
-
let settingSelection = false;
|
|
194
|
-
let detached = false;
|
|
195
|
-
/** A consumer write (`setValue` / `update`) that arrived mid-composition, held until
|
|
196
|
-
* `compositionend`. Only the latest survives: an earlier one has already been superseded
|
|
197
|
-
* by its own author, and replaying both would flash an intermediate value. */
|
|
198
|
-
let pendingWrite: (() => void) | null = null;
|
|
199
|
-
|
|
200
|
-
/** With a guide the field is always `format.length` long and unfilled positions show the
|
|
201
|
-
* guide; without one it shows only up to the last typed character (spec §2). */
|
|
202
|
-
const display = (value: string): string =>
|
|
203
|
-
hasGuide ? formatForDisplay(pattern, value) : formatRaw(pattern, value);
|
|
204
|
-
|
|
205
|
-
/** A caret offset in `formatted` -> a raw index, clamped to what is actually typed. */
|
|
206
|
-
const toRaw = (pos: number): number =>
|
|
207
|
-
Math.min(formattedToRawIndex(pattern, formatted, pos), raw.length);
|
|
208
|
-
|
|
209
|
-
function selectionRaw(): [number, number] {
|
|
210
|
-
const start = el.selectionStart ?? formatted.length;
|
|
211
|
-
const end = el.selectionEnd ?? formatted.length;
|
|
212
|
-
return [toRaw(start), toRaw(Math.max(start, end))];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function setSelection(start: number, end: number): void {
|
|
216
|
-
settingSelection = true;
|
|
217
|
-
try {
|
|
218
|
-
el.setSelectionRange(start, end);
|
|
219
|
-
} catch {
|
|
220
|
-
// `setSelectionRange` throws on input types that do not support selection
|
|
221
|
-
// (`number`, `email`, ...). The value write already landed; a caret is presentation,
|
|
222
|
-
// not data, so there is nothing here to report.
|
|
223
|
-
} finally {
|
|
224
|
-
settingSelection = false;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function reject(reason: InputMaskRejectReason, data: string): void {
|
|
229
|
-
// Loud, always (spec §5.3). The silent `preventDefault` this replaces is the exact
|
|
230
|
-
// shape CLAUDE.md calls the default-wrong choice: a decision made while withholding
|
|
231
|
-
// the information that it happened.
|
|
232
|
-
opts.onReject?.({ reason, data });
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ---------------------------------------------------------------------------------
|
|
236
|
-
// The single commit path (§5.5). Nothing else in this file assigns `el.value`, touches
|
|
237
|
-
// the selection, touches the undo stacks or calls a consumer callback.
|
|
238
|
-
// ---------------------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
/** `raw` travels in the options bag rather than being re-derived from `nextFormatted`
|
|
241
|
-
* for the reason given at the `raw` declaration: `rawFromFormatted` over a display
|
|
242
|
-
* string trusts the guide. Every caller already has the raw it just computed. */
|
|
243
|
-
function commit(
|
|
244
|
-
nextFormatted: string,
|
|
245
|
-
caret: number,
|
|
246
|
-
write: { undo: UndoMode; raw: string; notify?: boolean; selectionEnd?: number },
|
|
247
|
-
): void {
|
|
248
|
-
if (write.undo !== 'none') {
|
|
249
|
-
if (write.undo === 'break' || !runOpen) {
|
|
250
|
-
undoStack.push({
|
|
251
|
-
formatted,
|
|
252
|
-
pattern: formattedPattern,
|
|
253
|
-
selStart: el.selectionStart ?? formatted.length,
|
|
254
|
-
selEnd: el.selectionEnd ?? formatted.length,
|
|
255
|
-
});
|
|
256
|
-
if (undoStack.length > UNDO_LIMIT) undoStack.shift();
|
|
257
|
-
}
|
|
258
|
-
runOpen = write.undo === 'run';
|
|
259
|
-
redoStack.length = 0;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
formatted = nextFormatted;
|
|
263
|
-
formattedPattern = pattern;
|
|
264
|
-
raw = write.raw;
|
|
265
|
-
el.value = nextFormatted; // native setter; no descriptor shadowing (§5.8)
|
|
266
|
-
|
|
267
|
-
const start = Math.max(0, Math.min(caret, nextFormatted.length));
|
|
268
|
-
const end = Math.max(start, Math.min(write.selectionEnd ?? caret, nextFormatted.length));
|
|
269
|
-
setSelection(start, end);
|
|
270
|
-
|
|
271
|
-
if (write.notify !== false) opts.onInput?.({ canonical: getCanonicalValue(), formatted });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** The raw-space adapter every edit goes through: it is what guarantees the formatted
|
|
275
|
-
* text and the caret are always derived from the same raw. */
|
|
276
|
-
function commitRaw(nextRaw: string, rawCaret: number, write: { undo: UndoMode; notify?: boolean }): void {
|
|
277
|
-
const next = display(nextRaw);
|
|
278
|
-
commit(next, rawToFormattedIndex(pattern, next, rawCaret), { ...write, raw: nextRaw });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ---------------------------------------------------------------------------------
|
|
282
|
-
// Edit application
|
|
283
|
-
// ---------------------------------------------------------------------------------
|
|
284
|
-
|
|
285
|
-
/** A copy of the pattern with extra permissive fill positions, used ONLY to answer "was
|
|
286
|
-
* anything actually lost to capacity?".
|
|
287
|
-
*
|
|
288
|
-
* Without it, `absorbed.length < inserted.length` cannot tell an overflow from an
|
|
289
|
-
* absorbed separator: `chg 4821` is eight characters landing in seven fill positions
|
|
290
|
-
* with nothing lost, and reporting `over-capacity` for it would be a false alarm on the
|
|
291
|
-
* single most common paste in the target field family. Re-normalizing against a wider
|
|
292
|
-
* pattern answers it exactly. Returns `null` when the format is already at the engine's
|
|
293
|
-
* length cap, in which case the check is skipped rather than guessed. */
|
|
294
|
-
function widerPattern(extra: number): MaskPattern | null {
|
|
295
|
-
try {
|
|
296
|
-
return compileMask(pattern.format + '@'.repeat(extra));
|
|
297
|
-
} catch {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Replace raw `[rawStart, rawEnd)` with whatever of `inserted` the mask accepts.
|
|
303
|
-
*
|
|
304
|
-
* The whole edit is expressed as ONE normalization over `prefix + inserted + tail`,
|
|
305
|
-
* which is what keeps typing, pasting, autofill and the diff fallback on identical
|
|
306
|
-
* semantics. `formatRaw` re-inserts the literals in front of the insertion point so
|
|
307
|
-
* `normalizeToRaw` -- which walks the FORMAT, and is the literal-aware normalizer that
|
|
308
|
-
* fixes spec §5.7 -- sees a string aligned to the pattern from index 0. That is why
|
|
309
|
-
* pasting `V-123` under `V-***` yields `V-123` and not `V-V12`.
|
|
310
|
-
*
|
|
311
|
-
* Returns whether it committed. */
|
|
312
|
-
function applyEdit(rawStart: number, rawEnd: number, inserted: string, kind: 'insert' | 'bulk'): boolean {
|
|
313
|
-
const room = pattern.capacity - (raw.length - (rawEnd - rawStart));
|
|
314
|
-
if (inserted.length > 0 && room <= 0) {
|
|
315
|
-
reject('full', inserted);
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const prefix = raw.slice(0, rawStart);
|
|
320
|
-
const prefixText = formatRaw(pattern, prefix);
|
|
321
|
-
const withInsert = normalizeToRaw(pattern, prefixText + inserted, caseMode);
|
|
322
|
-
const absorbed = withInsert.slice(prefix.length);
|
|
323
|
-
const tail = raw.slice(rawEnd);
|
|
324
|
-
const nextRaw = normalizeToRaw(pattern, formatRaw(pattern, withInsert) + tail, caseMode);
|
|
325
|
-
|
|
326
|
-
if (inserted.length > 0 && absorbed.length === 0) {
|
|
327
|
-
reject('wrong-class', inserted);
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
if (nextRaw === raw) return false; // a delete with nothing to delete: no edit, no noise
|
|
331
|
-
|
|
332
|
-
commitRaw(nextRaw, prefix.length + absorbed.length, {
|
|
333
|
-
undo: kind === 'insert' ? 'run' : 'break',
|
|
334
|
-
notify: true,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Reported AFTER the commit, and deliberately alongside it rather than instead of it.
|
|
338
|
-
// `full` and `wrong-class` refuse the whole edit and leave the text unchanged, which is
|
|
339
|
-
// the §5.3 contract. `over-capacity` is the clip that `field-mask.ts` documents the
|
|
340
|
-
// CALLER as responsible for reporting ("input past the last fill position is clipped --
|
|
341
|
-
// the caller compares lengths"): refusing an entire paste for being one character long
|
|
342
|
-
// is worse than accepting what fits and saying so. `data` is the input that was
|
|
343
|
-
// refused or partly refused, not a computed remainder.
|
|
344
|
-
const lostTail = nextRaw.length < withInsert.length + tail.length;
|
|
345
|
-
const lostInsert =
|
|
346
|
-
withInsert.length === pattern.capacity &&
|
|
347
|
-
absorbed.length < inserted.length &&
|
|
348
|
-
overflowed(prefixText, inserted, withInsert.length);
|
|
349
|
-
if (lostTail || lostInsert) reject('over-capacity', inserted);
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function overflowed(prefixText: string, inserted: string, absorbedTotal: number): boolean {
|
|
354
|
-
const wider = widerPattern(inserted.length);
|
|
355
|
-
if (wider === null) return false;
|
|
356
|
-
return normalizeToRaw(wider, prefixText + inserted, caseMode).length > absorbedTotal;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** The §5.1 fallback: the browser already mutated the field, so work out what it did.
|
|
360
|
-
* Longest common prefix/suffix, caret-independent, and robust against alphanumeric
|
|
361
|
-
* literals -- the one idea carried over from the reference unchanged. */
|
|
362
|
-
function reconcile(): void {
|
|
363
|
-
const current = el.value;
|
|
364
|
-
const max = Math.min(current.length, formatted.length);
|
|
365
|
-
let pre = 0;
|
|
366
|
-
while (pre < max && current[pre] === formatted[pre]) pre += 1;
|
|
367
|
-
let suf = 0;
|
|
368
|
-
while (
|
|
369
|
-
suf < max - pre &&
|
|
370
|
-
current[current.length - 1 - suf] === formatted[formatted.length - 1 - suf]
|
|
371
|
-
) {
|
|
372
|
-
suf += 1;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const insertedText = current.slice(pre, current.length - suf);
|
|
376
|
-
const start = toRaw(pre);
|
|
377
|
-
const end = Math.max(start, toRaw(formatted.length - suf));
|
|
378
|
-
applyEdit(start, end, insertedText, 'bulk');
|
|
379
|
-
|
|
380
|
-
if (el.value !== formatted) {
|
|
381
|
-
// The edit was refused (or was a no-op). The browser's text is still on screen, so
|
|
382
|
-
// put the authoritative text back -- a refusal must leave the field unchanged, and
|
|
383
|
-
// "unchanged" here means undoing the browser, not leaving its version in place.
|
|
384
|
-
commit(formatted, rawToFormattedIndex(pattern, formatted, raw.length), {
|
|
385
|
-
undo: 'none',
|
|
386
|
-
raw,
|
|
387
|
-
notify: false,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ---------------------------------------------------------------------------------
|
|
393
|
-
// Undo / redo (§5.6)
|
|
394
|
-
// ---------------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
function snapshot(): UndoEntry {
|
|
397
|
-
return {
|
|
398
|
-
formatted,
|
|
399
|
-
pattern: formattedPattern,
|
|
400
|
-
selStart: el.selectionStart ?? formatted.length,
|
|
401
|
-
selEnd: el.selectionEnd ?? formatted.length,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function restore(entry: UndoEntry): void {
|
|
406
|
-
runOpen = false;
|
|
407
|
-
if (entry.pattern === pattern) {
|
|
408
|
-
commit(entry.formatted, entry.selStart, {
|
|
409
|
-
undo: 'none',
|
|
410
|
-
selectionEnd: entry.selEnd,
|
|
411
|
-
raw: rawFromFormatted(pattern, entry.formatted),
|
|
412
|
-
notify: true,
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
// Recorded under an older pattern (an `update()` happened since). Decode with the
|
|
417
|
-
// pattern that wrote it, then re-fit through the current one. The FORMATTED text is
|
|
418
|
-
// what gets re-normalized, never the bare raw: `normalizeToRaw` is not idempotent on
|
|
419
|
-
// raw when a fill character happens to equal a leading literal -- `V-***` holding
|
|
420
|
-
// `V12` would re-normalize to `12` and lose a character on every undo. With the
|
|
421
|
-
// literals present (`V-V12`) the positional walk consumes the leading `V` as the
|
|
422
|
-
// literal it is.
|
|
423
|
-
//
|
|
424
|
-
// Note what this canNOT do: if the new pattern is narrower, the re-fit clips again, so
|
|
425
|
-
// undo does not resurrect what `update()` already reported as clipped. Undo reverses an
|
|
426
|
-
// EDIT; a format change is not one. That is why the loud report below is the half that
|
|
427
|
-
// actually protects the value.
|
|
428
|
-
const decoded = rawFromFormatted(entry.pattern, entry.formatted);
|
|
429
|
-
const refit = normalizeToRaw(pattern, formatRaw(entry.pattern, decoded), caseMode);
|
|
430
|
-
commitRaw(refit, refit.length, { undo: 'none', notify: true });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function undo(): void {
|
|
434
|
-
const entry = undoStack.pop();
|
|
435
|
-
if (entry === undefined) return;
|
|
436
|
-
redoStack.push(snapshot());
|
|
437
|
-
restore(entry);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function redo(): void {
|
|
441
|
-
const entry = redoStack.pop();
|
|
442
|
-
if (entry === undefined) return;
|
|
443
|
-
undoStack.push(snapshot());
|
|
444
|
-
restore(entry);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ---------------------------------------------------------------------------------
|
|
448
|
-
// Clipboard (§5.10)
|
|
449
|
-
// ---------------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
/** Bullets at the FILLED `*` positions only. `#` and `@` positions, literals and guide
|
|
452
|
-
* characters stay revealed -- which is what makes `**** **** **** ####` mean "show the
|
|
453
|
-
* last four" with no `showLast` prop (spec §2 tier 3). */
|
|
454
|
-
function obscured(text: string): string {
|
|
455
|
-
const chars = text.split('');
|
|
456
|
-
for (let i = 0; i < pattern.fillIndexes.length && i < raw.length; i += 1) {
|
|
457
|
-
const index = pattern.fillIndexes[i]!;
|
|
458
|
-
if (pattern.format[index] === '*' && index < chars.length) chars[index] = BULLET;
|
|
459
|
-
}
|
|
460
|
-
return chars.join('');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function clipboardText(): string {
|
|
464
|
-
const start = el.selectionStart ?? 0;
|
|
465
|
-
const end = el.selectionEnd ?? 0;
|
|
466
|
-
switch (copyPolicy()) {
|
|
467
|
-
case 'blocked':
|
|
468
|
-
return '';
|
|
469
|
-
case 'formatted':
|
|
470
|
-
return formatted.slice(start, end);
|
|
471
|
-
case 'obscured':
|
|
472
|
-
return obscured(formatted).slice(start, end);
|
|
473
|
-
case 'canonical':
|
|
474
|
-
default: {
|
|
475
|
-
const [rawStart, rawEnd] = selectionRaw();
|
|
476
|
-
// A whole-field selection copies the canonical value -- the thing a consumer means
|
|
477
|
-
// by "copy this phone number". A partial selection has no canonical form of its
|
|
478
|
-
// own (its literals depend on where it sat), so it copies the characters it covers.
|
|
479
|
-
if (rawStart === 0 && rawEnd === raw.length) {
|
|
480
|
-
return canonicalize(pattern, formatRaw(pattern, raw), semantic);
|
|
481
|
-
}
|
|
482
|
-
return raw.slice(rawStart, rawEnd);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/** Explicit option wins; otherwise `obscure` picks the default (spec §5.10). */
|
|
488
|
-
function copyPolicy(): CopyPolicy {
|
|
489
|
-
return opts.copyPolicy ?? (obscure ? 'obscured' : 'canonical');
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ---------------------------------------------------------------------------------
|
|
493
|
-
// Listeners
|
|
494
|
-
// ---------------------------------------------------------------------------------
|
|
495
|
-
|
|
496
|
-
function onBeforeInput(event: Event): void {
|
|
497
|
-
if (detached || composing) return;
|
|
498
|
-
const e = event as InputEvent;
|
|
499
|
-
// Not cancelable -- composition on Android, several IMEs. Do nothing here and let the
|
|
500
|
-
// diff in `input` absorb whatever the browser does (§5.1).
|
|
501
|
-
if (!e.cancelable) return;
|
|
502
|
-
const inputType = e.inputType;
|
|
503
|
-
|
|
504
|
-
if (INSERT_TYPES.has(inputType)) {
|
|
505
|
-
e.preventDefault();
|
|
506
|
-
const data = e.data ?? e.dataTransfer?.getData('text/plain') ?? '';
|
|
507
|
-
const [start, end] = selectionRaw();
|
|
508
|
-
applyEdit(start, end, data, inputType === 'insertText' ? 'insert' : 'bulk');
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (DELETE_TYPES.has(inputType)) {
|
|
513
|
-
e.preventDefault();
|
|
514
|
-
let [start, end] = selectionRaw();
|
|
515
|
-
if (start === end) {
|
|
516
|
-
// A DOM selection that maps to an EMPTY raw range is a selection over literals
|
|
517
|
-
// only. It is not a collapsed caret, and running the directional branch on it
|
|
518
|
-
// destroys a character the user did not select: selecting exactly the `-` of
|
|
519
|
-
// `555-123-4567` and pressing Backspace would eat the `5` in front of it.
|
|
520
|
-
// A literal is not content, so there is nothing to delete and nothing to report --
|
|
521
|
-
// no decision is being withheld, the field visibly does not change.
|
|
522
|
-
if ((el.selectionStart ?? 0) !== (el.selectionEnd ?? 0)) return;
|
|
523
|
-
const backward = inputType.endsWith('Backward');
|
|
524
|
-
// A masked field has no words and no lines, so the word/line deletes take the whole
|
|
525
|
-
// side of the caret. Stated, not silent: the alternative is pretending `4821` is a
|
|
526
|
-
// word, which is a guess about a format the consumer chose.
|
|
527
|
-
const wide = /^delete(Word|SoftLine|HardLine)/.test(inputType);
|
|
528
|
-
if (wide) {
|
|
529
|
-
if (backward) start = 0;
|
|
530
|
-
else end = raw.length;
|
|
531
|
-
} else if (backward) {
|
|
532
|
-
if (start === 0) return;
|
|
533
|
-
start -= 1;
|
|
534
|
-
} else {
|
|
535
|
-
if (end >= raw.length) return;
|
|
536
|
-
end += 1;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
applyEdit(start, end, '', 'bulk');
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (inputType === 'historyUndo') {
|
|
544
|
-
e.preventDefault();
|
|
545
|
-
undo();
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
if (inputType === 'historyRedo') {
|
|
549
|
-
e.preventDefault();
|
|
550
|
-
redo();
|
|
551
|
-
}
|
|
552
|
-
// Anything else is the browser's: it happens, and `input` reconciles it.
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function onInputEvent(): void {
|
|
556
|
-
if (detached || composing) return;
|
|
557
|
-
if (el.value === formatted) return; // our own commit, or a genuine no-op
|
|
558
|
-
reconcile();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function onCompositionStart(): void {
|
|
562
|
-
if (detached) return;
|
|
563
|
-
composing = true;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function onCompositionEnd(): void {
|
|
567
|
-
if (detached) return;
|
|
568
|
-
composing = false;
|
|
569
|
-
// A consumer write that arrived mid-composition wins over the composed text: it is the
|
|
570
|
-
// later statement of intent, and it is the value the consumer's own model now holds.
|
|
571
|
-
const pending = pendingWrite;
|
|
572
|
-
pendingWrite = null;
|
|
573
|
-
if (pending !== null) {
|
|
574
|
-
pending();
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
// Otherwise exactly one reconciliation, here (§5.2). Every `input` that arrived during
|
|
578
|
-
// the composition was ignored on purpose.
|
|
579
|
-
if (el.value !== formatted) reconcile();
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/** Run a consumer write now, or hold it until the composition ends (§5.2 applied to the
|
|
583
|
-
* masker's own writes: a framework re-render calling `setValue` mid-composition is the
|
|
584
|
-
* classic controlled-input IME bug, and rewriting `.value` there kills the composition). */
|
|
585
|
-
function writeOrDefer(apply: () => void): void {
|
|
586
|
-
if (composing) {
|
|
587
|
-
pendingWrite = apply;
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
apply();
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function onKeyDown(event: Event): void {
|
|
594
|
-
if (detached || composing) return;
|
|
595
|
-
const e = event as KeyboardEvent;
|
|
596
|
-
const mod = e.ctrlKey || e.metaKey;
|
|
597
|
-
const key = e.key.toLowerCase();
|
|
598
|
-
if (mod && key === 'z') {
|
|
599
|
-
e.preventDefault();
|
|
600
|
-
if (e.shiftKey) redo();
|
|
601
|
-
else undo();
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
if (mod && key === 'y') {
|
|
605
|
-
e.preventDefault();
|
|
606
|
-
redo();
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
if (NAV_KEYS.has(e.key)) runOpen = false;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function onMouseDown(): void {
|
|
613
|
-
runOpen = false;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function onBlur(): void {
|
|
617
|
-
runOpen = false;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** The caret window: `[floor, frontier]`.
|
|
621
|
-
*
|
|
622
|
-
* FLOOR is the first fill position, so the caret never rests inside a leading literal
|
|
623
|
-
* run -- with `CHG-####` the field focuses at `CHG-|####` and Home, ArrowLeft and a click
|
|
624
|
-
* into the prefix all land there. On an UNGUIDED empty field the floor collapses to 0,
|
|
625
|
-
* because there is no rendered prefix to be trapped behind until a character exists;
|
|
626
|
-
* `rawToFormattedIndex` clamps to the text that exists, which is what makes that fall out
|
|
627
|
-
* rather than needing a special case.
|
|
628
|
-
*
|
|
629
|
-
* FRONTIER is one past the last filled character (`12/23/4|yyy`), so a click into the
|
|
630
|
-
* unfilled guide tail snaps back and End stops at the content rather than at the end of
|
|
631
|
-
* the template. Between the two, every position is legal and is left completely alone. */
|
|
632
|
-
function caretWindow(): [floor: number, frontier: number] {
|
|
633
|
-
return [
|
|
634
|
-
rawToFormattedIndex(pattern, formatted, 0),
|
|
635
|
-
rawToFormattedIndex(pattern, formatted, raw.length),
|
|
636
|
-
];
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/** ONE clamp function, idempotent, NO TIMERS (§5.9) -- but four triggers, and the trigger
|
|
640
|
-
* list is MEASURED, not assumed. §5.9's objection to the reference was the racing
|
|
641
|
-
* `mousedown`+rAF / `mouseup`+`setTimeout` pair and the keyboard-driven selection it
|
|
642
|
-
* missed; it is not an objection to listening for more than one event, and an earlier
|
|
643
|
-
* version of this comment asserted that `selectionchange` alone covered everything. It
|
|
644
|
-
* does not. Measured in real Chromium (tests/e2e/input-mask-ivp.spec.ts, scenarios 9-10),
|
|
645
|
-
* logging every candidate event with the offset it saw:
|
|
646
|
-
*
|
|
647
|
-
* Home -> ["el:keyup@0"]
|
|
648
|
-
* click -> ["el:selectionchange@4", "doc:selectionchange@4", "el:mouseup@0", "el:click@0"]
|
|
649
|
-
*
|
|
650
|
-
* Two facts fall out, and both were live defects the owner hit on a guided field:
|
|
651
|
-
* - A KEYBOARD caret move fires NO `selectionchange` whatsoever. Home, End and the
|
|
652
|
-
* arrows reached the clamp on no engine, which is exactly "I could only go back to
|
|
653
|
-
* the start of ####" failing to hold. `keyup` is the event that does fire.
|
|
654
|
-
* - A CLICK fires `selectionchange` early, and then the browser applies its own
|
|
655
|
-
* pixel-derived offset AFTER it -- note the `@4` from our clamp being replaced by the
|
|
656
|
-
* `@0` visible at `mouseup`. Clamping only on `selectionchange` gets overwritten by
|
|
657
|
-
* the very gesture it was reacting to. `mouseup` is where the offset is final.
|
|
658
|
-
*
|
|
659
|
-
* So: `selectionchange` (the general case), `keyup` (keyboard), `mouseup` (pointer), and
|
|
660
|
-
* `focus` -- the last because a selection that does not CHANGE fires nothing at all, so a
|
|
661
|
-
* field entered while its caret is already at 0 would otherwise render with the caret
|
|
662
|
-
* inside the literal prefix and never get a chance to be corrected. Every trigger is the
|
|
663
|
-
* same idempotent call that early-returns when the selection is already legal, so firing
|
|
664
|
-
* several times for one gesture costs a comparison and changes nothing. */
|
|
665
|
-
/** "Is the caret in THIS field?" -- asked of the element's own root, never of the
|
|
666
|
-
* document.
|
|
667
|
-
*
|
|
668
|
-
* `document.activeElement` RETARGETS: for a focused node inside a shadow tree it reports
|
|
669
|
-
* the OUTERMOST host, so on any element this kit actually ships the answer is `<kai-chat>`
|
|
670
|
-
* and never the input. `el.ownerDocument.activeElement !== el` was therefore true on every
|
|
671
|
-
* trigger, and the whole clamp early-returned -- the caret discipline was dead in the
|
|
672
|
-
* ops-console app (input at `kai-chat` shadow -> card -> `kai-form` shadow) while passing
|
|
673
|
-
* in Storybook, whose story mounts the input into a FLAT document where the two agree.
|
|
674
|
-
* Measured in that app on the dist build (m11-diagnose.mjs), focusing the `CHG-####`
|
|
675
|
-
* field:
|
|
676
|
-
*
|
|
677
|
-
* el:focus docActiveIsEl=false docActive=kai-chat rootActiveIsEl=true
|
|
678
|
-
* el:mouseup docActiveIsEl=false docActive=kai-chat rootActiveIsEl=true
|
|
679
|
-
* doc:selectionchange docActiveIsEl=false docActive=kai-chat rootActiveIsEl=true
|
|
680
|
-
*
|
|
681
|
-
* All four triggers FIRE at the right offsets through two shadow roots -- the trigger
|
|
682
|
-
* table above holds unchanged; it was only this guard that was wrong. `getRootNode()` is
|
|
683
|
-
* read per call rather than captured, because the element can be moved between roots, and
|
|
684
|
-
* it degrades correctly: in a flat document the root IS the document, and while detached
|
|
685
|
-
* the root is a fragment with no `activeElement`, which reads as not focused. */
|
|
686
|
-
function isFocused(): boolean {
|
|
687
|
-
const root = el.getRootNode() as Document | ShadowRoot;
|
|
688
|
-
return root.activeElement === el;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function clampSelection(): void {
|
|
692
|
-
if (detached || composing || settingSelection) return;
|
|
693
|
-
if (!isFocused()) return;
|
|
694
|
-
const start = el.selectionStart;
|
|
695
|
-
const end = el.selectionEnd;
|
|
696
|
-
if (start === null || end === null) return;
|
|
697
|
-
|
|
698
|
-
const [floor, frontier] = caretWindow();
|
|
699
|
-
|
|
700
|
-
let nextStart = start;
|
|
701
|
-
let nextEnd = end;
|
|
702
|
-
if (start === end) {
|
|
703
|
-
nextStart = nextEnd = Math.max(floor, Math.min(start, frontier));
|
|
704
|
-
} else {
|
|
705
|
-
// A range obeys the SAME window at both ends. Clamping only the end was the earlier
|
|
706
|
-
// reading, and it let Ctrl-A leave the anchor at 0 -- visibly selecting a literal
|
|
707
|
-
// prefix that is not content and cannot be edited. Collapsing to the window keeps
|
|
708
|
-
// select-all meaning "all of the value": the raw range is unchanged, so a canonical
|
|
709
|
-
// copy of a fully-selected field still yields the whole value, literals included.
|
|
710
|
-
nextEnd = Math.min(end, frontier);
|
|
711
|
-
nextStart = Math.min(Math.max(start, floor), nextEnd);
|
|
712
|
-
}
|
|
713
|
-
if (nextStart === start && nextEnd === end) return; // already valid: do nothing at all
|
|
714
|
-
setSelection(nextStart, nextEnd);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function onCopy(event: Event): void {
|
|
718
|
-
if (detached) return;
|
|
719
|
-
const e = event as ClipboardEvent;
|
|
720
|
-
e.preventDefault();
|
|
721
|
-
e.clipboardData?.setData('text/plain', clipboardText());
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function onCut(event: Event): void {
|
|
725
|
-
if (detached) return;
|
|
726
|
-
const e = event as ClipboardEvent;
|
|
727
|
-
e.preventDefault();
|
|
728
|
-
e.clipboardData?.setData('text/plain', clipboardText());
|
|
729
|
-
const [start, end] = selectionRaw();
|
|
730
|
-
if (start === end) return;
|
|
731
|
-
runOpen = false;
|
|
732
|
-
applyEdit(start, end, '', 'bulk');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function getCanonicalValue(): string {
|
|
736
|
-
// `formatRaw`, not the display string: a guide character must never be read as content.
|
|
737
|
-
return canonicalize(pattern, formatRaw(pattern, raw), semantic);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const elementListeners: Array<[string, EventListener]> = [
|
|
741
|
-
['beforeinput', onBeforeInput],
|
|
742
|
-
['input', onInputEvent],
|
|
743
|
-
['compositionstart', onCompositionStart],
|
|
744
|
-
// `compositionupdate` is deliberately NOT bound, though §5.2 names it: `composing` is
|
|
745
|
-
// already true for the whole window, so a handler would have nothing to do. Binding one
|
|
746
|
-
// to "keep the set complete" would be an empty listener implying a hook that is not there.
|
|
747
|
-
['compositionend', onCompositionEnd],
|
|
748
|
-
['keydown', onKeyDown],
|
|
749
|
-
['mousedown', onMouseDown],
|
|
750
|
-
// The three non-`selectionchange` clamp triggers. See `clampSelection` for the measured
|
|
751
|
-
// Chromium event logs that make each of them necessary rather than defensive.
|
|
752
|
-
['keyup', clampSelection],
|
|
753
|
-
['mouseup', clampSelection],
|
|
754
|
-
['focus', clampSelection],
|
|
755
|
-
['blur', onBlur],
|
|
756
|
-
['copy', onCopy],
|
|
757
|
-
['cut', onCut],
|
|
758
|
-
];
|
|
759
|
-
const doc = el.ownerDocument;
|
|
760
|
-
|
|
761
|
-
for (const [kind, handler] of elementListeners) el.addEventListener(kind, handler);
|
|
762
|
-
// `selectionchange` fires on the DOCUMENT for `<input>` in every browser this kit
|
|
763
|
-
// targets; the element-targeted version is newer and not yet universal.
|
|
764
|
-
doc.addEventListener('selectionchange', clampSelection);
|
|
765
|
-
|
|
766
|
-
{
|
|
767
|
-
const seed = normalizeToRaw(pattern, opts.initialValue ?? el.value, caseMode);
|
|
768
|
-
// No undo entry and no callback: attaching is not an edit the consumer made.
|
|
769
|
-
commitRaw(seed, seed.length, { undo: 'none', notify: false });
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
setValue(value: string): void {
|
|
774
|
-
writeOrDefer(() => {
|
|
775
|
-
const next = normalizeToRaw(pattern, value, caseMode);
|
|
776
|
-
// A bulk replacement, so it breaks the run and gets its own undo entry -- but it
|
|
777
|
-
// does NOT notify: a controlled widget that wrote this value would echo itself
|
|
778
|
-
// forever. (`update()` below is the opposite case, and for a stated reason.)
|
|
779
|
-
commitRaw(next, next.length, { undo: 'break', notify: false });
|
|
780
|
-
});
|
|
781
|
-
},
|
|
782
|
-
|
|
783
|
-
getRawValue: () => raw,
|
|
784
|
-
getCanonicalValue,
|
|
785
|
-
getFormattedValue: () => formatted,
|
|
786
|
-
|
|
787
|
-
setObscure(on: boolean): void {
|
|
788
|
-
obscure = on;
|
|
789
|
-
// Tier 3 (task 10) makes this change the rendered text. Today it selects the default
|
|
790
|
-
// copy policy and nothing else, which is stated on the option rather than implied.
|
|
791
|
-
},
|
|
792
|
-
|
|
793
|
-
update(next: Partial<InputMaskOptions>): void {
|
|
794
|
-
// Configuration lands immediately -- a new `onInput` must not be shadowed by the old
|
|
795
|
-
// one for the length of a composition. Only the VALUE WRITE is deferrable.
|
|
796
|
-
const previousText = formatRaw(pattern, raw);
|
|
797
|
-
const previousRaw = raw;
|
|
798
|
-
const previousFormatted = formatted;
|
|
799
|
-
|
|
800
|
-
// COMPILE FIRST, ASSIGN AFTER. `compileMask` throws on a format over the length cap
|
|
801
|
-
// or a guide that does not align, and the throw must leave this masker exactly as it
|
|
802
|
-
// was. Merging into `opts` first would park the rejected config in state where the
|
|
803
|
-
// NEXT update -- one that says nothing about `format` -- picks it up and applies a
|
|
804
|
-
// change that was already refused. The element facade hits precisely this shape when
|
|
805
|
-
// `format` and `guide` are separate reactive attributes that do not land in the same
|
|
806
|
-
// tick.
|
|
807
|
-
const merged: InputMaskOptions = { ...opts, ...next };
|
|
808
|
-
const nextPattern = compileMask(merged.format, merged.guide);
|
|
809
|
-
|
|
810
|
-
opts = merged;
|
|
811
|
-
pattern = nextPattern;
|
|
812
|
-
hasGuide = merged.guide !== undefined;
|
|
813
|
-
caseMode = merged.caseMode ?? 'preserve';
|
|
814
|
-
semantic = merged.semantic ?? 'custom';
|
|
815
|
-
obscure = merged.obscure ?? false;
|
|
816
|
-
runOpen = false;
|
|
817
|
-
// The undo history is NOT cleared: entries carry the pattern that wrote them and
|
|
818
|
-
// `restore` re-fits across a change. Redo is, because it holds futures that the new
|
|
819
|
-
// pattern may not be able to reach.
|
|
820
|
-
redoStack.length = 0;
|
|
821
|
-
|
|
822
|
-
writeOrDefer(() => {
|
|
823
|
-
const nextRaw = normalizeToRaw(pattern, previousText, caseMode);
|
|
824
|
-
const nextFormatted = display(nextRaw);
|
|
825
|
-
const changed = nextFormatted !== previousFormatted;
|
|
826
|
-
// A format change that cannot hold what the field already held DESTROYS user
|
|
827
|
-
// content, and the consumer's own model still has the old value -- so silence here
|
|
828
|
-
// is worse than an ordinary silent drop: nothing would ever converge. The element
|
|
829
|
-
// facade re-calls `update()` on any `format` prop change, so this fires on a
|
|
830
|
-
// routine reactive render. Report the loss, and notify so a controlled consumer
|
|
831
|
-
// catches up. `data` carries the pre-change text, which after this call is the only
|
|
832
|
-
// copy of the discarded characters anywhere.
|
|
833
|
-
if (nextRaw.length < previousRaw.length) reject('format-change-clipped', previousText);
|
|
834
|
-
// `undo: 'none'`, always. An entry pushed here would be DEAD BY CONSTRUCTION: it
|
|
835
|
-
// would hold `previousFormatted` under the old pattern, and `restore` re-fits a
|
|
836
|
-
// stale entry with exactly the computation two lines up
|
|
837
|
-
// (`normalizeToRaw(pattern, previousText)`), so restoring it lands on the text
|
|
838
|
-
// already on screen -- the first Ctrl+Z after a format change would do nothing
|
|
839
|
-
// visible. The history the user cares about is the entries UNDERNEATH, which
|
|
840
|
-
// survive because each carries the pattern that wrote it.
|
|
841
|
-
commitRaw(nextRaw, nextRaw.length, { undo: 'none', notify: changed });
|
|
842
|
-
});
|
|
843
|
-
},
|
|
844
|
-
|
|
845
|
-
detach(): void {
|
|
846
|
-
if (detached) return;
|
|
847
|
-
detached = true;
|
|
848
|
-
pendingWrite = null; // a deferred write must not fire into a field we no longer own
|
|
849
|
-
for (const [kind, handler] of elementListeners) el.removeEventListener(kind, handler);
|
|
850
|
-
doc.removeEventListener('selectionchange', clampSelection);
|
|
851
|
-
},
|
|
852
|
-
};
|
|
853
|
-
}
|