@kitnai/chat 0.7.0 → 0.8.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.
Files changed (212) hide show
  1. package/README.md +9 -9
  2. package/dist/custom-elements.json +1626 -883
  3. package/dist/kitn-chat.es.js +36 -36
  4. package/dist/llms/llms-full.txt +303 -142
  5. package/dist/llms/llms.txt +18 -18
  6. package/dist/schemas/card-envelope.schema.json +14 -0
  7. package/dist/schemas/card-event.schema.json +12 -0
  8. package/dist/schemas/confirm.schema.json +65 -0
  9. package/dist/schemas/embed.schema.json +65 -0
  10. package/dist/schemas/form.result.schema.json +7 -0
  11. package/dist/schemas/form.schema.json +33 -0
  12. package/dist/schemas/link.schema.json +56 -0
  13. package/dist/schemas/task-list.result.schema.json +16 -0
  14. package/dist/schemas/task-list.schema.json +78 -0
  15. package/dist/theme.tokens.css +65 -65
  16. package/dist/tsx-B8rCNbgL.js +1 -0
  17. package/dist/typescript-RycA9KXf.js +1 -0
  18. package/frameworks/react/index.tsx +356 -189
  19. package/frameworks/react/runtime.tsx +2 -2
  20. package/llms-full.txt +303 -142
  21. package/llms.txt +18 -18
  22. package/package.json +5 -2
  23. package/src/components/artifact.stories.tsx +138 -0
  24. package/src/components/artifact.tsx +581 -0
  25. package/src/components/attachments.stories.tsx +7 -8
  26. package/src/components/attachments.tsx +2 -2
  27. package/src/components/card.tsx +110 -0
  28. package/src/components/chain-of-thought.stories.tsx +7 -8
  29. package/src/components/chat-container.stories.tsx +7 -8
  30. package/src/components/chat-container.tsx +4 -0
  31. package/src/components/checkpoint.stories.tsx +7 -8
  32. package/src/components/code-block.stories.tsx +8 -9
  33. package/src/components/component-meta.json +3411 -0
  34. package/src/components/confirm-card.stories.tsx +74 -0
  35. package/src/components/confirm-card.tsx +299 -0
  36. package/src/components/context.stories.tsx +7 -8
  37. package/src/components/conversation-item.stories.tsx +7 -8
  38. package/src/components/conversation-item.tsx +2 -2
  39. package/src/components/conversation-list.stories.tsx +7 -8
  40. package/src/components/conversation-list.tsx +1 -1
  41. package/src/components/embed.tsx +196 -0
  42. package/src/components/empty.stories.tsx +8 -9
  43. package/src/components/feedback-bar.stories.tsx +7 -8
  44. package/src/components/file-tree.stories.tsx +73 -0
  45. package/src/components/file-tree.tsx +383 -0
  46. package/src/components/file-upload.stories.tsx +7 -8
  47. package/src/components/form-widgets.tsx +461 -0
  48. package/src/components/form.tsx +796 -0
  49. package/src/components/image.stories.tsx +7 -8
  50. package/src/components/link-card.tsx +194 -0
  51. package/src/components/loader.stories.tsx +7 -8
  52. package/src/components/markdown.stories.tsx +7 -8
  53. package/src/components/message-narrow.stories.tsx +12 -13
  54. package/src/components/message-skills.stories.tsx +16 -17
  55. package/src/components/message.stories.tsx +17 -18
  56. package/src/components/model-switcher.stories.tsx +7 -8
  57. package/src/components/prompt-input.stories.tsx +8 -9
  58. package/src/components/prompt-suggestion.stories.tsx +7 -8
  59. package/src/components/prompt-suggestion.tsx +3 -3
  60. package/src/components/reasoning.stories.tsx +7 -8
  61. package/src/components/scroll-button.stories.tsx +7 -8
  62. package/src/components/slash-command.stories.tsx +8 -9
  63. package/src/components/slash-command.tsx +2 -2
  64. package/src/components/source.stories.tsx +7 -8
  65. package/src/components/source.tsx +1 -1
  66. package/src/components/task-list-card.stories.tsx +78 -0
  67. package/src/components/task-list-card.tsx +388 -0
  68. package/src/components/text-shimmer.stories.tsx +7 -8
  69. package/src/components/thinking-bar.stories.tsx +7 -8
  70. package/src/components/tool.stories.tsx +7 -8
  71. package/src/components/tool.tsx +2 -2
  72. package/src/components/voice-input.stories.tsx +7 -8
  73. package/src/elements/artifact.stories.tsx +291 -0
  74. package/src/elements/artifact.tsx +72 -0
  75. package/src/elements/{kitn-attachments.stories.tsx → attachments.stories.tsx} +11 -20
  76. package/src/elements/attachments.tsx +4 -4
  77. package/src/elements/card.stories.tsx +118 -0
  78. package/src/elements/card.tsx +40 -0
  79. package/src/elements/catalog.stories.tsx +491 -0
  80. package/src/elements/{kitn-chain-of-thought.stories.tsx → chain-of-thought.stories.tsx} +13 -22
  81. package/src/elements/chain-of-thought.tsx +3 -3
  82. package/src/elements/{kitn-chat-scope-picker.stories.tsx → chat-scope-picker.stories.tsx} +10 -19
  83. package/src/elements/chat-scope-picker.tsx +4 -4
  84. package/src/elements/{kitn-chat-workspace.stories.tsx → chat-workspace.stories.tsx} +15 -23
  85. package/src/elements/chat-workspace.tsx +2 -2
  86. package/src/elements/{kitn-chat.stories.tsx → chat.stories.tsx} +12 -20
  87. package/src/elements/chat.tsx +2 -2
  88. package/src/elements/{kitn-checkpoint.stories.tsx → checkpoint.stories.tsx} +11 -20
  89. package/src/elements/checkpoint.tsx +4 -4
  90. package/src/elements/{kitn-code-block.stories.tsx → code-block.stories.tsx} +10 -19
  91. package/src/elements/code-block.tsx +3 -3
  92. package/src/elements/compiled.css +1 -1
  93. package/src/elements/composed-shell.stories.tsx +316 -0
  94. package/src/elements/confirm-card.stories.tsx +186 -0
  95. package/src/elements/confirm-card.tsx +45 -0
  96. package/src/elements/{kitn-context-meter.stories.tsx → context-meter.stories.tsx} +10 -19
  97. package/src/elements/context-meter.tsx +3 -3
  98. package/src/elements/{kitn-conversation-list.stories.tsx → conversation-list.stories.tsx} +12 -20
  99. package/src/elements/conversation-list.tsx +2 -2
  100. package/src/elements/css.ts +1 -1
  101. package/src/elements/define.tsx +10 -10
  102. package/src/elements/element-meta.json +1379 -733
  103. package/src/elements/element-types.d.ts +251 -125
  104. package/src/elements/embed.stories.tsx +197 -0
  105. package/src/elements/embed.tsx +35 -0
  106. package/src/elements/{kitn-empty.stories.tsx → empty.stories.tsx} +12 -21
  107. package/src/elements/empty.tsx +3 -3
  108. package/src/elements/{kitn-feedback-bar.stories.tsx → feedback-bar.stories.tsx} +11 -20
  109. package/src/elements/feedback-bar.tsx +4 -4
  110. package/src/elements/file-tree.stories.tsx +133 -0
  111. package/src/elements/file-tree.tsx +52 -0
  112. package/src/elements/{kitn-file-upload.stories.tsx → file-upload.stories.tsx} +12 -21
  113. package/src/elements/file-upload.tsx +4 -4
  114. package/src/elements/form.stories.tsx +204 -0
  115. package/src/elements/form.tsx +37 -0
  116. package/src/elements/{kitn-image.stories.tsx → image.stories.tsx} +10 -19
  117. package/src/elements/image.tsx +3 -3
  118. package/src/elements/link-card.stories.tsx +193 -0
  119. package/src/elements/link-card.tsx +34 -0
  120. package/src/elements/{kitn-loader.stories.tsx → loader.stories.tsx} +11 -20
  121. package/src/elements/loader.tsx +3 -3
  122. package/src/elements/{kitn-markdown.stories.tsx → markdown.stories.tsx} +10 -19
  123. package/src/elements/markdown.tsx +3 -3
  124. package/src/elements/{kitn-message-skills.stories.tsx → message-skills.stories.tsx} +10 -19
  125. package/src/elements/message-skills.tsx +3 -3
  126. package/src/elements/{kitn-message.stories.tsx → message.stories.tsx} +12 -21
  127. package/src/elements/message.tsx +5 -5
  128. package/src/elements/{kitn-model-switcher.stories.tsx → model-switcher.stories.tsx} +10 -19
  129. package/src/elements/model-switcher.tsx +5 -5
  130. package/src/elements/{kitn-prompt-input.stories.tsx → prompt-input.stories.tsx} +14 -22
  131. package/src/elements/prompt-input.tsx +3 -3
  132. package/src/elements/{kitn-prompt-suggestions.stories.tsx → prompt-suggestions.stories.tsx} +13 -22
  133. package/src/elements/prompt-suggestions.tsx +4 -4
  134. package/src/elements/{kitn-reasoning.stories.tsx → reasoning.stories.tsx} +10 -19
  135. package/src/elements/reasoning.tsx +4 -4
  136. package/src/elements/register.ts +11 -1
  137. package/src/elements/resizable.stories.tsx +200 -0
  138. package/src/elements/resizable.tsx +264 -0
  139. package/src/elements/{kitn-response-stream.stories.tsx → response-stream.stories.tsx} +10 -19
  140. package/src/elements/response-stream.tsx +4 -4
  141. package/src/elements/{kitn-source-list.stories.tsx → source-list.stories.tsx} +11 -20
  142. package/src/elements/{kitn-source.stories.tsx → source.stories.tsx} +12 -21
  143. package/src/elements/source.tsx +5 -5
  144. package/src/elements/styles.css +140 -1
  145. package/src/elements/task-list-card.stories.tsx +194 -0
  146. package/src/elements/task-list-card.tsx +40 -0
  147. package/src/elements/{kitn-text-shimmer.stories.tsx → text-shimmer.stories.tsx} +10 -19
  148. package/src/elements/text-shimmer.tsx +3 -3
  149. package/src/elements/{kitn-thinking-bar.stories.tsx → thinking-bar.stories.tsx} +11 -20
  150. package/src/elements/thinking-bar.tsx +5 -5
  151. package/src/elements/{kitn-tool.stories.tsx → tool.stories.tsx} +10 -19
  152. package/src/elements/tool.tsx +3 -3
  153. package/src/elements/{kitn-voice-input.stories.tsx → voice-input.stories.tsx} +10 -19
  154. package/src/elements/voice-input.tsx +4 -4
  155. package/src/index.ts +94 -2
  156. package/src/primitives/card-contract.ts +60 -0
  157. package/src/primitives/card-host.tsx +35 -0
  158. package/src/primitives/card-routing.ts +79 -0
  159. package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
  160. package/src/primitives/card-schemas/card-event.schema.json +12 -0
  161. package/src/primitives/card-schemas/confirm.schema.json +65 -0
  162. package/src/primitives/card-schemas/embed.schema.json +65 -0
  163. package/src/primitives/card-schemas/form.result.schema.json +7 -0
  164. package/src/primitives/card-schemas/form.schema.json +33 -0
  165. package/src/primitives/card-schemas/link.schema.json +56 -0
  166. package/src/primitives/card-schemas/task-list.result.schema.json +16 -0
  167. package/src/primitives/card-schemas/task-list.schema.json +78 -0
  168. package/src/primitives/card-validate.ts +95 -0
  169. package/src/primitives/embed-providers.ts +254 -0
  170. package/src/primitives/highlighter.ts +4 -0
  171. package/src/primitives/link-preview.ts +87 -0
  172. package/src/primitives/pdf-preview.ts +121 -0
  173. package/src/stories/chat-panel-layout.stories.tsx +2 -1
  174. package/src/stories/chat-scene.tsx +22 -21
  175. package/src/stories/checkpoint-restore.stories.tsx +10 -10
  176. package/src/stories/conversation-with-reasoning.stories.tsx +4 -4
  177. package/src/stories/conversation-with-sources.stories.tsx +7 -7
  178. package/src/stories/docs/Accessibility.mdx +2 -2
  179. package/src/stories/docs/ForAIAgents.mdx +3 -3
  180. package/src/stories/docs/GettingStarted.mdx +2 -2
  181. package/src/stories/docs/Installation.mdx +2 -2
  182. package/src/stories/docs/Integrations.mdx +29 -29
  183. package/src/stories/docs/Introduction.mdx +3 -3
  184. package/src/stories/docs/Theming.mdx +2 -2
  185. package/src/stories/docs/element-controls.ts +32 -0
  186. package/src/stories/docs/theme-editor/theme-editor.tsx +1 -0
  187. package/src/stories/examples/ChoosingComponents.mdx +94 -0
  188. package/src/stories/examples/sample-data.ts +79 -0
  189. package/src/stories/message-actions.stories.tsx +13 -13
  190. package/src/stories/pattern-centered-conversation.stories.tsx +3 -3
  191. package/src/stories/pattern-docked-widget.stories.tsx +1 -1
  192. package/src/stories/pattern-empty-state.stories.tsx +3 -3
  193. package/src/stories/prompt-input-variants.stories.tsx +13 -13
  194. package/src/stories/streaming-response.stories.tsx +3 -3
  195. package/src/stories/typography.stories.tsx +4 -4
  196. package/src/ui/avatar.stories.tsx +7 -8
  197. package/src/ui/badge.stories.tsx +7 -8
  198. package/src/ui/button.stories.tsx +8 -9
  199. package/src/ui/button.tsx +1 -0
  200. package/src/ui/collapsible.stories.tsx +6 -7
  201. package/src/ui/dropdown.stories.tsx +6 -7
  202. package/src/ui/hover-card.stories.tsx +6 -7
  203. package/src/ui/resizable.stories.tsx +74 -9
  204. package/src/ui/resizable.tsx +351 -71
  205. package/src/ui/scroll-area.stories.tsx +6 -7
  206. package/src/ui/scroll-area.tsx +3 -1
  207. package/src/ui/separator.stories.tsx +7 -8
  208. package/src/ui/skeleton.stories.tsx +7 -8
  209. package/src/ui/textarea.stories.tsx +6 -7
  210. package/src/ui/tooltip.stories.tsx +8 -9
  211. package/theme.css +65 -65
  212. package/src/stories/docs/element-spec.tsx +0 -86
@@ -0,0 +1,461 @@
1
+ import { type JSX, For, Show, createSignal } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { Textarea } from '../ui/textarea';
4
+ import { Star } from 'lucide-solid';
5
+ import type { FormField } from './form';
6
+
7
+ /** The shared prop shape every leaf widget receives from FieldRow. */
8
+ export interface WidgetProps {
9
+ id: string;
10
+ value: unknown;
11
+ field: FormField;
12
+ disabled: boolean;
13
+ placeholder?: string;
14
+ required: boolean;
15
+ invalid: boolean;
16
+ describedBy?: string;
17
+ label: string;
18
+ onInput: (value: unknown) => void;
19
+ onBlur: () => void;
20
+ }
21
+
22
+ const inputBase =
23
+ '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';
24
+
25
+ function ariaProps(p: WidgetProps) {
26
+ return {
27
+ 'aria-required': p.required || undefined,
28
+ 'aria-invalid': p.invalid || undefined,
29
+ 'aria-describedby': p.describedBy,
30
+ };
31
+ }
32
+
33
+ /** text / email / url / date / datetime / time / password — all <input> variants. */
34
+ export function TextWidget(
35
+ props: WidgetProps & { variant: 'text' | 'email' | 'url' | 'date' | 'datetime' | 'time' | 'password' },
36
+ ): JSX.Element {
37
+ const inputType = () => {
38
+ switch (props.variant) {
39
+ case 'email':
40
+ return 'email';
41
+ case 'url':
42
+ return 'url';
43
+ case 'date':
44
+ return 'date';
45
+ case 'datetime':
46
+ return 'datetime-local';
47
+ case 'time':
48
+ return 'time';
49
+ case 'password':
50
+ return 'password';
51
+ default:
52
+ return 'text';
53
+ }
54
+ };
55
+ return (
56
+ <input
57
+ id={props.id}
58
+ data-control
59
+ type={inputType()}
60
+ class={cn(inputBase, props.invalid && 'border-destructive dark:border-red-400/70')}
61
+ value={(props.value as string) ?? ''}
62
+ placeholder={props.placeholder}
63
+ disabled={props.disabled}
64
+ minLength={props.field.minLength}
65
+ maxLength={props.field.maxLength}
66
+ {...ariaProps(props)}
67
+ onInput={(e) => props.onInput(e.currentTarget.value)}
68
+ onBlur={props.onBlur}
69
+ />
70
+ );
71
+ }
72
+
73
+ export function TextareaWidget(props: WidgetProps): JSX.Element {
74
+ const len = () => ((props.value as string) ?? '').length;
75
+ return (
76
+ <div class="flex flex-col gap-1">
77
+ <Textarea
78
+ id={props.id}
79
+ data-control
80
+ class={cn(inputBase, props.invalid && 'border-destructive dark:border-red-400/70')}
81
+ value={(props.value as string) ?? ''}
82
+ placeholder={props.placeholder}
83
+ disabled={props.disabled}
84
+ maxLength={props.field.maxLength}
85
+ {...ariaProps(props)}
86
+ onInput={(e) => props.onInput(e.currentTarget.value)}
87
+ onBlur={props.onBlur}
88
+ />
89
+ <Show when={props.field.maxLength !== undefined}>
90
+ <span class="self-end text-xs text-muted-foreground">
91
+ {len()}/{props.field.maxLength}
92
+ </span>
93
+ </Show>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export function NumberWidget(props: WidgetProps): JSX.Element {
99
+ const step = () => props.field['x-kc-step'] ?? (props.field.type === 'integer' ? 1 : undefined);
100
+ return (
101
+ <input
102
+ id={props.id}
103
+ data-control
104
+ type="number"
105
+ class={cn(inputBase, props.invalid && 'border-destructive dark:border-red-400/70')}
106
+ value={props.value === undefined || props.value === null ? '' : String(props.value)}
107
+ placeholder={props.placeholder}
108
+ disabled={props.disabled}
109
+ min={props.field.minimum}
110
+ max={props.field.maximum}
111
+ step={step()}
112
+ {...ariaProps(props)}
113
+ onInput={(e) => props.onInput(e.currentTarget.value)}
114
+ onBlur={props.onBlur}
115
+ />
116
+ );
117
+ }
118
+
119
+ export function SliderWidget(props: WidgetProps): JSX.Element {
120
+ const min = () => props.field.minimum ?? 0;
121
+ const max = () => props.field.maximum ?? 100;
122
+ const step = () => props.field['x-kc-step'] ?? (props.field.type === 'integer' ? 1 : undefined);
123
+ const current = () => (props.value === undefined || props.value === null ? min() : Number(props.value));
124
+ const fill = (): string => {
125
+ const lo = min();
126
+ const hi = max();
127
+ return hi > lo ? `${((current() - lo) / (hi - lo)) * 100}%` : '0%';
128
+ };
129
+ return (
130
+ <div class="flex items-center gap-3">
131
+ <input
132
+ id={props.id}
133
+ data-control
134
+ type="range"
135
+ class="kc-range"
136
+ style={{ '--kc-range-fill': fill() }}
137
+ value={current()}
138
+ min={min()}
139
+ max={max()}
140
+ step={step()}
141
+ disabled={props.disabled}
142
+ aria-valuetext={String(current())}
143
+ {...ariaProps(props)}
144
+ onInput={(e) => props.onInput(Number(e.currentTarget.value))}
145
+ onBlur={props.onBlur}
146
+ />
147
+ <span class="min-w-9 shrink-0 rounded-md bg-background px-2 py-1 text-center text-sm font-medium tabular-nums text-foreground shadow-sm">
148
+ {current()}
149
+ </span>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ export function RatingWidget(props: WidgetProps): JSX.Element {
155
+ const max = () => props.field.maximum ?? 5;
156
+ const current = () => Number(props.value ?? 0);
157
+ const stars = () => Array.from({ length: max() }, (_, i) => i + 1);
158
+ const onKey = (e: KeyboardEvent): void => {
159
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
160
+ e.preventDefault();
161
+ props.onInput(Math.min(max(), current() + 1));
162
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
163
+ e.preventDefault();
164
+ props.onInput(Math.max(props.field.minimum ?? 1, current() - 1));
165
+ }
166
+ };
167
+ return (
168
+ <div
169
+ role="radiogroup"
170
+ aria-label={props.label}
171
+ data-control
172
+ tabindex={0}
173
+ class="flex items-center gap-1 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
174
+ {...ariaProps(props)}
175
+ onKeyDown={onKey}
176
+ >
177
+ <For each={stars()}>
178
+ {(n) => (
179
+ <button
180
+ type="button"
181
+ role="radio"
182
+ aria-checked={current() === n}
183
+ aria-label={`${n} ${n === 1 ? 'star' : 'stars'}`}
184
+ tabindex={-1}
185
+ disabled={props.disabled}
186
+ class="rounded p-0.5 text-muted-foreground hover:text-foreground"
187
+ onClick={() => props.onInput(n)}
188
+ >
189
+ <Star
190
+ size={20}
191
+ class={cn(n <= current() ? 'fill-current text-[var(--color-primary)]' : '')}
192
+ aria-hidden="true"
193
+ />
194
+ </button>
195
+ )}
196
+ </For>
197
+ </div>
198
+ );
199
+ }
200
+
201
+ export function SwitchWidget(props: WidgetProps): JSX.Element {
202
+ const on = () => props.value === true;
203
+ const toggle = (): void => {
204
+ if (props.disabled) return;
205
+ props.onInput(!on());
206
+ props.onBlur();
207
+ };
208
+ return (
209
+ <button
210
+ id={props.id}
211
+ data-control
212
+ type="button"
213
+ role="switch"
214
+ aria-checked={on()}
215
+ aria-label={props.label}
216
+ disabled={props.disabled}
217
+ class={cn(
218
+ 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
219
+ on() ? 'bg-[var(--color-primary)]' : 'bg-muted',
220
+ )}
221
+ {...ariaProps(props)}
222
+ onClick={toggle}
223
+ >
224
+ <span
225
+ class={cn(
226
+ 'inline-block h-5 w-5 transform rounded-full bg-background shadow transition-transform',
227
+ on() ? 'translate-x-5' : 'translate-x-0.5',
228
+ )}
229
+ />
230
+ </button>
231
+ );
232
+ }
233
+
234
+ export function CheckboxWidget(props: WidgetProps): JSX.Element {
235
+ return (
236
+ <label class="-mx-1.5 inline-flex cursor-pointer items-center gap-2.5 rounded-md px-1.5 py-1.5 text-sm text-foreground transition-colors hover:bg-muted/60">
237
+ <input
238
+ id={props.id}
239
+ data-control
240
+ type="checkbox"
241
+ class="kc-checkbox"
242
+ checked={props.value === true}
243
+ disabled={props.disabled}
244
+ {...ariaProps(props)}
245
+ onChange={(e) => {
246
+ props.onInput(e.currentTarget.checked);
247
+ props.onBlur();
248
+ }}
249
+ />
250
+ <span>{props.label}</span>
251
+ </label>
252
+ );
253
+ }
254
+
255
+ export function RadioGroupWidget(props: WidgetProps): JSX.Element {
256
+ const options = () => (props.field.enum ?? []) as unknown[];
257
+ return (
258
+ <div
259
+ role="radiogroup"
260
+ aria-label={props.label}
261
+ data-control
262
+ class="divide-y divide-border overflow-hidden rounded-lg border border-input"
263
+ {...ariaProps(props)}
264
+ >
265
+ <For each={options()}>
266
+ {(opt) => (
267
+ <label
268
+ class={cn(
269
+ 'flex cursor-pointer items-center gap-3 px-3 py-2.5 text-sm transition-colors',
270
+ props.value === opt
271
+ ? 'bg-accent font-medium text-accent-foreground'
272
+ : 'text-foreground hover:bg-muted/50',
273
+ )}
274
+ >
275
+ <input
276
+ type="radio"
277
+ name={props.id}
278
+ class="kc-radio"
279
+ value={String(opt)}
280
+ checked={props.value === opt}
281
+ disabled={props.disabled}
282
+ onChange={() => {
283
+ props.onInput(opt);
284
+ props.onBlur();
285
+ }}
286
+ />
287
+ <span>{String(opt)}</span>
288
+ </label>
289
+ )}
290
+ </For>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ export function SelectWidget(props: WidgetProps): JSX.Element {
296
+ const options = () => (props.field.enum ?? []) as unknown[];
297
+ return (
298
+ <select
299
+ id={props.id}
300
+ data-control
301
+ class={cn(inputBase, props.invalid && 'border-destructive dark:border-red-400/70')}
302
+ disabled={props.disabled}
303
+ {...ariaProps(props)}
304
+ onChange={(e) => {
305
+ props.onInput(e.currentTarget.value);
306
+ props.onBlur();
307
+ }}
308
+ >
309
+ <option value="" disabled selected={props.value === undefined || props.value === ''}>
310
+ {props.placeholder ?? 'Select…'}
311
+ </option>
312
+ <For each={options()}>
313
+ {(opt) => (
314
+ <option value={String(opt)} selected={props.value === opt}>
315
+ {String(opt)}
316
+ </option>
317
+ )}
318
+ </For>
319
+ </select>
320
+ );
321
+ }
322
+
323
+ function itemEnum(field: FormField): unknown[] {
324
+ const items = field.items;
325
+ if (items && 'enum' in items && Array.isArray(items.enum)) return items.enum;
326
+ return [];
327
+ }
328
+
329
+ export function CheckboxGroupWidget(props: WidgetProps): JSX.Element {
330
+ const selected = () => (Array.isArray(props.value) ? (props.value as unknown[]) : []);
331
+ const toggle = (opt: unknown): void => {
332
+ const set = selected();
333
+ const next = set.includes(opt) ? set.filter((v) => v !== opt) : [...set, opt];
334
+ props.onInput(next);
335
+ props.onBlur();
336
+ };
337
+ return (
338
+ <div
339
+ role="group"
340
+ aria-label={props.label}
341
+ data-control
342
+ class="divide-y divide-border overflow-hidden rounded-lg border border-input"
343
+ {...ariaProps(props)}
344
+ >
345
+ <For each={itemEnum(props.field)}>
346
+ {(opt) => (
347
+ <label
348
+ class={cn(
349
+ 'flex cursor-pointer items-center gap-3 px-3 py-2.5 text-sm transition-colors',
350
+ selected().includes(opt)
351
+ ? 'bg-accent font-medium text-accent-foreground'
352
+ : 'text-foreground hover:bg-muted/50',
353
+ )}
354
+ >
355
+ <input
356
+ type="checkbox"
357
+ class="kc-checkbox"
358
+ checked={selected().includes(opt)}
359
+ disabled={props.disabled}
360
+ onChange={() => toggle(opt)}
361
+ />
362
+ <span>{String(opt)}</span>
363
+ </label>
364
+ )}
365
+ </For>
366
+ </div>
367
+ );
368
+ }
369
+
370
+ export function MultiSelectWidget(props: WidgetProps): JSX.Element {
371
+ const selected = () => (Array.isArray(props.value) ? (props.value as unknown[]) : []);
372
+ return (
373
+ <select
374
+ id={props.id}
375
+ data-control
376
+ multiple
377
+ aria-label={props.label}
378
+ class={cn(inputBase, 'min-h-[6rem]', props.invalid && 'border-destructive dark:border-red-400/70')}
379
+ disabled={props.disabled}
380
+ {...ariaProps(props)}
381
+ onChange={(e) => {
382
+ const vals = Array.from(e.currentTarget.selectedOptions).map((o) => o.value);
383
+ props.onInput(vals);
384
+ props.onBlur();
385
+ }}
386
+ >
387
+ <For each={itemEnum(props.field)}>
388
+ {(opt) => (
389
+ <option value={String(opt)} selected={selected().includes(opt)}>
390
+ {String(opt)}
391
+ </option>
392
+ )}
393
+ </For>
394
+ </select>
395
+ );
396
+ }
397
+
398
+ export function TagListWidget(props: WidgetProps): JSX.Element {
399
+ const tags = () => (Array.isArray(props.value) ? (props.value as string[]) : []);
400
+ const [draft, setDraft] = createSignal('');
401
+ const add = (): void => {
402
+ const v = draft().trim();
403
+ if (!v) return;
404
+ props.onInput([...tags(), v]);
405
+ setDraft('');
406
+ props.onBlur();
407
+ };
408
+ const remove = (i: number): void => {
409
+ props.onInput(tags().filter((_, idx) => idx !== i));
410
+ props.onBlur();
411
+ };
412
+ return (
413
+ <div class="flex flex-col gap-2" role="group" aria-label={props.label}>
414
+ <div class="flex flex-wrap gap-1.5">
415
+ <For each={tags()}>
416
+ {(tag, i) => (
417
+ <span class="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground">
418
+ {tag}
419
+ <button
420
+ type="button"
421
+ aria-label={`Remove ${tag}`}
422
+ disabled={props.disabled}
423
+ class="text-muted-foreground hover:text-foreground"
424
+ onClick={() => remove(i())}
425
+ >
426
+
427
+ </button>
428
+ </span>
429
+ )}
430
+ </For>
431
+ </div>
432
+ <div class="flex items-center gap-2">
433
+ <input
434
+ id={props.id}
435
+ data-control
436
+ type="text"
437
+ class={cn(inputBase)}
438
+ value={draft()}
439
+ placeholder={props.placeholder ?? 'Add…'}
440
+ disabled={props.disabled}
441
+ {...ariaProps(props)}
442
+ onInput={(e) => setDraft(e.currentTarget.value)}
443
+ onKeyDown={(e) => {
444
+ if (e.key === 'Enter') {
445
+ e.preventDefault();
446
+ add();
447
+ }
448
+ }}
449
+ />
450
+ <button
451
+ type="button"
452
+ class="rounded-md border border-input px-3 py-2 text-sm text-foreground hover:bg-muted disabled:opacity-50"
453
+ disabled={props.disabled}
454
+ onClick={add}
455
+ >
456
+ Add
457
+ </button>
458
+ </div>
459
+ </div>
460
+ );
461
+ }