@kitnai/chat 0.6.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 (211) hide show
  1. package/README.md +9 -9
  2. package/dist/custom-elements.json +1676 -881
  3. package/dist/kitn-chat.es.js +36 -36
  4. package/dist/llms/llms-full.txt +316 -155
  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 +382 -193
  19. package/frameworks/react/runtime.tsx +2 -2
  20. package/llms-full.txt +316 -155
  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 -11
  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 -13
  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 -10
  83. package/src/elements/chat-scope-picker.tsx +4 -4
  84. package/src/elements/{kitn-chat-workspace.stories.tsx → chat-workspace.stories.tsx} +71 -29
  85. package/src/elements/chat-workspace.tsx +29 -3
  86. package/src/elements/{kitn-chat.stories.tsx → chat.stories.tsx} +61 -16
  87. package/src/elements/chat.tsx +23 -2
  88. package/src/elements/{kitn-checkpoint.stories.tsx → checkpoint.stories.tsx} +11 -11
  89. package/src/elements/checkpoint.tsx +4 -4
  90. package/src/elements/{kitn-code-block.stories.tsx → code-block.stories.tsx} +10 -10
  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 -10
  97. package/src/elements/context-meter.tsx +3 -3
  98. package/src/elements/{kitn-conversation-list.stories.tsx → conversation-list.stories.tsx} +35 -22
  99. package/src/elements/conversation-list.tsx +11 -2
  100. package/src/elements/css.ts +1 -1
  101. package/src/elements/define.tsx +10 -10
  102. package/src/elements/element-meta.json +2649 -0
  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 -12
  107. package/src/elements/empty.tsx +3 -3
  108. package/src/elements/{kitn-feedback-bar.stories.tsx → feedback-bar.stories.tsx} +11 -11
  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 -12
  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 -10
  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 -11
  121. package/src/elements/loader.tsx +3 -3
  122. package/src/elements/{kitn-markdown.stories.tsx → markdown.stories.tsx} +10 -10
  123. package/src/elements/markdown.tsx +3 -3
  124. package/src/elements/{kitn-message-skills.stories.tsx → message-skills.stories.tsx} +10 -10
  125. package/src/elements/message-skills.tsx +3 -3
  126. package/src/elements/{kitn-message.stories.tsx → message.stories.tsx} +12 -12
  127. package/src/elements/message.tsx +5 -5
  128. package/src/elements/{kitn-model-switcher.stories.tsx → model-switcher.stories.tsx} +10 -10
  129. package/src/elements/model-switcher.tsx +5 -5
  130. package/src/elements/{kitn-prompt-input.stories.tsx → prompt-input.stories.tsx} +41 -19
  131. package/src/elements/prompt-input.tsx +5 -5
  132. package/src/elements/{kitn-prompt-suggestions.stories.tsx → prompt-suggestions.stories.tsx} +13 -13
  133. package/src/elements/prompt-suggestions.tsx +4 -4
  134. package/src/elements/{kitn-reasoning.stories.tsx → reasoning.stories.tsx} +10 -10
  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 -10
  140. package/src/elements/response-stream.tsx +4 -4
  141. package/src/elements/{kitn-source-list.stories.tsx → source-list.stories.tsx} +11 -11
  142. package/src/elements/{kitn-source.stories.tsx → source.stories.tsx} +12 -12
  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 -10
  148. package/src/elements/text-shimmer.tsx +3 -3
  149. package/src/elements/{kitn-thinking-bar.stories.tsx → thinking-bar.stories.tsx} +11 -11
  150. package/src/elements/thinking-bar.tsx +5 -5
  151. package/src/elements/{kitn-tool.stories.tsx → tool.stories.tsx} +10 -10
  152. package/src/elements/tool.tsx +3 -3
  153. package/src/elements/{kitn-voice-input.stories.tsx → voice-input.stories.tsx} +10 -10
  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 +60 -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
@@ -92,7 +92,7 @@ function renderHighlighted(text: string, highlight: string) {
92
92
  const index = textLower.indexOf(highlightLower);
93
93
 
94
94
  if (index === -1) {
95
- return <span class="text-muted-foreground whitespace-pre-wrap">{text}</span>;
95
+ return <span class="text-foreground/70 whitespace-pre-wrap">{text}</span>;
96
96
  }
97
97
 
98
98
  const before = text.substring(0, index);
@@ -102,11 +102,11 @@ function renderHighlighted(text: string, highlight: string) {
102
102
  return (
103
103
  <>
104
104
  <Show when={before}>
105
- <span class="text-muted-foreground whitespace-pre-wrap">{before}</span>
105
+ <span class="text-foreground/70 whitespace-pre-wrap">{before}</span>
106
106
  </Show>
107
107
  <span class="text-primary font-medium whitespace-pre-wrap">{matched}</span>
108
108
  <Show when={after}>
109
- <span class="text-muted-foreground whitespace-pre-wrap">{after}</span>
109
+ <span class="text-foreground/70 whitespace-pre-wrap">{after}</span>
110
110
  </Show>
111
111
  </>
112
112
  );
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { fn } from 'storybook/test';
3
3
  import { createSignal } from 'solid-js';
4
4
  import { Reasoning, ReasoningTrigger, ReasoningContent } from './reasoning';
5
+ import { componentDescription } from '../stories/docs/element-controls';
5
6
 
6
7
  const meta = {
7
8
  title: 'Components/Reasoning',
@@ -10,14 +11,12 @@ const meta = {
10
11
  parameters: {
11
12
  layout: 'padded',
12
13
  docs: {
13
- description: {
14
- component: [
15
- 'A collapsible disclosure for a model\'s thinking/reasoning. Compose `Reasoning` (root) with `ReasoningTrigger` (the toggle) and `ReasoningContent` (the body, with optional markdown).',
16
- '**When to use:** to surface an assistant\'s chain-of-thought or scratch reasoning that should be available but collapsed by default. It can auto-open while streaming and auto-close when streaming ends.',
17
- '**How to use:** wrap a trigger and content in `Reasoning`. Leave it uncontrolled, or drive it with `open` + `onOpenChange`. Set `isStreaming` to auto-open during generation. Pass `markdown` on `ReasoningContent` to render a markdown string.',
18
- '**Placement:** inside or above an assistant message, before the final answer.',
19
- ].join('\n\n'),
20
- },
14
+ description: componentDescription([
15
+ 'A collapsible disclosure for a model\'s thinking/reasoning. Compose `Reasoning` (root) with `ReasoningTrigger` (the toggle) and `ReasoningContent` (the body, with optional markdown).',
16
+ '**When to use:** to surface an assistant\'s chain-of-thought or scratch reasoning that should be available but collapsed by default. It can auto-open while streaming and auto-close when streaming ends.',
17
+ '**How to use:** wrap a trigger and content in `Reasoning`. Leave it uncontrolled, or drive it with `open` + `onOpenChange`. Set `isStreaming` to auto-open during generation. Pass `markdown` on `ReasoningContent` to render a markdown string.',
18
+ '**Placement:** inside or above an assistant message, before the final answer.',
19
+ ]),
21
20
  controls: { exclude: ['use:eventListener'] },
22
21
  },
23
22
  },
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { For } from 'solid-js';
3
3
  import { ScrollButton } from './scroll-button';
4
4
  import { ChatContainerRoot, ChatContainerContent } from './chat-container';
5
+ import { componentDescription } from '../stories/docs/element-controls';
5
6
 
6
7
  /**
7
8
  * `ScrollButton` reads scroll state from the surrounding `ChatContainerRoot`
@@ -37,14 +38,12 @@ const meta = {
37
38
  parameters: {
38
39
  layout: 'padded',
39
40
  docs: {
40
- description: {
41
- component: [
42
- 'A floating "scroll to bottom" button wired to the enclosing `ChatContainerRoot`. It calls `scrollToBottom()` on click and animates in/out based on `isAtBottom`.',
43
- '**When to use:** in a scrollable message log, to let the user jump back to the latest message after scrolling up. It hides itself automatically while pinned to the bottom.',
44
- '**How to use:** render it inside a `ChatContainerRoot` (it consumes that context). Position it with absolute layout and optionally restyle via `variant`, `size`, and `class`.',
45
- '**Placement:** overlaid near the bottom-center of the chat message area.',
46
- ].join('\n\n'),
47
- },
41
+ description: componentDescription([
42
+ 'A floating "scroll to bottom" button wired to the enclosing `ChatContainerRoot`. It calls `scrollToBottom()` on click and animates in/out based on `isAtBottom`.',
43
+ '**When to use:** in a scrollable message log, to let the user jump back to the latest message after scrolling up. It hides itself automatically while pinned to the bottom.',
44
+ '**How to use:** render it inside a `ChatContainerRoot` (it consumes that context). Position it with absolute layout and optionally restyle via `variant`, `size`, and `class`.',
45
+ '**Placement:** overlaid near the bottom-center of the chat message area.',
46
+ ]),
48
47
  controls: { exclude: ['use:eventListener'] },
49
48
  },
50
49
  },
@@ -4,6 +4,7 @@ import { createSignal } from "solid-js";
4
4
  import { SlashCommand, type SlashCommandItem } from "./slash-command";
5
5
  import { PromptInput, PromptInputTextarea } from "./prompt-input";
6
6
  import { ChatConfig } from "../primitives/chat-config";
7
+ import { componentDescription } from '../stories/docs/element-controls';
7
8
 
8
9
  const skillCommands: SlashCommandItem[] = [
9
10
  { id: "caveman", label: "Caveman", description: "Ultra-compressed terse responses", category: "Skills" },
@@ -43,7 +44,7 @@ function SlashDemo(props: { commands: SlashCommandItem[]; activeIds?: string[];
43
44
  />
44
45
  </PromptInput>
45
46
  </div>
46
- <p class="text-xs text-muted-foreground/40 mt-2 text-center">
47
+ <p class="text-xs text-muted-foreground mt-2 text-center">
47
48
  Type <code class="bg-muted px-1 rounded">/</code> to see commands. Arrow keys + Tab/Enter to select.
48
49
  </p>
49
50
  </div>
@@ -58,14 +59,12 @@ const meta = {
58
59
  parameters: {
59
60
  layout: "centered",
60
61
  docs: {
61
- description: {
62
- component: [
63
- "A keyboard-navigable command palette that pops above a `PromptInput` when the user types `/`. Filters and groups commands by category, with arrow-key navigation and Tab/Enter to select.",
64
- "**When to use:** to offer slash commands or toggleable skills/modes directly from the prompt input as the user types `/name`.",
65
- "**How to use:** render it as a child of `PromptInput` (it consumes that context). Pass `commands`, handle `onSelect`, and optionally pass `activeIds` to mark active toggles. Set `compact={false}` for two-line rows.",
66
- "**Placement:** inside the relative-positioned `PromptInput`, absolutely anchored to the top of the textarea.",
67
- ].join("\n\n"),
68
- },
62
+ description: componentDescription([
63
+ "A keyboard-navigable command palette that pops above a `PromptInput` when the user types `/`. Filters and groups commands by category, with arrow-key navigation and Tab/Enter to select.",
64
+ "**When to use:** to offer slash commands or toggleable skills/modes directly from the prompt input as the user types `/name`.",
65
+ "**How to use:** render it as a child of `PromptInput` (it consumes that context). Pass `commands`, handle `onSelect`, and optionally pass `activeIds` to mark active toggles. Set `compact={false}` for two-line rows.",
66
+ "**Placement:** inside the relative-positioned `PromptInput`, absolutely anchored to the top of the textarea.",
67
+ ]),
69
68
  controls: { exclude: ["use:eventListener"] },
70
69
  },
71
70
  },
@@ -187,8 +187,8 @@ function SlashCommand(props: SlashCommandProps) {
187
187
  "w-full flex items-center gap-2 px-3 text-left transition-colors",
188
188
  isCompact ? "py-1" : "py-1.5",
189
189
  selectedIndex() === idx
190
- ? "bg-muted/50 text-foreground"
191
- : "text-foreground/80 hover:bg-muted/30",
190
+ ? "bg-muted text-foreground"
191
+ : "text-foreground hover:bg-muted",
192
192
  )}
193
193
  onMouseEnter={() => setSelectedIndex(idx)}
194
194
  onClick={() => selectItem(item)}
@@ -1,5 +1,6 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { Source, SourceTrigger, SourceContent, SourceList } from './source';
3
+ import { componentDescription } from '../stories/docs/element-controls';
3
4
 
4
5
  const meta = {
5
6
  title: 'Components/Source',
@@ -8,14 +9,12 @@ const meta = {
8
9
  parameters: {
9
10
  layout: 'padded',
10
11
  docs: {
11
- description: {
12
- component: [
13
- 'An inline citation chip with a hover-card preview. Compose `Source` (root, holds the `href`) with `SourceTrigger` (the clickable pill) and `SourceContent` (the hover preview). `SourceList` lays out several side by side.',
14
- '**When to use:** to cite a referenced web source inline in an assistant message show a compact domain/number pill that previews the title and description on hover.',
15
- '**How to use:** wrap `SourceTrigger` and `SourceContent` in `Source` with the target `href`. Use `label` for custom text or a citation number, `showFavicon` for the site icon, and pass `title`/`description` to `SourceContent`.',
16
- '**Placement:** within message body text as citations, or grouped under a message in a `SourceList`.',
17
- ].join('\n\n'),
18
- },
12
+ description: componentDescription([
13
+ 'An inline citation chip with a hover-card preview. Compose `Source` (root, holds the `href`) with `SourceTrigger` (the clickable pill) and `SourceContent` (the hover preview). `SourceList` lays out several side by side.',
14
+ '**When to use:** to cite a referenced web source inline in an assistant message show a compact domain/number pill that previews the title and description on hover.',
15
+ '**How to use:** wrap `SourceTrigger` and `SourceContent` in `Source` with the target `href`. Use `label` for custom text or a citation number, `showFavicon` for the site icon, and pass `title`/`description` to `SourceContent`.',
16
+ '**Placement:** within message body text as citations, or grouped under a message in a `SourceList`.',
17
+ ]),
19
18
  controls: { exclude: ['use:eventListener'] },
20
19
  },
21
20
  },
@@ -61,7 +61,7 @@ function SourceTrigger(props: SourceTriggerProps) {
61
61
  target="_blank"
62
62
  rel="noopener noreferrer"
63
63
  class={cn(
64
- 'bg-muted text-muted-foreground hover:bg-muted-foreground/30 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150',
64
+ 'bg-muted text-foreground/80 hover:bg-muted-foreground/30 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150',
65
65
  props.showFavicon ? 'pr-2 pl-1' : 'px-2',
66
66
  props.class
67
67
  )}
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import { TaskListCard, type TaskListCardData } from './task-list-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 <TaskListCard> with a capturing `host`, logging emitted events. */
10
+ function Demo(props: { def: TaskListCardData; 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
+ <TaskListCard 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 PLAN: TaskListCardData = {
34
+ selectAll: true,
35
+ confirmLabel: 'Run selected',
36
+ tasks: [
37
+ { id: 'lint', label: 'Run linter', checked: true },
38
+ { id: 'test', label: 'Run unit tests', checked: true },
39
+ { id: 'build', label: 'Build production bundle' },
40
+ { id: 'deploy', label: 'Deploy to staging', description: 'Reversible; staging only' },
41
+ ],
42
+ };
43
+
44
+ const BOUNDED: TaskListCardData = {
45
+ confirmLabel: 'Request review',
46
+ min: 1,
47
+ max: 2,
48
+ tasks: [
49
+ { id: 'ana', label: 'Ana' },
50
+ { id: 'ben', label: 'Ben' },
51
+ { id: 'cat', label: 'Cat' },
52
+ ],
53
+ };
54
+
55
+ const meta = {
56
+ title: 'Components/TaskListCard',
57
+ component: TaskListCard,
58
+ tags: ['autodocs'],
59
+ parameters: {
60
+ layout: 'padded',
61
+ docs: {
62
+ description: componentDescription([
63
+ 'The SolidJS layer behind `<kc-task-list>`. Pass a `host` (a `CardHost`) to receive the emitted `CardEvent`s directly (the native-host path), or wrap in a `CardProvider`. Toggling rows is local; only confirm emits `submit-data` with `{ selected }` in input order.',
64
+ ]),
65
+ },
66
+ },
67
+ } satisfies Meta<typeof TaskListCard>;
68
+
69
+ export default meta;
70
+ type Story = StoryObj<typeof TaskListCard>;
71
+
72
+ export const SelectAPlan: Story = {
73
+ render: () => <Demo def={PLAN} heading="Approve the plan steps" cardId="card-plan" />,
74
+ };
75
+
76
+ export const Bounded: Story = {
77
+ render: () => <Demo def={BOUNDED} heading="Pick up to 2 reviewers" cardId="card-bounded" />,
78
+ };
@@ -0,0 +1,388 @@
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 { Card } from './card';
17
+ import type { CardEnvelope, CardEvent, CardHost } from '../primitives/card-contract';
18
+ import { emitCardEvent } from '../primitives/card-routing';
19
+ import { useCardHost } from '../primitives/card-host';
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Types (task-list.schema.json) — see src/primitives/card-schemas/task-list.schema.json
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ export interface TaskListTask {
26
+ id: string;
27
+ label: string;
28
+ description?: string;
29
+ checked?: boolean;
30
+ disabled?: boolean;
31
+ }
32
+
33
+ export interface TaskListCardData {
34
+ mode?: 'select'; // future: 'select' | 'progress'
35
+ heading?: string;
36
+ tasks: TaskListTask[]; // >=1
37
+ selectAll?: boolean;
38
+ confirmLabel?: string;
39
+ allowEmpty?: boolean;
40
+ min?: number;
41
+ max?: number;
42
+ }
43
+
44
+ export interface TaskListCardResult {
45
+ selected: string[];
46
+ }
47
+
48
+ export type TaskListCardEnvelope = CardEnvelope<'task-list', TaskListCardData>;
49
+
50
+ export const TASK_LIST_CARD_TYPE = 'task-list' as const;
51
+
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+ // Pure helpers (unit-tested in isolation).
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+
56
+ /** De-dupe tasks by id (first wins). Returns the usable list + an optional error. */
57
+ export function normalizeTasks(tasks: unknown): { tasks: TaskListTask[]; error?: string } {
58
+ if (!Array.isArray(tasks) || tasks.length === 0) {
59
+ return { tasks: [], error: "This card couldn't be displayed." };
60
+ }
61
+ const seen = new Set<string>();
62
+ const out: TaskListTask[] = [];
63
+ for (const t of tasks) {
64
+ if (!t || typeof t !== 'object') continue;
65
+ const task = t as Partial<TaskListTask>;
66
+ if (typeof task.id !== 'string' || task.id.length === 0) continue;
67
+ if (typeof task.label !== 'string' || task.label.length === 0) continue;
68
+ if (seen.has(task.id)) {
69
+ // eslint-disable-next-line no-console
70
+ console.warn(`[kc-task-list] duplicate task id "${task.id}" ignored`);
71
+ continue;
72
+ }
73
+ seen.add(task.id);
74
+ out.push({
75
+ id: task.id,
76
+ label: task.label,
77
+ description: typeof task.description === 'string' ? task.description : undefined,
78
+ checked: task.checked === true,
79
+ disabled: task.disabled === true,
80
+ });
81
+ }
82
+ if (out.length === 0) return { tasks: [], error: "This card couldn't be displayed." };
83
+ return { tasks: out };
84
+ }
85
+
86
+ /** The ids of the initially-checked tasks (in input order). */
87
+ export function initialSelected(tasks: TaskListTask[]): string[] {
88
+ return tasks.filter((t) => t.checked).map((t) => t.id);
89
+ }
90
+
91
+ /** The selected ids in input order (selection set ∩ tasks, preserving task order). */
92
+ export function selectedInOrder(tasks: TaskListTask[], selected: Set<string>): string[] {
93
+ return tasks.filter((t) => selected.has(t.id)).map((t) => t.id);
94
+ }
95
+
96
+ /** The toggleable (non-disabled) task ids. */
97
+ export function toggleableIds(tasks: TaskListTask[]): string[] {
98
+ return tasks.filter((t) => !t.disabled).map((t) => t.id);
99
+ }
100
+
101
+ export type SelectAllState = 'checked' | 'unchecked' | 'indeterminate';
102
+
103
+ /** Select-all tri-state over the toggleable rows. */
104
+ export function selectAllState(tasks: TaskListTask[], selected: Set<string>): SelectAllState {
105
+ const ids = toggleableIds(tasks);
106
+ if (ids.length === 0) return 'unchecked';
107
+ const n = ids.filter((id) => selected.has(id)).length;
108
+ if (n === 0) return 'unchecked';
109
+ if (n === ids.length) return 'checked';
110
+ return 'indeterminate';
111
+ }
112
+
113
+ /** Whether select-all should be shown: requested AND not blocked by `max` (since
114
+ * "all" would violate max). */
115
+ export function showSelectAll(data: TaskListCardData, tasks: TaskListTask[]): boolean {
116
+ if (data.selectAll !== true) return false;
117
+ const count = toggleableIds(tasks).length;
118
+ if (data.max !== undefined && count > data.max) return false;
119
+ return true;
120
+ }
121
+
122
+ /** Whether confirm is enabled for the current selection count. */
123
+ export function canConfirm(data: TaskListCardData, count: number): boolean {
124
+ const min = data.min ?? (data.allowEmpty ? 0 : 1);
125
+ if (count < min) return false;
126
+ if (data.max !== undefined && count > data.max) return false;
127
+ return true;
128
+ }
129
+
130
+ /** Whether an unchecked row is blocked because `max` is reached. */
131
+ export function isMaxReached(data: TaskListCardData, count: number): boolean {
132
+ return data.max !== undefined && count >= data.max;
133
+ }
134
+
135
+ /** The disabled-reason text for confirm (for aria-describedby), or undefined. */
136
+ export function confirmReason(data: TaskListCardData, count: number): string | undefined {
137
+ if (canConfirm(data, count)) return undefined;
138
+ const min = data.min ?? (data.allowEmpty ? 0 : 1);
139
+ if (count < min) return `Select at least ${min}.`;
140
+ if (data.max !== undefined && count > data.max) return `Select at most ${data.max}.`;
141
+ return undefined;
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // The <TaskListCard> component.
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+
148
+ export interface TaskListCardProps {
149
+ /** The task-list definition (CardEnvelope.data). */
150
+ data?: TaskListCardData;
151
+ /** The card id used to correlate every emitted CardEvent. */
152
+ cardId?: string;
153
+ /** The envelope title rendered in the card chrome. */
154
+ heading?: string;
155
+ /** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
156
+ * bubbling `kc-card` CustomEvent off `hostElement`). */
157
+ host?: CardHost;
158
+ /** The custom-element host node, for the bubbling `kc-card` fallback emit. */
159
+ hostElement?: HTMLElement;
160
+ class?: string;
161
+ }
162
+
163
+ const DEFAULT_DATA: TaskListCardData = { tasks: [] };
164
+
165
+ /**
166
+ * `TaskListCard` — a selectable task/plan list (checkbox rows + optional select-all
167
+ * + a confirm button) inside `Card` chrome. Row toggling and select-all are local
168
+ * UI state; only the final confirm emits the Card contract's `submit-data` verb
169
+ * (`{ kind:'submit-data', cardId, data:{ selected } }`) with the checked ids in
170
+ * input order. Emits `ready` on mount and `error` for an unusable definition.
171
+ */
172
+ export function TaskListCard(props: TaskListCardProps): JSX.Element {
173
+ const merged = mergeProps({ cardId: 'kc-task-list' }, props);
174
+ const [local] = splitProps(merged, ['data', 'cardId', 'heading', 'host', 'hostElement', 'class']);
175
+
176
+ const ctxHost = useCardHost();
177
+ const uid = createUniqueId();
178
+
179
+ const emit = (event: CardEvent): void => {
180
+ const h = local.host ?? ctxHost;
181
+ if (h) h.emit(event);
182
+ else if (local.hostElement) emitCardEvent(local.hostElement, event);
183
+ };
184
+
185
+ const normalized = createMemo(() => normalizeTasks(local.data?.tasks));
186
+ const valid = createMemo(() => normalized().error === undefined);
187
+ const errorMessage = createMemo(() => normalized().error ?? '');
188
+ const tasks = createMemo(() => normalized().tasks);
189
+ const data = createMemo<TaskListCardData>(() => local.data ?? DEFAULT_DATA);
190
+
191
+ const [selected, setSelected] = createSignal<Set<string>>(new Set());
192
+ const [submitted, setSubmitted] = createSignal(false);
193
+
194
+ // Seed selection from the tasks' initial checked state when a NEW definition arrives.
195
+ createEffect(
196
+ on(
197
+ () => local.data,
198
+ () => {
199
+ setSelected(new Set(initialSelected(tasks())));
200
+ setSubmitted(false);
201
+ },
202
+ ),
203
+ );
204
+
205
+ // ready / error lifecycle emits.
206
+ createEffect(
207
+ on(valid, (ok) => {
208
+ if (ok) emit({ kind: 'ready', cardId: local.cardId });
209
+ else emit({ kind: 'error', cardId: local.cardId, message: errorMessage() });
210
+ }),
211
+ );
212
+
213
+ const count = createMemo(() => selected().size);
214
+ const confirmLabel = () => data().confirmLabel ?? 'Confirm';
215
+ const masterState = createMemo(() => selectAllState(tasks(), selected()));
216
+ const showMaster = createMemo(() => showSelectAll(data(), tasks()));
217
+ const reason = createMemo(() => confirmReason(data(), count()));
218
+ const confirmEnabled = createMemo(() => canConfirm(data(), count()) && !submitted());
219
+
220
+ const toggle = (id: string, on: boolean): void => {
221
+ if (submitted()) return;
222
+ const next = new Set(selected());
223
+ if (on) {
224
+ // Respect max: block adding past max.
225
+ if (isMaxReached(data(), next.size) && !next.has(id)) return;
226
+ next.add(id);
227
+ } else {
228
+ next.delete(id);
229
+ }
230
+ setSelected(next);
231
+ };
232
+
233
+ const toggleAll = (on: boolean): void => {
234
+ if (submitted()) return;
235
+ const next = new Set(selected());
236
+ for (const id of toggleableIds(tasks())) {
237
+ if (on) next.add(id);
238
+ else next.delete(id);
239
+ }
240
+ setSelected(next);
241
+ };
242
+
243
+ const onConfirm = (): void => {
244
+ if (!confirmEnabled()) return;
245
+ const result: TaskListCardResult = { selected: selectedInOrder(tasks(), selected()) };
246
+ emit({ kind: 'submit-data', cardId: local.cardId, data: result });
247
+ setSubmitted(true);
248
+ };
249
+
250
+ const reasonId = `kc-tl-reason-${uid}`;
251
+ const countId = `kc-tl-count-${uid}`;
252
+
253
+ return (
254
+ <Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
255
+ <ErrorBoundary
256
+ fallback={() => {
257
+ emit({ kind: 'error', cardId: local.cardId, message: 'The card failed to render.' });
258
+ return <Card heading={local.heading} errorMessage="The card failed to render." />;
259
+ }}
260
+ >
261
+ <Card
262
+ heading={local.heading ?? local.data?.heading}
263
+ actions={
264
+ <div class="flex w-full flex-wrap items-center justify-between gap-2">
265
+ <span id={countId} aria-live="polite" class="text-xs text-muted-foreground">
266
+ {count()} selected
267
+ </span>
268
+ <div class="ml-auto flex items-center gap-2">
269
+ <Show when={reason()}>
270
+ <span id={reasonId} class="sr-only">
271
+ {reason()}
272
+ </span>
273
+ </Show>
274
+ <Button
275
+ type="button"
276
+ disabled={!confirmEnabled()}
277
+ aria-describedby={reason() ? reasonId : undefined}
278
+ onClick={onConfirm}
279
+ >
280
+ {confirmLabel()}
281
+ </Button>
282
+ </div>
283
+ </div>
284
+ }
285
+ >
286
+ <div
287
+ role="group"
288
+ aria-label={local.heading ?? local.data?.heading ?? 'Tasks'}
289
+ class={cn('flex flex-col', local.class)}
290
+ onKeyDown={(e) => {
291
+ // Enter anywhere in the card (off a checkbox) confirms when enabled.
292
+ if (e.key !== 'Enter') return;
293
+ const target = e.target as HTMLElement;
294
+ if (target.tagName === 'INPUT') return;
295
+ if (confirmEnabled()) onConfirm();
296
+ }}
297
+ >
298
+ <div class="divide-y divide-border overflow-hidden rounded-lg border border-input">
299
+ <Show when={showMaster()}>
300
+ {(() => {
301
+ const indeterminate = () => masterState() === 'indeterminate';
302
+ return (
303
+ <label
304
+ class={cn(
305
+ 'flex cursor-pointer items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors',
306
+ masterState() === 'checked'
307
+ ? 'bg-accent text-accent-foreground'
308
+ : 'text-foreground hover:bg-muted/50',
309
+ )}
310
+ >
311
+ <input
312
+ type="checkbox"
313
+ class="kc-checkbox"
314
+ checked={masterState() === 'checked'}
315
+ aria-checked={indeterminate() ? 'mixed' : masterState() === 'checked'}
316
+ disabled={submitted()}
317
+ ref={(el) => {
318
+ createEffect(() => {
319
+ el.indeterminate = indeterminate();
320
+ });
321
+ }}
322
+ onChange={(e) => toggleAll(e.currentTarget.checked)}
323
+ />
324
+ <span>Select all</span>
325
+ </label>
326
+ );
327
+ })()}
328
+ </Show>
329
+
330
+ <For each={tasks()}>
331
+ {(task) => {
332
+ const checked = () => selected().has(task.id);
333
+ const blocked = () =>
334
+ task.disabled ||
335
+ submitted() ||
336
+ (!checked() && isMaxReached(data(), count()));
337
+ const descId = `kc-tl-desc-${uid}-${task.id}`;
338
+ return (
339
+ <label
340
+ class={cn(
341
+ 'flex items-start gap-3 px-3 py-2.5 text-sm transition-colors',
342
+ blocked()
343
+ ? 'cursor-not-allowed opacity-60'
344
+ : 'cursor-pointer hover:bg-muted/50',
345
+ checked() && !blocked()
346
+ ? 'bg-accent font-medium text-accent-foreground'
347
+ : 'text-foreground',
348
+ )}
349
+ data-task-id={task.id}
350
+ >
351
+ <input
352
+ type="checkbox"
353
+ class="kc-checkbox mt-0.5"
354
+ checked={checked()}
355
+ disabled={blocked()}
356
+ aria-disabled={blocked() ? 'true' : undefined}
357
+ aria-describedby={task.description ? descId : undefined}
358
+ onChange={(e) => toggle(task.id, e.currentTarget.checked)}
359
+ />
360
+ <span class="flex flex-col gap-0.5">
361
+ <span>{task.label}</span>
362
+ <Show when={task.description}>
363
+ <span id={descId} class="text-xs font-normal text-muted-foreground">
364
+ {task.description}
365
+ </span>
366
+ </Show>
367
+ </span>
368
+ </label>
369
+ );
370
+ }}
371
+ </For>
372
+ </div>
373
+
374
+ <Show when={data().max !== undefined}>
375
+ <p class="pt-1 text-xs text-muted-foreground">Up to {data().max} selected.</p>
376
+ </Show>
377
+
378
+ <Show when={submitted()}>
379
+ <p role="status" class="pt-1 text-sm text-muted-foreground">
380
+ Submitted.
381
+ </p>
382
+ </Show>
383
+ </div>
384
+ </Card>
385
+ </ErrorBoundary>
386
+ </Show>
387
+ );
388
+ }