@kitn.ai/ui 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/kitn-chat.es.js +1 -1
  2. package/package.json +6 -1
  3. package/src/components/component-meta.json +10 -10
  4. package/src/elements/compiled.css +1 -1
  5. package/src/remote/host-embed.ts +1 -1
  6. package/src/components/artifact.stories.tsx +0 -138
  7. package/src/components/attachments.stories.tsx +0 -303
  8. package/src/components/attachments.test.tsx +0 -64
  9. package/src/components/chain-of-thought.stories.tsx +0 -211
  10. package/src/components/chat-container.stories.tsx +0 -187
  11. package/src/components/chat-thread.test.tsx +0 -58
  12. package/src/components/checkpoint.stories.tsx +0 -102
  13. package/src/components/code-block.stories.tsx +0 -150
  14. package/src/components/confirm-card.stories.tsx +0 -74
  15. package/src/components/context.stories.tsx +0 -229
  16. package/src/components/context.test.tsx +0 -133
  17. package/src/components/conversation-item.stories.tsx +0 -125
  18. package/src/components/conversation-list.stories.tsx +0 -133
  19. package/src/components/default-input.test.tsx +0 -111
  20. package/src/components/empty.stories.tsx +0 -434
  21. package/src/components/feedback-bar.stories.tsx +0 -138
  22. package/src/components/feedback-bar.test.tsx +0 -103
  23. package/src/components/file-tree.stories.tsx +0 -73
  24. package/src/components/file-upload.stories.tsx +0 -156
  25. package/src/components/image.stories.tsx +0 -89
  26. package/src/components/loader.stories.tsx +0 -181
  27. package/src/components/markdown.stories.tsx +0 -180
  28. package/src/components/message-action-bar.test.tsx +0 -128
  29. package/src/components/message-narrow.stories.tsx +0 -329
  30. package/src/components/message-skills.stories.tsx +0 -211
  31. package/src/components/message.stories.tsx +0 -281
  32. package/src/components/model-switcher.stories.tsx +0 -123
  33. package/src/components/model-switcher.test.tsx +0 -65
  34. package/src/components/prompt-input.stories.tsx +0 -222
  35. package/src/components/prompt-suggestion.stories.tsx +0 -142
  36. package/src/components/reasoning.stories.tsx +0 -140
  37. package/src/components/resizable.test.tsx +0 -226
  38. package/src/components/scroll-button.stories.tsx +0 -100
  39. package/src/components/slash-command.stories.tsx +0 -163
  40. package/src/components/source-list.test.tsx +0 -152
  41. package/src/components/source.stories.tsx +0 -124
  42. package/src/components/tasks-card.stories.tsx +0 -78
  43. package/src/components/text-shimmer.stories.tsx +0 -87
  44. package/src/components/thinking-bar.stories.tsx +0 -87
  45. package/src/components/tool.stories.tsx +0 -153
  46. package/src/components/voice-input.stories.tsx +0 -83
  47. package/src/elements/artifact.stories.tsx +0 -506
  48. package/src/elements/attachments.stories.tsx +0 -190
  49. package/src/elements/card.stories.tsx +0 -113
  50. package/src/elements/cards.stories.tsx +0 -59
  51. package/src/elements/catalog.stories.tsx +0 -491
  52. package/src/elements/chain-of-thought.declarative.test.tsx +0 -153
  53. package/src/elements/chain-of-thought.stories.tsx +0 -113
  54. package/src/elements/chat-scope-picker.stories.tsx +0 -73
  55. package/src/elements/chat-workspace.stories.tsx +0 -288
  56. package/src/elements/chat.stories.tsx +0 -151
  57. package/src/elements/checkpoint.stories.tsx +0 -72
  58. package/src/elements/choice.stories.tsx +0 -240
  59. package/src/elements/code-block.stories.tsx +0 -83
  60. package/src/elements/composed-shell.stories.tsx +0 -316
  61. package/src/elements/confirm-card.stories.tsx +0 -227
  62. package/src/elements/context-meter.stories.tsx +0 -151
  63. package/src/elements/conversation-list.declarative.test.tsx +0 -137
  64. package/src/elements/conversation-list.stories.tsx +0 -243
  65. package/src/elements/embed.stories.tsx +0 -213
  66. package/src/elements/empty.stories.tsx +0 -111
  67. package/src/elements/feedback-bar.stories.tsx +0 -114
  68. package/src/elements/file-tree.stories.tsx +0 -134
  69. package/src/elements/file-upload.stories.tsx +0 -82
  70. package/src/elements/form.stories.tsx +0 -248
  71. package/src/elements/image.stories.tsx +0 -71
  72. package/src/elements/link-preview.stories.tsx +0 -209
  73. package/src/elements/loader.stories.tsx +0 -88
  74. package/src/elements/markdown.stories.tsx +0 -76
  75. package/src/elements/message-skills.declarative.test.tsx +0 -104
  76. package/src/elements/message-skills.stories.tsx +0 -114
  77. package/src/elements/message.stories.tsx +0 -218
  78. package/src/elements/model-switcher.declarative.test.tsx +0 -130
  79. package/src/elements/model-switcher.stories.tsx +0 -198
  80. package/src/elements/popover.stories.tsx +0 -80
  81. package/src/elements/prompt-input-slash-command.test.tsx +0 -128
  82. package/src/elements/prompt-input-stoppable.test.tsx +0 -88
  83. package/src/elements/prompt-input-toolbar-actions.test.tsx +0 -120
  84. package/src/elements/prompt-input.stories.tsx +0 -331
  85. package/src/elements/prompt-suggestions.declarative.test.tsx +0 -164
  86. package/src/elements/prompt-suggestions.stories.tsx +0 -213
  87. package/src/elements/reasoning.stories.tsx +0 -77
  88. package/src/elements/remote.stories.tsx +0 -314
  89. package/src/elements/resizable.stories.tsx +0 -426
  90. package/src/elements/response-stream.stories.tsx +0 -79
  91. package/src/elements/scroll-button.stories.tsx +0 -202
  92. package/src/elements/scroll-button.test.tsx +0 -148
  93. package/src/elements/source-list.stories.tsx +0 -143
  94. package/src/elements/source.stories.tsx +0 -88
  95. package/src/elements/switch.stories.tsx +0 -56
  96. package/src/elements/tasks.stories.tsx +0 -237
  97. package/src/elements/text-shimmer.stories.tsx +0 -64
  98. package/src/elements/thinking-bar.stories.tsx +0 -72
  99. package/src/elements/tool.stories.tsx +0 -89
  100. package/src/elements/voice-input.stories.tsx +0 -88
  101. package/src/primitives/controllable.test.ts +0 -47
  102. package/src/stories/chat-panel-layout.stories.tsx +0 -145
  103. package/src/stories/chat-scene.tsx +0 -571
  104. package/src/stories/checkpoint-restore.stories.tsx +0 -256
  105. package/src/stories/context-usage.stories.tsx +0 -212
  106. package/src/stories/conversation-with-reasoning.stories.tsx +0 -182
  107. package/src/stories/conversation-with-sources.stories.tsx +0 -471
  108. package/src/stories/docs/Accessibility.mdx +0 -119
  109. package/src/stories/docs/ChildElements.mdx +0 -269
  110. package/src/stories/docs/ForAIAgents.mdx +0 -162
  111. package/src/stories/docs/GettingStarted.mdx +0 -65
  112. package/src/stories/docs/Installation.mdx +0 -83
  113. package/src/stories/docs/Introduction.mdx +0 -50
  114. package/src/stories/docs/SolidJsAdvanced.mdx +0 -17
  115. package/src/stories/docs/Theming.mdx +0 -85
  116. package/src/stories/docs/element-controls.ts +0 -121
  117. package/src/stories/docs/frameworks/Angular.mdx +0 -245
  118. package/src/stories/docs/frameworks/Html.mdx +0 -227
  119. package/src/stories/docs/frameworks/Overview.mdx +0 -47
  120. package/src/stories/docs/frameworks/React.mdx +0 -227
  121. package/src/stories/docs/frameworks/Solid.mdx +0 -173
  122. package/src/stories/docs/frameworks/Svelte.mdx +0 -194
  123. package/src/stories/docs/frameworks/Vue.mdx +0 -238
  124. package/src/stories/docs/generative-ui-overview.mdx +0 -186
  125. package/src/stories/docs/recipes/SpeechToText.mdx +0 -7
  126. package/src/stories/docs/recipes/Streaming.mdx +0 -68
  127. package/src/stories/docs/recipes/TextToSpeech.mdx +0 -35
  128. package/src/stories/docs/theme-editor/canvas.tsx +0 -32
  129. package/src/stories/docs/theme-editor/inspector.tsx +0 -66
  130. package/src/stories/docs/theme-editor/presets.test.ts +0 -32
  131. package/src/stories/docs/theme-editor/presets.ts +0 -64
  132. package/src/stories/docs/theme-editor/theme-css.test.ts +0 -19
  133. package/src/stories/docs/theme-editor/theme-css.ts +0 -15
  134. package/src/stories/docs/theme-editor/theme-editor.tsx +0 -146
  135. package/src/stories/docs/theme-tokens.tsx +0 -174
  136. package/src/stories/examples/ChoosingComponents.mdx +0 -106
  137. package/src/stories/examples/sample-data.ts +0 -79
  138. package/src/stories/examples/usage/checkpoint-restore.ts +0 -156
  139. package/src/stories/examples/usage/context-usage.ts +0 -591
  140. package/src/stories/examples/usage/conversation-with-reasoning.ts +0 -224
  141. package/src/stories/examples/usage/conversation-with-sources.ts +0 -589
  142. package/src/stories/examples/usage/empty-state.ts +0 -144
  143. package/src/stories/examples/usage/full-chat-app.ts +0 -277
  144. package/src/stories/examples/usage/index.ts +0 -63
  145. package/src/stories/examples/usage/message-actions.ts +0 -704
  146. package/src/stories/examples/usage/pattern-centered-conversation.ts +0 -446
  147. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +0 -442
  148. package/src/stories/examples/usage/pattern-docked-widget.ts +0 -479
  149. package/src/stories/examples/usage/prompt-input-variants.ts +0 -1356
  150. package/src/stories/examples/usage/streaming-response.ts +0 -687
  151. package/src/stories/examples/usage/types.ts +0 -33
  152. package/src/stories/full-chat.stories.tsx +0 -18
  153. package/src/stories/message-actions.stories.tsx +0 -230
  154. package/src/stories/pattern-centered-conversation.stories.tsx +0 -93
  155. package/src/stories/pattern-docked-widget.stories.tsx +0 -93
  156. package/src/stories/pattern-empty-state.stories.tsx +0 -76
  157. package/src/stories/prompt-input-variants.stories.tsx +0 -456
  158. package/src/stories/streaming-response.stories.tsx +0 -438
  159. package/src/stories/theme-editor.stories.tsx +0 -16
  160. package/src/stories/token-reference.stories.tsx +0 -18
  161. package/src/stories/typography.stories.tsx +0 -78
  162. package/src/ui/avatar.stories.tsx +0 -103
  163. package/src/ui/badge.stories.tsx +0 -86
  164. package/src/ui/button.stories.tsx +0 -145
  165. package/src/ui/collapsible.stories.tsx +0 -69
  166. package/src/ui/dropdown.stories.tsx +0 -59
  167. package/src/ui/hover-card.stories.tsx +0 -77
  168. package/src/ui/hover-card.test.tsx +0 -30
  169. package/src/ui/overlay.stories.tsx +0 -115
  170. package/src/ui/popover.stories.tsx +0 -81
  171. package/src/ui/popover.test.tsx +0 -99
  172. package/src/ui/resizable.stories.tsx +0 -236
  173. package/src/ui/scroll-area.stories.tsx +0 -50
  174. package/src/ui/separator.stories.tsx +0 -81
  175. package/src/ui/skeleton.stories.tsx +0 -337
  176. package/src/ui/switch.stories.tsx +0 -51
  177. package/src/ui/switch.test.tsx +0 -59
  178. package/src/ui/textarea.stories.tsx +0 -76
  179. package/src/ui/tooltip.stories.tsx +0 -74
@@ -1,76 +0,0 @@
1
- import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
- import { onMount } from 'solid-js';
3
- import './register'; // side effect: registers the custom elements
4
- import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
-
6
- // The web components are custom DOM elements, so declare the tags for JSX.
7
- declare module 'solid-js' {
8
- // eslint-disable-next-line @typescript-eslint/no-namespace
9
- namespace JSX {
10
- interface IntrinsicElements {
11
- 'kai-markdown': JSX.HTMLAttributes<HTMLElement>;
12
- }
13
- }
14
- }
15
-
16
- const sampleMarkdown = `### Markdown
17
- Renders **bold**, _italic_, \`code\`, and lists:
18
- - one
19
- - two
20
-
21
- > and blockquotes.
22
-
23
- \`\`\`ts
24
- export function add(a: number, b: number): number {
25
- return a + b;
26
- }
27
- \`\`\``;
28
-
29
- /** Render the actual `<kai-markdown>` custom element with a `content` property. */
30
- function MarkdownElement(props: { content: string }) {
31
- let el: (HTMLElement & { content?: string }) | undefined;
32
- onMount(() => {
33
- if (el) el.content = props.content;
34
- });
35
- return (
36
- <kai-markdown ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px', 'max-width': '720px' }} />
37
- );
38
- }
39
-
40
- const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
41
- <kai-markdown id="md" code-theme="github-dark-dimmed"></kai-markdown>
42
-
43
- <script type="module">
44
- import '@kitn.ai/ui/elements'; // registers the custom elements
45
-
46
- const md = document.getElementById('md');
47
- md.content = '### Hello\\nRenders **bold**, _italic_, and \\\`code\\\`.';
48
- // md.setAttribute('code-highlight', 'false'); // skip Shiki entirely
49
- </script>`;
50
-
51
- const meta = {
52
- title: 'Components/Markdown',
53
- tags: ['autodocs'],
54
- argTypes: argTypesFor('kai-markdown'),
55
- parameters: {
56
- layout: 'fullscreen',
57
- docs: {
58
- description: specDescription('kai-markdown', [
59
- '`<kai-markdown>` is the framework-agnostic **web component** that renders a markdown string (with fenced-code syntax highlighting via Shiki) as a standalone element, isolated in **Shadow DOM**.',
60
- '**When to use:** showing model output or any markdown in a non-Solid app without pulling in a markdown stack. In SolidJS, use the `Markdown` primitive directly.',
61
- '**Placement:** as a block inside a message row, doc viewer, or standalone content pane; it is `display: block` and expands to fill its container width, so constrain width on the parent.',
62
- "**How to use:** register once with `import '@kitn.ai/ui/elements'`, set the source via the `content` **property** (`el.content = '...'`), and tune rendering with the `prose-size`, `code-theme`, and `code-highlight` attributes.",
63
- 'See the **Code** tab for HTML usage.',
64
- ]),
65
- },
66
- },
67
- } satisfies Meta;
68
-
69
- export default meta;
70
- type Story = StoryObj;
71
-
72
- /** Headings, emphasis, lists, a blockquote, and a highlighted code fence. */
73
- export const Default: Story = {
74
- render: () => <MarkdownElement content={sampleMarkdown} />,
75
- parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
76
- };
@@ -1,104 +0,0 @@
1
- /**
2
- * Unit tests for the declarative `<kai-skill>` light-DOM API of `<kai-skills>`.
3
- *
4
- * Strategy: the `defineWebComponent` call registers a real Shadow-DOM custom
5
- * element which requires a full browser environment (Constructable Stylesheets,
6
- * shadow roots, etc.) and is therefore not suitable for jsdom unit tests.
7
- * Instead we:
8
- * 1. Test the exported `parseKaiSkillElement` helper in isolation — it has no
9
- * DOM dependencies beyond `Element`, which jsdom provides perfectly.
10
- * 2. Test that the merged list of prop + slotted items renders badge spans
11
- * correctly via `MessageSkills` directly, mirroring the pattern used in
12
- * `prompt-suggestions.declarative.test.tsx`.
13
- */
14
- import { describe, it, expect, afterEach } from 'vitest';
15
- import '@testing-library/jest-dom/vitest';
16
- import { render, cleanup } from '@solidjs/testing-library';
17
- import { For } from 'solid-js';
18
- import { parseKaiSkillElement } from './message-skills';
19
- import { MessageSkills } from '../components/message-skills';
20
-
21
- afterEach(cleanup);
22
-
23
- // ---------------------------------------------------------------------------
24
- // parseKaiSkillElement — pure helper
25
- // ---------------------------------------------------------------------------
26
-
27
- describe('parseKaiSkillElement', () => {
28
- function makeNode(textContent: string, id?: string): Element {
29
- const el = document.createElement('kai-skill');
30
- el.textContent = textContent;
31
- if (id !== undefined) el.setAttribute('id', id);
32
- return el;
33
- }
34
-
35
- it('uses textContent as name', () => {
36
- const item = parseKaiSkillElement(makeNode('Web Search'));
37
- expect(item).toMatchObject({ name: 'Web Search' });
38
- });
39
-
40
- it('uses id attribute as id when present', () => {
41
- const item = parseKaiSkillElement(makeNode('Web Search', 'web-search'));
42
- expect(item).toMatchObject({ id: 'web-search', name: 'Web Search' });
43
- });
44
-
45
- it('falls back to textContent as id when id attribute is absent', () => {
46
- const item = parseKaiSkillElement(makeNode('Web Search'));
47
- expect(item).toMatchObject({ id: 'Web Search', name: 'Web Search' });
48
- });
49
-
50
- it('trims leading/trailing whitespace from textContent', () => {
51
- const item = parseKaiSkillElement(makeNode(' Code Interpreter '));
52
- expect(item).toMatchObject({ name: 'Code Interpreter' });
53
- });
54
-
55
- it('handles empty textContent with an id attribute', () => {
56
- const item = parseKaiSkillElement(makeNode('', 'code'));
57
- expect(item).toMatchObject({ id: 'code', name: '' });
58
- });
59
- });
60
-
61
- // ---------------------------------------------------------------------------
62
- // Merge + render: prop skills + slotted skills combine correctly
63
- // ---------------------------------------------------------------------------
64
-
65
- interface Skill {
66
- id: string;
67
- name: string;
68
- }
69
-
70
- describe('MessageSkills rendering with merged skills', () => {
71
- it('renders one badge span per skill', () => {
72
- const { getByText } = render(() => (
73
- <MessageSkills skills={[{ id: 'web-search', name: 'Web Search' }, { id: 'code', name: 'Code' }]} />
74
- ));
75
- expect(getByText('Web Search')).toBeInTheDocument();
76
- expect(getByText('Code')).toBeInTheDocument();
77
- });
78
-
79
- it('renders nothing when skills is empty', () => {
80
- const { container } = render(() => <MessageSkills skills={[]} />);
81
- expect(container.firstChild).toBeNull();
82
- });
83
-
84
- it('renders prop items before declarative (slotted) items', () => {
85
- // Simulate the merge: prop first, slotted second.
86
- const propSkills: Skill[] = [{ id: 'prop', name: 'Prop Skill' }];
87
- const slottedSkills: Skill[] = [{ id: 'slotted', name: 'Slotted Skill' }];
88
- const merged = [...propSkills, ...slottedSkills];
89
-
90
- const { getAllByText } = render(() => <MessageSkills skills={merged} />);
91
- // Both must be present; order is enforced by DOM order
92
- expect(getAllByText(/Prop Skill|Slotted Skill/)).toHaveLength(2);
93
- });
94
-
95
- it('parseKaiSkillElement produces items that render correctly', () => {
96
- const el = document.createElement('kai-skill');
97
- el.textContent = 'Memory';
98
- el.setAttribute('id', 'memory');
99
-
100
- const parsed = parseKaiSkillElement(el);
101
- const { getByText } = render(() => <MessageSkills skills={[parsed]} />);
102
- expect(getByText('Memory')).toBeInTheDocument();
103
- });
104
- });
@@ -1,114 +0,0 @@
1
- import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
- import { onMount } from 'solid-js';
3
- import './register'; // side effect: registers the custom elements
4
- import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
-
6
- // The web components are custom DOM elements, so declare the tags for JSX.
7
- declare module 'solid-js' {
8
- // eslint-disable-next-line @typescript-eslint/no-namespace
9
- namespace JSX {
10
- interface IntrinsicElements {
11
- 'kai-skills': JSX.HTMLAttributes<HTMLElement>;
12
- /** Light-DOM data carrier for declarative skill badges inside `<kai-skills>`. */
13
- 'kai-skill': JSX.HTMLAttributes<HTMLElement> & {
14
- id?: string;
15
- };
16
- }
17
- }
18
- }
19
-
20
- interface Skill {
21
- id: string;
22
- name: string;
23
- }
24
-
25
- const sampleSkills: Skill[] = [
26
- { id: 's1', name: 'web-search' },
27
- { id: 's2', name: 'code' },
28
- ];
29
-
30
- /** Render the actual `<kai-skills>` custom element with a `skills` property. */
31
- function MessageSkillsElement(props: { skills: Skill[] }) {
32
- let el: (HTMLElement & { skills?: Skill[] }) | undefined;
33
- onMount(() => {
34
- if (el) el.skills = props.skills;
35
- });
36
- return (
37
- <kai-skills ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px' }} />
38
- );
39
- }
40
-
41
- const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
42
- <kai-skills id="skills"></kai-skills>
43
-
44
- <script type="module">
45
- import '@kitn.ai/ui/elements'; // registers the custom elements
46
-
47
- const skills = document.getElementById('skills');
48
- skills.skills = [
49
- { id: 's1', name: 'web-search' },
50
- { id: 's2', name: 'code' },
51
- ];
52
- </script>`;
53
-
54
- const meta = {
55
- title: 'Components/Skills',
56
- tags: ['autodocs'],
57
- argTypes: argTypesFor('kai-skills'),
58
- parameters: {
59
- layout: 'fullscreen',
60
- docs: {
61
- description: specDescription('kai-skills', [
62
- '`<kai-skills>` is the framework-agnostic **web component** that badges which skills were active for a message, isolated in **Shadow DOM**.',
63
- '**When to use:** annotating a message row with the skills/tools it used, in a non-Solid app. In SolidJS, use the `MessageSkills` primitive directly.',
64
- '**Placement:** beneath or inline with the message content, typically in the footer of a message row alongside action buttons; it is `display: block` and wraps badges horizontally.',
65
- "**How to use:** register once with `import '@kitn.ai/ui/elements'`. Then either (a) set the `skills` **property** to an array of `{ id, name }` objects, or (b) compose `<kai-skill>` child elements — light-DOM data carriers hidden by the Shadow DOM — where the tag's text content is the badge label and the optional `id` attribute is the stable identifier. When both are provided, prop items render first and declarative children are appended after.",
66
- 'See the **Code** tab for HTML usage.',
67
- ]),
68
- },
69
- },
70
- } satisfies Meta;
71
-
72
- export default meta;
73
- type Story = StoryObj;
74
-
75
- /** Two active-skill badges set via the `skills` JS property. */
76
- export const Default: Story = {
77
- render: () => <MessageSkillsElement skills={sampleSkills} />,
78
- parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
79
- };
80
-
81
- const DECLARATIVE_HTML_SNIPPET = `<!-- Works in any framework or plain HTML — no JS property assignment needed -->
82
- <kai-skills>
83
- <kai-skill id="web-search">Web Search</kai-skill>
84
- <kai-skill id="code">Code</kai-skill>
85
- <kai-skill id="memory">Memory</kai-skill>
86
- </kai-skills>
87
-
88
- <script type="module">
89
- import '@kitn.ai/ui/elements'; // registers the custom elements
90
- </script>`;
91
-
92
- /**
93
- * Declare each skill badge as a `<kai-skill>` child element — no `skills`
94
- * property or JS array wiring needed. The tag's text content is the badge
95
- * label; the optional `id` attribute is the stable identifier (falls back to
96
- * the label when absent). Children are light-DOM data carriers hidden by the
97
- * Shadow DOM — pure data, no visible output of their own.
98
- * Mix with the `skills` prop: prop items render first, declarative children after.
99
- */
100
- export const DeclarativeSkills: Story = {
101
- name: 'Declarative Skills (kai-skill)',
102
- render: () => (
103
- <kai-skills style={{ display: 'block', padding: '16px' }}>
104
- <kai-skill id="web-search">Web Search</kai-skill>
105
- <kai-skill id="code">Code</kai-skill>
106
- <kai-skill id="memory">Memory</kai-skill>
107
- </kai-skills>
108
- ),
109
- parameters: {
110
- docs: {
111
- source: { code: DECLARATIVE_HTML_SNIPPET, language: 'html' },
112
- },
113
- },
114
- };
@@ -1,218 +0,0 @@
1
- import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
- import { onMount } from 'solid-js';
3
- import './register'; // side effect: registers the custom elements
4
- import type { ChatMessage } from './chat-types';
5
- import { argTypesFor, specDescription } from '../stories/docs/element-controls';
6
-
7
- // The web components are custom DOM elements, so declare the tags for JSX.
8
- declare module 'solid-js' {
9
- // eslint-disable-next-line @typescript-eslint/no-namespace
10
- namespace JSX {
11
- interface IntrinsicElements {
12
- 'kai-message': JSX.HTMLAttributes<HTMLElement>;
13
- 'kai-action': JSX.HTMLAttributes<HTMLElement> & { icon?: string; tooltip?: string };
14
- }
15
- }
16
- }
17
-
18
- const assistantMessage: ChatMessage = {
19
- id: 'm-a',
20
- role: 'assistant',
21
- content:
22
- "Here's the plan, with a quick code sample:\n\n```js\nconst kit = useChat();\n```\n\nThat wires the kit into your component.",
23
- reasoning: { text: 'The user wants X, so I should do Y then Z.', label: 'Reasoning' },
24
- tools: [{ type: 'search', state: 'output-available', input: { query: 'kitn docs' }, output: { hits: 3 } }],
25
- attachments: [
26
- {
27
- id: '1',
28
- type: 'file',
29
- filename: 'architecture.png',
30
- mediaType: 'image/png',
31
- url:
32
- 'data:image/svg+xml;utf8,' +
33
- encodeURIComponent(
34
- '<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96"><rect width="96" height="96" rx="12" fill="#7c3aed"/><text x="48" y="60" font-size="42" text-anchor="middle" fill="white">◆</text></svg>',
35
- ),
36
- },
37
- ],
38
- actions: ['copy', 'like', 'dislike', 'regenerate'],
39
- };
40
-
41
- const userMessage: ChatMessage = {
42
- id: 'm-u',
43
- role: 'user',
44
- content: 'How do I compose these myself?',
45
- };
46
-
47
- const AVATAR_SVG =
48
- 'data:image/svg+xml;utf8,' +
49
- encodeURIComponent(
50
- '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#10b981"/><text x="32" y="42" font-size="28" text-anchor="middle" fill="white">K</text></svg>',
51
- );
52
-
53
- /** An assistant message with an avatar (from `message.avatar`) plus custom + built-in actions. */
54
- const avatarMessage: ChatMessage = {
55
- id: 'm-av',
56
- role: 'assistant',
57
- content: 'I have an avatar, a custom **Share** action, and the built-in copy/like actions.',
58
- avatar: { src: AVATAR_SVG, alt: 'Kitn', fallback: 'K' },
59
- actions: ['copy', 'like', { id: 'share', label: 'Share', icon: 'share' }],
60
- };
61
-
62
- /** Render the actual `<kai-message>` custom element with a `message` property and optional attrs. */
63
- function MessageElement(props: { message: ChatMessage; actionsReveal?: 'always' | 'hover' }) {
64
- let el: (HTMLElement & { message?: ChatMessage }) | undefined;
65
- onMount(() => {
66
- if (!el) return;
67
- if (props.actionsReveal) el.setAttribute('actions-reveal', props.actionsReveal);
68
- el.message = props.message;
69
- el.addEventListener('kai-message-action', (e) =>
70
- console.log('kai-message-action', (e as CustomEvent).detail.action),
71
- );
72
- });
73
- return (
74
- <kai-message ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px', 'max-width': '720px' }} />
75
- );
76
- }
77
-
78
- const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
79
- <kai-message id="msg" style="display:block;"></kai-message>
80
-
81
- <script type="module">
82
- import '@kitn.ai/ui/elements'; // registers the custom elements
83
-
84
- const msg = document.getElementById('msg');
85
- msg.message = {
86
- id: 'm-a', role: 'assistant',
87
- content: "Here's the plan:\\n\\n\\\`\\\`\\\`js\\nconst kit = useChat();\\n\\\`\\\`\\\`",
88
- reasoning: { text: 'The user wants X, so I should do Y.' },
89
- tools: [{ type: 'search', state: 'output-available', output: { hits: 3 } }],
90
- actions: ['copy', 'like', 'dislike', 'regenerate'],
91
- };
92
-
93
- // events are CustomEvents on the element (they do not bubble)
94
- msg.addEventListener('kai-message-action', (e) => console.log(e.detail.action, e.detail.messageId));
95
- </script>`;
96
-
97
- const meta = {
98
- title: 'Components/Message',
99
- tags: ['autodocs'],
100
- argTypes: argTypesFor('kai-message'),
101
- parameters: {
102
- layout: 'fullscreen',
103
- docs: {
104
- description: specDescription('kai-message', [
105
- '`<kai-message>` is the framework-agnostic **web component** for a single message row — markdown/plain content, an optional reasoning block, tool calls, attachments, and action buttons — all rendered from one `message` object (the same shape `<kai-chat>` uses per message). It is the keystone of the "compose your own message list" pattern, isolated in **Shadow DOM**.',
106
- "**When to use:** building a custom message thread in a non-Solid app, or anywhere you want to lay out the list yourself but keep the kit's rich message rendering. In SolidJS, compose the `Message` primitives for finer control.",
107
- "**How to use:** register once with `import '@kitn.ai/ui/elements'`, set the whole row via the `message` **property** (`el.message = {...}`), and listen for the `kai-message-action` **CustomEvent** for action-button clicks. For simple cases, set `role` + `content` attributes instead of a full object.",
108
- 'See the **Code** tab for HTML usage.',
109
- ]),
110
- },
111
- },
112
- } satisfies Meta;
113
-
114
- export default meta;
115
- type Story = StoryObj;
116
-
117
- /** A rich assistant message: markdown, reasoning, a tool call, an attachment, and actions. */
118
- export const Assistant: Story = {
119
- render: () => <MessageElement message={assistantMessage} />,
120
- parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
121
- };
122
-
123
- /** A plain user message (markdown defaults off for the user role). */
124
- export const User: Story = {
125
- render: () => <MessageElement message={userMessage} />,
126
- };
127
-
128
- /** An avatar (from `message.avatar`) laid out beside the content column, with a
129
- * custom **Share** action (curated `icon`) alongside the built-in copy/like. */
130
- export const WithAvatarAndCustomAction: Story = {
131
- name: 'Avatar + Custom Action',
132
- render: () => <MessageElement message={avatarMessage} />,
133
- parameters: {
134
- docs: {
135
- source: {
136
- code: `<kai-message id="msg" style="display:block;"></kai-message>
137
- <script type="module">
138
- import '@kitn.ai/ui/elements';
139
- const msg = document.getElementById('msg');
140
- msg.message = {
141
- id: 'm-av', role: 'assistant', content: 'I have an avatar and a custom Share action.',
142
- avatar: { src: '/k.png', alt: 'Kitn', fallback: 'K' },
143
- // built-in names AND custom { id, label, icon } descriptors:
144
- actions: ['copy', 'like', { id: 'share', label: 'Share', icon: 'share' }],
145
- };
146
- msg.addEventListener('kai-message-action', (e) => console.log(e.detail.action)); // 'copy' | 'like' | 'share'
147
- </script>`,
148
- language: 'html',
149
- },
150
- },
151
- },
152
- };
153
-
154
- /** Declarative actions — `<kai-action>` light-DOM children instead of a
155
- * `message.actions` array. Each carries `id`, a curated `icon`, an optional
156
- * `tooltip`, and optional text (the accessible label). Great for plain HTML. */
157
- export const DeclarativeActions: Story = {
158
- name: 'Declarative Actions (kai-action)',
159
- render: () => {
160
- let el: HTMLElement | undefined;
161
- onMount(() => {
162
- if (!el) return;
163
- el.setAttribute('avatar-fallback', 'AI');
164
- el.setAttribute(
165
- 'content',
166
- 'Declare each button as a `<kai-action>` child — no `message` object or JS wiring needed.',
167
- );
168
- el.addEventListener('kai-message-action', (e) =>
169
- console.log('kai-message-action', (e as CustomEvent).detail.action),
170
- );
171
- });
172
- return (
173
- <kai-message
174
- ref={(e) => (el = e as HTMLElement)}
175
- style={{ display: 'block', padding: '16px', 'max-width': '720px' }}
176
- >
177
- <kai-action id="copy" icon="copy" tooltip="Copy"></kai-action>
178
- <kai-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kai-action>
179
- <kai-action id="share" icon="share" tooltip="Share">Share</kai-action>
180
- <kai-action id="bookmark" icon="bookmark" tooltip="Bookmark">Bookmark</kai-action>
181
- </kai-message>
182
- );
183
- },
184
- parameters: {
185
- docs: {
186
- source: {
187
- code: `<kai-message role="assistant" avatar-fallback="AI" content="..." style="display:block;">
188
- <kai-action id="copy" icon="copy" tooltip="Copy"></kai-action>
189
- <kai-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kai-action>
190
- <kai-action id="share" icon="share" tooltip="Share">Share</kai-action>
191
- <kai-action id="bookmark" icon="bookmark" tooltip="Bookmark">Bookmark</kai-action>
192
- </kai-message>
193
-
194
- <script type="module">
195
- import '@kitn.ai/ui/elements';
196
- document.querySelector('kai-message')
197
- .addEventListener('kai-message-action', (e) => console.log(e.detail.action)); // 'copy' | 'share' | …
198
- </script>`,
199
- language: 'html',
200
- },
201
- },
202
- },
203
- };
204
-
205
- /** `actions-reveal="hover"` — the action bar is hidden until you hover the row. */
206
- export const ActionsRevealOnHover: Story = {
207
- name: 'Actions Reveal on Hover',
208
- render: () => <MessageElement message={avatarMessage} actionsReveal="hover" />,
209
- parameters: {
210
- docs: {
211
- source: {
212
- code: `<!-- actions-reveal: 'always' (default) | 'hover' -->
213
- <kai-message actions-reveal="hover" id="msg" style="display:block;"></kai-message>`,
214
- language: 'html',
215
- },
216
- },
217
- },
218
- };
@@ -1,130 +0,0 @@
1
- /**
2
- * Unit tests for the declarative `<kai-model>` light-DOM API of
3
- * `<kai-model-switcher>`.
4
- *
5
- * Strategy: the `defineWebComponent` call registers a real Shadow-DOM custom
6
- * element which requires a full browser environment (Constructable Stylesheets,
7
- * shadow roots, etc.) and is therefore not suitable for jsdom unit tests.
8
- * Instead we:
9
- * 1. Test the exported `parseKaiModelElement` helper in isolation — it has no
10
- * DOM dependencies beyond `Element`, which jsdom provides perfectly.
11
- * 2. Test that the merged list of prop + slotted models combines correctly,
12
- * mirroring the pattern used in `prompt-suggestions.declarative.test.tsx`.
13
- */
14
- import { describe, it, expect } from 'vitest';
15
- import { parseKaiModelElement } from './model-switcher';
16
- import type { ModelOption } from '../types';
17
-
18
- // ---------------------------------------------------------------------------
19
- // parseKaiModelElement — pure helper
20
- // ---------------------------------------------------------------------------
21
-
22
- describe('parseKaiModelElement', () => {
23
- function makeNode(textContent: string, id?: string, provider?: string): Element {
24
- const el = document.createElement('kai-model');
25
- el.textContent = textContent;
26
- if (id !== undefined) el.setAttribute('id', id);
27
- if (provider !== undefined) el.setAttribute('provider', provider);
28
- return el;
29
- }
30
-
31
- it('maps textContent to name', () => {
32
- const item = parseKaiModelElement(makeNode('GPT-4o', 'gpt-4o'));
33
- expect(item.name).toBe('GPT-4o');
34
- });
35
-
36
- it('maps the id attribute to id', () => {
37
- const item = parseKaiModelElement(makeNode('GPT-4o', 'gpt-4o'));
38
- expect(item.id).toBe('gpt-4o');
39
- });
40
-
41
- it('maps the provider attribute to provider', () => {
42
- const item = parseKaiModelElement(makeNode('GPT-4o', 'gpt-4o', 'OpenAI'));
43
- expect(item.provider).toBe('OpenAI');
44
- });
45
-
46
- it('sets provider to undefined when the attribute is absent', () => {
47
- const item = parseKaiModelElement(makeNode('Claude Sonnet', 'sonnet'));
48
- expect(item.provider).toBeUndefined();
49
- });
50
-
51
- it('trims leading/trailing whitespace from textContent (name)', () => {
52
- const item = parseKaiModelElement(makeNode(' Claude Sonnet ', 'sonnet'));
53
- expect(item.name).toBe('Claude Sonnet');
54
- });
55
-
56
- it('returns empty string for id when id attribute is absent', () => {
57
- const item = parseKaiModelElement(makeNode('GPT-4o'));
58
- expect(item.id).toBe('');
59
- });
60
-
61
- it('produces the full ModelOption shape', () => {
62
- const item = parseKaiModelElement(makeNode('GPT-4o', 'gpt-4o', 'OpenAI'));
63
- expect(item).toEqual<ModelOption>({ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI', description: undefined, group: undefined });
64
- });
65
-
66
- it('maps the description and group attributes', () => {
67
- const el = document.createElement('kai-model');
68
- el.textContent = 'GPT-4o';
69
- el.setAttribute('id', 'gpt-4o');
70
- el.setAttribute('description', 'Flagship model');
71
- el.setAttribute('group', 'Legacy models');
72
- const item = parseKaiModelElement(el);
73
- expect(item.description).toBe('Flagship model');
74
- expect(item.group).toBe('Legacy models');
75
- });
76
- });
77
-
78
- // ---------------------------------------------------------------------------
79
- // Merge logic: prop models + slotted models combine correctly
80
- // ---------------------------------------------------------------------------
81
-
82
- describe('prop + declarative merge', () => {
83
- function makeNode(textContent: string, id: string, provider?: string): Element {
84
- const el = document.createElement('kai-model');
85
- el.textContent = textContent;
86
- el.setAttribute('id', id);
87
- if (provider !== undefined) el.setAttribute('provider', provider);
88
- return el;
89
- }
90
-
91
- it('renders prop items before declarative (slotted) items', () => {
92
- const propModels: ModelOption[] = [{ id: 'opus', name: 'Claude Opus', provider: 'Anthropic' }];
93
- const slottedModels = [makeNode('GPT-4o', 'gpt-4o', 'OpenAI')].map(parseKaiModelElement);
94
- const merged = [...propModels, ...slottedModels];
95
- expect(merged).toHaveLength(2);
96
- expect(merged[0].id).toBe('opus');
97
- expect(merged[1].id).toBe('gpt-4o');
98
- });
99
-
100
- it('works with only declarative children (empty prop array)', () => {
101
- const slottedModels = [
102
- makeNode('GPT-4o', 'gpt-4o', 'OpenAI'),
103
- makeNode('GPT-4o mini', 'gpt-4o-mini', 'OpenAI'),
104
- ].map(parseKaiModelElement);
105
- const merged = [...[], ...slottedModels];
106
- expect(merged).toHaveLength(2);
107
- expect(merged[0]).toMatchObject({ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' });
108
- expect(merged[1]).toMatchObject({ id: 'gpt-4o-mini', name: 'GPT-4o mini', provider: 'OpenAI' });
109
- });
110
-
111
- it('works with only prop models (no declarative children)', () => {
112
- const propModels: ModelOption[] = [
113
- { id: 'sonnet', name: 'Claude Sonnet', provider: 'Anthropic' },
114
- { id: 'haiku', name: 'Claude Haiku', provider: 'Anthropic' },
115
- ];
116
- const merged = [...propModels, ...([] as ModelOption[])];
117
- expect(merged).toHaveLength(2);
118
- expect(merged[0].id).toBe('sonnet');
119
- expect(merged[1].id).toBe('haiku');
120
- });
121
-
122
- it('parseKaiModelElement produces items with the correct event payload key (id → modelId)', () => {
123
- // Confirm the parsed `id` field is what gets dispatched as `modelId` in kai-model-change.
124
- // The element fires: dispatch('kai-model-change', { modelId }) where modelId = item.id.
125
- const node = makeNode('GPT-4o', 'gpt-4o', 'OpenAI');
126
- const parsed = parseKaiModelElement(node);
127
- // The model-switcher component passes item.id as the modelId in the event.
128
- expect(parsed.id).toBe('gpt-4o'); // this becomes e.detail.modelId
129
- });
130
- });