@kitn.ai/ui 0.28.0 → 0.29.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 +1 -1
- package/dist/agent-tooling/construct/public.d.ts +15 -0
- package/dist/agent-tooling/construct/schema.d.ts +111 -0
- package/dist/construct-cli.es.js +3 -3
- package/dist/construct.d.ts +5 -0
- package/dist/construct.js +327 -0
- package/dist/core-C8fzo39E.js +1 -12
- package/dist/engine-javascript-C1x7zo1_.js +1 -141
- package/dist/mcp.es.js +1 -1
- package/package.json +16 -16
- package/bin/route.test.js +0 -58
- 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/styles.css +0 -328
- 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
package/src/ui/input.tsx
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { type JSX, Show, splitProps, createUniqueId, createEffect, onCleanup, untrack } from 'solid-js';
|
|
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';
|
|
11
|
-
|
|
12
|
-
export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onInput' | 'onChange' | 'size'> {
|
|
13
|
-
/** Field label rendered above the control and linked via `for`/`id`. */
|
|
14
|
-
label?: string;
|
|
15
|
-
/** Helper text rendered below the control. */
|
|
16
|
-
hint?: string;
|
|
17
|
-
/** Error text; rendered below the control and flips the field invalid. */
|
|
18
|
-
error?: string;
|
|
19
|
-
/** Control density. Defaults to `md`. */
|
|
20
|
-
size?: 'sm' | 'md';
|
|
21
|
-
/** Force the invalid (destructive-border) state without an `error` string. */
|
|
22
|
-
invalid?: boolean;
|
|
23
|
-
/** Leading affix (icon, unit). Rendered inside the field row, before the input. */
|
|
24
|
-
leading?: JSX.Element;
|
|
25
|
-
/** Trailing affix (icon, inline button). Rendered inside the field row, after the input. */
|
|
26
|
-
trailing?: JSX.Element;
|
|
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. */
|
|
30
|
-
onValueInput?: (value: string) => void;
|
|
31
|
-
/** Fires on commit (blur) with the current value; canonical when a mask is active. */
|
|
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;
|
|
68
|
-
}
|
|
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
|
-
|
|
79
|
-
// The single source of the field shell styling — lifted verbatim from the
|
|
80
|
-
// `inputBase` constant that used to live in `src/components/form-widgets.tsx`.
|
|
81
|
-
// `Input` now owns it; the form widgets render `Input` rather than re-pasting it.
|
|
82
|
-
export const FIELD_BASE =
|
|
83
|
-
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none';
|
|
84
|
-
|
|
85
|
-
// When a leading/trailing affix is present the border + padding wrap the whole
|
|
86
|
-
// row and the focus ring is driven off the row (`focus-within`), so the input
|
|
87
|
-
// itself sits borderless and transparent inside it.
|
|
88
|
-
const FIELD_ROW =
|
|
89
|
-
'flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-ring';
|
|
90
|
-
const ROW_INPUT =
|
|
91
|
-
'min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed';
|
|
92
|
-
|
|
93
|
-
const SIZE_SM = 'px-2.5 py-1';
|
|
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;
|
|
100
|
-
|
|
101
|
-
// Suppress the native search affordances Chrome/WebKit render for `type="search"`.
|
|
102
|
-
// Without this the browser's `::-webkit-search-cancel-button` (×) stacks on top of
|
|
103
|
-
// a custom clear control (e.g. kai-search's `part="clear"`) — a double ×. Applied
|
|
104
|
-
// to the inner `<input>` in BOTH layouts (the field can be `type="search"` either
|
|
105
|
-
// way; kai-search uses the affix layout for its leading icon).
|
|
106
|
-
const SEARCH_RESET =
|
|
107
|
-
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-cancel-button]:hidden';
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* `Input`: the token-themed single-line text field shell. A label/hint/error
|
|
111
|
-
* stack around a field row that holds an optional `leading` affix, the
|
|
112
|
-
* `<input>`, and an optional `trailing` affix. The shared border/background/ring
|
|
113
|
-
* styling lives here (the single field source the form widgets build on).
|
|
114
|
-
*
|
|
115
|
-
* Parts: `field` (the bordered control), `input`, `label`, `hint`.
|
|
116
|
-
* a11y: a generated id links `<label for>`; `invalid`/`error` set `aria-invalid`;
|
|
117
|
-
* the hint/error text is linked via `aria-describedby`.
|
|
118
|
-
*/
|
|
119
|
-
export function Input(props: InputProps): JSX.Element {
|
|
120
|
-
const [local, rest] = splitProps(props, [
|
|
121
|
-
'class', 'label', 'hint', 'error', 'size', 'invalid',
|
|
122
|
-
'leading', 'trailing', 'onValueInput', 'onValueChange', 'onBlur', 'id', 'disabled',
|
|
123
|
-
'format', 'guide', 'semantic', 'caseMode', 'copyPolicy', 'onMaskReject',
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
const id = local.id ?? createUniqueId();
|
|
127
|
-
const hintId = `${id}-hint`;
|
|
128
|
-
const isInvalid = () => !!local.invalid || !!local.error;
|
|
129
|
-
const hasHint = () => !!local.error || !!local.hint;
|
|
130
|
-
const hasAffix = () => local.leading != null || local.trailing != null;
|
|
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
|
-
|
|
264
|
-
// The class arrives as an ACCESSOR, not a string, and this is load-bearing.
|
|
265
|
-
//
|
|
266
|
-
// Solid evaluates a `<Show>` `fallback` inside the Show's own memo. When the
|
|
267
|
-
// class was computed at the call site — `inputEl(cn(FIELD_BASE, …,
|
|
268
|
-
// isInvalid() && INVALID, local.class), …)` — those reads happened in that
|
|
269
|
-
// memo, so any of them changing re-ran the memo and BUILT A NEW `<input>`
|
|
270
|
-
// NODE: focus, caret and IME composition state all died with the old one. A
|
|
271
|
-
// consumer whose `invalid` is derived from the value (`kai-form` did exactly
|
|
272
|
-
// that) lost focus after every character.
|
|
273
|
-
//
|
|
274
|
-
// Passing a function moves every reactive read inside the element, where
|
|
275
|
-
// Solid compiles it into a nested effect that sets the attribute on the
|
|
276
|
-
// EXISTING node. Same reason the affix branch was always fine: it inserts the
|
|
277
|
-
// input through a function. Pinned by `tests/ui/input-node-identity.test.tsx`.
|
|
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 => (
|
|
303
|
-
<input
|
|
304
|
-
{...rest}
|
|
305
|
-
ref={register}
|
|
306
|
-
id={id}
|
|
307
|
-
part={part}
|
|
308
|
-
disabled={local.disabled}
|
|
309
|
-
aria-invalid={isInvalid() ? 'true' : undefined}
|
|
310
|
-
aria-describedby={hasHint() ? hintId : undefined}
|
|
311
|
-
inputmode={inputmodeAttr()}
|
|
312
|
-
autocomplete={autocompleteAttr()}
|
|
313
|
-
spellcheck={spellcheckAttr()}
|
|
314
|
-
autocorrect={autocorrectAttr()}
|
|
315
|
-
autocapitalize={autocapitalizeAttr()}
|
|
316
|
-
class={cn(SEARCH_RESET, cls())}
|
|
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
|
-
}}
|
|
325
|
-
onBlur={(e) => {
|
|
326
|
-
const handler = local.onBlur;
|
|
327
|
-
if (typeof handler === 'function') handler(e);
|
|
328
|
-
local.onValueChange?.(mask?.getCanonicalValue() ?? e.currentTarget.value);
|
|
329
|
-
}}
|
|
330
|
-
/>
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
const fieldClass = () =>
|
|
334
|
-
cn(FIELD_BASE, local.size === 'sm' && SIZE_SM, isInvalid() && INVALID, local.class);
|
|
335
|
-
|
|
336
|
-
// Created on first use and then REUSED, so toggling an affix on or off does
|
|
337
|
-
// not discard a focused input either. The lazy cache also keeps the unused
|
|
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.
|
|
348
|
-
let plainNode: JSX.Element;
|
|
349
|
-
const plainInput = () =>
|
|
350
|
-
(plainNode ??= inputEl(fieldClass, 'field input', (el) => (plainEl = el)));
|
|
351
|
-
let rowNode: JSX.Element;
|
|
352
|
-
const rowInput = () =>
|
|
353
|
-
(rowNode ??= inputEl(() => ROW_INPUT, 'input', (el) => (rowEl = el)));
|
|
354
|
-
|
|
355
|
-
return (
|
|
356
|
-
<div class="flex w-full flex-col gap-1.5">
|
|
357
|
-
<Show when={local.label}>
|
|
358
|
-
<label part="label" for={id} class="text-sm font-medium text-foreground">
|
|
359
|
-
{local.label}
|
|
360
|
-
</label>
|
|
361
|
-
</Show>
|
|
362
|
-
|
|
363
|
-
<Show when={hasAffix()} fallback={plainInput()}>
|
|
364
|
-
<div
|
|
365
|
-
part="field"
|
|
366
|
-
class={cn(
|
|
367
|
-
FIELD_ROW,
|
|
368
|
-
local.size === 'sm' && SIZE_SM,
|
|
369
|
-
isInvalid() && INVALID,
|
|
370
|
-
local.disabled && 'opacity-50 pointer-events-none',
|
|
371
|
-
local.class,
|
|
372
|
-
)}
|
|
373
|
-
>
|
|
374
|
-
<Show when={local.leading}>
|
|
375
|
-
<span class="flex shrink-0 items-center text-muted-foreground">{local.leading}</span>
|
|
376
|
-
</Show>
|
|
377
|
-
{rowInput()}
|
|
378
|
-
<Show when={local.trailing}>
|
|
379
|
-
<span class="flex shrink-0 items-center text-muted-foreground">{local.trailing}</span>
|
|
380
|
-
</Show>
|
|
381
|
-
</div>
|
|
382
|
-
</Show>
|
|
383
|
-
|
|
384
|
-
<Show when={hasHint()}>
|
|
385
|
-
<p part="hint" id={hintId} class={cn('text-xs', local.error ? 'text-destructive' : 'text-muted-foreground')}>
|
|
386
|
-
{local.error ?? local.hint}
|
|
387
|
-
</p>
|
|
388
|
-
</Show>
|
|
389
|
-
</div>
|
|
390
|
-
);
|
|
391
|
-
}
|
package/src/ui/kbd.tsx
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { type JSX, For, Show, splitProps } from 'solid-js';
|
|
2
|
-
import { cn } from '../utils/cn';
|
|
3
|
-
|
|
4
|
-
export type KbdPlatform = 'auto' | 'mac' | 'other';
|
|
5
|
-
|
|
6
|
-
export interface KbdProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
7
|
-
/** Shortcut spec — tokens joined by `+` (e.g. `Mod+Shift+K`). When omitted,
|
|
8
|
-
* children render verbatim. Display only; `kai-kbd` does not bind keys. */
|
|
9
|
-
keys?: string;
|
|
10
|
-
/** Glyph platform. `mac` uses ⌘/⌥; `other` uses Ctrl. `auto` (default for the
|
|
11
|
-
* facade) sniffs the OS, SSR-safe (falls back to `other` on the server). */
|
|
12
|
-
platform?: KbdPlatform;
|
|
13
|
-
/** Cap size. Defaults to `md`. */
|
|
14
|
-
size?: 'sm' | 'md';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** SSR-safe mac check: prefers UA-CH `platform`, falls back to `navigator.platform`. */
|
|
18
|
-
function isMacOS(): boolean {
|
|
19
|
-
if (typeof navigator === 'undefined') return false;
|
|
20
|
-
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
21
|
-
const platform = nav.userAgentData?.platform ?? nav.platform ?? '';
|
|
22
|
-
return /mac/i.test(platform);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Resolve `auto` to a concrete platform; `mac`/`other` pass through. */
|
|
26
|
-
function resolvePlatform(platform: KbdPlatform): 'mac' | 'other' {
|
|
27
|
-
if (platform === 'mac') return 'mac';
|
|
28
|
-
if (platform === 'other') return 'other';
|
|
29
|
-
return isMacOS() ? 'mac' : 'other';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Map a single token to its display glyph. Unknown tokens uppercase (so `k` → `K`). */
|
|
33
|
-
function glyph(token: string, mac: boolean): string {
|
|
34
|
-
const t = token.trim();
|
|
35
|
-
switch (t.toLowerCase()) {
|
|
36
|
-
case 'mod':
|
|
37
|
-
return mac ? '⌘' : 'Ctrl';
|
|
38
|
-
case 'cmd':
|
|
39
|
-
case 'command':
|
|
40
|
-
case 'meta':
|
|
41
|
-
return '⌘';
|
|
42
|
-
case 'ctrl':
|
|
43
|
-
case 'control':
|
|
44
|
-
return 'Ctrl';
|
|
45
|
-
case 'shift':
|
|
46
|
-
return '⇧';
|
|
47
|
-
case 'alt':
|
|
48
|
-
case 'opt':
|
|
49
|
-
case 'option':
|
|
50
|
-
return '⌥';
|
|
51
|
-
case 'arrowup':
|
|
52
|
-
return '↑';
|
|
53
|
-
case 'arrowdown':
|
|
54
|
-
return '↓';
|
|
55
|
-
case 'arrowleft':
|
|
56
|
-
return '←';
|
|
57
|
-
case 'arrowright':
|
|
58
|
-
return '→';
|
|
59
|
-
case 'enter':
|
|
60
|
-
case 'return':
|
|
61
|
-
return '⏎';
|
|
62
|
-
case 'esc':
|
|
63
|
-
case 'escape':
|
|
64
|
-
return 'Esc';
|
|
65
|
-
case 'space':
|
|
66
|
-
return 'Space';
|
|
67
|
-
default:
|
|
68
|
-
return t.toUpperCase();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const SIZE: Record<'sm' | 'md', string> = {
|
|
73
|
-
sm: 'h-5 min-w-5 px-1 text-caption',
|
|
74
|
-
md: 'h-6 min-w-6 px-1.5 text-xs',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `Kbd`: a keyboard-shortcut display. Splits `keys` on `+`, maps each token to a
|
|
79
|
-
* platform glyph (`Mod` → ⌘ on mac else Ctrl, `Shift` → ⇧, `ArrowUp` → ↑, …) and
|
|
80
|
-
* renders each as an inset `part="key"` cap, with an empty `part="separator"` hook
|
|
81
|
-
* between caps. Token-driven, light + dark. Display only — it does not bind keys.
|
|
82
|
-
*
|
|
83
|
-
* When `keys` is omitted, the children render verbatim (use any custom content).
|
|
84
|
-
*/
|
|
85
|
-
export function Kbd(props: KbdProps): JSX.Element {
|
|
86
|
-
const [local, rest] = splitProps(props, ['keys', 'platform', 'size', 'class', 'children']);
|
|
87
|
-
const mac = () => resolvePlatform(local.platform ?? 'other') === 'mac';
|
|
88
|
-
const tokens = () =>
|
|
89
|
-
(local.keys ?? '')
|
|
90
|
-
.split('+')
|
|
91
|
-
.map((t) => t.trim())
|
|
92
|
-
.filter(Boolean);
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<kbd {...rest} class={cn('inline-flex items-center gap-0.5 align-middle font-sans', local.class)}>
|
|
96
|
-
<Show when={local.keys != null} fallback={local.children}>
|
|
97
|
-
<For each={tokens()}>
|
|
98
|
-
{(token, i) => (
|
|
99
|
-
<>
|
|
100
|
-
<Show when={i() > 0}>
|
|
101
|
-
<span part="separator" aria-hidden="true" class="text-muted-foreground" />
|
|
102
|
-
</Show>
|
|
103
|
-
<span
|
|
104
|
-
part="key"
|
|
105
|
-
class={cn(
|
|
106
|
-
'inline-flex items-center justify-center rounded border border-border bg-muted font-medium leading-none tabular-nums text-muted-foreground',
|
|
107
|
-
SIZE[local.size ?? 'md'],
|
|
108
|
-
)}
|
|
109
|
-
>
|
|
110
|
-
{glyph(token, mac())}
|
|
111
|
-
</span>
|
|
112
|
-
</>
|
|
113
|
-
)}
|
|
114
|
-
</For>
|
|
115
|
-
</Show>
|
|
116
|
-
</kbd>
|
|
117
|
-
);
|
|
118
|
-
}
|