@kitnai/chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +314 -0
  3. package/dist/bash-InADTalH.js +6 -0
  4. package/dist/core-AYMC6_lb.js +5874 -0
  5. package/dist/engine-javascript-vq0WuIJl.js +2643 -0
  6. package/dist/github-dark-dimmed-DUshB20C.js +4 -0
  7. package/dist/github-light-JYsPkUQd.js +4 -0
  8. package/dist/javascript-C25yR2R2.js +6 -0
  9. package/dist/json-DxJze_jm.js +6 -0
  10. package/dist/kitn-chat.es.js +6632 -0
  11. package/dist/tsx-B8rCNbgL.js +6 -0
  12. package/dist/typescript-RycA9KXf.js +6 -0
  13. package/package.json +80 -0
  14. package/src/components/attachments.stories.tsx +304 -0
  15. package/src/components/attachments.tsx +394 -0
  16. package/src/components/chain-of-thought.stories.tsx +212 -0
  17. package/src/components/chain-of-thought.tsx +139 -0
  18. package/src/components/chat-container.stories.tsx +188 -0
  19. package/src/components/chat-container.tsx +78 -0
  20. package/src/components/chat-scope-picker.tsx +47 -0
  21. package/src/components/checkpoint.stories.tsx +103 -0
  22. package/src/components/checkpoint.tsx +81 -0
  23. package/src/components/code-block.stories.tsx +151 -0
  24. package/src/components/code-block.tsx +99 -0
  25. package/src/components/context.stories.tsx +180 -0
  26. package/src/components/context.tsx +323 -0
  27. package/src/components/conversation-item.stories.tsx +126 -0
  28. package/src/components/conversation-item.tsx +18 -0
  29. package/src/components/conversation-list.stories.tsx +134 -0
  30. package/src/components/conversation-list.tsx +100 -0
  31. package/src/components/empty.stories.tsx +435 -0
  32. package/src/components/empty.tsx +166 -0
  33. package/src/components/feedback-bar.stories.tsx +101 -0
  34. package/src/components/feedback-bar.tsx +58 -0
  35. package/src/components/file-upload.stories.tsx +157 -0
  36. package/src/components/file-upload.tsx +161 -0
  37. package/src/components/image.stories.tsx +90 -0
  38. package/src/components/image.tsx +67 -0
  39. package/src/components/loader.stories.tsx +182 -0
  40. package/src/components/loader.tsx +333 -0
  41. package/src/components/markdown.stories.tsx +181 -0
  42. package/src/components/markdown.tsx +81 -0
  43. package/src/components/message-narrow.stories.tsx +330 -0
  44. package/src/components/message-skills.stories.tsx +212 -0
  45. package/src/components/message-skills.tsx +36 -0
  46. package/src/components/message.stories.tsx +282 -0
  47. package/src/components/message.tsx +149 -0
  48. package/src/components/model-switcher.stories.tsx +98 -0
  49. package/src/components/model-switcher.tsx +36 -0
  50. package/src/components/prompt-input.stories.tsx +223 -0
  51. package/src/components/prompt-input.tsx +190 -0
  52. package/src/components/prompt-suggestion.stories.tsx +143 -0
  53. package/src/components/prompt-suggestion.tsx +115 -0
  54. package/src/components/reasoning.stories.tsx +141 -0
  55. package/src/components/reasoning.tsx +157 -0
  56. package/src/components/response-stream.tsx +103 -0
  57. package/src/components/scroll-button.stories.tsx +101 -0
  58. package/src/components/scroll-button.tsx +33 -0
  59. package/src/components/slash-command.stories.tsx +164 -0
  60. package/src/components/slash-command.tsx +223 -0
  61. package/src/components/source.stories.tsx +125 -0
  62. package/src/components/source.tsx +129 -0
  63. package/src/components/text-shimmer.stories.tsx +88 -0
  64. package/src/components/text-shimmer.tsx +37 -0
  65. package/src/components/thinking-bar.stories.tsx +88 -0
  66. package/src/components/thinking-bar.tsx +50 -0
  67. package/src/components/tool.stories.tsx +154 -0
  68. package/src/components/tool.tsx +173 -0
  69. package/src/components/voice-input.stories.tsx +84 -0
  70. package/src/components/voice-input.tsx +103 -0
  71. package/src/elements/chat-types.ts +14 -0
  72. package/src/elements/chat.tsx +111 -0
  73. package/src/elements/compiled.css +2 -0
  74. package/src/elements/conversation-list.tsx +26 -0
  75. package/src/elements/css.ts +5 -0
  76. package/src/elements/default-input.tsx +53 -0
  77. package/src/elements/define.tsx +54 -0
  78. package/src/elements/kitn-chat.stories.tsx +105 -0
  79. package/src/elements/kitn-conversation-list.stories.tsx +177 -0
  80. package/src/elements/kitn-prompt-input.stories.tsx +123 -0
  81. package/src/elements/prompt-input.tsx +39 -0
  82. package/src/elements/register.ts +9 -0
  83. package/src/elements/styles.css +12 -0
  84. package/src/index.ts +128 -0
  85. package/src/primitives/chat-config.tsx +76 -0
  86. package/src/primitives/highlighter.ts +150 -0
  87. package/src/primitives/use-auto-resize.ts +31 -0
  88. package/src/primitives/use-stick-to-bottom.ts +43 -0
  89. package/src/primitives/use-text-stream.ts +112 -0
  90. package/src/primitives/use-voice-recorder.ts +50 -0
  91. package/src/stories/chat-panel-layout.stories.tsx +144 -0
  92. package/src/stories/chat-scene.tsx +570 -0
  93. package/src/stories/checkpoint-restore.stories.tsx +224 -0
  94. package/src/stories/context-usage.stories.tsx +155 -0
  95. package/src/stories/conversation-with-reasoning.stories.tsx +151 -0
  96. package/src/stories/conversation-with-sources.stories.tsx +165 -0
  97. package/src/stories/docs/GettingStarted.mdx +76 -0
  98. package/src/stories/docs/Installation.mdx +48 -0
  99. package/src/stories/docs/Integrations.mdx +110 -0
  100. package/src/stories/docs/Introduction.mdx +29 -0
  101. package/src/stories/docs/Theming.mdx +87 -0
  102. package/src/stories/docs/theme-editor/canvas.tsx +32 -0
  103. package/src/stories/docs/theme-editor/inspector.tsx +66 -0
  104. package/src/stories/docs/theme-editor/presets.test.ts +32 -0
  105. package/src/stories/docs/theme-editor/presets.ts +64 -0
  106. package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
  107. package/src/stories/docs/theme-editor/theme-css.ts +15 -0
  108. package/src/stories/docs/theme-editor/theme-editor.tsx +145 -0
  109. package/src/stories/docs/theme-tokens.tsx +174 -0
  110. package/src/stories/full-chat.stories.tsx +18 -0
  111. package/src/stories/message-actions.stories.tsx +167 -0
  112. package/src/stories/prompt-input-variants.stories.tsx +179 -0
  113. package/src/stories/streaming-response.stories.tsx +234 -0
  114. package/src/stories/theme-editor.stories.tsx +16 -0
  115. package/src/stories/token-reference.stories.tsx +18 -0
  116. package/src/types.ts +41 -0
  117. package/src/ui/avatar.stories.tsx +104 -0
  118. package/src/ui/avatar.tsx +23 -0
  119. package/src/ui/badge.stories.tsx +87 -0
  120. package/src/ui/badge.tsx +21 -0
  121. package/src/ui/button.stories.tsx +146 -0
  122. package/src/ui/button.tsx +37 -0
  123. package/src/ui/collapsible.tsx +14 -0
  124. package/src/ui/dialog.tsx +21 -0
  125. package/src/ui/dropdown.tsx +26 -0
  126. package/src/ui/hover-card.tsx +48 -0
  127. package/src/ui/resizable.stories.tsx +171 -0
  128. package/src/ui/resizable.tsx +219 -0
  129. package/src/ui/scroll-area.tsx +13 -0
  130. package/src/ui/separator.stories.tsx +82 -0
  131. package/src/ui/separator.tsx +10 -0
  132. package/src/ui/skeleton.stories.tsx +338 -0
  133. package/src/ui/skeleton.tsx +16 -0
  134. package/src/ui/textarea.tsx +21 -0
  135. package/src/ui/tooltip.stories.tsx +75 -0
  136. package/src/ui/tooltip.tsx +22 -0
  137. package/src/utils/cn.ts +6 -0
  138. package/theme.css +115 -0
@@ -0,0 +1,219 @@
1
+ import { type JSX, splitProps, createSignal, createContext, useContext, onCleanup, children as resolveChildren } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+
4
+ // --- Types ---
5
+
6
+ type Orientation = 'horizontal' | 'vertical';
7
+
8
+ interface ResizableContextValue {
9
+ orientation: Orientation;
10
+ registerPanel: (id: string, opts: { defaultSize?: number; minSize?: number; maxSize?: number }) => void;
11
+ }
12
+
13
+ const ResizableContext = createContext<ResizableContextValue>();
14
+
15
+ // --- ResizablePanelGroup ---
16
+
17
+ export interface ResizablePanelGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
18
+ orientation?: Orientation;
19
+ children: JSX.Element;
20
+ }
21
+
22
+ function ResizablePanelGroup(props: ResizablePanelGroupProps) {
23
+ const [local, rest] = splitProps(props, ['orientation', 'children', 'class']);
24
+ const orientation = () => local.orientation ?? 'horizontal';
25
+
26
+ return (
27
+ <ResizableContext.Provider value={{ orientation: orientation(), registerPanel: () => {} }}>
28
+ <div
29
+ class={cn(
30
+ 'flex h-full w-full',
31
+ orientation() === 'vertical' ? 'flex-col' : 'flex-row',
32
+ local.class
33
+ )}
34
+ data-orientation={orientation()}
35
+ {...rest}
36
+ >
37
+ {local.children}
38
+ </div>
39
+ </ResizableContext.Provider>
40
+ );
41
+ }
42
+
43
+ // --- ResizablePanel ---
44
+
45
+ export interface ResizablePanelProps extends JSX.HTMLAttributes<HTMLDivElement> {
46
+ defaultSize?: number;
47
+ minSize?: number;
48
+ maxSize?: number;
49
+ children: JSX.Element;
50
+ }
51
+
52
+ function ResizablePanel(props: ResizablePanelProps) {
53
+ const [local, rest] = splitProps(props, ['defaultSize', 'minSize', 'maxSize', 'children', 'class', 'style']);
54
+
55
+ const sizeStyle = () => {
56
+ const s = local.defaultSize;
57
+ if (s !== undefined) {
58
+ return { 'flex-basis': `${s}%`, 'flex-grow': '0', 'flex-shrink': '0' };
59
+ }
60
+ return { flex: '1 1 0%' };
61
+ };
62
+
63
+ return (
64
+ <div
65
+ class={cn('overflow-hidden', local.class)}
66
+ style={{ ...sizeStyle(), ...(typeof local.style === 'object' ? local.style : {}) }}
67
+ {...rest}
68
+ >
69
+ {local.children}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ // --- ResizableHandle ---
75
+
76
+ export interface ResizableHandleProps extends JSX.HTMLAttributes<HTMLDivElement> {
77
+ withHandle?: boolean;
78
+ onPanelResize?: (delta: number) => void;
79
+ }
80
+
81
+ function ResizableHandle(props: ResizableHandleProps) {
82
+ const [local, rest] = splitProps(props, ['withHandle', 'onPanelResize', 'class']);
83
+ const ctx = useContext(ResizableContext);
84
+ const orientation = () => ctx?.orientation ?? 'horizontal';
85
+ const [isDragging, setIsDragging] = createSignal(false);
86
+
87
+ let startPos = 0;
88
+ let prevEl: HTMLElement | null = null;
89
+ let nextEl: HTMLElement | null = null;
90
+ let prevSize = 0;
91
+ let nextSize = 0;
92
+
93
+ const handlePointerDown = (e: PointerEvent) => {
94
+ const handle = e.currentTarget as HTMLElement;
95
+ prevEl = handle.previousElementSibling as HTMLElement;
96
+ nextEl = handle.nextElementSibling as HTMLElement;
97
+
98
+ if (!prevEl || !nextEl) return;
99
+
100
+ e.preventDefault();
101
+ setIsDragging(true);
102
+ handle.setPointerCapture(e.pointerId);
103
+
104
+ if (orientation() === 'horizontal') {
105
+ startPos = e.clientX;
106
+ prevSize = prevEl.getBoundingClientRect().width;
107
+ nextSize = nextEl.getBoundingClientRect().width;
108
+ } else {
109
+ startPos = e.clientY;
110
+ prevSize = prevEl.getBoundingClientRect().height;
111
+ nextSize = nextEl.getBoundingClientRect().height;
112
+ }
113
+ };
114
+
115
+ const handlePointerMove = (e: PointerEvent) => {
116
+ if (!isDragging() || !prevEl || !nextEl) return;
117
+
118
+ const currentPos = orientation() === 'horizontal' ? e.clientX : e.clientY;
119
+ const delta = currentPos - startPos;
120
+
121
+ const newPrevSize = prevSize + delta;
122
+ const newNextSize = nextSize - delta;
123
+
124
+ // Get min/max from data attributes or use defaults
125
+ const prevMin = parseInt(prevEl.dataset.minSize || '0', 10);
126
+ const prevMax = parseInt(prevEl.dataset.maxSize || '999999', 10);
127
+ const nextMin = parseInt(nextEl.dataset.minSize || '0', 10);
128
+ const nextMax = parseInt(nextEl.dataset.maxSize || '999999', 10);
129
+
130
+ if (newPrevSize < prevMin || newNextSize < nextMin || newPrevSize > prevMax || newNextSize > nextMax) return;
131
+
132
+ prevEl.style.flexBasis = `${newPrevSize}px`;
133
+ prevEl.style.flexGrow = '0';
134
+ prevEl.style.flexShrink = '0';
135
+ nextEl.style.flexBasis = `${newNextSize}px`;
136
+ nextEl.style.flexGrow = '0';
137
+ nextEl.style.flexShrink = '0';
138
+
139
+ local.onPanelResize?.(delta);
140
+ };
141
+
142
+ const handlePointerUp = () => {
143
+ // Convert pixel flex-basis to percentages so panels scale with window resize
144
+ if (prevEl && nextEl) {
145
+ const container = prevEl.parentElement;
146
+ if (container) {
147
+ const totalSize = orientation() === 'horizontal'
148
+ ? container.getBoundingClientRect().width
149
+ : container.getBoundingClientRect().height;
150
+ const handleSize = (prevEl.nextElementSibling as HTMLElement)?.getBoundingClientRect()[
151
+ orientation() === 'horizontal' ? 'width' : 'height'
152
+ ] ?? 0;
153
+ const available = totalSize - handleSize;
154
+ if (available > 0) {
155
+ const prevPct = (prevEl.getBoundingClientRect()[orientation() === 'horizontal' ? 'width' : 'height'] / available) * 100;
156
+ const nextPct = 100 - prevPct;
157
+ prevEl.style.flexBasis = `${prevPct}%`;
158
+ nextEl.style.flexBasis = `${nextPct}%`;
159
+ }
160
+ }
161
+ }
162
+ setIsDragging(false);
163
+ prevEl = null;
164
+ nextEl = null;
165
+ };
166
+
167
+ const isHoriz = () => orientation() === 'horizontal';
168
+
169
+ return (
170
+ <div
171
+ class={cn(
172
+ 'relative flex items-center justify-center',
173
+ local.class
174
+ )}
175
+ style={{
176
+ cursor: isHoriz() ? 'col-resize' : 'row-resize',
177
+ [isHoriz() ? 'width' : 'height']: '8px',
178
+ 'background': isDragging() ? 'var(--color-muted-foreground, #98989f)' : 'transparent',
179
+ 'opacity': isDragging() ? '0.3' : '1',
180
+ }}
181
+ onPointerDown={handlePointerDown}
182
+ onPointerMove={handlePointerMove}
183
+ onPointerUp={handlePointerUp}
184
+ role="separator"
185
+ tabIndex={0}
186
+ data-orientation={orientation()}
187
+ {...rest}
188
+ >
189
+ {local.withHandle && (
190
+ <div
191
+ class={cn(
192
+ 'z-10 flex items-center justify-center',
193
+ orientation() === 'horizontal'
194
+ ? 'h-6 w-3 flex-col'
195
+ : 'h-3 w-6 flex-row',
196
+ )}
197
+ >
198
+ <svg
199
+ class={cn(
200
+ 'text-muted-foreground/40',
201
+ orientation() === 'horizontal' ? 'h-3 w-2' : 'h-2 w-3 rotate-90'
202
+ )}
203
+ viewBox="0 0 4 8"
204
+ fill="currentColor"
205
+ >
206
+ <circle cx="1" cy="1.5" r="0.6" />
207
+ <circle cx="3" cy="1.5" r="0.6" />
208
+ <circle cx="1" cy="4" r="0.6" />
209
+ <circle cx="3" cy="4" r="0.6" />
210
+ <circle cx="1" cy="6.5" r="0.6" />
211
+ <circle cx="3" cy="6.5" r="0.6" />
212
+ </svg>
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
@@ -0,0 +1,13 @@
1
+ import { type JSX, splitProps } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> { children: JSX.Element; }
5
+
6
+ export function ScrollArea(props: ScrollAreaProps) {
7
+ const [local, rest] = splitProps(props, ['children', 'class']);
8
+ return (
9
+ <div class={cn('overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent', local.class)} {...rest}>
10
+ {local.children}
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { Separator } from './separator';
3
+
4
+ const meta = {
5
+ title: 'UI/Separator',
6
+ component: Separator,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ controls: { exclude: ['use:eventListener'] },
12
+ description: {
13
+ component: [
14
+ 'A thin **divider** line (a `role="separator"` element) that visually splits content either horizontally or vertically.',
15
+ '**When to use:** to separate stacked sections, group items in a menu/list, or divide inline controls in a toolbar.',
16
+ '**How to use:** set `orientation` to `horizontal` (full-width line) or `vertical` (full-height line). For a vertical separator, give the parent a height so the line has something to fill.',
17
+ '**Placement:** between message groups, in dropdown/menu sections, header toolbars, and between sidebar regions.',
18
+ ].join('\n\n'),
19
+ },
20
+ },
21
+ },
22
+ argTypes: {
23
+ orientation: {
24
+ control: 'select',
25
+ options: ['horizontal', 'vertical'],
26
+ description: 'Direction of the divider line.',
27
+ table: { defaultValue: { summary: 'horizontal' } },
28
+ },
29
+ },
30
+ args: {
31
+ orientation: 'horizontal',
32
+ },
33
+ render: (args) => (
34
+ <div class="w-64 p-4">
35
+ <p class="text-sm text-foreground mb-3">Above</p>
36
+ <Separator {...args} />
37
+ <p class="text-sm text-foreground mt-3">Below</p>
38
+ </div>
39
+ ),
40
+ } satisfies Meta<typeof Separator>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ const IMPORT = `import { Separator } from '@kitnai/chat';`;
46
+ const src = (code: string) => ({
47
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
48
+ });
49
+
50
+ /** Interactive playground — flip the orientation. */
51
+ export const Playground: Story = {
52
+ ...src(`<Separator orientation="horizontal" />`),
53
+ };
54
+
55
+ export const Horizontal: Story = {
56
+ args: { orientation: 'horizontal' },
57
+ ...src(`<div class="w-64 p-4">
58
+ <p class="text-sm mb-3">Above</p>
59
+ <Separator />
60
+ <p class="text-sm mt-3">Below</p>
61
+ </div>`),
62
+ };
63
+
64
+ /** Vertical divider — needs a parent with a fixed height (showcase). */
65
+ export const Vertical: Story = {
66
+ render: () => (
67
+ <div class="flex items-center gap-3 p-4">
68
+ <span class="text-sm text-foreground">Left</span>
69
+ <div class="h-6">
70
+ <Separator orientation="vertical" />
71
+ </div>
72
+ <span class="text-sm text-foreground">Right</span>
73
+ </div>
74
+ ),
75
+ ...src(`<div class="flex items-center gap-3 p-4">
76
+ <span class="text-sm">Left</span>
77
+ <div class="h-6">
78
+ <Separator orientation="vertical" />
79
+ </div>
80
+ <span class="text-sm">Right</span>
81
+ </div>`),
82
+ };
@@ -0,0 +1,10 @@
1
+ import { type JSX, splitProps } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export interface SeparatorProps extends JSX.HTMLAttributes<HTMLDivElement> { orientation?: 'horizontal' | 'vertical'; }
5
+
6
+ export function Separator(props: SeparatorProps) {
7
+ const [local, rest] = splitProps(props, ['orientation', 'class']);
8
+ const isVertical = () => local.orientation === 'vertical';
9
+ return <div role="separator" class={cn('shrink-0 bg-border', isVertical() ? 'h-full w-px' : 'h-px w-full', local.class)} {...rest} />;
10
+ }
@@ -0,0 +1,338 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { Skeleton } from './skeleton';
3
+
4
+ const meta = {
5
+ title: 'UI/Skeleton',
6
+ component: Skeleton,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ controls: { exclude: ['use:eventListener'] },
12
+ description: {
13
+ component: [
14
+ 'A pulsing **placeholder** block used to indicate loading content. It has no shape of its own — size and rounding come from the `class` you pass.',
15
+ '**When to use:** while content (messages, lists, cards, tool output) is loading, to preserve layout and signal progress without a spinner.',
16
+ '**How to use:** compose one or more `Skeleton` elements and set width/height/rounding via utility classes (e.g. `class="h-4 w-3/4"`). Build skeletons that mirror the real layout they replace.',
17
+ '**Placement:** message bubbles, conversation lists, code blocks, tool calls, input areas, and full-page loading states.',
18
+ ].join('\n\n'),
19
+ },
20
+ },
21
+ },
22
+ argTypes: {
23
+ class: {
24
+ control: 'text',
25
+ description: 'Utility classes that set the size and rounding of the placeholder.',
26
+ },
27
+ },
28
+ args: {
29
+ class: 'h-4 w-64',
30
+ },
31
+ render: (args) => <Skeleton {...args} />,
32
+ } satisfies Meta<typeof Skeleton>;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ const IMPORT = `import { Skeleton } from '@kitnai/chat';`;
38
+ const src = (code: string) => ({
39
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
40
+ });
41
+
42
+ /** Interactive playground — edit `class` to resize the placeholder. */
43
+ export const Playground: Story = {
44
+ ...src(`<Skeleton class="h-4 w-64" />`),
45
+ };
46
+
47
+ export const Basic: Story = {
48
+ render: () => (
49
+ <div class="space-y-3 w-80">
50
+ <Skeleton class="h-4 w-full" />
51
+ <Skeleton class="h-4 w-3/4" />
52
+ <Skeleton class="h-4 w-1/2" />
53
+ </div>
54
+ ),
55
+ ...src(`<div class="space-y-3 w-80">
56
+ <Skeleton class="h-4 w-full" />
57
+ <Skeleton class="h-4 w-3/4" />
58
+ <Skeleton class="h-4 w-1/2" />
59
+ </div>`),
60
+ };
61
+
62
+ export const MessageBubble: Story = {
63
+ name: 'Message Bubble',
64
+ render: () => (
65
+ <div class="space-y-6 w-full max-w-2xl">
66
+ {/* User message skeleton */}
67
+ <div class="flex justify-end">
68
+ <Skeleton class="h-12 w-64 rounded-3xl" />
69
+ </div>
70
+
71
+ {/* Assistant message skeleton */}
72
+ <div class="flex gap-3 items-start">
73
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
74
+ <div class="flex-1 space-y-2">
75
+ <Skeleton class="h-4 w-full" />
76
+ <Skeleton class="h-4 w-full" />
77
+ <Skeleton class="h-4 w-5/6" />
78
+ <Skeleton class="h-4 w-2/3" />
79
+ </div>
80
+ </div>
81
+ </div>
82
+ ),
83
+ ...src(`<div class="space-y-6 w-full max-w-2xl">
84
+ <div class="flex justify-end">
85
+ <Skeleton class="h-12 w-64 rounded-3xl" />
86
+ </div>
87
+ <div class="flex gap-3 items-start">
88
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
89
+ <div class="flex-1 space-y-2">
90
+ <Skeleton class="h-4 w-full" />
91
+ <Skeleton class="h-4 w-full" />
92
+ <Skeleton class="h-4 w-5/6" />
93
+ <Skeleton class="h-4 w-2/3" />
94
+ </div>
95
+ </div>
96
+ </div>`),
97
+ };
98
+
99
+ export const MessageWithCode: Story = {
100
+ name: 'Message with Code Block',
101
+ render: () => (
102
+ <div class="flex gap-3 items-start w-full max-w-2xl">
103
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
104
+ <div class="flex-1 space-y-3">
105
+ <div class="space-y-2">
106
+ <Skeleton class="h-4 w-full" />
107
+ <Skeleton class="h-4 w-4/5" />
108
+ </div>
109
+ {/* Code block skeleton */}
110
+ <Skeleton class="h-32 w-full rounded-xl" />
111
+ <div class="space-y-2">
112
+ <Skeleton class="h-4 w-full" />
113
+ <Skeleton class="h-4 w-3/5" />
114
+ </div>
115
+ </div>
116
+ </div>
117
+ ),
118
+ ...src(`<div class="flex gap-3 items-start w-full max-w-2xl">
119
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
120
+ <div class="flex-1 space-y-3">
121
+ <div class="space-y-2">
122
+ <Skeleton class="h-4 w-full" />
123
+ <Skeleton class="h-4 w-4/5" />
124
+ </div>
125
+ <Skeleton class="h-32 w-full rounded-xl" />
126
+ <div class="space-y-2">
127
+ <Skeleton class="h-4 w-full" />
128
+ <Skeleton class="h-4 w-3/5" />
129
+ </div>
130
+ </div>
131
+ </div>`),
132
+ };
133
+
134
+ export const ConversationList: Story = {
135
+ name: 'Conversation List',
136
+ render: () => (
137
+ <div class="w-64 space-y-1">
138
+ {/* Section header */}
139
+ <Skeleton class="h-3 w-12 mb-2" />
140
+ {[1, 2, 3].map(() => (
141
+ <div class="flex items-center gap-2 px-2 py-2">
142
+ <Skeleton class="h-4 w-4 rounded shrink-0" />
143
+ <Skeleton class="h-4 flex-1" />
144
+ </div>
145
+ ))}
146
+ {/* Section header */}
147
+ <Skeleton class="h-3 w-16 mt-4 mb-2" />
148
+ {[1, 2].map(() => (
149
+ <div class="flex items-center gap-2 px-2 py-2">
150
+ <Skeleton class="h-4 w-4 rounded shrink-0" />
151
+ <Skeleton class="h-4 flex-1" />
152
+ </div>
153
+ ))}
154
+ </div>
155
+ ),
156
+ ...src(`<div class="w-64 space-y-1">
157
+ <Skeleton class="h-3 w-12 mb-2" />
158
+ {[1, 2, 3].map(() => (
159
+ <div class="flex items-center gap-2 px-2 py-2">
160
+ <Skeleton class="h-4 w-4 rounded shrink-0" />
161
+ <Skeleton class="h-4 flex-1" />
162
+ </div>
163
+ ))}
164
+ </div>`),
165
+ };
166
+
167
+ export const ToolCall: Story = {
168
+ name: 'Tool Call',
169
+ render: () => (
170
+ <div class="w-full max-w-2xl">
171
+ <div class="border border-border rounded-xl overflow-hidden">
172
+ {/* Tool header */}
173
+ <div class="flex items-center gap-2 px-3 py-2.5">
174
+ <Skeleton class="h-4 w-4 rounded-full" />
175
+ <Skeleton class="h-4 w-32" />
176
+ <Skeleton class="h-5 w-20 rounded-full" />
177
+ </div>
178
+ {/* Tool body */}
179
+ <div class="border-t border-border p-3 space-y-3">
180
+ <div>
181
+ <Skeleton class="h-3 w-10 mb-2" />
182
+ <Skeleton class="h-20 w-full rounded" />
183
+ </div>
184
+ <div>
185
+ <Skeleton class="h-3 w-12 mb-2" />
186
+ <Skeleton class="h-16 w-full rounded" />
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ ),
192
+ ...src(`<div class="w-full max-w-2xl">
193
+ <div class="border border-border rounded-xl overflow-hidden">
194
+ <div class="flex items-center gap-2 px-3 py-2.5">
195
+ <Skeleton class="h-4 w-4 rounded-full" />
196
+ <Skeleton class="h-4 w-32" />
197
+ <Skeleton class="h-5 w-20 rounded-full" />
198
+ </div>
199
+ <div class="border-t border-border p-3 space-y-3">
200
+ <Skeleton class="h-3 w-10 mb-2" />
201
+ <Skeleton class="h-20 w-full rounded" />
202
+ </div>
203
+ </div>
204
+ </div>`),
205
+ };
206
+
207
+ export const Card: Story = {
208
+ name: 'Content Card',
209
+ render: () => (
210
+ <div class="w-72">
211
+ <Skeleton class="h-40 w-full rounded-xl mb-3" />
212
+ <div class="flex gap-3">
213
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
214
+ <div class="flex-1 space-y-2">
215
+ <Skeleton class="h-4 w-full" />
216
+ <Skeleton class="h-3 w-24" />
217
+ <Skeleton class="h-3 w-32" />
218
+ </div>
219
+ </div>
220
+ </div>
221
+ ),
222
+ ...src(`<div class="w-72">
223
+ <Skeleton class="h-40 w-full rounded-xl mb-3" />
224
+ <div class="flex gap-3">
225
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
226
+ <div class="flex-1 space-y-2">
227
+ <Skeleton class="h-4 w-full" />
228
+ <Skeleton class="h-3 w-24" />
229
+ <Skeleton class="h-3 w-32" />
230
+ </div>
231
+ </div>
232
+ </div>`),
233
+ };
234
+
235
+ export const InputArea: Story = {
236
+ name: 'Input Area',
237
+ render: () => (
238
+ <div class="w-full max-w-2xl">
239
+ <div class="border border-border rounded-2xl p-3 space-y-3">
240
+ <Skeleton class="h-10 w-full rounded-lg" />
241
+ <div class="flex items-center justify-between">
242
+ <div class="flex gap-2">
243
+ <Skeleton class="h-8 w-8 rounded-full" />
244
+ <Skeleton class="h-8 w-8 rounded-full" />
245
+ <Skeleton class="h-8 w-8 rounded-full" />
246
+ </div>
247
+ <Skeleton class="h-8 w-8 rounded-full" />
248
+ </div>
249
+ </div>
250
+ </div>
251
+ ),
252
+ ...src(`<div class="w-full max-w-2xl">
253
+ <div class="border border-border rounded-2xl p-3 space-y-3">
254
+ <Skeleton class="h-10 w-full rounded-lg" />
255
+ <div class="flex items-center justify-between">
256
+ <div class="flex gap-2">
257
+ <Skeleton class="h-8 w-8 rounded-full" />
258
+ <Skeleton class="h-8 w-8 rounded-full" />
259
+ <Skeleton class="h-8 w-8 rounded-full" />
260
+ </div>
261
+ <Skeleton class="h-8 w-8 rounded-full" />
262
+ </div>
263
+ </div>
264
+ </div>`),
265
+ };
266
+
267
+ export const FullChat: Story = {
268
+ name: 'Full Chat Layout',
269
+ render: () => (
270
+ <div class="flex w-full max-w-4xl h-96 border border-border rounded-xl overflow-hidden">
271
+ {/* Sidebar */}
272
+ <div class="w-56 border-r border-border p-3 space-y-3 shrink-0">
273
+ <Skeleton class="h-8 w-full rounded-lg" />
274
+ <div class="space-y-1 mt-4">
275
+ <Skeleton class="h-3 w-12 mb-2" />
276
+ {[1, 2, 3].map(() => (
277
+ <Skeleton class="h-8 w-full rounded-lg" />
278
+ ))}
279
+ <Skeleton class="h-3 w-16 mt-3 mb-2" />
280
+ {[1, 2].map(() => (
281
+ <Skeleton class="h-8 w-full rounded-lg" />
282
+ ))}
283
+ </div>
284
+ </div>
285
+
286
+ {/* Main */}
287
+ <div class="flex-1 flex flex-col">
288
+ {/* Header */}
289
+ <div class="flex items-center justify-between px-4 py-3 border-b border-border">
290
+ <Skeleton class="h-4 w-48" />
291
+ <div class="flex gap-2">
292
+ <Skeleton class="h-8 w-28 rounded-lg" />
293
+ <Skeleton class="h-8 w-8 rounded-full" />
294
+ </div>
295
+ </div>
296
+
297
+ {/* Messages */}
298
+ <div class="flex-1 p-4 space-y-6 overflow-hidden">
299
+ <div class="flex justify-end">
300
+ <Skeleton class="h-10 w-52 rounded-3xl" />
301
+ </div>
302
+ <div class="flex gap-3 items-start">
303
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
304
+ <div class="flex-1 space-y-2">
305
+ <Skeleton class="h-4 w-full" />
306
+ <Skeleton class="h-4 w-5/6" />
307
+ <Skeleton class="h-4 w-2/3" />
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ {/* Input */}
313
+ <div class="p-3">
314
+ <Skeleton class="h-14 w-full rounded-2xl" />
315
+ </div>
316
+ </div>
317
+ </div>
318
+ ),
319
+ ...src(`<div class="flex w-full max-w-4xl h-96 border rounded-xl overflow-hidden">
320
+ <div class="w-56 border-r p-3 space-y-3 shrink-0">
321
+ <Skeleton class="h-8 w-full rounded-lg" />
322
+ </div>
323
+ <div class="flex-1 flex flex-col">
324
+ <div class="flex-1 p-4 space-y-6">
325
+ <div class="flex gap-3 items-start">
326
+ <Skeleton class="h-8 w-8 rounded-full shrink-0" />
327
+ <div class="flex-1 space-y-2">
328
+ <Skeleton class="h-4 w-full" />
329
+ <Skeleton class="h-4 w-5/6" />
330
+ </div>
331
+ </div>
332
+ </div>
333
+ <div class="p-3">
334
+ <Skeleton class="h-14 w-full rounded-2xl" />
335
+ </div>
336
+ </div>
337
+ </div>`),
338
+ };
@@ -0,0 +1,16 @@
1
+ import { type JSX, splitProps } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export interface SkeletonProps extends JSX.HTMLAttributes<HTMLDivElement> {}
5
+
6
+ function Skeleton(props: SkeletonProps) {
7
+ const [local, rest] = splitProps(props, ['class']);
8
+ return (
9
+ <div
10
+ class={cn('animate-pulse rounded-md bg-muted', local.class)}
11
+ {...rest}
12
+ />
13
+ );
14
+ }
15
+
16
+ export { Skeleton };