@kitn.ai/ui 0.14.1
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/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/bash-InADTalH.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -0
- package/dist/css-M7EaDHN_.js +1 -0
- package/dist/custom-elements.json +4739 -0
- package/dist/engine-javascript-vq0WuIJl.js +141 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -0
- package/dist/github-light-JYsPkUQd.js +1 -0
- package/dist/host-embed.d.ts +33 -0
- package/dist/html-CPZ3oZQ7.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/javascript-C25yR2R2.js +1 -0
- package/dist/json-DxJze_jm.js +1 -0
- package/dist/kitn-chat-provider.d.ts +2 -0
- package/dist/kitn-chat-provider.es.js +240 -0
- package/dist/kitn-chat.es.js +77 -0
- package/dist/llms/llms-full.txt +1018 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/origin.d.ts +5 -0
- package/dist/provider-runtime.d.ts +27 -0
- package/dist/schemas/card-envelope.schema.json +14 -0
- package/dist/schemas/card-event.schema.json +12 -0
- package/dist/schemas/choice.schema.json +70 -0
- package/dist/schemas/confirm.schema.json +65 -0
- package/dist/schemas/embed.schema.json +65 -0
- package/dist/schemas/form.result.schema.json +7 -0
- package/dist/schemas/form.schema.json +33 -0
- package/dist/schemas/link.schema.json +56 -0
- package/dist/schemas/tasks.result.schema.json +16 -0
- package/dist/schemas/tasks.schema.json +78 -0
- package/dist/svelte--5p79yCD.js +1 -0
- package/dist/theme.tokens.css +176 -0
- package/dist/tsx-B8rCNbgL.js +1 -0
- package/dist/typescript-RycA9KXf.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/version.d.ts +4 -0
- package/dist/vue-BmIZj4XD.js +1 -0
- package/dist/wire.d.ts +53 -0
- package/frameworks/react/index.tsx +948 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +1018 -0
- package/llms.txt +104 -0
- package/package.json +141 -0
- package/src/components/artifact.stories.tsx +138 -0
- package/src/components/artifact.tsx +743 -0
- package/src/components/attachments.stories.tsx +303 -0
- package/src/components/attachments.test.tsx +64 -0
- package/src/components/attachments.tsx +399 -0
- package/src/components/card-fallback.tsx +28 -0
- package/src/components/card-renderer.tsx +52 -0
- package/src/components/card.tsx +110 -0
- package/src/components/chain-of-thought.stories.tsx +211 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +187 -0
- package/src/components/chat-container.tsx +82 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/chat-thread.test.tsx +58 -0
- package/src/components/chat-thread.tsx +249 -0
- package/src/components/checkpoint.stories.tsx +102 -0
- package/src/components/checkpoint.tsx +88 -0
- package/src/components/choice-card.tsx +569 -0
- package/src/components/code-block.stories.tsx +150 -0
- package/src/components/code-block.tsx +105 -0
- package/src/components/component-meta.json +3735 -0
- package/src/components/confirm-card.stories.tsx +74 -0
- package/src/components/confirm-card.tsx +315 -0
- package/src/components/context.stories.tsx +229 -0
- package/src/components/context.test.tsx +133 -0
- package/src/components/context.tsx +363 -0
- package/src/components/conversation-item.stories.tsx +125 -0
- package/src/components/conversation-item.tsx +16 -0
- package/src/components/conversation-list.stories.tsx +133 -0
- package/src/components/conversation-list.tsx +101 -0
- package/src/components/default-input.test.tsx +111 -0
- package/src/components/embed.tsx +196 -0
- package/src/components/empty.stories.tsx +434 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +138 -0
- package/src/components/feedback-bar.test.tsx +103 -0
- package/src/components/feedback-bar.tsx +166 -0
- package/src/components/file-tree.stories.tsx +73 -0
- package/src/components/file-tree.tsx +383 -0
- package/src/components/file-upload.stories.tsx +156 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/form-widgets.tsx +461 -0
- package/src/components/form.tsx +873 -0
- package/src/components/image.stories.tsx +89 -0
- package/src/components/image.tsx +67 -0
- package/src/components/link-preview.tsx +194 -0
- package/src/components/loader.stories.tsx +181 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +180 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-action-bar.test.tsx +128 -0
- package/src/components/message-narrow.stories.tsx +329 -0
- package/src/components/message-skills.stories.tsx +211 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +281 -0
- package/src/components/message.tsx +227 -0
- package/src/components/model-switcher.stories.tsx +123 -0
- package/src/components/model-switcher.test.tsx +65 -0
- package/src/components/model-switcher.tsx +87 -0
- package/src/components/prompt-input.stories.tsx +222 -0
- package/src/components/prompt-input.tsx +211 -0
- package/src/components/prompt-suggestion.stories.tsx +142 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +140 -0
- package/src/components/reasoning.tsx +156 -0
- package/src/components/resizable.test.tsx +226 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +100 -0
- package/src/components/scroll-button.tsx +34 -0
- package/src/components/slash-command.stories.tsx +163 -0
- package/src/components/slash-command.tsx +232 -0
- package/src/components/source-list.test.tsx +152 -0
- package/src/components/source.stories.tsx +124 -0
- package/src/components/source.tsx +129 -0
- package/src/components/tasks-card.stories.tsx +78 -0
- package/src/components/tasks-card.tsx +432 -0
- package/src/components/text-shimmer.stories.tsx +87 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +87 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +153 -0
- package/src/components/tool.tsx +184 -0
- package/src/components/use-card-resolution.ts +28 -0
- package/src/components/voice-input.stories.tsx +83 -0
- package/src/components/voice-input.tsx +107 -0
- package/src/elements/artifact.stories.tsx +506 -0
- package/src/elements/artifact.tsx +137 -0
- package/src/elements/attachments.stories.tsx +190 -0
- package/src/elements/attachments.tsx +136 -0
- package/src/elements/card.stories.tsx +113 -0
- package/src/elements/card.tsx +40 -0
- package/src/elements/cards.stories.tsx +59 -0
- package/src/elements/cards.tsx +97 -0
- package/src/elements/catalog.stories.tsx +491 -0
- package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
- package/src/elements/chain-of-thought.stories.tsx +113 -0
- package/src/elements/chain-of-thought.tsx +92 -0
- package/src/elements/chat-scope-picker.stories.tsx +73 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-types.ts +42 -0
- package/src/elements/chat-workspace.stories.tsx +288 -0
- package/src/elements/chat-workspace.tsx +161 -0
- package/src/elements/chat.stories.tsx +151 -0
- package/src/elements/chat.tsx +80 -0
- package/src/elements/checkpoint.stories.tsx +72 -0
- package/src/elements/checkpoint.tsx +47 -0
- package/src/elements/choice.stories.tsx +240 -0
- package/src/elements/choice.tsx +48 -0
- package/src/elements/code-block.stories.tsx +83 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/composed-shell.stories.tsx +316 -0
- package/src/elements/confirm-card.stories.tsx +227 -0
- package/src/elements/confirm-card.tsx +51 -0
- package/src/elements/context-meter.stories.tsx +151 -0
- package/src/elements/context-meter.tsx +121 -0
- package/src/elements/conversation-list.declarative.test.tsx +137 -0
- package/src/elements/conversation-list.stories.tsx +243 -0
- package/src/elements/conversation-list.tsx +80 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +247 -0
- package/src/elements/define.tsx +171 -0
- package/src/elements/element-meta.json +3314 -0
- package/src/elements/element-types.d.ts +710 -0
- package/src/elements/embed.stories.tsx +213 -0
- package/src/elements/embed.tsx +35 -0
- package/src/elements/empty.stories.tsx +111 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.stories.tsx +114 -0
- package/src/elements/feedback-bar.tsx +61 -0
- package/src/elements/file-tree.stories.tsx +134 -0
- package/src/elements/file-tree.tsx +52 -0
- package/src/elements/file-upload.stories.tsx +82 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/form.stories.tsx +248 -0
- package/src/elements/form.tsx +43 -0
- package/src/elements/framework-usage.json +563 -0
- package/src/elements/image.stories.tsx +71 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/link-preview.stories.tsx +209 -0
- package/src/elements/link-preview.tsx +34 -0
- package/src/elements/loader.stories.tsx +88 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.stories.tsx +76 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.declarative.test.tsx +104 -0
- package/src/elements/message-skills.stories.tsx +114 -0
- package/src/elements/message-skills.tsx +85 -0
- package/src/elements/message.stories.tsx +218 -0
- package/src/elements/message.tsx +162 -0
- package/src/elements/model-switcher.declarative.test.tsx +130 -0
- package/src/elements/model-switcher.stories.tsx +198 -0
- package/src/elements/model-switcher.tsx +93 -0
- package/src/elements/popover.stories.tsx +80 -0
- package/src/elements/popover.tsx +59 -0
- package/src/elements/prompt-input-slash-command.test.tsx +128 -0
- package/src/elements/prompt-input-stoppable.test.tsx +88 -0
- package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
- package/src/elements/prompt-input.stories.tsx +331 -0
- package/src/elements/prompt-input.tsx +185 -0
- package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
- package/src/elements/prompt-suggestions.stories.tsx +213 -0
- package/src/elements/prompt-suggestions.tsx +94 -0
- package/src/elements/reasoning.stories.tsx +77 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +56 -0
- package/src/elements/remote.stories.tsx +314 -0
- package/src/elements/remote.tsx +221 -0
- package/src/elements/resizable.d.ts +27 -0
- package/src/elements/resizable.stories.tsx +426 -0
- package/src/elements/resizable.tsx +553 -0
- package/src/elements/response-stream.stories.tsx +79 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/scroll-button.stories.tsx +202 -0
- package/src/elements/scroll-button.test.tsx +148 -0
- package/src/elements/scroll-button.tsx +126 -0
- package/src/elements/source-list.stories.tsx +143 -0
- package/src/elements/source.stories.tsx +88 -0
- package/src/elements/source.tsx +120 -0
- package/src/elements/styles.css +199 -0
- package/src/elements/switch.stories.tsx +56 -0
- package/src/elements/switch.tsx +43 -0
- package/src/elements/tasks.stories.tsx +237 -0
- package/src/elements/tasks.tsx +46 -0
- package/src/elements/text-shimmer.stories.tsx +64 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.stories.tsx +72 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.stories.tsx +89 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.stories.tsx +88 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +253 -0
- package/src/primitives/card-contract.ts +71 -0
- package/src/primitives/card-host.tsx +35 -0
- package/src/primitives/card-registry.tsx +67 -0
- package/src/primitives/card-resolution.ts +36 -0
- package/src/primitives/card-routing.ts +79 -0
- package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
- package/src/primitives/card-schemas/card-event.schema.json +12 -0
- package/src/primitives/card-schemas/choice.schema.json +70 -0
- package/src/primitives/card-schemas/confirm.schema.json +65 -0
- package/src/primitives/card-schemas/embed.schema.json +65 -0
- package/src/primitives/card-schemas/form.result.schema.json +7 -0
- package/src/primitives/card-schemas/form.schema.json +33 -0
- package/src/primitives/card-schemas/link.schema.json +56 -0
- package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
- package/src/primitives/card-schemas/tasks.schema.json +78 -0
- package/src/primitives/card-validate.ts +95 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/controllable.test.ts +47 -0
- package/src/primitives/controllable.ts +21 -0
- package/src/primitives/embed-providers.ts +254 -0
- package/src/primitives/highlighter.ts +157 -0
- package/src/primitives/link-preview.ts +87 -0
- package/src/primitives/pdf-preview.ts +121 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-resize-observer.ts +12 -0
- package/src/primitives/use-stick-to-bottom.ts +43 -0
- package/src/primitives/use-text-stream.ts +112 -0
- package/src/primitives/use-voice-recorder.ts +50 -0
- package/src/remote/host-embed.ts +345 -0
- package/src/remote/index.ts +2 -0
- package/src/remote/origin.ts +30 -0
- package/src/remote/provider-runtime.ts +259 -0
- package/src/remote/provider.ts +2 -0
- package/src/remote/validate.ts +22 -0
- package/src/remote/version.ts +12 -0
- package/src/remote/wire.ts +48 -0
- package/src/stories/chat-panel-layout.stories.tsx +145 -0
- package/src/stories/chat-scene.tsx +571 -0
- package/src/stories/checkpoint-restore.stories.tsx +256 -0
- package/src/stories/context-usage.stories.tsx +212 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
- package/src/stories/conversation-with-sources.stories.tsx +471 -0
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ChildElements.mdx +269 -0
- package/src/stories/docs/ForAIAgents.mdx +162 -0
- package/src/stories/docs/GettingStarted.mdx +65 -0
- package/src/stories/docs/Installation.mdx +83 -0
- package/src/stories/docs/Introduction.mdx +50 -0
- package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
- package/src/stories/docs/Theming.mdx +85 -0
- package/src/stories/docs/element-controls.ts +121 -0
- package/src/stories/docs/frameworks/Angular.mdx +245 -0
- package/src/stories/docs/frameworks/Html.mdx +227 -0
- package/src/stories/docs/frameworks/Overview.mdx +47 -0
- package/src/stories/docs/frameworks/React.mdx +227 -0
- package/src/stories/docs/frameworks/Solid.mdx +173 -0
- package/src/stories/docs/frameworks/Svelte.mdx +194 -0
- package/src/stories/docs/frameworks/Vue.mdx +238 -0
- package/src/stories/docs/generative-ui-overview.mdx +186 -0
- package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
- package/src/stories/docs/recipes/Streaming.mdx +68 -0
- package/src/stories/docs/recipes/TextToSpeech.mdx +35 -0
- package/src/stories/docs/theme-editor/canvas.tsx +32 -0
- package/src/stories/docs/theme-editor/inspector.tsx +66 -0
- package/src/stories/docs/theme-editor/presets.test.ts +32 -0
- package/src/stories/docs/theme-editor/presets.ts +64 -0
- package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
- package/src/stories/docs/theme-editor/theme-css.ts +15 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +146 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/examples/ChoosingComponents.mdx +106 -0
- package/src/stories/examples/sample-data.ts +79 -0
- package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
- package/src/stories/examples/usage/context-usage.ts +591 -0
- package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
- package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
- package/src/stories/examples/usage/empty-state.ts +144 -0
- package/src/stories/examples/usage/full-chat-app.ts +277 -0
- package/src/stories/examples/usage/index.ts +63 -0
- package/src/stories/examples/usage/message-actions.ts +704 -0
- package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
- package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
- package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
- package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
- package/src/stories/examples/usage/streaming-response.ts +687 -0
- package/src/stories/examples/usage/types.ts +33 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +230 -0
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/stories/prompt-input-variants.stories.tsx +456 -0
- package/src/stories/streaming-response.stories.tsx +438 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/types.ts +47 -0
- package/src/ui/action-icons.ts +52 -0
- package/src/ui/avatar.stories.tsx +103 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +86 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +145 -0
- package/src/ui/button.tsx +38 -0
- package/src/ui/collapsible.stories.tsx +69 -0
- package/src/ui/collapsible.tsx +125 -0
- package/src/ui/dropdown.stories.tsx +59 -0
- package/src/ui/dropdown.tsx +196 -0
- package/src/ui/hover-card.stories.tsx +77 -0
- package/src/ui/hover-card.test.tsx +30 -0
- package/src/ui/hover-card.tsx +172 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +158 -0
- package/src/ui/popover.stories.tsx +81 -0
- package/src/ui/popover.test.tsx +99 -0
- package/src/ui/popover.tsx +92 -0
- package/src/ui/resizable.stories.tsx +236 -0
- package/src/ui/resizable.tsx +576 -0
- package/src/ui/scroll-area.stories.tsx +50 -0
- package/src/ui/scroll-area.tsx +15 -0
- package/src/ui/separator.stories.tsx +81 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +337 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/switch.stories.tsx +51 -0
- package/src/ui/switch.test.tsx +59 -0
- package/src/ui/switch.tsx +62 -0
- package/src/ui/textarea.stories.tsx +76 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +74 -0
- package/src/ui/tooltip.tsx +68 -0
- package/src/utils/cn.ts +24 -0
- package/theme.css +187 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
For,
|
|
4
|
+
Show,
|
|
5
|
+
splitProps,
|
|
6
|
+
mergeProps,
|
|
7
|
+
createSignal,
|
|
8
|
+
createMemo,
|
|
9
|
+
createEffect,
|
|
10
|
+
on,
|
|
11
|
+
ErrorBoundary,
|
|
12
|
+
createUniqueId,
|
|
13
|
+
} from 'solid-js';
|
|
14
|
+
import { cn } from '../utils/cn';
|
|
15
|
+
import { Button } from '../ui/button';
|
|
16
|
+
import { HoverCard } from '../ui/hover-card';
|
|
17
|
+
import { Card } from './card';
|
|
18
|
+
import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
|
|
19
|
+
import { useCardResolution } from './use-card-resolution';
|
|
20
|
+
import { emitCardEvent } from '../primitives/card-routing';
|
|
21
|
+
import { useCardHost } from '../primitives/card-host';
|
|
22
|
+
import { Check } from 'lucide-solid';
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Types (choice.schema.json) — see src/primitives/card-schemas/choice.schema.json
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface ChoiceOptionMedia {
|
|
29
|
+
image?: string;
|
|
30
|
+
imageAlt?: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChoiceOption {
|
|
35
|
+
id: string; // required, unique within the card
|
|
36
|
+
label: string; // required
|
|
37
|
+
description?: string;
|
|
38
|
+
media?: ChoiceOptionMedia;
|
|
39
|
+
meta?: string; // trailing freeform text (e.g. price/badge)
|
|
40
|
+
recommended?: boolean; // renders a "Recommended" pill
|
|
41
|
+
disabled?: boolean; // not selectable; skipped in keyboard nav
|
|
42
|
+
payload?: unknown; // echoed back in the emitted action
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ChoiceAllowOther = boolean | { label?: string; placeholder?: string };
|
|
46
|
+
|
|
47
|
+
export interface ChoiceCardData {
|
|
48
|
+
prompt?: string; // optional question/body above the options
|
|
49
|
+
options: ChoiceOption[]; // 1..N
|
|
50
|
+
allowOther?: ChoiceAllowOther; // free-text escape
|
|
51
|
+
submitLabel?: string; // label for the Submit button (default 'Submit')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ChoiceCardEnvelope = CardEnvelope<'choice', ChoiceCardData>;
|
|
55
|
+
|
|
56
|
+
export const CHOICE_CARD_TYPE = 'choice' as const;
|
|
57
|
+
|
|
58
|
+
/** The reserved action id emitted by the `allowOther` free-text submit. */
|
|
59
|
+
export const OTHER_ACTION = '__other__' as const;
|
|
60
|
+
|
|
61
|
+
const DEFAULT_OTHER_LABEL = 'Other…';
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Pure helpers (unit-tested in isolation).
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/** De-dupe options by id (first wins) and validate their shape. Returns the usable
|
|
68
|
+
* list + an optional error message when there's nothing renderable. Modeled on
|
|
69
|
+
* `normalizeActions`. */
|
|
70
|
+
export function normalizeOptions(options: unknown): {
|
|
71
|
+
options: ChoiceOption[];
|
|
72
|
+
error?: string;
|
|
73
|
+
} {
|
|
74
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
75
|
+
return { options: [], error: "This card couldn't be displayed." };
|
|
76
|
+
}
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const out: ChoiceOption[] = [];
|
|
79
|
+
for (const o of options) {
|
|
80
|
+
if (!o || typeof o !== 'object') continue;
|
|
81
|
+
const opt = o as Partial<ChoiceOption>;
|
|
82
|
+
if (typeof opt.id !== 'string' || opt.id.length === 0) continue;
|
|
83
|
+
if (typeof opt.label !== 'string' || opt.label.length === 0) continue;
|
|
84
|
+
if (seen.has(opt.id)) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.warn(`[kai-choice] duplicate option id "${opt.id}" ignored`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(opt.id);
|
|
90
|
+
const media =
|
|
91
|
+
opt.media && typeof opt.media === 'object'
|
|
92
|
+
? {
|
|
93
|
+
image: typeof opt.media.image === 'string' ? opt.media.image : undefined,
|
|
94
|
+
imageAlt: typeof opt.media.imageAlt === 'string' ? opt.media.imageAlt : undefined,
|
|
95
|
+
icon: typeof opt.media.icon === 'string' ? opt.media.icon : undefined,
|
|
96
|
+
}
|
|
97
|
+
: undefined;
|
|
98
|
+
out.push({
|
|
99
|
+
id: opt.id,
|
|
100
|
+
label: opt.label,
|
|
101
|
+
description: typeof opt.description === 'string' ? opt.description : undefined,
|
|
102
|
+
media,
|
|
103
|
+
meta: typeof opt.meta === 'string' ? opt.meta : undefined,
|
|
104
|
+
recommended: opt.recommended === true,
|
|
105
|
+
disabled: opt.disabled === true,
|
|
106
|
+
payload: opt.payload,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (out.length === 0) return { options: [], error: "This card couldn't be displayed." };
|
|
110
|
+
return { options: out };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve the `allowOther` data field to a config (or null when off). */
|
|
114
|
+
export function resolveOtherConfig(
|
|
115
|
+
allowOther: ChoiceAllowOther | undefined,
|
|
116
|
+
): { label: string; placeholder: string | undefined } | null {
|
|
117
|
+
if (!allowOther) return null;
|
|
118
|
+
if (allowOther === true) return { label: DEFAULT_OTHER_LABEL, placeholder: undefined };
|
|
119
|
+
if (typeof allowOther === 'object') {
|
|
120
|
+
return {
|
|
121
|
+
label:
|
|
122
|
+
typeof allowOther.label === 'string' && allowOther.label.length > 0
|
|
123
|
+
? allowOther.label
|
|
124
|
+
: DEFAULT_OTHER_LABEL,
|
|
125
|
+
placeholder: typeof allowOther.placeholder === 'string' ? allowOther.placeholder : undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Index of the next non-disabled option, moving by `dir` (+1/-1) from `from`,
|
|
132
|
+
* wrapping. Returns `from` when nothing else is focusable. */
|
|
133
|
+
export function nextEnabledIndex(options: ChoiceOption[], from: number, dir: 1 | -1): number {
|
|
134
|
+
const n = options.length;
|
|
135
|
+
if (n === 0) return from;
|
|
136
|
+
for (let step = 1; step <= n; step += 1) {
|
|
137
|
+
const i = (((from + dir * step) % n) + n) % n;
|
|
138
|
+
if (!options[i].disabled) return i;
|
|
139
|
+
}
|
|
140
|
+
return from;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The first non-disabled option index (the initial roving tab stop), or -1. */
|
|
144
|
+
export function firstEnabledIndex(options: ChoiceOption[]): number {
|
|
145
|
+
return options.findIndex((o) => !o.disabled);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// The <ChoiceCard> component.
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export interface ChoiceCardProps {
|
|
153
|
+
/** The choice definition (CardEnvelope.data). */
|
|
154
|
+
data?: ChoiceCardData;
|
|
155
|
+
/** The card id used to correlate every emitted CardEvent. */
|
|
156
|
+
cardId?: string;
|
|
157
|
+
/** The envelope title rendered in the card chrome. */
|
|
158
|
+
heading?: string;
|
|
159
|
+
/** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
|
|
160
|
+
* bubbling `kai-card` CustomEvent off `hostElement`). */
|
|
161
|
+
host?: CardHost;
|
|
162
|
+
/** The custom-element host node, for the bubbling `kai-card` fallback emit. */
|
|
163
|
+
hostElement?: HTMLElement;
|
|
164
|
+
class?: string;
|
|
165
|
+
/** When set, render the chromed read-only view instead of the interactive radiogroup. */
|
|
166
|
+
resolution?: CardResolution;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `ChoiceCard` — a single-select "pick one of N rich options" card (plans, products,
|
|
171
|
+
* flights, quick replies) inside `Card` chrome. The options are a WAI-ARIA radiogroup
|
|
172
|
+
* (list rows) with roving tabindex: clicking a row (or Space/Enter on the focused row)
|
|
173
|
+
* **selects** it locally without emitting; a **Submit** button below the list then emits
|
|
174
|
+
* the Card contract's **`action`** verb (`{ kind:'action', cardId, action: option.id,
|
|
175
|
+
* payload }`) and resolves the card (chosen option shown read-only) so the same pick can't
|
|
176
|
+
* double-fire. An optional `allowOther` free-text escape appends a selectable "Other…" row;
|
|
177
|
+
* selecting it reveals an inline text input, and the same Submit emits `action:'__other__'`
|
|
178
|
+
* with `{ text }`. Emits `ready` on mount and `error` for an unusable definition (inline
|
|
179
|
+
* error state).
|
|
180
|
+
*/
|
|
181
|
+
export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
|
|
182
|
+
const merged = mergeProps({ cardId: 'kai-choice' }, props);
|
|
183
|
+
const [local] = splitProps(merged, ['data', 'cardId', 'heading', 'host', 'hostElement', 'class', 'resolution']);
|
|
184
|
+
|
|
185
|
+
const ctxHost = useCardHost();
|
|
186
|
+
const uid = createUniqueId();
|
|
187
|
+
|
|
188
|
+
const emit = (event: CardEvent): void => {
|
|
189
|
+
const h = local.host ?? ctxHost;
|
|
190
|
+
if (h) h.emit(event);
|
|
191
|
+
else if (local.hostElement) emitCardEvent(local.hostElement, event);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const normalized = createMemo(() => normalizeOptions(local.data?.options));
|
|
195
|
+
const valid = createMemo(() => normalized().error === undefined);
|
|
196
|
+
const errorMessage = createMemo(() => normalized().error ?? '');
|
|
197
|
+
const baseOptions = createMemo(() => normalized().options);
|
|
198
|
+
const otherCfg = createMemo(() => resolveOtherConfig(local.data?.allowOther));
|
|
199
|
+
|
|
200
|
+
// The full radio list = base options + a synthetic "Other" option when allowOther is set.
|
|
201
|
+
const options = createMemo<ChoiceOption[]>(() => {
|
|
202
|
+
const cfg = otherCfg();
|
|
203
|
+
if (!cfg) return baseOptions();
|
|
204
|
+
return [...baseOptions(), { id: OTHER_ACTION, label: cfg.label }];
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const res = useCardResolution({ prop: () => local.resolution, data: () => local.data });
|
|
208
|
+
|
|
209
|
+
const resolvedId = createMemo(() => {
|
|
210
|
+
const r = res.resolution();
|
|
211
|
+
return r && r.kind === 'action' ? r.action : undefined;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const [focusIndex, setFocusIndex] = createSignal(0);
|
|
215
|
+
const [selectedId, setSelectedId] = createSignal<string | undefined>(undefined);
|
|
216
|
+
const [otherText, setOtherText] = createSignal('');
|
|
217
|
+
|
|
218
|
+
let groupRef: HTMLDivElement | undefined;
|
|
219
|
+
let otherInputRef: HTMLInputElement | undefined;
|
|
220
|
+
|
|
221
|
+
const otherSelected = createMemo(() => selectedId() === OTHER_ACTION);
|
|
222
|
+
|
|
223
|
+
// Reset all transient state whenever a NEW definition arrives.
|
|
224
|
+
createEffect(
|
|
225
|
+
on(
|
|
226
|
+
() => local.data,
|
|
227
|
+
() => {
|
|
228
|
+
setSelectedId(undefined);
|
|
229
|
+
setOtherText('');
|
|
230
|
+
setFocusIndex(Math.max(0, firstEnabledIndex(options())));
|
|
231
|
+
},
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const resolvedChoice = createMemo(() => {
|
|
236
|
+
const r = res.resolution();
|
|
237
|
+
if (!r || r.kind !== 'action') return undefined;
|
|
238
|
+
if (r.action === OTHER_ACTION) {
|
|
239
|
+
const text = (r.payload as { text?: string } | undefined)?.text ?? '';
|
|
240
|
+
return { other: true as const, text };
|
|
241
|
+
}
|
|
242
|
+
const opt = baseOptions().find((o) => o.id === r.action);
|
|
243
|
+
return { other: false as const, opt, id: r.action };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ready / error lifecycle emits.
|
|
247
|
+
createEffect(
|
|
248
|
+
on(valid, (ok) => {
|
|
249
|
+
if (ok) emit({ kind: 'ready', cardId: local.cardId });
|
|
250
|
+
else emit({ kind: 'error', cardId: local.cardId, message: errorMessage() });
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Surface the resolved option id for host styling.
|
|
255
|
+
createEffect(() => {
|
|
256
|
+
const el = local.hostElement;
|
|
257
|
+
if (!el) return;
|
|
258
|
+
const id = resolvedId();
|
|
259
|
+
if (id !== undefined) el.setAttribute('data-kai-resolved', id);
|
|
260
|
+
else el.removeAttribute('data-kai-resolved');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const isOther = (opt: ChoiceOption): boolean => opt.id === OTHER_ACTION;
|
|
264
|
+
|
|
265
|
+
// Select (do NOT emit) the given option locally.
|
|
266
|
+
const select = (opt: ChoiceOption): void => {
|
|
267
|
+
if (res.isResolved()) return; // single-shot
|
|
268
|
+
if (opt.disabled) return;
|
|
269
|
+
setSelectedId(opt.id);
|
|
270
|
+
if (isOther(opt)) {
|
|
271
|
+
queueMicrotask(() => otherInputRef?.focus());
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Whether the current selection is submittable.
|
|
276
|
+
const canSubmit = createMemo(() => {
|
|
277
|
+
if (res.isResolved()) return false;
|
|
278
|
+
const id = selectedId();
|
|
279
|
+
if (id === undefined) return false;
|
|
280
|
+
if (id === OTHER_ACTION) return otherText().trim().length > 0;
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const submitLabel = (): string => local.data?.submitLabel ?? 'Submit';
|
|
285
|
+
|
|
286
|
+
// Submit the current selection: emit `action` then resolve (single-shot).
|
|
287
|
+
const submit = (): void => {
|
|
288
|
+
if (res.isResolved()) return;
|
|
289
|
+
const id = selectedId();
|
|
290
|
+
if (id === undefined) return;
|
|
291
|
+
if (id === OTHER_ACTION) {
|
|
292
|
+
const text = otherText().trim();
|
|
293
|
+
if (text.length === 0) return;
|
|
294
|
+
emit({ kind: 'action', cardId: local.cardId, action: OTHER_ACTION, payload: { text } });
|
|
295
|
+
res.setLocal({ kind: 'action', action: OTHER_ACTION, payload: { text } });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const opt = baseOptions().find((o) => o.id === id);
|
|
299
|
+
if (!opt) return;
|
|
300
|
+
emit({
|
|
301
|
+
kind: 'action',
|
|
302
|
+
cardId: local.cardId,
|
|
303
|
+
action: opt.id,
|
|
304
|
+
...(opt.payload !== undefined ? { payload: opt.payload } : {}),
|
|
305
|
+
});
|
|
306
|
+
res.setLocal({
|
|
307
|
+
kind: 'action',
|
|
308
|
+
action: opt.id,
|
|
309
|
+
...(opt.payload !== undefined ? { payload: opt.payload } : {}),
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const focusRadio = (index: number): void => {
|
|
314
|
+
const radios = groupRef?.querySelectorAll<HTMLElement>('[role="radio"]');
|
|
315
|
+
radios?.[index]?.focus();
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const onGroupKeyDown = (e: KeyboardEvent): void => {
|
|
319
|
+
if (res.isResolved()) return;
|
|
320
|
+
const opts = options();
|
|
321
|
+
const i = focusIndex();
|
|
322
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
const next = nextEnabledIndex(opts, i, 1);
|
|
325
|
+
setFocusIndex(next);
|
|
326
|
+
focusRadio(next);
|
|
327
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
const next = nextEnabledIndex(opts, i, -1);
|
|
330
|
+
setFocusIndex(next);
|
|
331
|
+
focusRadio(next);
|
|
332
|
+
} else if (e.key === 'Home') {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
const next = Math.max(0, firstEnabledIndex(opts));
|
|
335
|
+
setFocusIndex(next);
|
|
336
|
+
focusRadio(next);
|
|
337
|
+
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
const opt = opts[i];
|
|
340
|
+
if (opt) select(opt);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const groupLabel = (): string => local.heading ?? local.data?.prompt ?? 'Choose an option';
|
|
345
|
+
const promptId = `kai-choice-prompt-${uid}`;
|
|
346
|
+
const otherInputId = `kai-choice-other-${uid}`;
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
|
|
350
|
+
<ErrorBoundary
|
|
351
|
+
fallback={() => {
|
|
352
|
+
emit({ kind: 'error', cardId: local.cardId, message: 'The card failed to render.' });
|
|
353
|
+
return <Card heading={local.heading} errorMessage="The card failed to render." />;
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
<Card heading={local.heading}>
|
|
357
|
+
<div class={cn('flex flex-col gap-3', local.class)}>
|
|
358
|
+
<Show when={local.data?.prompt}>
|
|
359
|
+
<p id={promptId} class="text-sm text-foreground">
|
|
360
|
+
{local.data?.prompt}
|
|
361
|
+
</p>
|
|
362
|
+
</Show>
|
|
363
|
+
|
|
364
|
+
<Show
|
|
365
|
+
when={!res.isResolved()}
|
|
366
|
+
fallback={<ResolvedChoice choice={resolvedChoice()!} optimistic={res.isOptimistic()} />}
|
|
367
|
+
>
|
|
368
|
+
<div
|
|
369
|
+
ref={groupRef}
|
|
370
|
+
role="radiogroup"
|
|
371
|
+
aria-label={groupLabel()}
|
|
372
|
+
aria-describedby={local.data?.prompt ? promptId : undefined}
|
|
373
|
+
class="divide-y divide-border overflow-hidden rounded-lg border border-input"
|
|
374
|
+
onKeyDown={onGroupKeyDown}
|
|
375
|
+
>
|
|
376
|
+
<For each={options()}>
|
|
377
|
+
{(opt, index) => {
|
|
378
|
+
const checked = () => selectedId() === opt.id;
|
|
379
|
+
const tabStop = () =>
|
|
380
|
+
!opt.disabled && index() === focusIndex() && !res.isResolved();
|
|
381
|
+
const descId = `kai-choice-desc-${uid}-${opt.id}`;
|
|
382
|
+
const hasDesc = () => Boolean(opt.description);
|
|
383
|
+
return (
|
|
384
|
+
<ListRow
|
|
385
|
+
opt={opt}
|
|
386
|
+
checked={checked()}
|
|
387
|
+
tabStop={tabStop()}
|
|
388
|
+
descId={descId}
|
|
389
|
+
hasDesc={hasDesc()}
|
|
390
|
+
onPick={() => {
|
|
391
|
+
setFocusIndex(index());
|
|
392
|
+
select(opt);
|
|
393
|
+
}}
|
|
394
|
+
onFocus={() => setFocusIndex(index())}
|
|
395
|
+
/>
|
|
396
|
+
);
|
|
397
|
+
}}
|
|
398
|
+
</For>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<Show when={otherSelected()}>
|
|
402
|
+
<div class="flex flex-col gap-2">
|
|
403
|
+
<label for={otherInputId} class="sr-only">
|
|
404
|
+
{otherCfg()?.label ?? DEFAULT_OTHER_LABEL}
|
|
405
|
+
</label>
|
|
406
|
+
<input
|
|
407
|
+
id={otherInputId}
|
|
408
|
+
ref={otherInputRef}
|
|
409
|
+
type="text"
|
|
410
|
+
class="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"
|
|
411
|
+
placeholder={otherCfg()?.placeholder}
|
|
412
|
+
value={otherText()}
|
|
413
|
+
onInput={(e) => setOtherText(e.currentTarget.value)}
|
|
414
|
+
onKeyDown={(e) => {
|
|
415
|
+
if (e.key === 'Enter') {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
submit();
|
|
418
|
+
}
|
|
419
|
+
}}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
</Show>
|
|
423
|
+
|
|
424
|
+
<Button type="button" class="self-end" disabled={!canSubmit()} onClick={submit}>
|
|
425
|
+
{submitLabel()}
|
|
426
|
+
</Button>
|
|
427
|
+
</Show>
|
|
428
|
+
</div>
|
|
429
|
+
</Card>
|
|
430
|
+
</ErrorBoundary>
|
|
431
|
+
</Show>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
436
|
+
// Internal list-row presentation: a `role="radio"` with the kit's
|
|
437
|
+
// selectable-list styling.
|
|
438
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
interface RowProps {
|
|
441
|
+
opt: ChoiceOption;
|
|
442
|
+
checked: boolean;
|
|
443
|
+
tabStop: boolean;
|
|
444
|
+
descId: string;
|
|
445
|
+
hasDesc: boolean;
|
|
446
|
+
onPick: () => void;
|
|
447
|
+
onFocus: () => void;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function RecommendedPill(): JSX.Element {
|
|
451
|
+
return (
|
|
452
|
+
<span class="inline-flex items-center rounded-full bg-[var(--color-primary)] px-1.5 py-0.5 text-[0.625rem] font-medium uppercase leading-none tracking-wide text-[var(--color-primary-foreground,white)]">
|
|
453
|
+
Recommended
|
|
454
|
+
</span>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** A leading image thumbnail (alt required for a11y; decorative → empty alt). */
|
|
459
|
+
function Thumb(props: { media: ChoiceOptionMedia; class?: string }): JSX.Element {
|
|
460
|
+
return (
|
|
461
|
+
<img
|
|
462
|
+
src={props.media.image}
|
|
463
|
+
alt={props.media.imageAlt ?? ''}
|
|
464
|
+
class={cn('shrink-0 rounded-md object-cover', props.class)}
|
|
465
|
+
/>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** A leading named-icon badge (decorative; the label carries the meaning). */
|
|
470
|
+
function IconBadge(props: { name: string }): JSX.Element {
|
|
471
|
+
return (
|
|
472
|
+
<span
|
|
473
|
+
aria-hidden="true"
|
|
474
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-sm font-medium text-muted-foreground"
|
|
475
|
+
>
|
|
476
|
+
{props.name.slice(0, 2)}
|
|
477
|
+
</span>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function ListRow(props: RowProps): JSX.Element {
|
|
482
|
+
return (
|
|
483
|
+
<div
|
|
484
|
+
role="radio"
|
|
485
|
+
aria-checked={props.checked}
|
|
486
|
+
aria-disabled={props.opt.disabled ? 'true' : undefined}
|
|
487
|
+
aria-describedby={props.hasDesc ? props.descId : undefined}
|
|
488
|
+
data-option-id={props.opt.id}
|
|
489
|
+
tabindex={props.opt.disabled ? -1 : props.tabStop ? 0 : -1}
|
|
490
|
+
onClick={() => !props.opt.disabled && props.onPick()}
|
|
491
|
+
onFocus={props.onFocus}
|
|
492
|
+
class={cn(
|
|
493
|
+
'flex items-center gap-3 px-3 py-2.5 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
|
494
|
+
props.opt.disabled
|
|
495
|
+
? 'cursor-not-allowed opacity-60 text-foreground'
|
|
496
|
+
: 'cursor-pointer hover:bg-muted/50',
|
|
497
|
+
props.checked ? 'bg-accent font-medium text-accent-foreground' : 'text-foreground',
|
|
498
|
+
)}
|
|
499
|
+
>
|
|
500
|
+
<Show when={props.opt.media?.image}>
|
|
501
|
+
<HoverCard
|
|
502
|
+
openDelay={150}
|
|
503
|
+
class="p-1 w-auto"
|
|
504
|
+
placement="right-start"
|
|
505
|
+
trigger={<Thumb media={props.opt.media!} class="h-9 w-9" />}
|
|
506
|
+
>
|
|
507
|
+
<img
|
|
508
|
+
src={props.opt.media!.image}
|
|
509
|
+
alt={props.opt.media!.imageAlt ?? ''}
|
|
510
|
+
class="max-h-80 max-w-80 rounded-md object-contain"
|
|
511
|
+
/>
|
|
512
|
+
</HoverCard>
|
|
513
|
+
</Show>
|
|
514
|
+
<Show when={!props.opt.media?.image && props.opt.media?.icon}>
|
|
515
|
+
<IconBadge name={props.opt.media!.icon!} />
|
|
516
|
+
</Show>
|
|
517
|
+
<span
|
|
518
|
+
aria-hidden="true"
|
|
519
|
+
class={cn(
|
|
520
|
+
'flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center rounded-full border-[1.5px] transition-colors',
|
|
521
|
+
props.checked ? 'border-[var(--color-primary)]' : 'border-input bg-background',
|
|
522
|
+
)}
|
|
523
|
+
>
|
|
524
|
+
<Show when={props.checked}>
|
|
525
|
+
<span class="h-2 w-2 rounded-full bg-[var(--color-primary)]" />
|
|
526
|
+
</Show>
|
|
527
|
+
</span>
|
|
528
|
+
<span class="flex min-w-0 flex-col gap-0.5">
|
|
529
|
+
<span class="flex items-center gap-2">
|
|
530
|
+
<span class="truncate">{props.opt.label}</span>
|
|
531
|
+
<Show when={props.opt.recommended}>
|
|
532
|
+
<RecommendedPill />
|
|
533
|
+
</Show>
|
|
534
|
+
</span>
|
|
535
|
+
<Show when={props.opt.description}>
|
|
536
|
+
<span id={props.descId} class="text-xs font-normal text-muted-foreground">
|
|
537
|
+
{props.opt.description}
|
|
538
|
+
</span>
|
|
539
|
+
</Show>
|
|
540
|
+
</span>
|
|
541
|
+
<Show when={props.opt.meta}>
|
|
542
|
+
<span class="ml-auto shrink-0 text-xs font-normal text-muted-foreground">
|
|
543
|
+
{props.opt.meta}
|
|
544
|
+
</span>
|
|
545
|
+
</Show>
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function ResolvedChoice(props: {
|
|
551
|
+
choice: { other: true; text: string } | { other: false; opt?: ChoiceOption; id: string };
|
|
552
|
+
optimistic: boolean;
|
|
553
|
+
}): JSX.Element {
|
|
554
|
+
const c = props.choice;
|
|
555
|
+
return (
|
|
556
|
+
<div
|
|
557
|
+
class="flex items-center gap-3 rounded-lg border border-input bg-accent px-3 py-2.5 text-sm font-medium text-accent-foreground"
|
|
558
|
+
role={props.optimistic ? 'status' : undefined}
|
|
559
|
+
>
|
|
560
|
+
<Check size={16} aria-hidden="true" />
|
|
561
|
+
<Show when={!c.other} fallback={<span>Other: {c.other ? c.text : ''}</span>}>
|
|
562
|
+
<span>{!c.other ? (c.opt?.label ?? c.id) : ''}</span>
|
|
563
|
+
<Show when={!c.other && c.opt?.meta}>
|
|
564
|
+
<span class="ml-auto text-xs font-normal text-muted-foreground">{!c.other ? c.opt?.meta : ''}</span>
|
|
565
|
+
</Show>
|
|
566
|
+
</Show>
|
|
567
|
+
</div>
|
|
568
|
+
);
|
|
569
|
+
}
|