@kitn.ai/ui 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/bin/mcp.js +16 -3
- package/bin/route.js +23 -0
- package/bin/route.test.js +58 -0
- package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-BnFKqqtY.js} +1 -1
- package/dist/action-icons-CBskfBMz.js +1 -0
- package/dist/arrow-down-Br0qtpsz.js +1 -0
- package/dist/arrow-left-Dj7xgE12.js +1 -0
- package/dist/artifact-CvXEm1lS.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-C43ZegEq.js +1 -0
- package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-B-79y1Tj.js} +1 -1
- package/dist/badge-BpUbq8c0.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-sn17Ip9v.js +1 -0
- package/dist/{elements/chunks/button-flUbeufF.js → button-BH6rN0U-.js} +1 -1
- package/dist/card-renderer-Cb1gQmth.js +1 -0
- package/dist/card-routing-DifQgS7n.js +1 -0
- package/dist/check-DPeeMUYx.js +1 -0
- package/dist/checkbox-Dc9Yov_B.js +1 -0
- package/dist/checkbox-group-B1kDLLYk.js +1 -0
- package/dist/chevron-down-CHSWk7ZT.js +1 -0
- package/dist/chevron-right-B7l_fMgx.js +1 -0
- package/dist/choice-card-BT3c1S8g.js +1 -0
- package/dist/circle-Bb0yOpqM.js +1 -0
- package/dist/circle-check-BGZpCiz7.js +1 -0
- package/dist/circle-x-wobWkoSx.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/{elements/chunks/code-block-BdLiTitd.js → code-block-CUQ5mtv0.js} +1 -1
- package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-CPJbf6FM.js} +1 -1
- package/dist/components/attachment-types.d.ts +7 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
- package/dist/components/card-renderer.d.ts +13 -0
- package/dist/components/card.d.ts +4 -0
- package/dist/components/chat-thread.d.ts +46 -1
- package/dist/components/conversation-item.d.ts +19 -7
- package/dist/components/conversation-list.d.ts +17 -5
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +48 -1
- package/dist/components/message.d.ts +25 -3
- package/dist/components/reasoning.d.ts +6 -0
- package/dist/components/scroll-button.d.ts +11 -0
- package/dist/components/thread.d.ts +5 -0
- package/dist/components/tool-types.d.ts +9 -0
- package/dist/composer-CjngLzal.js +106 -0
- package/dist/confirm-card-DCx_A4bU.js +1 -0
- package/dist/construct-cli.es.js +1169 -0
- package/dist/{elements/chunks/context-VLIL3sJ-.js → context-BY8V-Nlp.js} +1 -1
- package/dist/controllable-D43BKmtX.js +1 -0
- package/dist/conversation-list-D4lLU_yS.js +1 -0
- package/dist/copy-B6DO4PqB.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-CQED4T0g.js → create-tween-BPzaefTp.js} +1 -1
- package/dist/{create-tween-BFT6c-Y8.js → create-tween-CnMHviKB.js} +1 -1
- package/dist/{create-tween-CJNlzoaO.js → create-tween-D1aiGZZh.js} +1 -1
- package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-DUSQUKpr.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +1400 -218
- package/dist/default-input-CucQEOqH.js +1 -0
- package/dist/define-CGGX-7Vg.js +1 -0
- package/dist/define.d.ts +4 -0
- package/dist/define.js +510 -0
- package/dist/define.server.d.ts +5 -0
- package/dist/define.server.js +506 -0
- package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-CbLtajUQ.js} +1 -1
- package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-BwhViJFd.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/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkbox-group.d.ts +1 -0
- package/dist/elements/checkbox-group.js +1 -0
- package/dist/elements/checkbox.d.ts +1 -0
- package/dist/elements/checkbox.js +1 -0
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-item.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/define-entry.d.ts +12 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.js +101 -7
- package/dist/elements/dropdown.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-data-types.d.ts +37 -0
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-grid.d.ts +1 -0
- package/dist/elements/pane-grid.js +1 -0
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +1 -0
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/select.d.ts +1 -0
- package/dist/elements/select.js +1 -0
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +1 -0
- package/dist/elements/slots.d.ts +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/elements.d.ts +362 -24
- package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-DFr4Vqr3.js} +1 -1
- package/dist/embed-5s_vU4J0.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-u9d85ORQ.js} +1 -1
- package/dist/file-text-CX7_x42o.js +1 -0
- package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-D3kBjXI5.js} +1 -1
- package/dist/folder-V-YdIL8f.js +1 -0
- package/dist/form-BRNUMCN2.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-DaeiYrTA.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-BpixdZNq.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7330 -5852
- package/dist/index.server.js +5823 -4759
- package/dist/info-8mFsIjS3.js +1 -0
- package/dist/input-d1Yuu-hz.js +1 -0
- package/dist/javascript-C25yR2R2.js +1 -6
- package/dist/json-DxJze_jm.js +1 -6
- package/dist/kai.es.js +1 -1
- package/dist/kbd-961zMPFB.js +1 -0
- package/dist/link-DlZZBGaR.js +1 -0
- package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DIIbV2Sd.js} +1 -1
- package/dist/loader-YQszHFaW.js +1 -0
- package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-CVk1MPsj.js} +1 -1
- package/dist/mcp.es.js +1285 -79
- package/dist/message-CwKVEdZg.js +1 -0
- package/dist/message-DS8lzHc1.js +1 -0
- package/dist/message-circle-Xf8Q-vZp.js +1 -0
- package/dist/message-square-Cxnwgkns.js +1 -0
- package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-UEZmP2QT.js} +1 -1
- package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-Kk1jEQ5t.js} +1 -1
- package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-C1od6LOz.js} +1 -1
- package/dist/panel-right-ryET0yyn.js +1 -0
- package/dist/paperclip-BDNoZFJP.js +1 -0
- package/dist/play-OFIvYXUQ.js +1 -0
- package/dist/primitives/card-data-types.d.ts +49 -0
- package/dist/primitives/card-routing.d.ts +1 -33
- package/dist/primitives/card-validate.d.ts +2 -2
- package/dist/primitives/chat-config.d.ts +29 -2
- package/dist/primitives/field-mask.d.ts +96 -0
- package/dist/primitives/field-semantics.d.ts +36 -0
- package/dist/primitives/input-mask.d.ts +46 -0
- package/dist/primitives/url-scheme-policy.d.ts +34 -0
- package/dist/primitives/use-resize-observer.d.ts +2 -2
- package/dist/progress-bar-BvYX0aoy.js +1 -0
- package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
- package/dist/radio-DcL23cNJ.js +1 -0
- package/dist/react/index.d.ts +193 -19
- package/dist/react.js +216 -180
- package/dist/reasoning-Cackq4Rh.js +1 -0
- package/dist/register-impl-C7rENvPK.js +424 -0
- package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-Cp9HUYih.js} +1 -1
- package/dist/rotate-ccw-0DLRDHH5.js +1 -0
- package/dist/rotate-cw-DOB9IGcU.js +1 -0
- package/dist/schemas/form.schema.json +21 -1
- package/dist/schemas/tool-defs.d.ts +11 -1
- package/dist/schemas.js +81 -81
- package/dist/scroll-area-emVmWP4K.js +1 -0
- package/dist/scroll-button-CeP_gv1x.js +1 -0
- package/dist/select-CWmu00tS.js +1 -0
- package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-CE6vly6z.js} +1 -1
- package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-CE-78SvZ.js} +1 -1
- package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-ZFWNxFQR.js} +1 -1
- package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-BebdP4mf.js} +1 -1
- package/dist/slider-BAKRYUlz.js +1 -0
- package/dist/{solid-BtdqNZI7.js → solid-BE5ui6sy.js} +8727 -7212
- package/dist/{solid-CnavwBye.js → solid-DFhsce3O.js} +7015 -5886
- package/dist/solid.d.ts +10 -0
- package/dist/solid.js +240 -234
- package/dist/solid.server.js +240 -234
- package/dist/source-B0FlguSx.js +1 -0
- package/dist/star-CBMZvLuN.js +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/mock.d.ts +32 -2
- package/dist/state.js +133 -108
- package/dist/{elements/chunks/store-Cjq0qwYN.js → store-7eoadydQ.js} +1 -1
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-CViARZCy.js +1 -0
- package/dist/tasks-card-MjCcOhR_.js +1 -0
- package/dist/text-shimmer-DKlwRF2G.js +1 -0
- package/dist/textarea-3ay2Me2-.js +1 -0
- package/dist/theme.tokens.css +82 -13
- package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-DQcu2HMO.js} +1 -1
- package/dist/toast-store-C6yKOhbk.js +1 -0
- package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-BCYNlfKy.js} +1 -1
- package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-05zI4hjs.js} +1 -1
- package/dist/trash-2-BR1RxGky.js +1 -0
- package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DHohDOCy.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- package/dist/typescript-RycA9KXf.js +1 -6
- package/dist/ui/checkbox-group.d.ts +83 -0
- package/dist/ui/checkbox.d.ts +40 -0
- package/dist/ui/dock.d.ts +22 -0
- package/dist/ui/icon.d.ts +3 -0
- package/dist/ui/input.d.ts +48 -2
- package/dist/ui/radio.d.ts +85 -0
- package/dist/ui/select.d.ts +71 -0
- package/dist/ui/slider.d.ts +65 -0
- package/dist/ui/switch.d.ts +9 -2
- package/dist/upload-BbGW6A_u.js +1 -0
- package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
- package/dist/use-card-resolution-CJW4grFa.js +1 -0
- package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-BUOVgPOB.js} +2 -2
- package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-CUsUJvFI.js} +2 -2
- package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-CW2i4bfr.js} +1 -1
- package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-xiMPty8f.js} +1 -1
- package/dist/{variant-custom-L16JLLZy.js → variant-custom-BQtOnmeO.js} +1 -1
- package/dist/{variant-custom-C714mqKl.js → variant-custom-CAm9L-E5.js} +2 -2
- package/dist/{variant-custom-BdvYC7af.js → variant-custom-CAnzzppW.js} +2 -2
- package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-D91HjAKz.js} +1 -1
- package/dist/{variant-wave-ecwykeyL.js → variant-wave-Ca1Ls4_y.js} +2 -2
- package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CxX39DKd.js} +2 -2
- package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-DYyzIIZv.js} +1 -1
- package/dist/{variant-wave-2S4La2Ka.js → variant-wave-uY5crU_K.js} +1 -1
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/x-C5m1JFgi.js +1 -0
- package/frameworks/react/index.tsx +198 -20
- package/llms-full.txt +1178 -21
- package/llms.txt +6 -3
- package/package.json +17 -4
- package/src/agent-tooling/catalog/invariants.ts +1 -1
- package/src/agent-tooling/catalog/surfaces.ts +72 -0
- package/src/agent-tooling/construct/cli-entry.ts +8 -0
- package/src/agent-tooling/construct/cli.ts +131 -0
- package/src/agent-tooling/construct/codegen.ts +1353 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +250 -0
- package/src/agent-tooling/construct/dev.ts +148 -0
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +23 -0
- package/src/agent-tooling/construct/schema.ts +371 -0
- package/src/agent-tooling/mcp/server.ts +2 -1
- package/src/agent-tooling/mcp/tools/construct.ts +130 -0
- package/src/agent-tooling/mcp/tools/debug.ts +126 -4
- package/src/agent-tooling/mcp/tools/reference.ts +104 -3
- package/src/agent-tooling/recipes/composed-thread.ts +714 -0
- package/src/agent-tooling/recipes/index.ts +22 -0
- package/src/agent-tooling/recipes/types.ts +32 -0
- package/src/agent-tooling/registry.ts +5 -0
- package/src/components/attachment-types.ts +7 -0
- package/src/components/audio-visualizer/fit-scale.ts +1 -1
- package/src/components/card-renderer.tsx +39 -8
- package/src/components/card.tsx +10 -2
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +72 -9
- package/src/components/choice-card.tsx +102 -37
- package/src/components/coachmark.tsx +1 -1
- package/src/components/code-block.tsx +2 -2
- package/src/components/composer.tsx +89 -15
- package/src/components/confirm-card.tsx +20 -4
- package/src/components/conversation-item.tsx +33 -9
- package/src/components/conversation-list.tsx +25 -10
- package/src/components/empty.tsx +14 -3
- package/src/components/file-tree.tsx +5 -1
- package/src/components/form-widgets.tsx +257 -195
- package/src/components/form.tsx +297 -9
- package/src/components/loader.tsx +12 -5
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +71 -11
- package/src/components/prompt-input.tsx +7 -1
- package/src/components/prompt-suggestion.tsx +4 -1
- package/src/components/reasoning.tsx +30 -3
- package/src/components/response-compare.tsx +2 -2
- package/src/components/scroll-button.tsx +56 -4
- package/src/components/source.tsx +6 -1
- package/src/components/tasks-card.tsx +58 -16
- package/src/components/thread.tsx +10 -1
- package/src/components/toast.tsx +1 -1
- package/src/components/tool-types.ts +9 -0
- package/src/elements/attachments.tsx +3 -1
- package/src/elements/chat-workspace.tsx +1 -1
- package/src/elements/chat.tsx +22 -4
- package/src/elements/checkbox-group.tsx +164 -0
- package/src/elements/checkbox.tsx +134 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/composer.tsx +12 -1
- package/src/elements/conversation-item.tsx +62 -22
- package/src/elements/conversation-list.tsx +2 -2
- package/src/elements/define-entry.ts +12 -0
- package/src/elements/dock.tsx +11 -0
- package/src/elements/element-data-types.ts +40 -0
- package/src/elements/element-manifest.json +24 -0
- package/src/elements/element-meta.json +736 -46
- package/src/elements/element-nonscalar.json +15 -0
- package/src/elements/element-types.d.ts +362 -24
- package/src/elements/feedback-bar.tsx +5 -0
- package/src/elements/icon-names.json +29 -0
- package/src/elements/input.tsx +312 -23
- package/src/elements/message.tsx +2 -0
- package/src/elements/pane-grid.tsx +111 -0
- package/src/elements/prompt-input.tsx +8 -2
- package/src/elements/radio-group.tsx +129 -0
- package/src/elements/register-impl.ts +6 -0
- package/src/elements/scroll-button.tsx +54 -6
- package/src/elements/select.tsx +165 -0
- package/src/elements/slider.tsx +171 -0
- package/src/elements/slots.ts +1 -1
- package/src/elements/styles.css +89 -3
- package/src/elements/thread.tsx +2 -0
- package/src/elements/toast.tsx +14 -1
- package/src/index.ts +2 -1
- package/src/primitives/card-data-types.ts +49 -0
- package/src/primitives/card-host.tsx +4 -2
- package/src/primitives/card-routing.ts +7 -56
- package/src/primitives/card-schemas/form.schema.json +21 -1
- package/src/primitives/card-validate.ts +10 -2
- package/src/primitives/chat-config.tsx +33 -6
- package/src/primitives/field-mask.ts +256 -0
- package/src/primitives/field-semantics.ts +115 -0
- package/src/primitives/input-mask.ts +853 -0
- package/src/primitives/toast-store.ts +66 -4
- package/src/primitives/url-scheme-policy.ts +70 -0
- package/src/primitives/use-resize-observer.ts +2 -2
- package/src/schemas/tool-defs.ts +12 -2
- package/src/solid.ts +10 -0
- package/src/state/index.ts +1 -1
- package/src/state/mock.ts +92 -6
- package/src/ui/agent-card.tsx +2 -2
- package/src/ui/avatar.tsx +1 -1
- package/src/ui/checkbox-group.tsx +153 -0
- package/src/ui/checkbox.tsx +63 -0
- package/src/ui/dock.tsx +145 -6
- package/src/ui/icon.tsx +53 -0
- package/src/ui/input.tsx +249 -9
- package/src/ui/kbd.tsx +1 -1
- package/src/ui/nav.tsx +1 -1
- package/src/ui/pane-group.tsx +2 -2
- package/src/ui/radio.tsx +150 -0
- package/src/ui/select.tsx +168 -0
- package/src/ui/slider.tsx +178 -0
- package/src/ui/switch.tsx +49 -18
- package/src/utils/cn.ts +11 -5
- package/theme.css +84 -13
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
- package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
- package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
- package/dist/elements/chunks/bash-InADTalH.js +0 -1
- package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
- package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
- package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
- package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
- package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
- package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
- package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
- package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
- package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
- package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
- package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
- package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
- package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
- package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
- package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
- package/dist/elements/chunks/form-CQirdC4W.js +0 -1
- package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
- package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
- package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
- package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
- package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
- package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
- package/dist/elements/chunks/json-DxJze_jm.js +0 -1
- package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
- package/dist/elements/chunks/link-2My69BqT.js +0 -1
- package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
- package/dist/elements/chunks/message-VMZohrXx.js +0 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
- package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
- package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
- package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
- package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
- package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
- package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
- package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
- package/dist/elements/chunks/source-UWCvmVak.js +0 -1
- package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
- package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
- package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
- package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
- package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
- package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
- package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
- package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
- package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
- package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
- package/dist/llms/llms-full.txt +0 -2949
- package/dist/llms/llms.txt +0 -156
- package/dist/register-impl-DpE7icIb.js +0 -293
- package/dist/ui/stat.d.ts +0 -16
- package/src/ui/stat.tsx +0 -41
- /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
- /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
- /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
package/src/ui/dock.tsx
CHANGED
|
@@ -42,6 +42,19 @@ export interface DockProps {
|
|
|
42
42
|
unread?: boolean;
|
|
43
43
|
/** Disable the launcher. */
|
|
44
44
|
disabled?: boolean;
|
|
45
|
+
/** Suppress the dock's own built-in mobile `[part="close"]` X. Off (rendered) by
|
|
46
|
+
* default — the built-in X is the FALLBACK close route for panel content with no
|
|
47
|
+
* header of its own, which is the general case the dock has to cover since it
|
|
48
|
+
* never reads what it holds. Set `true` when the slotted/composed content
|
|
49
|
+
* supplies its own close affordance in its own header row (e.g. `ChatThread`'s
|
|
50
|
+
* `headerEndContent`) — with both present the two X's stack, one floating over
|
|
51
|
+
* the other's row, which is exactly the "doesn't look intentional" feedback this
|
|
52
|
+
* prop exists to let a caller avoid. TRADEOFF: the mobile panel reserves a
|
|
53
|
+
* padding band above its content so the built-in X never paints over it; that
|
|
54
|
+
* band stays reserved unless you set `hideClose` true, so only set it once your
|
|
55
|
+
* own control is actually in place — otherwise you get the band with no X to
|
|
56
|
+
* justify it. */
|
|
57
|
+
hideClose?: boolean;
|
|
45
58
|
/** Where focus lands on open. Defaults to `content`. */
|
|
46
59
|
focusOnOpen?: DockFocusOnOpen;
|
|
47
60
|
/** Controlled open state. When set, the component never changes it itself — drive it
|
|
@@ -139,7 +152,12 @@ const DOCK_CSS = `
|
|
|
139
152
|
/* ABOVE the panel, always. At <=480px the panel goes fixed/inset:0 and it comes
|
|
140
153
|
after the launcher in DOM order, so at z-index:auto it painted OVER the launcher
|
|
141
154
|
and a touch user lost every pointer route to closing. Both reference
|
|
142
|
-
implementations stacked the launcher over the panel for the same reason.
|
|
155
|
+
implementations stacked the launcher over the panel for the same reason. The
|
|
156
|
+
launcher itself is now HIDDEN while the panel is open at that width (see the
|
|
157
|
+
<=480px block below) in favour of the in-panel [part="close"] X, but this
|
|
158
|
+
z-index still matters for the moment between "open" landing and the panel's
|
|
159
|
+
own transition finishing, and for any consumer CSS that reintroduces the
|
|
160
|
+
launcher there. */
|
|
143
161
|
z-index: 1;
|
|
144
162
|
flex: none;
|
|
145
163
|
display: inline-flex;
|
|
@@ -168,6 +186,58 @@ const DOCK_CSS = `
|
|
|
168
186
|
[data-kai-dock] [part="launcher"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
|
|
169
187
|
[data-kai-dock] [part="launcher"]:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
170
188
|
|
|
189
|
+
/* The mobile-only close affordance rendered INSIDE the panel. Hidden by default and
|
|
190
|
+
only switched on inside the <=480px media block below, alongside the rule that
|
|
191
|
+
hides the launcher while the panel is open at that width — same query drives both,
|
|
192
|
+
CSS-only, no JS viewport logic. Absolutely positioned against the panel, which is
|
|
193
|
+
the panel's own containing block once the narrow-viewport rule below sets it to
|
|
194
|
+
position: fixed.
|
|
195
|
+
|
|
196
|
+
The inset is a CONSUMER-OVERRIDABLE TOKEN, same idiom as every other geometry
|
|
197
|
+
value in this file (--kai-dock-width and friends). The dock is content-agnostic —
|
|
198
|
+
it never reads what is slotted into the panel — so it cannot know whether that
|
|
199
|
+
content renders its own trailing controls (a ChatThread header-end slot with
|
|
200
|
+
share/settings icons, say) in the same top-right corner.
|
|
201
|
+
|
|
202
|
+
The mobile block below reserves a BAND of the panel's own padding above whatever
|
|
203
|
+
is slotted, sized to this button's own footprint, so nothing paints under this X
|
|
204
|
+
by default -- verified fix (round 1) against a real ChatThread header-end slot
|
|
205
|
+
colliding with it at 375px. Owner feedback on the real widget (round 2): for a
|
|
206
|
+
construct with only a title in the header, that band read as a dead empty strip
|
|
207
|
+
with the X floating alone in it -- "that doesn't look like the empty component
|
|
208
|
+
either, lets do better." The actual fix composes instead of reserving space:
|
|
209
|
+
ChatThread now has its own headerEndContent escape hatch (see its prop doc) so a
|
|
210
|
+
caller puts the close control INSIDE the header row, sharing it with the title --
|
|
211
|
+
zero collision by construction, no band needed. hideClose on this component (see
|
|
212
|
+
its prop doc) then suppresses this built-in X for exactly that case, and the
|
|
213
|
+
band above is scoped to :not([data-hide-close]) so it comes off ONLY there --
|
|
214
|
+
every other consumer (anyone who hasn't opted into hideClose, including a
|
|
215
|
+
hand-authored dock with real header-end content) keeps the band, same as round 1
|
|
216
|
+
shipped, so the collision it fixed stays fixed for them. What remains here for
|
|
217
|
+
that default case is a plain absolute overlay, top-right of the panel, with
|
|
218
|
+
these consumer-overridable inset tokens for the rare case it should deliberately
|
|
219
|
+
sit in front of something (e.g. a single small badge). */
|
|
220
|
+
[data-kai-dock] [part="close"] {
|
|
221
|
+
display: none;
|
|
222
|
+
position: absolute;
|
|
223
|
+
inset-block-start: var(--kai-dock-close-inset-block, 0.75rem);
|
|
224
|
+
inset-inline-end: var(--kai-dock-close-inset-inline, 0.75rem);
|
|
225
|
+
z-index: 1;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
inline-size: 2.25rem;
|
|
229
|
+
block-size: 2.25rem;
|
|
230
|
+
padding: 0;
|
|
231
|
+
border: 0;
|
|
232
|
+
border-radius: 9999px;
|
|
233
|
+
background: var(--color-background);
|
|
234
|
+
color: var(--color-foreground);
|
|
235
|
+
box-shadow: 0 2px 8px -2px rgb(0 0 0 / 0.3);
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
}
|
|
238
|
+
[data-kai-dock] [part="close"]:hover { background: var(--color-muted, var(--color-background)); }
|
|
239
|
+
[data-kai-dock] [part="close"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
|
|
240
|
+
|
|
171
241
|
[data-kai-dock] [part="badge"] {
|
|
172
242
|
position: absolute;
|
|
173
243
|
inset-block-start: 3px;
|
|
@@ -222,10 +292,12 @@ const DOCK_CSS = `
|
|
|
222
292
|
[data-kai-dock][data-position^="top"] [part="panel"][data-closed] { transform: translateY(-0.5rem); }
|
|
223
293
|
|
|
224
294
|
/* 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.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
295
|
+
references wrote this rule independently, which makes it a HOW. While the panel is
|
|
296
|
+
OPEN at this width the launcher hides and the panel's own [part="close"] X becomes
|
|
297
|
+
the pointer close route instead — a FAB floating over a full-bleed panel read as
|
|
298
|
+
wrong (owner feedback) and Escape alone strands a touch user with no visible
|
|
299
|
+
affordance at all. Closed, the launcher is unaffected: it is the panel's own
|
|
300
|
+
[data-expanded] state that gates both rules below, not the media query alone. */
|
|
229
301
|
@media (max-width: 480px) {
|
|
230
302
|
[data-kai-dock] [part="panel"] {
|
|
231
303
|
position: fixed;
|
|
@@ -237,6 +309,41 @@ const DOCK_CSS = `
|
|
|
237
309
|
border: 0;
|
|
238
310
|
border-radius: 0;
|
|
239
311
|
}
|
|
312
|
+
|
|
313
|
+
/* Reserve a band for the built-in [part="close"] X ABOVE whatever is slotted,
|
|
314
|
+
rather than overlaying it on top of the panel's own content — the fix round 1
|
|
315
|
+
collision protection (verified against a real ChatThread header-end slot at
|
|
316
|
+
375px: without this, the X painted directly over trailing icon buttons).
|
|
317
|
+
DERIVED from the button's own inset + size tokens, not a second hand-typed
|
|
318
|
+
number.
|
|
319
|
+
|
|
320
|
+
SCOPED to :not([data-hide-close]) — round 2 (owner feedback: the band read as
|
|
321
|
+
a dead strip when nothing but the X occupied it) removed this UNCONDITIONALLY
|
|
322
|
+
and reintroduced the very collision round 1 fixed, for every consumer who
|
|
323
|
+
never opted into hideClose. hideClose is only true for a caller that supplies
|
|
324
|
+
its OWN close control in its own header row (ChatThread's headerEndContent is
|
|
325
|
+
the one that does today) — the built-in X and this band both come off
|
|
326
|
+
together for that case, since there is nothing left for the band to protect.
|
|
327
|
+
Every other consumer -- including a hand-authored kai-dock with real
|
|
328
|
+
slot="header-end" content and no opinion on hideClose -- keeps the band, the
|
|
329
|
+
same as round 1 shipped. */
|
|
330
|
+
[data-kai-dock]:not([data-hide-close]) [part="panel"] {
|
|
331
|
+
padding-block-start: calc(2 * var(--kai-dock-close-inset-block, 0.75rem) + 2.25rem);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* The full-bleed panel gets its own close affordance instead of relying on the
|
|
335
|
+
launcher, which is hidden below while the panel is open at this width — a FAB
|
|
336
|
+
floating over a full-bleed panel with no other visible close route is exactly
|
|
337
|
+
what the owner flagged. Escape and the X both still work; the launcher comes
|
|
338
|
+
back the instant the panel closes because data-expanded is gone by then.
|
|
339
|
+
Absolutely positioned, so the padding-block-start above (a content-box
|
|
340
|
+
concern) does not shift IT — only the in-flow slotted content. Content that
|
|
341
|
+
supplies its own close control passes hideClose, which drops the button from
|
|
342
|
+
the render tree entirely (see the component body) rather than being gated
|
|
343
|
+
here — so this rule stays a plain, unconditional width-query switch; the band
|
|
344
|
+
above is what actually varies with hideClose. */
|
|
345
|
+
[data-kai-dock] [part="close"] { display: inline-flex; }
|
|
346
|
+
[data-kai-dock]:has([part="panel"][data-expanded]) [part="launcher"] { display: none; }
|
|
240
347
|
}
|
|
241
348
|
|
|
242
349
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -310,6 +417,15 @@ function isWithin(root: Node, node: Node | null): boolean {
|
|
|
310
417
|
* own menus; swallowing a key from a page you are a guest on is a decide-quietly move.
|
|
311
418
|
* - **No focus trap**, deliberately, unlike `kai-dialog`. The page staying usable is
|
|
312
419
|
* the whole point of "docked", which is also why the panel is `aria-modal="false"`.
|
|
420
|
+
* - **Mobile close route.** At <=480px the panel goes full-bleed and the launcher
|
|
421
|
+
* hides while it is open — a floating launcher over a full-bleed panel is not an
|
|
422
|
+
* obvious close affordance. The panel gets its own `[part="close"]` X, top-right,
|
|
423
|
+
* CSS-gated by the SAME media query and `[data-expanded]`, so it needs no viewport
|
|
424
|
+
* JS. Desktop is unaffected: the launcher keeps toggling and no X ever renders.
|
|
425
|
+
* This is the FALLBACK route, for panel content with no header of its own to hold
|
|
426
|
+
* a close control — content that does (a `ChatThread` using `headerEndContent`,
|
|
427
|
+
* say) passes `hideClose` to drop this X instead of stacking a second one over
|
|
428
|
+
* its own header row.
|
|
313
429
|
*
|
|
314
430
|
* WHAT IT REFUSES: it never aborts a request, never persists its own open state, never
|
|
315
431
|
* decides what "unread" means, and never clears `unread` on open — writing over a
|
|
@@ -393,7 +509,17 @@ export function Dock(props: DockProps) {
|
|
|
393
509
|
return (
|
|
394
510
|
<>
|
|
395
511
|
<style>{DOCK_CSS}</style>
|
|
396
|
-
<div
|
|
512
|
+
<div
|
|
513
|
+
data-kai-dock
|
|
514
|
+
data-position={props.position ?? 'bottom-end'}
|
|
515
|
+
// Drives the mobile panel's reserved-band rule below: present only when the
|
|
516
|
+
// built-in [part="close"] X is ACTUALLY suppressed (hideClose true), so the
|
|
517
|
+
// band stays on for every consumer who hasn't opted into their own close
|
|
518
|
+
// control — see the [part="close"]/reserved-band comments for why removing
|
|
519
|
+
// it unconditionally regressed the collision it existed to prevent.
|
|
520
|
+
bool:data-hide-close={!!props.hideClose}
|
|
521
|
+
onKeyDown={onKeyDown}
|
|
522
|
+
>
|
|
397
523
|
<button
|
|
398
524
|
ref={(el) => { launcher = el; props.launcherRef?.(el); }}
|
|
399
525
|
part="launcher"
|
|
@@ -437,6 +563,19 @@ export function Dock(props: DockProps) {
|
|
|
437
563
|
'pointer-events': isOpen() ? 'auto' : 'none',
|
|
438
564
|
}}
|
|
439
565
|
>
|
|
566
|
+
{/* Mobile-only (see the <=480px rule in DOCK_CSS): the full-bleed panel's
|
|
567
|
+
own close affordance, since the launcher is hidden by the same query
|
|
568
|
+
while the panel is open. Reuses the launcher's derived close name
|
|
569
|
+
rather than a new prop — same string, same i18n override. Dropped
|
|
570
|
+
from the render tree entirely (not just hidden) when `hideClose` is
|
|
571
|
+
set — the panel content supplies its own, in its own header, and a
|
|
572
|
+
second X floating over that row is the exact "doesn't look
|
|
573
|
+
intentional" feedback `hideClose` exists to avoid. */}
|
|
574
|
+
<Show when={!props.hideClose}>
|
|
575
|
+
<button type="button" part="close" aria-label={launcherName()} onClick={() => setOpen(false)}>
|
|
576
|
+
<DockCloseGlyph />
|
|
577
|
+
</button>
|
|
578
|
+
</Show>
|
|
440
579
|
{props.children}
|
|
441
580
|
</div>
|
|
442
581
|
</div>
|
package/src/ui/icon.tsx
CHANGED
|
@@ -9,6 +9,16 @@ import {
|
|
|
9
9
|
Archive, ArrowUp, Bookmark, Copy, Ellipsis, ExternalLink, Flag,
|
|
10
10
|
GitBranch, GitPullRequest, ListFilter, RotateCw,
|
|
11
11
|
Moon, Sun,
|
|
12
|
+
// Device / viewport glyphs.
|
|
13
|
+
Tablet, Smartphone, Laptop,
|
|
14
|
+
// Direction glyphs completing the chevron + arrow quartets.
|
|
15
|
+
ChevronRight, ChevronUp, ArrowRight, ArrowDown, PanelRight,
|
|
16
|
+
// Status glyphs (kai-notice / kai-toast / kai-tool render these already).
|
|
17
|
+
Check, Info, TriangleAlert, CircleCheck, CircleAlert, CircleX,
|
|
18
|
+
// Action glyphs the kit performs but could not be named.
|
|
19
|
+
Trash2, Download, Upload, Link, RotateCcw,
|
|
20
|
+
// Pane / media chrome.
|
|
21
|
+
Maximize2, Minimize2, Eye, EyeOff, Bell, Square, Minus, Play,
|
|
12
22
|
} from 'lucide-solid';
|
|
13
23
|
|
|
14
24
|
type IconComponent = Component<{ class?: string }>;
|
|
@@ -68,8 +78,51 @@ const NAMED_ICONS: Record<string, IconComponent> = {
|
|
|
68
78
|
// Theme / appearance glyphs.
|
|
69
79
|
moon: Moon,
|
|
70
80
|
sun: Sun,
|
|
81
|
+
// Device / viewport glyphs. `desktop` and `mobile` are aliases onto the same
|
|
82
|
+
// components as `monitor`/`smartphone` — lucide's names are not the words a
|
|
83
|
+
// consumer reaches for, and an alias costs nothing in the bundle.
|
|
84
|
+
desktop: Monitor,
|
|
85
|
+
laptop: Laptop,
|
|
86
|
+
tablet: Tablet,
|
|
87
|
+
smartphone: Smartphone,
|
|
88
|
+
mobile: Smartphone,
|
|
89
|
+
// Direction glyphs — chevron-down/left and arrow-left/up were here without
|
|
90
|
+
// their opposites; both quartets are now complete.
|
|
91
|
+
'chevron-right': ChevronRight,
|
|
92
|
+
'chevron-up': ChevronUp,
|
|
93
|
+
'arrow-right': ArrowRight,
|
|
94
|
+
'arrow-down': ArrowDown,
|
|
95
|
+
'panel-right': PanelRight,
|
|
96
|
+
// Status glyphs. kai-notice / kai-toast / kai-tool already paint these, so a
|
|
97
|
+
// host building matching UI needs to be able to name them.
|
|
98
|
+
check: Check,
|
|
99
|
+
info: Info,
|
|
100
|
+
'triangle-alert': TriangleAlert,
|
|
101
|
+
'circle-check': CircleCheck,
|
|
102
|
+
'circle-alert': CircleAlert,
|
|
103
|
+
'circle-x': CircleX,
|
|
104
|
+
// Action glyphs. `trash`/`download`/`link` match the keys action-icons.ts
|
|
105
|
+
// already uses for message actions; `upload` is download's missing sibling.
|
|
106
|
+
trash: Trash2,
|
|
107
|
+
download: Download,
|
|
108
|
+
upload: Upload,
|
|
109
|
+
link: Link,
|
|
110
|
+
'rotate-ccw': RotateCcw,
|
|
111
|
+
// Pane / media chrome (kai-pane, kai-artifact, kai-embed, kai-agent-card).
|
|
112
|
+
'maximize-2': Maximize2,
|
|
113
|
+
'minimize-2': Minimize2,
|
|
114
|
+
eye: Eye,
|
|
115
|
+
'eye-off': EyeOff,
|
|
116
|
+
bell: Bell,
|
|
117
|
+
square: Square,
|
|
118
|
+
minus: Minus,
|
|
119
|
+
play: Play,
|
|
71
120
|
};
|
|
72
121
|
|
|
122
|
+
/** Every curated icon name, sorted. Derived from `NAMED_ICONS` so a gallery,
|
|
123
|
+
* a story control or a test can never drift from the map itself. */
|
|
124
|
+
export const ICON_NAMES: readonly string[] = Object.keys(NAMED_ICONS).sort();
|
|
125
|
+
|
|
73
126
|
/** Render an item icon.
|
|
74
127
|
*
|
|
75
128
|
* Resolution order:
|
package/src/ui/input.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { type JSX, Show, splitProps, createUniqueId } from 'solid-js';
|
|
1
|
+
import { type JSX, Show, splitProps, createUniqueId, createEffect, onCleanup, untrack } from 'solid-js';
|
|
2
2
|
import { cn } from '../utils/cn';
|
|
3
|
+
import type { CaseMode } from '../primitives/field-mask';
|
|
4
|
+
import { fieldSemantics, type FieldSemanticType } from '../primitives/field-semantics';
|
|
5
|
+
import {
|
|
6
|
+
createInputMask,
|
|
7
|
+
type CopyPolicy,
|
|
8
|
+
type InputMask,
|
|
9
|
+
type InputMaskRejectReason,
|
|
10
|
+
} from '../primitives/input-mask';
|
|
3
11
|
|
|
4
12
|
export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onInput' | 'onChange' | 'size'> {
|
|
5
13
|
/** Field label rendered above the control and linked via `for`/`id`. */
|
|
@@ -16,12 +24,58 @@ export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElemen
|
|
|
16
24
|
leading?: JSX.Element;
|
|
17
25
|
/** Trailing affix (icon, inline button). Rendered inside the field row, after the input. */
|
|
18
26
|
trailing?: JSX.Element;
|
|
19
|
-
/** Fires per keystroke with the current value
|
|
27
|
+
/** Fires per keystroke with the current value — the CANONICAL value when a mask is
|
|
28
|
+
* active (digits for `tel`/`ssn`/`credit-card`, the formatted text for
|
|
29
|
+
* `custom`), and the raw text of the field otherwise. */
|
|
20
30
|
onValueInput?: (value: string) => void;
|
|
21
|
-
/** Fires on commit (blur) with the current value. */
|
|
31
|
+
/** Fires on commit (blur) with the current value; canonical when a mask is active. */
|
|
22
32
|
onValueChange?: (value: string) => void;
|
|
33
|
+
|
|
34
|
+
// --- Form-field formats (spec §7.2). All five are SCALARS, which is what lets them
|
|
35
|
+
// survive as HTML attributes on the `<kai-input>` facade. Absent `format` AND absent
|
|
36
|
+
// `semantic` is the behavior of today, byte for byte (owner decision 1 / spec §1.1): no mask,
|
|
37
|
+
// no extra attributes, nothing.
|
|
38
|
+
|
|
39
|
+
/** Tier-2 mask pattern: `#` a digit, `@` an alphanumeric, `*` an obscurable
|
|
40
|
+
* alphanumeric, everything else a positional literal (`@@@-####`).
|
|
41
|
+
*
|
|
42
|
+
* The literal `default` is the OPT-IN sentinel: it resolves to the default format of
|
|
43
|
+
* `semantic` (`tel` → `###-###-####`). It cannot collide with a real pattern — `default`
|
|
44
|
+
* as a format is eight literals with no fill position, i.e. a field that can hold
|
|
45
|
+
* nothing — and an opt-in token is needed because decision 1 forbids a bare semantic
|
|
46
|
+
* type from starting to mask on its own. */
|
|
47
|
+
format?: string;
|
|
48
|
+
/** The placeholder guide shown at unfilled positions, aligned position-for-position
|
|
49
|
+
* with `format` (` - - `, `mm/dd/yyyy`). Without it the field shows only up
|
|
50
|
+
* to the last typed character. */
|
|
51
|
+
guide?: string;
|
|
52
|
+
/** Tier-1 semantic type. On its own it applies `inputmode` / `autocomplete` /
|
|
53
|
+
* `spellcheck` / `autocorrect` / `autocapitalize` and decides the canonical value —
|
|
54
|
+
* it never starts masking by itself. */
|
|
55
|
+
semantic?: FieldSemanticType;
|
|
56
|
+
/** Case folding applied to typed and pasted text. Defaults to `preserve`. */
|
|
57
|
+
caseMode?: CaseMode;
|
|
58
|
+
/** What a copy or cut of a masked field puts on the clipboard. Defaults to `canonical`. */
|
|
59
|
+
copyPolicy?: CopyPolicy;
|
|
60
|
+
/** A mask refused, or partly refused, some content — `full`, `wrong-class`,
|
|
61
|
+
* `over-capacity`, or `format-change-clipped`. Not a scalar and therefore not an
|
|
62
|
+
* attribute: the facade projects this onto its `kai-input-rejected` event.
|
|
63
|
+
*
|
|
64
|
+
* NOT an error state. `format-change-clipped` in particular fires on a routine
|
|
65
|
+
* reactive `format` change, so this widget deliberately does not touch `invalid` —
|
|
66
|
+
* validity belongs to the consumer, and stays there. */
|
|
67
|
+
onMaskReject?: (detail: { reason: InputMaskRejectReason; data: string }) => void;
|
|
23
68
|
}
|
|
24
69
|
|
|
70
|
+
/** The `format` value that means: resolve the default format of the semantic type.
|
|
71
|
+
*
|
|
72
|
+
* EXPORTED because `src/elements/input.tsx` has to recognise the same sentinel when it
|
|
73
|
+
* resolves a seeded value ahead of the masker, and a restated literal there would fail
|
|
74
|
+
* SILENTLY if this string ever changed: `format === 'default'` would simply go false and
|
|
75
|
+
* `compileMask('default')` would compile seven literals, garbling the seed with no error
|
|
76
|
+
* anywhere. One exported constant instead of two copies of a magic string. */
|
|
77
|
+
export const DEFAULT_FORMAT = 'default';
|
|
78
|
+
|
|
25
79
|
// The single source of the field shell styling — lifted verbatim from the
|
|
26
80
|
// `inputBase` constant that used to live in `src/components/form-widgets.tsx`.
|
|
27
81
|
// `Input` now owns it; the form widgets render `Input` rather than re-pasting it.
|
|
@@ -37,7 +91,12 @@ const ROW_INPUT =
|
|
|
37
91
|
'min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed';
|
|
38
92
|
|
|
39
93
|
const SIZE_SM = 'px-2.5 py-1';
|
|
40
|
-
|
|
94
|
+
// The invalid-state border, EXPORTED because `src/ui/select.tsx` renders the same
|
|
95
|
+
// field box and a second hand-typed copy of this string is exactly the kind of
|
|
96
|
+
// restatement that rots (`docs/coupling-map.md` §4). `INVALID` stays as the local
|
|
97
|
+
// alias so the three call sites below read unchanged.
|
|
98
|
+
export const FIELD_INVALID = 'border-destructive dark:border-red-400/70';
|
|
99
|
+
const INVALID = FIELD_INVALID;
|
|
41
100
|
|
|
42
101
|
// Suppress the native search affordances Chrome/WebKit render for `type="search"`.
|
|
43
102
|
// Without this the browser's `::-webkit-search-cancel-button` (×) stacks on top of
|
|
@@ -61,6 +120,7 @@ export function Input(props: InputProps): JSX.Element {
|
|
|
61
120
|
const [local, rest] = splitProps(props, [
|
|
62
121
|
'class', 'label', 'hint', 'error', 'size', 'invalid',
|
|
63
122
|
'leading', 'trailing', 'onValueInput', 'onValueChange', 'onBlur', 'id', 'disabled',
|
|
123
|
+
'format', 'guide', 'semantic', 'caseMode', 'copyPolicy', 'onMaskReject',
|
|
64
124
|
]);
|
|
65
125
|
|
|
66
126
|
const id = local.id ?? createUniqueId();
|
|
@@ -69,6 +129,138 @@ export function Input(props: InputProps): JSX.Element {
|
|
|
69
129
|
const hasHint = () => !!local.error || !!local.hint;
|
|
70
130
|
const hasAffix = () => local.leading != null || local.trailing != null;
|
|
71
131
|
|
|
132
|
+
// -------------------------------------------------------------------------------
|
|
133
|
+
// Form-field formats (spec §7.2). Tier 1 is a handful of attributes; tier 2 is one
|
|
134
|
+
// `createInputMask` bound to whichever `<input>` node is currently mounted.
|
|
135
|
+
// -------------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
const semantics = () => (local.semantic === undefined ? undefined : fieldSemantics(local.semantic));
|
|
138
|
+
|
|
139
|
+
// Both warn paths below run from a TRACKED accessor or from a reconfigure, i.e. from a
|
|
140
|
+
// routine reactive render, and both interpolate a value that reaches this widget from
|
|
141
|
+
// the form card and therefore from MODEL output. So: report each distinct bad value
|
|
142
|
+
// ONCE per widget, and clip what gets interpolated. The console is not a sink — this is
|
|
143
|
+
// about a 500-character pattern printing 500 characters on every keystroke, not about
|
|
144
|
+
// injection.
|
|
145
|
+
const warned = new Set<string>();
|
|
146
|
+
const warnOnce = (key: string, message: string): void => {
|
|
147
|
+
if (warned.has(key)) return;
|
|
148
|
+
warned.add(key);
|
|
149
|
+
console.warn(message);
|
|
150
|
+
};
|
|
151
|
+
const clip = (text: string): string =>
|
|
152
|
+
text.length <= 32 ? text : `${text.slice(0, 32)}… (${text.length} chars)`;
|
|
153
|
+
|
|
154
|
+
/** The pattern to mask with, or `undefined` for no mask at all. */
|
|
155
|
+
const maskFormat = (): string | undefined => {
|
|
156
|
+
const format = local.format;
|
|
157
|
+
if (format === undefined || format === '') return undefined;
|
|
158
|
+
if (format !== DEFAULT_FORMAT) return format;
|
|
159
|
+
const resolved = semantics()?.defaultFormat;
|
|
160
|
+
if (resolved === undefined) {
|
|
161
|
+
// Loud, never a silent no-mask: the consumer asked for a mask and is not getting
|
|
162
|
+
// one. `custom` has no default by design (its `format` IS the pattern), and a
|
|
163
|
+
// semantic type that never arrived is the more likely mistake.
|
|
164
|
+
warnOnce(
|
|
165
|
+
`default:${String(local.semantic)}`,
|
|
166
|
+
`Input: format=${DEFAULT_FORMAT} resolves the default format of a semantic type, and ` +
|
|
167
|
+
`semantic=${JSON.stringify(local.semantic)} has none. Rendering unmasked.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return resolved;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let plainEl: HTMLInputElement | undefined;
|
|
174
|
+
let rowEl: HTMLInputElement | undefined;
|
|
175
|
+
let mask: InputMask | undefined;
|
|
176
|
+
/** The node `mask` is bound to. The affix toggle is the one thing that changes it. */
|
|
177
|
+
let maskedEl: HTMLInputElement | undefined;
|
|
178
|
+
|
|
179
|
+
const detachMask = (): void => {
|
|
180
|
+
mask?.detach();
|
|
181
|
+
mask = undefined;
|
|
182
|
+
maskedEl = undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const warnBadFormat = (format: string, err: unknown, kept: boolean): void => {
|
|
186
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
187
|
+
warnOnce(
|
|
188
|
+
`format:${format}`,
|
|
189
|
+
`Input: format ${JSON.stringify(clip(format))} did not compile (${reason}); ` +
|
|
190
|
+
(kept ? 'the field keeps its previous format.' : 'rendering unmasked.'),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
createEffect(() => {
|
|
195
|
+
// Everything reactive is read HERE, so the effect re-runs on a config change or an
|
|
196
|
+
// affix toggle and on nothing else.
|
|
197
|
+
const format = maskFormat();
|
|
198
|
+
const config = {
|
|
199
|
+
guide: local.guide,
|
|
200
|
+
semantic: local.semantic,
|
|
201
|
+
caseMode: local.caseMode,
|
|
202
|
+
copyPolicy: local.copyPolicy,
|
|
203
|
+
};
|
|
204
|
+
// WHICH NODE IS MOUNTED. `Input` caches two `<input>`s and `<Show>` swaps between
|
|
205
|
+
// them when a leading/trailing affix appears or disappears — the one legitimate node
|
|
206
|
+
// change in this file (spec §8.1). The `ref` of a node fires only when it is BUILT, and
|
|
207
|
+
// the cached node is built once, so a toggle back to an existing node notifies
|
|
208
|
+
// nobody. Re-attachment therefore has to be explicit, and this read is what makes it
|
|
209
|
+
// happen. Everything else about this widget is pinned to keeping the SAME node alive
|
|
210
|
+
// (`tests/ui/input-node-identity.test.tsx`), which is the only reason a long-lived
|
|
211
|
+
// masker on it is safe at all.
|
|
212
|
+
//
|
|
213
|
+
// THE ASSUMPTION THIS RELIES ON, stated: `plainEl`/`rowEl` are plain mutable refs, so
|
|
214
|
+
// this reads a node only because Solid runs the `<Show>` render effect that BUILDS it
|
|
215
|
+
// (and its `ref`) before this user effect. If that ever stopped holding, `el` would be
|
|
216
|
+
// `undefined` on the toggle and the branch below would detach with nothing queued to
|
|
217
|
+
// re-attach — silently. The affix tests in the node-identity file are what would catch it.
|
|
218
|
+
const el = hasAffix() ? rowEl : plainEl;
|
|
219
|
+
|
|
220
|
+
untrack(() => {
|
|
221
|
+
if (format === undefined || el === undefined) {
|
|
222
|
+
detachMask();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (mask !== undefined && maskedEl === el) {
|
|
227
|
+
// Same node, new configuration: RE-CONFIGURE. Re-creating would drop the undo
|
|
228
|
+
// stack and re-seed from the DOM text. `update()` is atomic — a format that
|
|
229
|
+
// fails to compile leaves the masker exactly as it was — so a half-typed
|
|
230
|
+
// reactive pattern cannot corrupt the field.
|
|
231
|
+
try {
|
|
232
|
+
mask.update({ format, ...config });
|
|
233
|
+
} catch (err) {
|
|
234
|
+
warnBadFormat(format, err, true);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// First attach, or the affix toggle. Carry the value across: the incoming node is
|
|
240
|
+
// a different element and knows nothing about what the user typed into the old one.
|
|
241
|
+
const carried = mask?.getFormattedValue() ?? el.value;
|
|
242
|
+
detachMask();
|
|
243
|
+
try {
|
|
244
|
+
mask = createInputMask(el, {
|
|
245
|
+
...config,
|
|
246
|
+
format,
|
|
247
|
+
initialValue: carried,
|
|
248
|
+
// The callback of the masker, not the DOM `input` event, is the emission point: a
|
|
249
|
+
// canceled `beforeinput` means a real browser never fires `input` at all for a
|
|
250
|
+
// typed character.
|
|
251
|
+
onInput: ({ canonical }) => local.onValueInput?.(canonical),
|
|
252
|
+
onReject: (detail) => local.onMaskReject?.(detail),
|
|
253
|
+
});
|
|
254
|
+
maskedEl = el;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// A bad pattern falls back to a plain text field, loudly (spec §7.3).
|
|
257
|
+
warnBadFormat(format, err, false);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
onCleanup(detachMask);
|
|
263
|
+
|
|
72
264
|
// The class arrives as an ACCESSOR, not a string, and this is load-bearing.
|
|
73
265
|
//
|
|
74
266
|
// Solid evaluates a `<Show>` `fallback` inside the Show's own memo. When the
|
|
@@ -83,20 +275,57 @@ export function Input(props: InputProps): JSX.Element {
|
|
|
83
275
|
// Solid compiles it into a nested effect that sets the attribute on the
|
|
84
276
|
// EXISTING node. Same reason the affix branch was always fine: it inserts the
|
|
85
277
|
// input through a function. Pinned by `tests/ui/input-node-identity.test.tsx`.
|
|
86
|
-
|
|
278
|
+
// Tier-1 attributes (spec §2). Each is a DEFAULT the semantic type supplies: an
|
|
279
|
+
// explicit prop always wins, because the consumer knows something the enum does not.
|
|
280
|
+
// With no `semantic` every one of these is `undefined` — the attribute is simply not
|
|
281
|
+
// set, which is what byte-for-byte parity with the behavior of today means here.
|
|
282
|
+
const inputmodeAttr = () => rest.inputmode ?? semantics()?.inputmode;
|
|
283
|
+
const autocompleteAttr = () =>
|
|
284
|
+
(rest.autocomplete ?? semantics()?.autocomplete) as JSX.InputHTMLAttributes<HTMLInputElement>['autocomplete'];
|
|
285
|
+
const spellcheckAttr = () => {
|
|
286
|
+
const explicit = rest.spellcheck;
|
|
287
|
+
// Normalized to the attribute spelling so both forms land as an attribute rather
|
|
288
|
+
// than one of them as a property: `spellcheck={false}` is a real value, not an
|
|
289
|
+
// absent one, and it must stay visible in the DOM.
|
|
290
|
+
if (explicit !== undefined) return explicit === true || explicit === 'true' ? 'true' : 'false';
|
|
291
|
+
const semantic = semantics();
|
|
292
|
+
if (semantic === undefined) return undefined;
|
|
293
|
+
return semantic.spellcheck ? 'true' : 'false';
|
|
294
|
+
};
|
|
295
|
+
const autocorrectAttr = () => rest.autocorrect ?? semantics()?.autocorrect;
|
|
296
|
+
const autocapitalizeAttr = () => rest.autocapitalize ?? semantics()?.autocapitalize;
|
|
297
|
+
|
|
298
|
+
const inputEl = (
|
|
299
|
+
cls: () => string,
|
|
300
|
+
part: string,
|
|
301
|
+
register: (el: HTMLInputElement) => void,
|
|
302
|
+
): JSX.Element => (
|
|
87
303
|
<input
|
|
88
304
|
{...rest}
|
|
305
|
+
ref={register}
|
|
89
306
|
id={id}
|
|
90
307
|
part={part}
|
|
91
308
|
disabled={local.disabled}
|
|
92
309
|
aria-invalid={isInvalid() ? 'true' : undefined}
|
|
93
310
|
aria-describedby={hasHint() ? hintId : undefined}
|
|
311
|
+
inputmode={inputmodeAttr()}
|
|
312
|
+
autocomplete={autocompleteAttr()}
|
|
313
|
+
spellcheck={spellcheckAttr()}
|
|
314
|
+
autocorrect={autocorrectAttr()}
|
|
315
|
+
autocapitalize={autocapitalizeAttr()}
|
|
94
316
|
class={cn(SEARCH_RESET, cls())}
|
|
95
|
-
|
|
317
|
+
// When a mask is active the masker owns the emission (see its `onInput` above) and
|
|
318
|
+
// this handler stands down — otherwise a jsdom-style `input` event, which reaches
|
|
319
|
+
// both, would emit twice, and the raw text it reads is the FORMATTED value rather
|
|
320
|
+
// than the canonical one.
|
|
321
|
+
onInput={(e) => {
|
|
322
|
+
if (mask !== undefined) return;
|
|
323
|
+
local.onValueInput?.(e.currentTarget.value);
|
|
324
|
+
}}
|
|
96
325
|
onBlur={(e) => {
|
|
97
326
|
const handler = local.onBlur;
|
|
98
327
|
if (typeof handler === 'function') handler(e);
|
|
99
|
-
local.onValueChange?.(e.currentTarget.value);
|
|
328
|
+
local.onValueChange?.(mask?.getCanonicalValue() ?? e.currentTarget.value);
|
|
100
329
|
}}
|
|
101
330
|
/>
|
|
102
331
|
);
|
|
@@ -107,10 +336,21 @@ export function Input(props: InputProps): JSX.Element {
|
|
|
107
336
|
// Created on first use and then REUSED, so toggling an affix on or off does
|
|
108
337
|
// not discard a focused input either. The lazy cache also keeps the unused
|
|
109
338
|
// branch's node (and its effects) from being built at all.
|
|
339
|
+
//
|
|
340
|
+
// KEEP PROSE IN THIS FILE FREE OF STRAY QUOTE CHARACTERS. The part-name guard in
|
|
341
|
+
// `src/elements/slots.test.ts` scans this source with a naive quote regex, so one
|
|
342
|
+
// apostrophe in a comment shifts its parity and swallows the part literals below —
|
|
343
|
+
// that guard then reddens over a comment, naming a part that is still right there.
|
|
344
|
+
//
|
|
345
|
+
// Each branch registers its node so the mask effect above can find whichever one is
|
|
346
|
+
// mounted; `ref` fires once per node, at construction, which is exactly when a node
|
|
347
|
+
// first becomes findable.
|
|
110
348
|
let plainNode: JSX.Element;
|
|
111
|
-
const plainInput = () =>
|
|
349
|
+
const plainInput = () =>
|
|
350
|
+
(plainNode ??= inputEl(fieldClass, 'field input', (el) => (plainEl = el)));
|
|
112
351
|
let rowNode: JSX.Element;
|
|
113
|
-
const rowInput = () =>
|
|
352
|
+
const rowInput = () =>
|
|
353
|
+
(rowNode ??= inputEl(() => ROW_INPUT, 'input', (el) => (rowEl = el)));
|
|
114
354
|
|
|
115
355
|
return (
|
|
116
356
|
<div class="flex w-full flex-col gap-1.5">
|
package/src/ui/kbd.tsx
CHANGED
package/src/ui/nav.tsx
CHANGED
|
@@ -221,7 +221,7 @@ function NavRows(props: NavRowsProps) {
|
|
|
221
221
|
</span>
|
|
222
222
|
</Show>
|
|
223
223
|
<Show when={item.badge}>
|
|
224
|
-
<Badge variant="default" class="px-1.5 py-0 text-
|
|
224
|
+
<Badge variant="default" class="px-1.5 py-0 text-micro font-medium uppercase tracking-wide">{item.badge}</Badge>
|
|
225
225
|
</Show>
|
|
226
226
|
<Show when={item.trailing}>{renderIcon(item.trailing, { class: 'size-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-60' })}</Show>
|
|
227
227
|
</button>
|
package/src/ui/pane-group.tsx
CHANGED
|
@@ -201,7 +201,7 @@ export function PaneGroup(props: PaneGroupProps) {
|
|
|
201
201
|
</Show>
|
|
202
202
|
<span
|
|
203
203
|
class={cn(
|
|
204
|
-
'relative flex size-[18px] items-center justify-center rounded text-
|
|
204
|
+
'relative flex size-[18px] items-center justify-center rounded text-caption font-bold leading-none tabular-nums',
|
|
205
205
|
TONE_BG[tone()],
|
|
206
206
|
TONE_BADGE_FG[tone()],
|
|
207
207
|
)}
|
|
@@ -215,7 +215,7 @@ export function PaneGroup(props: PaneGroupProps) {
|
|
|
215
215
|
<Show when={tab().status?.label}>
|
|
216
216
|
<span
|
|
217
217
|
class={cn(
|
|
218
|
-
'truncate text-
|
|
218
|
+
'truncate text-micro font-medium',
|
|
219
219
|
TONE_TEXT[tone()],
|
|
220
220
|
alwaysWord() ? 'inline' : 'hidden group-hover/tab:inline',
|
|
221
221
|
)}
|