@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,74 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import { ConfirmCard, type ConfirmCardData } from './confirm-card';
4
+ import { componentDescription } from '../stories/docs/element-controls';
5
+ import type { CardEvent, CardHost, CardContext } from '../primitives/card-contract';
6
+
7
+ const ctx: CardContext = { theme: { mode: 'light' }, locale: 'en' };
8
+
9
+ /** Renders the Solid <ConfirmCard> with a capturing `host`, logging emitted events. */
10
+ function Demo(props: { def: ConfirmCardData; heading?: string; cardId: string }) {
11
+ const [log, setLog] = createSignal<CardEvent[]>([]);
12
+ const host: CardHost = { context: () => ctx, emit: (e) => setLog((p) => [...p, e]) };
13
+ return (
14
+ <div style={{ 'max-width': '460px', display: 'flex', 'flex-direction': 'column', gap: '12px' }}>
15
+ <ConfirmCard host={host} data={props.def} heading={props.heading} cardId={props.cardId} />
16
+ <pre
17
+ style={{
18
+ margin: 0,
19
+ 'max-height': '180px',
20
+ overflow: 'auto',
21
+ background: 'var(--color-muted, #f4f4f5)',
22
+ 'border-radius': '8px',
23
+ padding: '8px',
24
+ 'font-size': '12px',
25
+ }}
26
+ >
27
+ {log().length === 0 ? '// emitted CardEvents appear here' : JSON.stringify(log(), null, 2)}
28
+ </pre>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const APPROVE: ConfirmCardData = {
34
+ body: 'This will apply 3 pending migrations to production. This cannot be undone.',
35
+ tone: 'warning',
36
+ actions: [
37
+ { id: 'approve', label: 'Run migration', style: 'primary', default: true },
38
+ { id: 'reject', label: 'Cancel' },
39
+ ],
40
+ };
41
+
42
+ const DESTRUCTIVE: ConfirmCardData = {
43
+ body: 'Permanently delete 12 files? This cannot be undone.',
44
+ tone: 'danger',
45
+ actions: [
46
+ { id: 'delete', label: 'Delete files', style: 'destructive', default: true },
47
+ { id: 'cancel', label: 'Keep them' },
48
+ ],
49
+ };
50
+
51
+ const meta = {
52
+ title: 'Components/ConfirmCard',
53
+ component: ConfirmCard,
54
+ tags: ['autodocs'],
55
+ parameters: {
56
+ layout: 'padded',
57
+ docs: {
58
+ description: componentDescription([
59
+ 'The SolidJS layer behind `<kc-confirm>`. Pass a `host` (a `CardHost`) to receive the emitted `CardEvent`s directly (the native-host path), or wrap in a `CardProvider`. Activating an action emits the `action` verb and resolves the card.',
60
+ ]),
61
+ },
62
+ },
63
+ } satisfies Meta<typeof ConfirmCard>;
64
+
65
+ export default meta;
66
+ type Story = StoryObj<typeof ConfirmCard>;
67
+
68
+ export const ApproveReject: Story = {
69
+ render: () => <Demo def={APPROVE} heading="Run database migration?" cardId="card-approve" />,
70
+ };
71
+
72
+ export const Destructive: Story = {
73
+ render: () => <Demo def={DESTRUCTIVE} heading="Delete files?" cardId="card-delete" />,
74
+ };
@@ -0,0 +1,299 @@
1
+ import {
2
+ type JSX,
3
+ For,
4
+ Show,
5
+ splitProps,
6
+ mergeProps,
7
+ createSignal,
8
+ createMemo,
9
+ createEffect,
10
+ on,
11
+ ErrorBoundary,
12
+ } from 'solid-js';
13
+ import { cn } from '../utils/cn';
14
+ import { Button } from '../ui/button';
15
+ import { Card } from './card';
16
+ import type { CardEnvelope, CardEvent, CardHost } from '../primitives/card-contract';
17
+ import { emitCardEvent } from '../primitives/card-routing';
18
+ import { useCardHost } from '../primitives/card-host';
19
+ import { AlertTriangle, Check, X } from 'lucide-solid';
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Types (confirm.schema.json) — see src/primitives/card-schemas/confirm.schema.json
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ export type ConfirmActionStyle = 'primary' | 'default' | 'destructive';
26
+ export type ConfirmTone = 'default' | 'warning' | 'danger';
27
+
28
+ export interface ConfirmAction {
29
+ id: string;
30
+ label: string;
31
+ style?: ConfirmActionStyle;
32
+ payload?: unknown;
33
+ default?: boolean;
34
+ }
35
+
36
+ export interface ConfirmCardData {
37
+ heading?: string;
38
+ body?: string;
39
+ tone?: ConfirmTone;
40
+ actions: ConfirmAction[]; // 1..4
41
+ dismissible?: boolean;
42
+ }
43
+
44
+ export type ConfirmCardEnvelope = CardEnvelope<'confirm', ConfirmCardData>;
45
+
46
+ export const CONFIRM_CARD_TYPE = 'confirm' as const;
47
+
48
+ const VALID_STYLES = new Set<ConfirmActionStyle>(['primary', 'default', 'destructive']);
49
+
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // Pure helpers (unit-tested in isolation).
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ /** Map an action's `style` to a Button variant (falls back + warns on unknown). */
55
+ export function buttonVariantForStyle(
56
+ style: ConfirmActionStyle | undefined,
57
+ ): 'default' | 'outline' | 'destructive' {
58
+ switch (style) {
59
+ case 'primary':
60
+ return 'default';
61
+ case 'destructive':
62
+ return 'destructive';
63
+ case 'default':
64
+ case undefined:
65
+ return 'outline';
66
+ default:
67
+ // eslint-disable-next-line no-console
68
+ console.warn(`[kc-confirm] unknown action style "${style as string}"; using default`);
69
+ return 'outline';
70
+ }
71
+ }
72
+
73
+ /** De-dupe actions by id (first wins) and validate their shape. Returns the usable
74
+ * list + an optional error message when there's nothing renderable. */
75
+ export function normalizeActions(actions: unknown): {
76
+ actions: ConfirmAction[];
77
+ error?: string;
78
+ } {
79
+ if (!Array.isArray(actions) || actions.length === 0) {
80
+ return { actions: [], error: "This card couldn't be displayed." };
81
+ }
82
+ const seen = new Set<string>();
83
+ const out: ConfirmAction[] = [];
84
+ for (const a of actions) {
85
+ if (!a || typeof a !== 'object') continue;
86
+ const action = a as Partial<ConfirmAction>;
87
+ if (typeof action.id !== 'string' || action.id.length === 0) continue;
88
+ if (typeof action.label !== 'string' || action.label.length === 0) continue;
89
+ if (seen.has(action.id)) {
90
+ // eslint-disable-next-line no-console
91
+ console.warn(`[kc-confirm] duplicate action id "${action.id}" ignored`);
92
+ continue;
93
+ }
94
+ seen.add(action.id);
95
+ const style = VALID_STYLES.has(action.style as ConfirmActionStyle)
96
+ ? (action.style as ConfirmActionStyle)
97
+ : undefined;
98
+ if (action.style !== undefined && style === undefined) {
99
+ // eslint-disable-next-line no-console
100
+ console.warn(`[kc-confirm] unknown action style "${String(action.style)}"; using default`);
101
+ }
102
+ out.push({
103
+ id: action.id,
104
+ label: action.label,
105
+ style,
106
+ payload: action.payload,
107
+ default: action.default === true,
108
+ });
109
+ }
110
+ if (out.length === 0) return { actions: [], error: "This card couldn't be displayed." };
111
+ return { actions: out };
112
+ }
113
+
114
+ /** The id of the default action (first with `default:true`), if any. */
115
+ export function defaultActionId(actions: ConfirmAction[]): string | undefined {
116
+ return actions.find((a) => a.default)?.id;
117
+ }
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // The <ConfirmCard> component.
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ export interface ConfirmCardProps {
124
+ /** The confirm definition (CardEnvelope.data). */
125
+ data?: ConfirmCardData;
126
+ /** The card id used to correlate every emitted CardEvent. */
127
+ cardId?: string;
128
+ /** The envelope title rendered in the card chrome. */
129
+ heading?: string;
130
+ /** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
131
+ * bubbling `kc-card` CustomEvent off `hostElement`). */
132
+ host?: CardHost;
133
+ /** The custom-element host node, for the bubbling `kc-card` fallback emit. */
134
+ hostElement?: HTMLElement;
135
+ /** Focus the default action on mount. Default OFF (no focus-stealing mid-stream). */
136
+ autofocus?: boolean;
137
+ class?: string;
138
+ }
139
+
140
+ /**
141
+ * `ConfirmCard` — a named-intent approval card. Renders a title + body + a small
142
+ * set of action buttons inside `Card` chrome. Activating an action emits the Card
143
+ * contract's `action` verb (`{ kind:'action', cardId, action, payload }`) and
144
+ * resolves the card (other actions disabled, the chosen one marked) so the same
145
+ * approval can't double-fire. Emits `ready` on mount, `dismiss` for the optional
146
+ * close affordance, and `error` for an unusable definition (inline error state).
147
+ */
148
+ export function ConfirmCard(props: ConfirmCardProps): JSX.Element {
149
+ const merged = mergeProps({ cardId: 'kc-confirm', autofocus: false }, props);
150
+ const [local] = splitProps(merged, [
151
+ 'data',
152
+ 'cardId',
153
+ 'heading',
154
+ 'host',
155
+ 'hostElement',
156
+ 'autofocus',
157
+ 'class',
158
+ ]);
159
+
160
+ const ctxHost = useCardHost();
161
+
162
+ const emit = (event: CardEvent): void => {
163
+ const h = local.host ?? ctxHost;
164
+ if (h) h.emit(event);
165
+ else if (local.hostElement) emitCardEvent(local.hostElement, event);
166
+ };
167
+
168
+ const normalized = createMemo(() => normalizeActions(local.data?.actions));
169
+ const valid = createMemo(() => normalized().error === undefined);
170
+ const errorMessage = createMemo(() => normalized().error ?? '');
171
+ const actions = createMemo(() => normalized().actions);
172
+ const tone = (): ConfirmTone => local.data?.tone ?? 'default';
173
+ const isDanger = () => tone() === 'danger';
174
+ const defaultId = createMemo(() => defaultActionId(actions()));
175
+
176
+ const [resolved, setResolved] = createSignal<string | undefined>(undefined);
177
+
178
+ // Reset resolved state whenever a NEW definition arrives.
179
+ createEffect(on(() => local.data, () => setResolved(undefined)));
180
+
181
+ // ready / error lifecycle emits.
182
+ createEffect(
183
+ on(valid, (ok) => {
184
+ if (ok) emit({ kind: 'ready', cardId: local.cardId });
185
+ else emit({ kind: 'error', cardId: local.cardId, message: errorMessage() });
186
+ }),
187
+ );
188
+
189
+ const onAction = (action: ConfirmAction): void => {
190
+ if (resolved() !== undefined) return; // single-shot
191
+ emit({
192
+ kind: 'action',
193
+ cardId: local.cardId,
194
+ action: action.id,
195
+ ...(action.payload !== undefined ? { payload: action.payload } : {}),
196
+ });
197
+ setResolved(action.id);
198
+ };
199
+
200
+ const onDismiss = (): void => emit({ kind: 'dismiss', cardId: local.cardId });
201
+
202
+ let bodyRef: HTMLDivElement | undefined;
203
+
204
+ // Surface the resolved action id for host styling.
205
+ createEffect(() => {
206
+ const el = local.hostElement;
207
+ if (!el) return;
208
+ const id = resolved();
209
+ if (id !== undefined) el.setAttribute('data-kc-resolved', id);
210
+ else el.removeAttribute('data-kc-resolved');
211
+ });
212
+
213
+ return (
214
+ <Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
215
+ <ErrorBoundary
216
+ fallback={() => {
217
+ emit({ kind: 'error', cardId: local.cardId, message: 'The card failed to render.' });
218
+ return <Card heading={local.heading} errorMessage="The card failed to render." />;
219
+ }}
220
+ >
221
+ <Card
222
+ heading={local.heading}
223
+ actions={
224
+ <div class="flex w-full flex-wrap items-center justify-between gap-2">
225
+ <Show when={local.data?.dismissible === true}>
226
+ <Button
227
+ type="button"
228
+ variant="ghost"
229
+ size="icon-sm"
230
+ aria-label="Dismiss"
231
+ onClick={onDismiss}
232
+ >
233
+ <X size={16} aria-hidden="true" />
234
+ </Button>
235
+ </Show>
236
+ <div class="ml-auto flex flex-wrap items-center gap-2">
237
+ <For each={actions()}>
238
+ {(action) => {
239
+ const isChosen = () => resolved() === action.id;
240
+ return (
241
+ <Button
242
+ type="button"
243
+ variant={buttonVariantForStyle(action.style)}
244
+ disabled={resolved() !== undefined && !isChosen()}
245
+ aria-pressed={isChosen() ? 'true' : undefined}
246
+ data-action-id={action.id}
247
+ data-kc-default={action.default ? 'true' : undefined}
248
+ ref={(el) => {
249
+ if (local.autofocus && action.id === defaultId()) {
250
+ queueMicrotask(() => el.focus());
251
+ }
252
+ }}
253
+ onClick={() => onAction(action)}
254
+ >
255
+ <Show when={isChosen()}>
256
+ <Check size={16} aria-hidden="true" />
257
+ </Show>
258
+ {action.label}
259
+ </Button>
260
+ );
261
+ }}
262
+ </For>
263
+ </div>
264
+ </div>
265
+ }
266
+ >
267
+ <div
268
+ ref={bodyRef}
269
+ class={cn('flex flex-col gap-2', local.class)}
270
+ onKeyDown={(e) => {
271
+ // Enter on the card body (not on a focused button) invokes the default action.
272
+ if (e.key !== 'Enter') return;
273
+ const target = e.target as HTMLElement;
274
+ if (target.tagName === 'BUTTON') return;
275
+ const id = defaultId();
276
+ if (id === undefined) return;
277
+ const action = actions().find((a) => a.id === id);
278
+ if (action) onAction(action);
279
+ }}
280
+ tabindex={defaultId() !== undefined ? 0 : undefined}
281
+ >
282
+ <Show when={isDanger()}>
283
+ <div class="flex items-center gap-2 text-sm font-medium text-destructive dark:text-red-400">
284
+ <AlertTriangle size={16} class="shrink-0" aria-hidden="true" />
285
+ <span>Heads up</span>
286
+ </div>
287
+ </Show>
288
+ <Show when={local.data?.heading}>
289
+ <p class="text-sm font-semibold text-foreground">{local.data?.heading}</p>
290
+ </Show>
291
+ <Show when={local.data?.body}>
292
+ <p class="text-sm text-foreground">{local.data?.body}</p>
293
+ </Show>
294
+ </div>
295
+ </Card>
296
+ </ErrorBoundary>
297
+ </Show>
298
+ );
299
+ }
@@ -11,6 +11,7 @@ import {
11
11
  ContextReasoningUsage,
12
12
  ContextCacheUsage,
13
13
  } from './context';
14
+ import { componentDescription } from '../stories/docs/element-controls';
14
15
 
15
16
  /**
16
17
  * Story for the compound `Context` family. `Context` is the root provider that
@@ -25,14 +26,12 @@ const meta = {
25
26
  parameters: {
26
27
  layout: 'padded',
27
28
  docs: {
28
- description: {
29
- component: [
30
- 'A model context-window usage indicator: a hover-card trigger showing the used-percent ring, with a popover breaking down token usage (input / output / reasoning / cache) and estimated cost.',
31
- '**When to use:** to surface how much of a model\'s context window a conversation has consumed and roughly what it costs near the prompt input or in a chat header.',
32
- '**How to use:** wrap the composition in `<Context>` and pass `usedTokens` / `maxTokens` (plus optional `inputTokens`, `outputTokens`, `reasoningTokens`, `cacheTokens`, `estimatedCost`). Compose `ContextTrigger`, `ContextContent` (with `Header`/`Body`/`Footer`), and the usage rows.',
33
- '**Placement:** chat toolbars, prompt-input action bars, and conversation headers.',
34
- ].join('\n\n'),
35
- },
29
+ description: componentDescription([
30
+ 'A model context-window usage indicator: a hover-card trigger showing the used-percent ring, with a popover breaking down token usage (input / output / reasoning / cache) and estimated cost.',
31
+ '**When to use:** to surface how much of a model\'s context window a conversation has consumed and roughly what it costs near the prompt input or in a chat header.',
32
+ '**How to use:** wrap the composition in `<Context>` and pass `usedTokens` / `maxTokens` (plus optional `inputTokens`, `outputTokens`, `reasoningTokens`, `cacheTokens`, `estimatedCost`). Compose `ContextTrigger`, `ContextContent` (with `Header`/`Body`/`Footer`), and the usage rows.',
33
+ '**Placement:** chat toolbars, prompt-input action bars, and conversation headers.',
34
+ ]),
36
35
  controls: { exclude: ['use:eventListener'] },
37
36
  },
38
37
  },
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { fn } from 'storybook/test';
3
3
  import { ConversationItem } from './conversation-item';
4
+ import { componentDescription } from '../stories/docs/element-controls';
4
5
 
5
6
  const baseConversation = {
6
7
  id: '1',
@@ -22,14 +23,12 @@ const meta = {
22
23
  parameters: {
23
24
  layout: 'padded',
24
25
  docs: {
25
- description: {
26
- component: [
27
- 'A single conversation row shows the title and message count, truncating long titles, with a highlighted active state.',
28
- '**When to use:** as the leaf item inside a chat history sidebar. Usually rendered for you by `ConversationList`; use it directly to build a custom list.',
29
- '**How to use:** pass a `conversation` summary, an `isActive` flag, and an `onSelect(id)` handler fired when the row is clicked.',
30
- '**Placement:** inside a conversation/chat history sidebar list.',
31
- ].join('\n\n'),
32
- },
26
+ description: componentDescription([
27
+ 'A single conversation row — shows the title and message count, truncating long titles, with a highlighted active state.',
28
+ '**When to use:** as the leaf item inside a chat history sidebar. Usually rendered for you by `ConversationList`; use it directly to build a custom list.',
29
+ '**How to use:** pass a `conversation` summary, an `isActive` flag, and an `onSelect(id)` handler fired when the row is clicked.',
30
+ '**Placement:** inside a conversation/chat history sidebar list.',
31
+ ]),
33
32
  controls: { exclude: ['use:eventListener'] },
34
33
  },
35
34
  },
@@ -9,8 +9,8 @@ export function ConversationItem(props: ConversationItemProps) {
9
9
  return (
10
10
  <button data-conversation-id={local.conversation.id} onClick={() => local.onSelect(local.conversation.id)}
11
11
  class={cn('w-full text-left rounded-lg px-2.5 py-2 transition-colors', local.isActive ? 'bg-muted' : 'hover:bg-muted/50', local.class)}>
12
- <div class={cn('truncate text-sm', local.isActive ? 'text-foreground font-medium' : 'text-muted-foreground')}>{local.conversation.title}</div>
13
- <div class="text-muted-foreground truncate mt-0.5 text-xs">{local.conversation.messageCount} messages</div>
12
+ <div class={cn('truncate text-sm', local.isActive ? 'text-foreground font-medium' : 'text-foreground/80')}>{local.conversation.title}</div>
13
+ <div class={cn('truncate mt-0.5 text-xs', local.isActive ? 'text-foreground/70' : 'text-muted-foreground')}>{local.conversation.messageCount} messages</div>
14
14
  </button>
15
15
  );
16
16
  }
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { fn } from 'storybook/test';
3
3
  import { ConversationList, type ConversationListProps } from './conversation-list';
4
4
  import type { ConversationSummary, ConversationGroup } from '../types';
5
+ import { componentDescription } from '../stories/docs/element-controls';
5
6
 
6
7
  const scope = { type: 'document' as const };
7
8
 
@@ -31,14 +32,12 @@ const meta = {
31
32
  parameters: {
32
33
  layout: 'padded',
33
34
  docs: {
34
- description: {
35
- component: [
36
- 'A full chat-history sidebar: header (sidebar toggle + new chat), a built-in search box that filters by title, and conversations bucketed into collapsible, count-badged groups.',
37
- '**When to use:** as the left-hand navigation for a chat app browsing, searching, and switching between past conversations.',
38
- '**How to use:** pass `groups` and `conversations` arrays, the `activeId`, and handlers `onSelect(id)` / `onNewChat()` (plus optional `onToggleSidebar()`). Give it a sized, overflow-hidden container.',
39
- '**Placement:** the persistent left sidebar of a chat layout.',
40
- ].join('\n\n'),
41
- },
35
+ description: componentDescription([
36
+ 'A full chat-history sidebar: header (sidebar toggle + new chat), a built-in search box that filters by title, and conversations bucketed into collapsible, count-badged groups.',
37
+ '**When to use:** as the left-hand navigation for a chat app browsing, searching, and switching between past conversations.',
38
+ '**How to use:** pass `groups` and `conversations` arrays, the `activeId`, and handlers `onSelect(id)` / `onNewChat()` (plus optional `onToggleSidebar()`). Give it a sized, overflow-hidden container.',
39
+ '**Placement:** the persistent left sidebar of a chat layout.',
40
+ ]),
42
41
  controls: { exclude: ['use:eventListener'] },
43
42
  },
44
43
  },
@@ -83,7 +83,7 @@ function GroupSection(props: { name: string; count: number; conversations: Conve
83
83
  const [open, setOpen] = createSignal(true);
84
84
  return (
85
85
  <Collapsible open={open()} onOpenChange={setOpen}>
86
- <CollapsibleTrigger class="flex items-center gap-1.5 w-full px-1.5 py-1 rounded-md bg-muted/30 text-[13px] text-muted-foreground font-medium hover:bg-muted/50 transition-colors cursor-pointer mt-1.5">
86
+ <CollapsibleTrigger class="flex items-center gap-1.5 w-full px-1.5 py-1 rounded-md bg-muted text-[13px] text-foreground/80 font-medium hover:bg-muted/70 transition-colors cursor-pointer mt-1.5">
87
87
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
88
88
  class={cn('transition-transform', !open() && '-rotate-90')}><polyline points="6 9 12 15 18 9"/></svg>
89
89
  <span>{props.name}</span>