@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,226 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeAll } from 'vitest';
2
- import '@testing-library/jest-dom/vitest';
3
- import { render, cleanup } from '@solidjs/testing-library';
4
- import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../ui/resizable';
5
-
6
- // NOTE on file location: the task brief names `src/components/resizable.tsx`, but
7
- // in this tree the resizable UI primitives live in `src/ui/resizable.tsx` and the
8
- // custom element in `src/elements/resizable.tsx`. This test (kept at the requested
9
- // `src/components/` path) exercises both the UI primitive (`ResizableHandle`
10
- // dblclick-reset + default-size reflection) and the element (`<kai-resizable>`
11
- // size-preservation across content-only re-renders).
12
-
13
- // jsdom doesn't implement layout: getBoundingClientRect returns all-zeros, so the
14
- // pixel-driven DRAG math (pointer move → flex-basis) and the live-percent settle
15
- // are NOT meaningfully testable here. We test the *state logic* that is layout-
16
- // independent: (a) the panel reflects its default size to a data-* attribute, and
17
- // (b) dblclick on a handle restores adjacent panels' flex-basis from those
18
- // defaults, even after their inline basis has been mutated (simulating a drag).
19
- // The element test asserts that a content-only mutation does NOT clobber a panel's
20
- // dragged inline flex-basis.
21
-
22
- afterEach(cleanup);
23
-
24
- describe('ResizablePanel default-size reflection', () => {
25
- it('reflects defaultSize to data-default-size-pct (percent) so a handle can reset to it', () => {
26
- const { container } = render(() => (
27
- <ResizablePanelGroup orientation="horizontal">
28
- <ResizablePanel defaultSize={30}>a</ResizablePanel>
29
- <ResizableHandle />
30
- <ResizablePanel>b</ResizablePanel>
31
- </ResizablePanelGroup>
32
- ));
33
- const panels = container.querySelectorAll('[style*="flex-basis"]');
34
- const first = panels[0] as HTMLElement;
35
- expect(first.dataset.defaultSizePct).toBe('30');
36
- });
37
-
38
- it('reflects a px defaultSize to data-default-size (pixels)', () => {
39
- const { container } = render(() => (
40
- <ResizablePanelGroup orientation="horizontal">
41
- <ResizablePanel defaultSize="280px">a</ResizablePanel>
42
- <ResizableHandle />
43
- <ResizablePanel>b</ResizablePanel>
44
- </ResizablePanelGroup>
45
- ));
46
- const first = container.querySelector('[data-default-size]') as HTMLElement;
47
- expect(first?.dataset.defaultSize).toBe('280');
48
- });
49
- });
50
-
51
- describe('ResizableHandle dblclick resets adjacent panels to defaults', () => {
52
- it('restores both adjacent panels flex-basis from their default sizes on dblclick', () => {
53
- const { container } = render(() => (
54
- <ResizablePanelGroup orientation="horizontal">
55
- <ResizablePanel defaultSize={30}>a</ResizablePanel>
56
- <ResizableHandle />
57
- <ResizablePanel defaultSize={70}>b</ResizablePanel>
58
- </ResizablePanelGroup>
59
- ));
60
- const handle = container.querySelector('[role="separator"]') as HTMLElement;
61
- const prev = handle.previousElementSibling as HTMLElement;
62
- const next = handle.nextElementSibling as HTMLElement;
63
-
64
- // Simulate the result of a drag: panels carry pixel flex-basis inline.
65
- prev.style.flexBasis = '120px';
66
- next.style.flexBasis = '480px';
67
-
68
- handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
69
-
70
- expect(prev.style.flexBasis).toBe('30%');
71
- expect(next.style.flexBasis).toBe('70%');
72
- });
73
-
74
- it('resets a panel that has no explicit default back to flexible (clears inline basis)', () => {
75
- const { container } = render(() => (
76
- <ResizablePanelGroup orientation="horizontal">
77
- <ResizablePanel defaultSize={30}>a</ResizablePanel>
78
- <ResizableHandle />
79
- <ResizablePanel>b</ResizablePanel>
80
- </ResizablePanelGroup>
81
- ));
82
- const handle = container.querySelector('[role="separator"]') as HTMLElement;
83
- const prev = handle.previousElementSibling as HTMLElement;
84
- const next = handle.nextElementSibling as HTMLElement;
85
-
86
- prev.style.flexBasis = '120px';
87
- next.style.flexBasis = '480px';
88
- next.style.flexGrow = '0';
89
- next.style.flexShrink = '0';
90
-
91
- handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
92
-
93
- expect(prev.style.flexBasis).toBe('30%');
94
- // The flexible panel (no default) returns to growing: basis cleared, grow:1.
95
- expect(next.style.flexBasis).toBe('');
96
- expect(next.style.flexGrow).toBe('1');
97
- });
98
-
99
- it('does nothing on a static (locked) handle', () => {
100
- const { container } = render(() => (
101
- <ResizablePanelGroup orientation="horizontal">
102
- <ResizablePanel defaultSize={30}>a</ResizablePanel>
103
- <ResizableHandle static />
104
- <ResizablePanel defaultSize={70}>b</ResizablePanel>
105
- </ResizablePanelGroup>
106
- ));
107
- const handle = container.querySelector('[role="separator"]') as HTMLElement;
108
- const prev = handle.previousElementSibling as HTMLElement;
109
- prev.style.flexBasis = '120px';
110
-
111
- handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
112
-
113
- expect(prev.style.flexBasis).toBe('120px');
114
- });
115
- });
116
-
117
- describe('<kai-resizable> size preservation across content-only re-renders', () => {
118
- beforeAll(async () => {
119
- // Importing registers the custom element. jsdom supports customElements.
120
- await import('../elements/resizable');
121
- });
122
-
123
- function mount(html: string): HTMLElement {
124
- const host = document.createElement('div');
125
- host.innerHTML = html;
126
- document.body.appendChild(host);
127
- return host;
128
- }
129
-
130
- afterEach(() => {
131
- document.body.innerHTML = '';
132
- });
133
-
134
- it('does not clobber a dragged panel flex-basis when an item child re-renders content', async () => {
135
- const host = mount(`
136
- <kai-resizable orientation="horizontal" style="height:300px;width:600px">
137
- <kai-resizable-item size="30%"><p id="content">hi</p></kai-resizable-item>
138
- <kai-resizable-item><p>right</p></kai-resizable-item>
139
- </kai-resizable>
140
- `);
141
- const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
142
- // Let solid-element upgrade + onMount run.
143
- await Promise.resolve();
144
- await new Promise((r) => setTimeout(r, 0));
145
-
146
- const panel = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
147
- expect(panel).toBeTruthy();
148
-
149
- // Simulate a settled drag: the handle writes inline flex-basis on the panel.
150
- panel.style.flexBasis = '55%';
151
-
152
- // Now mutate ONLY the slotted content of an existing item (no add/remove).
153
- const content = host.querySelector('#content') as HTMLElement;
154
- content.textContent = 'a new chat message arrived';
155
-
156
- // Allow the MutationObserver microtask + any re-render to flush.
157
- await Promise.resolve();
158
- await new Promise((r) => setTimeout(r, 0));
159
-
160
- const panelAfter = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
161
- // The dragged basis must survive (the bug reset it to "30%").
162
- expect(panelAfter.style.flexBasis).toBe('55%');
163
- });
164
-
165
- it('reflects each item default size onto its panel for handle dblclick-reset', async () => {
166
- const host = mount(`
167
- <kai-resizable orientation="horizontal" style="height:300px;width:600px">
168
- <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
169
- <kai-resizable-item><p>b</p></kai-resizable-item>
170
- </kai-resizable>
171
- `);
172
- const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
173
- await Promise.resolve();
174
- await new Promise((r) => setTimeout(r, 0));
175
-
176
- const panels = el.shadowRoot.querySelectorAll('[data-panel]');
177
- expect((panels[0] as HTMLElement).dataset.defaultSizePct).toBe('30');
178
- // Second panel has no explicit default → no reflected default size.
179
- expect((panels[1] as HTMLElement).dataset.defaultSizePct).toBeUndefined();
180
- });
181
-
182
- it('keeps the ORIGINAL default for dblclick-reset even after sizes are persisted', async () => {
183
- const host = mount(`
184
- <kai-resizable orientation="horizontal" style="height:300px;width:600px">
185
- <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
186
- <kai-resizable-item size="70%"><p>b</p></kai-resizable-item>
187
- </kai-resizable>
188
- `);
189
- const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
190
- await Promise.resolve();
191
- await new Promise((r) => setTimeout(r, 0));
192
-
193
- // Simulate persistence overwriting the live `size` attribute after a drag.
194
- const item = host.querySelector('kai-resizable-item') as HTMLElement;
195
- item.setAttribute('size', '55%');
196
- await Promise.resolve();
197
- await new Promise((r) => setTimeout(r, 0));
198
-
199
- const panel = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
200
- // The reflected DEFAULT (for dblclick-reset) stays the original 30, not 55.
201
- expect(panel.dataset.defaultSizePct).toBe('30');
202
- });
203
-
204
- it('still re-initializes when an item is actually added', async () => {
205
- const host = mount(`
206
- <kai-resizable orientation="horizontal" style="height:300px;width:600px">
207
- <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
208
- <kai-resizable-item><p>b</p></kai-resizable-item>
209
- </kai-resizable>
210
- `);
211
- const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
212
- await Promise.resolve();
213
- await new Promise((r) => setTimeout(r, 0));
214
-
215
- expect(el.shadowRoot.querySelectorAll('[data-panel]').length).toBe(2);
216
-
217
- const item = document.createElement('kai-resizable-item');
218
- item.innerHTML = '<p>c</p>';
219
- el.appendChild(item);
220
-
221
- await Promise.resolve();
222
- await new Promise((r) => setTimeout(r, 0));
223
-
224
- expect(el.shadowRoot.querySelectorAll('[data-panel]').length).toBe(3);
225
- });
226
- });
@@ -1,100 +0,0 @@
1
- import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
- import { For } from 'solid-js';
3
- import { ScrollButton } from './scroll-button';
4
- import { ChatContainerRoot, ChatContainerContent } from './chat-container';
5
- import { componentDescription } from '../stories/docs/element-controls';
6
-
7
- /**
8
- * `ScrollButton` reads scroll state from the surrounding `ChatContainerRoot`
9
- * context, so every story wraps it in a scrollable container. It is hidden
10
- * (faded out) while pinned to the bottom and appears once you scroll up.
11
- */
12
- function ScrollDemo(props: { variant?: 'outline' | 'ghost' | 'default'; size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm'; class?: string }) {
13
- return (
14
- <div class="relative h-64 w-80 overflow-hidden rounded-lg border">
15
- <ChatContainerRoot class="h-full p-4">
16
- <ChatContainerContent class="gap-2">
17
- <For each={Array.from({ length: 20 })}>
18
- {(_, i) => (
19
- <div class="rounded-md bg-muted/40 px-3 py-2 text-sm">Message {i() + 1}</div>
20
- )}
21
- </For>
22
- </ChatContainerContent>
23
- {/* ScrollButton must live INSIDE ChatContainerRoot (it reads that
24
- context); it's absolutely positioned relative to the outer .relative
25
- box, so it stays pinned and doesn't scroll with the content. */}
26
- <div class="absolute inset-x-0 bottom-3 flex justify-center">
27
- <ScrollButton variant={props.variant} size={props.size} class={props.class} />
28
- </div>
29
- </ChatContainerRoot>
30
- </div>
31
- );
32
- }
33
-
34
- const meta = {
35
- title: 'Solid (Advanced)/Elements/ScrollButton',
36
- component: ScrollButton,
37
- tags: ['autodocs'],
38
- parameters: {
39
- layout: 'padded',
40
- docs: {
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
- ]),
47
- controls: { exclude: ['use:eventListener'] },
48
- },
49
- },
50
- argTypes: {
51
- variant: {
52
- control: 'select',
53
- options: ['default', 'ghost', 'outline'],
54
- description: 'Underlying button visual emphasis.',
55
- table: { defaultValue: { summary: 'outline' } },
56
- },
57
- size: {
58
- control: 'select',
59
- options: ['sm', 'md', 'lg', 'icon', 'icon-sm'],
60
- description: 'Underlying button size preset.',
61
- table: { defaultValue: { summary: 'sm' } },
62
- },
63
- class: {
64
- control: 'text',
65
- description: 'Additional classes merged onto the button.',
66
- },
67
- },
68
- args: {
69
- variant: 'outline',
70
- size: 'sm',
71
- },
72
- render: (args) => <ScrollDemo variant={args.variant} size={args.size} class={args.class} />,
73
- } satisfies Meta<typeof ScrollButton>;
74
-
75
- export default meta;
76
- type Story = StoryObj<typeof meta>;
77
-
78
- const IMPORT = `import { ScrollButton, ChatContainerRoot, ChatContainerContent } from '@kitn.ai/ui';`;
79
- const src = (code: string) => ({
80
- parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
81
- });
82
-
83
- /** Interactive playground — scroll up to reveal the button; tweak `variant`/`size`. */
84
- export const Playground: Story = {
85
- ...src(`<div class="relative">
86
- <ChatContainerRoot class="h-full overflow-y-auto">
87
- <ChatContainerContent>{/* messages */}</ChatContainerContent>
88
- {/* inside the Root (reads its context); absolutely positioned to stay pinned */}
89
- <div class="absolute inset-x-0 bottom-3 flex justify-center">
90
- <ScrollButton />
91
- </div>
92
- </ChatContainerRoot>
93
- </div>`),
94
- };
95
-
96
- /** Ghost variant overlaid in a chat area (showcase). */
97
- export const Ghost: Story = {
98
- args: { variant: 'ghost' },
99
- ...src(`<ScrollButton variant="ghost" />`),
100
- };
@@ -1,163 +0,0 @@
1
- import type { Meta, StoryObj } from "storybook-solidjs-vite";
2
- import { fn } from "storybook/test";
3
- import { createSignal } from "solid-js";
4
- import { SlashCommand, type SlashCommandItem } from "./slash-command";
5
- import { PromptInput, PromptInputTextarea } from "./prompt-input";
6
- import { ChatConfig } from "../primitives/chat-config";
7
- import { componentDescription } from '../stories/docs/element-controls';
8
-
9
- const skillCommands: SlashCommandItem[] = [
10
- { id: "caveman", label: "Caveman", description: "Ultra-compressed terse responses", category: "Skills" },
11
- { id: "detailed", label: "Detailed", description: "Thorough, comprehensive responses", category: "Skills" },
12
- { id: "eli5", label: "ELI5", description: "Explain simply, avoid jargon", category: "Skills" },
13
- { id: "socratic", label: "Socratic", description: "Ask guiding questions", category: "Skills" },
14
- { id: "concise", label: "Concise", description: "Short, direct answers", category: "Skills" },
15
- ];
16
-
17
- const mixedCommands: SlashCommandItem[] = [
18
- ...skillCommands,
19
- { id: "clear", label: "Clear", description: "Clear conversation history", category: "Actions" },
20
- { id: "export", label: "Export", description: "Export conversation as markdown", category: "Actions" },
21
- ];
22
-
23
- /**
24
- * `SlashCommand` reads the input value from the enclosing `PromptInput`
25
- * context, so every story mounts it inside a `PromptInput`. The popup opens
26
- * when the input starts with `/`.
27
- */
28
- function SlashDemo(props: { commands: SlashCommandItem[]; activeIds?: string[]; compact?: boolean; onSelect: (cmd: SlashCommandItem) => void }) {
29
- const [value, setValue] = createSignal("/");
30
- return (
31
- <ChatConfig proseSize="sm">
32
- <div style={{ width: "420px" }} class="bg-card rounded-lg p-4">
33
- <div class="relative">
34
- <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue("")}>
35
- <PromptInputTextarea placeholder="Type / for commands..." class="min-h-[36px] pt-2 pl-3" />
36
- <SlashCommand
37
- commands={props.commands}
38
- activeIds={props.activeIds}
39
- compact={props.compact}
40
- onSelect={(cmd) => {
41
- props.onSelect(cmd);
42
- setValue("");
43
- }}
44
- />
45
- </PromptInput>
46
- </div>
47
- <p class="text-xs text-muted-foreground mt-2 text-center">
48
- Type <code class="bg-muted px-1 rounded">/</code> to see commands. Arrow keys + Tab/Enter to select.
49
- </p>
50
- </div>
51
- </ChatConfig>
52
- );
53
- }
54
-
55
- const meta = {
56
- title: "Solid (Advanced)/Elements/SlashCommand",
57
- component: SlashCommand,
58
- tags: ["autodocs"],
59
- parameters: {
60
- layout: "centered",
61
- docs: {
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
- ]),
68
- controls: { exclude: ["use:eventListener"] },
69
- },
70
- },
71
- argTypes: {
72
- commands: {
73
- control: "object",
74
- description: "List of selectable commands ({ id, label, description?, category? }).",
75
- },
76
- activeIds: {
77
- control: "object",
78
- description: "IDs currently marked active — selecting an active command toggles it off.",
79
- },
80
- compact: {
81
- control: "boolean",
82
- description: "Single-line rows (label + description side by side). When false, two-line rows.",
83
- table: { defaultValue: { summary: "true" } },
84
- },
85
- onSelect: {
86
- action: "select",
87
- description: "Fired with the chosen command when an item is selected.",
88
- table: { category: "Events" },
89
- },
90
- class: { control: "text", description: "Additional classes on the popup container." },
91
- },
92
- args: {
93
- commands: skillCommands,
94
- compact: true,
95
- activeIds: [],
96
- onSelect: fn(),
97
- },
98
- render: (args) => (
99
- <SlashDemo commands={args.commands} activeIds={args.activeIds} compact={args.compact} onSelect={args.onSelect} />
100
- ),
101
- } satisfies Meta<typeof SlashCommand>;
102
-
103
- export default meta;
104
- type Story = StoryObj<typeof meta>;
105
-
106
- const IMPORT = `import { SlashCommand, PromptInput, PromptInputTextarea } from '@kitn.ai/ui';`;
107
- const src = (code: string) => ({
108
- parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
109
- });
110
-
111
- /** Interactive playground — type `/` in the input; tweak `commands`/`compact`/`activeIds`. */
112
- export const Playground: Story = {
113
- ...src(`<PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
114
- <PromptInputTextarea placeholder="Type / for commands..." />
115
- <SlashCommand commands={commands} onSelect={(cmd) => { /* apply */ }} />
116
- </PromptInput>`),
117
- };
118
-
119
- /** Compact single-line rows (default). */
120
- export const Compact: Story = {
121
- args: { compact: true },
122
- ...src(`<SlashCommand commands={commands} onSelect={handleSelect} />`),
123
- };
124
-
125
- /** Two-line rows with label above description. */
126
- export const Expanded: Story = {
127
- args: { compact: false },
128
- ...src(`<SlashCommand commands={commands} compact={false} onSelect={handleSelect} />`),
129
- };
130
-
131
- /** Commands grouped by category (Skills + Actions). */
132
- export const WithCategories: Story = {
133
- args: { commands: mixedCommands },
134
- ...src(`const commands = [
135
- { id: 'eli5', label: 'ELI5', description: 'Explain simply', category: 'Skills' },
136
- { id: 'clear', label: 'Clear', description: 'Clear conversation', category: 'Actions' },
137
- ];
138
-
139
- <SlashCommand commands={commands} onSelect={handleSelect} />`),
140
- };
141
-
142
- /** Toggleable skills — active IDs are marked and toggle off when reselected (showcase). */
143
- export const ActiveToggles: Story = {
144
- render: () => {
145
- const [selected, setSelected] = createSignal<string[]>(["eli5"]);
146
- return (
147
- <SlashDemo
148
- commands={skillCommands}
149
- activeIds={selected()}
150
- onSelect={(cmd) =>
151
- setSelected((prev) =>
152
- prev.includes(cmd.id) ? prev.filter((id) => id !== cmd.id) : [...prev, cmd.id],
153
- )
154
- }
155
- />
156
- );
157
- },
158
- ...src(`<SlashCommand
159
- commands={skillCommands}
160
- activeIds={selected()}
161
- onSelect={(cmd) => toggle(cmd.id)}
162
- />`),
163
- };
@@ -1,152 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import '@testing-library/jest-dom/vitest';
3
- import { render, cleanup } from '@solidjs/testing-library';
4
- import { For } from 'solid-js';
5
- import { Source, SourceTrigger, SourceContent, SourceList } from './source';
6
- import { parseKaiSourceElement } from '../elements/source';
7
-
8
- afterEach(cleanup);
9
-
10
- // ---------------------------------------------------------------------------
11
- // parseKaiSourceElement — unit-tests for the attribute→SourceItem mapping
12
- // ---------------------------------------------------------------------------
13
-
14
- describe('parseKaiSourceElement', () => {
15
- function makeEl(attrs: Record<string, string | null>): Element {
16
- const el = document.createElement('kai-source');
17
- for (const [k, v] of Object.entries(attrs)) {
18
- if (v !== null) el.setAttribute(k, v);
19
- }
20
- return el;
21
- }
22
-
23
- it('maps href, label, headline→title, description', () => {
24
- const el = makeEl({
25
- href: 'https://kitn.dev',
26
- label: 'kitn',
27
- headline: 'kitn — the kit',
28
- description: 'Composable SolidJS chat UI.',
29
- });
30
- expect(parseKaiSourceElement(el)).toEqual({
31
- href: 'https://kitn.dev',
32
- label: 'kitn',
33
- title: 'kitn — the kit',
34
- description: 'Composable SolidJS chat UI.',
35
- showFavicon: false,
36
- });
37
- });
38
-
39
- it('sets showFavicon=true for a bare show-favicon attribute', () => {
40
- const el = makeEl({ href: 'https://kitn.dev', 'show-favicon': '' });
41
- expect(parseKaiSourceElement(el).showFavicon).toBe(true);
42
- });
43
-
44
- it('sets showFavicon=false when show-favicon="false"', () => {
45
- const el = makeEl({ href: 'https://kitn.dev', 'show-favicon': 'false' });
46
- expect(parseKaiSourceElement(el).showFavicon).toBe(false);
47
- });
48
-
49
- it('returns undefined for optional attrs when absent', () => {
50
- const el = makeEl({ href: 'https://kitn.dev' });
51
- const item = parseKaiSourceElement(el);
52
- expect(item.label).toBeUndefined();
53
- expect(item.title).toBeUndefined();
54
- expect(item.description).toBeUndefined();
55
- });
56
-
57
- it('falls back to empty string for missing href', () => {
58
- const el = makeEl({});
59
- expect(parseKaiSourceElement(el).href).toBe('');
60
- });
61
- });
62
-
63
- // ---------------------------------------------------------------------------
64
- // SourceList component — verify citations render as anchor links
65
- // ---------------------------------------------------------------------------
66
-
67
- describe('SourceList with Source children', () => {
68
- it('renders citation links for each source', () => {
69
- const sources = [
70
- { href: 'https://kitn.dev', title: 'kitn', description: 'Composable chat UI.' },
71
- { href: 'https://solidjs.com', title: 'SolidJS', description: 'A reactive UI library.' },
72
- ];
73
-
74
- const { container } = render(() => (
75
- <SourceList>
76
- {sources.map((s) => (
77
- <Source href={s.href}>
78
- <SourceTrigger />
79
- <SourceContent title={s.title} description={s.description} />
80
- </Source>
81
- ))}
82
- </SourceList>
83
- ));
84
-
85
- const links = container.querySelectorAll('a[href]');
86
- // SourceTrigger and SourceContent each render an <a>; 2 sources × 2 = 4
87
- expect(links.length).toBeGreaterThanOrEqual(2);
88
- const hrefs = [...links].map((a) => a.getAttribute('href'));
89
- expect(hrefs).toContain('https://kitn.dev');
90
- expect(hrefs).toContain('https://solidjs.com');
91
- });
92
- });
93
-
94
- // ---------------------------------------------------------------------------
95
- // numbered prop — kai-sources web component behavior reproduced at Solid level
96
- // ---------------------------------------------------------------------------
97
-
98
- describe('numbered citations', () => {
99
- /** Helper: render N sources with explicit numeric labels (mirroring numbered mode). */
100
- function renderNumbered(sources: { href: string; label?: string | number }[]) {
101
- return render(() => (
102
- <SourceList>
103
- <For each={sources}>
104
- {(s, i) => (
105
- <Source href={s.href}>
106
- {/* When numbered is active the element replaces label with i()+1. */}
107
- <SourceTrigger label={i() + 1} />
108
- <SourceContent title="" description="" />
109
- </Source>
110
- )}
111
- </For>
112
- </SourceList>
113
- ));
114
- }
115
-
116
- it('labels N sources 1..N in order', () => {
117
- const sources = [
118
- { href: 'https://kitn.dev' },
119
- { href: 'https://solidjs.com' },
120
- { href: 'https://vitejs.dev' },
121
- ];
122
-
123
- const { container } = renderNumbered(sources);
124
-
125
- // SourceTrigger renders a <span> with the label inside the trigger <a>.
126
- // Grab all trigger anchors — they have target="_blank".
127
- const triggerLinks = [...container.querySelectorAll('a[target="_blank"]')];
128
- // One trigger per source (SourceContent also renders an <a target="_blank">,
129
- // so we narrow to ones whose text content is a number 1–N).
130
- const labels = triggerLinks
131
- .map((a) => a.querySelector('span')?.textContent?.trim())
132
- .filter(Boolean);
133
-
134
- expect(labels).toEqual(['1', '2', '3']);
135
- });
136
-
137
- it('label sequence resets from 1 for each independent list', () => {
138
- const firstSources = [{ href: 'https://kitn.dev' }, { href: 'https://solidjs.com' }];
139
- const secondSources = [{ href: 'https://vitejs.dev' }, { href: 'https://vitest.dev' }];
140
-
141
- const { container: c1 } = renderNumbered(firstSources);
142
- const { container: c2 } = renderNumbered(secondSources);
143
-
144
- const getLabels = (c: HTMLElement) =>
145
- [...c.querySelectorAll('a[target="_blank"]')]
146
- .map((a) => a.querySelector('span')?.textContent?.trim())
147
- .filter(Boolean);
148
-
149
- expect(getLabels(c1)).toEqual(['1', '2']);
150
- expect(getLabels(c2)).toEqual(['1', '2']);
151
- });
152
- });