@kitn.ai/ui 0.15.0 → 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 (177) hide show
  1. package/dist/kitn-chat.es.js +1 -1
  2. package/package.json +6 -1
  3. package/src/elements/compiled.css +1 -1
  4. package/src/components/artifact.stories.tsx +0 -138
  5. package/src/components/attachments.stories.tsx +0 -303
  6. package/src/components/attachments.test.tsx +0 -64
  7. package/src/components/chain-of-thought.stories.tsx +0 -211
  8. package/src/components/chat-container.stories.tsx +0 -187
  9. package/src/components/chat-thread.test.tsx +0 -58
  10. package/src/components/checkpoint.stories.tsx +0 -102
  11. package/src/components/code-block.stories.tsx +0 -150
  12. package/src/components/confirm-card.stories.tsx +0 -74
  13. package/src/components/context.stories.tsx +0 -229
  14. package/src/components/context.test.tsx +0 -133
  15. package/src/components/conversation-item.stories.tsx +0 -125
  16. package/src/components/conversation-list.stories.tsx +0 -133
  17. package/src/components/default-input.test.tsx +0 -111
  18. package/src/components/empty.stories.tsx +0 -434
  19. package/src/components/feedback-bar.stories.tsx +0 -138
  20. package/src/components/feedback-bar.test.tsx +0 -103
  21. package/src/components/file-tree.stories.tsx +0 -73
  22. package/src/components/file-upload.stories.tsx +0 -156
  23. package/src/components/image.stories.tsx +0 -89
  24. package/src/components/loader.stories.tsx +0 -181
  25. package/src/components/markdown.stories.tsx +0 -180
  26. package/src/components/message-action-bar.test.tsx +0 -128
  27. package/src/components/message-narrow.stories.tsx +0 -329
  28. package/src/components/message-skills.stories.tsx +0 -211
  29. package/src/components/message.stories.tsx +0 -281
  30. package/src/components/model-switcher.stories.tsx +0 -123
  31. package/src/components/model-switcher.test.tsx +0 -65
  32. package/src/components/prompt-input.stories.tsx +0 -222
  33. package/src/components/prompt-suggestion.stories.tsx +0 -142
  34. package/src/components/reasoning.stories.tsx +0 -140
  35. package/src/components/resizable.test.tsx +0 -226
  36. package/src/components/scroll-button.stories.tsx +0 -100
  37. package/src/components/slash-command.stories.tsx +0 -163
  38. package/src/components/source-list.test.tsx +0 -152
  39. package/src/components/source.stories.tsx +0 -124
  40. package/src/components/tasks-card.stories.tsx +0 -78
  41. package/src/components/text-shimmer.stories.tsx +0 -87
  42. package/src/components/thinking-bar.stories.tsx +0 -87
  43. package/src/components/tool.stories.tsx +0 -153
  44. package/src/components/voice-input.stories.tsx +0 -83
  45. package/src/elements/artifact.stories.tsx +0 -506
  46. package/src/elements/attachments.stories.tsx +0 -190
  47. package/src/elements/card.stories.tsx +0 -113
  48. package/src/elements/cards.stories.tsx +0 -59
  49. package/src/elements/catalog.stories.tsx +0 -491
  50. package/src/elements/chain-of-thought.declarative.test.tsx +0 -153
  51. package/src/elements/chain-of-thought.stories.tsx +0 -113
  52. package/src/elements/chat-scope-picker.stories.tsx +0 -73
  53. package/src/elements/chat-workspace.stories.tsx +0 -288
  54. package/src/elements/chat.stories.tsx +0 -151
  55. package/src/elements/checkpoint.stories.tsx +0 -72
  56. package/src/elements/choice.stories.tsx +0 -240
  57. package/src/elements/code-block.stories.tsx +0 -83
  58. package/src/elements/composed-shell.stories.tsx +0 -316
  59. package/src/elements/confirm-card.stories.tsx +0 -227
  60. package/src/elements/context-meter.stories.tsx +0 -151
  61. package/src/elements/conversation-list.declarative.test.tsx +0 -137
  62. package/src/elements/conversation-list.stories.tsx +0 -243
  63. package/src/elements/embed.stories.tsx +0 -213
  64. package/src/elements/empty.stories.tsx +0 -111
  65. package/src/elements/feedback-bar.stories.tsx +0 -114
  66. package/src/elements/file-tree.stories.tsx +0 -134
  67. package/src/elements/file-upload.stories.tsx +0 -82
  68. package/src/elements/form.stories.tsx +0 -248
  69. package/src/elements/image.stories.tsx +0 -71
  70. package/src/elements/link-preview.stories.tsx +0 -209
  71. package/src/elements/loader.stories.tsx +0 -88
  72. package/src/elements/markdown.stories.tsx +0 -76
  73. package/src/elements/message-skills.declarative.test.tsx +0 -104
  74. package/src/elements/message-skills.stories.tsx +0 -114
  75. package/src/elements/message.stories.tsx +0 -218
  76. package/src/elements/model-switcher.declarative.test.tsx +0 -130
  77. package/src/elements/model-switcher.stories.tsx +0 -198
  78. package/src/elements/popover.stories.tsx +0 -80
  79. package/src/elements/prompt-input-slash-command.test.tsx +0 -128
  80. package/src/elements/prompt-input-stoppable.test.tsx +0 -88
  81. package/src/elements/prompt-input-toolbar-actions.test.tsx +0 -120
  82. package/src/elements/prompt-input.stories.tsx +0 -331
  83. package/src/elements/prompt-suggestions.declarative.test.tsx +0 -164
  84. package/src/elements/prompt-suggestions.stories.tsx +0 -213
  85. package/src/elements/reasoning.stories.tsx +0 -77
  86. package/src/elements/remote.stories.tsx +0 -314
  87. package/src/elements/resizable.stories.tsx +0 -426
  88. package/src/elements/response-stream.stories.tsx +0 -79
  89. package/src/elements/scroll-button.stories.tsx +0 -202
  90. package/src/elements/scroll-button.test.tsx +0 -148
  91. package/src/elements/source-list.stories.tsx +0 -143
  92. package/src/elements/source.stories.tsx +0 -88
  93. package/src/elements/switch.stories.tsx +0 -56
  94. package/src/elements/tasks.stories.tsx +0 -237
  95. package/src/elements/text-shimmer.stories.tsx +0 -64
  96. package/src/elements/thinking-bar.stories.tsx +0 -72
  97. package/src/elements/tool.stories.tsx +0 -89
  98. package/src/elements/voice-input.stories.tsx +0 -88
  99. package/src/primitives/controllable.test.ts +0 -47
  100. package/src/stories/chat-panel-layout.stories.tsx +0 -145
  101. package/src/stories/chat-scene.tsx +0 -571
  102. package/src/stories/checkpoint-restore.stories.tsx +0 -256
  103. package/src/stories/context-usage.stories.tsx +0 -212
  104. package/src/stories/conversation-with-reasoning.stories.tsx +0 -182
  105. package/src/stories/conversation-with-sources.stories.tsx +0 -471
  106. package/src/stories/docs/Accessibility.mdx +0 -119
  107. package/src/stories/docs/ChildElements.mdx +0 -269
  108. package/src/stories/docs/ForAIAgents.mdx +0 -162
  109. package/src/stories/docs/GettingStarted.mdx +0 -65
  110. package/src/stories/docs/Installation.mdx +0 -83
  111. package/src/stories/docs/Introduction.mdx +0 -50
  112. package/src/stories/docs/SolidJsAdvanced.mdx +0 -17
  113. package/src/stories/docs/Theming.mdx +0 -85
  114. package/src/stories/docs/element-controls.ts +0 -121
  115. package/src/stories/docs/frameworks/Angular.mdx +0 -245
  116. package/src/stories/docs/frameworks/Html.mdx +0 -227
  117. package/src/stories/docs/frameworks/Overview.mdx +0 -47
  118. package/src/stories/docs/frameworks/React.mdx +0 -227
  119. package/src/stories/docs/frameworks/Solid.mdx +0 -173
  120. package/src/stories/docs/frameworks/Svelte.mdx +0 -194
  121. package/src/stories/docs/frameworks/Vue.mdx +0 -238
  122. package/src/stories/docs/generative-ui-overview.mdx +0 -186
  123. package/src/stories/docs/recipes/SpeechToText.mdx +0 -7
  124. package/src/stories/docs/recipes/Streaming.mdx +0 -68
  125. package/src/stories/docs/recipes/TextToSpeech.mdx +0 -35
  126. package/src/stories/docs/theme-editor/canvas.tsx +0 -32
  127. package/src/stories/docs/theme-editor/inspector.tsx +0 -66
  128. package/src/stories/docs/theme-editor/presets.test.ts +0 -32
  129. package/src/stories/docs/theme-editor/presets.ts +0 -64
  130. package/src/stories/docs/theme-editor/theme-css.test.ts +0 -19
  131. package/src/stories/docs/theme-editor/theme-css.ts +0 -15
  132. package/src/stories/docs/theme-editor/theme-editor.tsx +0 -146
  133. package/src/stories/docs/theme-tokens.tsx +0 -174
  134. package/src/stories/examples/ChoosingComponents.mdx +0 -106
  135. package/src/stories/examples/sample-data.ts +0 -79
  136. package/src/stories/examples/usage/checkpoint-restore.ts +0 -156
  137. package/src/stories/examples/usage/context-usage.ts +0 -591
  138. package/src/stories/examples/usage/conversation-with-reasoning.ts +0 -224
  139. package/src/stories/examples/usage/conversation-with-sources.ts +0 -589
  140. package/src/stories/examples/usage/empty-state.ts +0 -144
  141. package/src/stories/examples/usage/full-chat-app.ts +0 -277
  142. package/src/stories/examples/usage/index.ts +0 -63
  143. package/src/stories/examples/usage/message-actions.ts +0 -704
  144. package/src/stories/examples/usage/pattern-centered-conversation.ts +0 -446
  145. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +0 -442
  146. package/src/stories/examples/usage/pattern-docked-widget.ts +0 -479
  147. package/src/stories/examples/usage/prompt-input-variants.ts +0 -1356
  148. package/src/stories/examples/usage/streaming-response.ts +0 -687
  149. package/src/stories/examples/usage/types.ts +0 -33
  150. package/src/stories/full-chat.stories.tsx +0 -18
  151. package/src/stories/message-actions.stories.tsx +0 -230
  152. package/src/stories/pattern-centered-conversation.stories.tsx +0 -93
  153. package/src/stories/pattern-docked-widget.stories.tsx +0 -93
  154. package/src/stories/pattern-empty-state.stories.tsx +0 -76
  155. package/src/stories/prompt-input-variants.stories.tsx +0 -456
  156. package/src/stories/streaming-response.stories.tsx +0 -438
  157. package/src/stories/theme-editor.stories.tsx +0 -16
  158. package/src/stories/token-reference.stories.tsx +0 -18
  159. package/src/stories/typography.stories.tsx +0 -78
  160. package/src/ui/avatar.stories.tsx +0 -103
  161. package/src/ui/badge.stories.tsx +0 -86
  162. package/src/ui/button.stories.tsx +0 -145
  163. package/src/ui/collapsible.stories.tsx +0 -69
  164. package/src/ui/dropdown.stories.tsx +0 -59
  165. package/src/ui/hover-card.stories.tsx +0 -77
  166. package/src/ui/hover-card.test.tsx +0 -30
  167. package/src/ui/overlay.stories.tsx +0 -115
  168. package/src/ui/popover.stories.tsx +0 -81
  169. package/src/ui/popover.test.tsx +0 -99
  170. package/src/ui/resizable.stories.tsx +0 -236
  171. package/src/ui/scroll-area.stories.tsx +0 -50
  172. package/src/ui/separator.stories.tsx +0 -81
  173. package/src/ui/skeleton.stories.tsx +0 -337
  174. package/src/ui/switch.stories.tsx +0 -51
  175. package/src/ui/switch.test.tsx +0 -59
  176. package/src/ui/textarea.stories.tsx +0 -76
  177. 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
- });