@kitnai/chat 0.4.0 → 0.6.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.
- package/README.md +74 -5
- package/dist/custom-elements.json +475 -0
- package/dist/kitn-chat.es.js +18 -18
- package/dist/llms/llms-full.txt +55 -4
- package/dist/llms/llms.txt +3 -3
- package/dist/theme.tokens.css +31 -3
- package/frameworks/react/index.tsx +54 -0
- package/llms-full.txt +55 -4
- package/llms.txt +3 -3
- package/package.json +20 -2
- package/src/components/chat-thread.tsx +217 -0
- package/src/components/prompt-input.tsx +5 -0
- package/src/elements/chat-workspace.tsx +122 -0
- package/src/elements/chat.tsx +30 -271
- package/src/elements/compiled.css +1 -1
- package/src/elements/define.tsx +1 -1
- package/src/elements/element-types.d.ts +40 -0
- package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
- package/src/elements/register.ts +1 -0
- package/src/elements/styles.css +35 -0
- package/src/primitives/chat-config.tsx +1 -1
- package/src/stories/docs/Installation.mdx +27 -0
- package/src/stories/docs/Integrations.mdx +2 -0
- package/src/stories/docs/Introduction.mdx +12 -3
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/ui/collapsible.stories.tsx +70 -0
- package/src/ui/dropdown.stories.tsx +60 -0
- package/src/ui/hover-card.stories.tsx +78 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +1 -1
- package/src/ui/scroll-area.stories.tsx +51 -0
- package/src/ui/scroll-area.tsx +1 -1
- package/src/ui/textarea.stories.tsx +77 -0
- package/theme.css +31 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { onMount } from 'solid-js';
|
|
3
|
+
import './register'; // side effect: registers all kitn custom elements including <kitn-chat-workspace>
|
|
4
|
+
import type { ConversationGroup, ConversationSummary, ModelOption } from '../types';
|
|
5
|
+
import type { ChatMessage } from './chat-types';
|
|
6
|
+
|
|
7
|
+
// The web components are custom DOM elements, so declare the tags for JSX.
|
|
8
|
+
declare module 'solid-js' {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
10
|
+
namespace JSX {
|
|
11
|
+
interface IntrinsicElements {
|
|
12
|
+
'kitn-chat-workspace': JSX.HTMLAttributes<HTMLElement>;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sampleGroups: ConversationGroup[] = [
|
|
18
|
+
{ id: 'today', name: 'Today', sortOrder: 0, createdAt: '2026-06-13T00:00:00.000Z' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const sampleConversations: ConversationSummary[] = [
|
|
22
|
+
{
|
|
23
|
+
id: '1',
|
|
24
|
+
title: 'Web component architecture',
|
|
25
|
+
groupId: 'today',
|
|
26
|
+
scope: { type: 'document' },
|
|
27
|
+
messageCount: 12,
|
|
28
|
+
lastMessageAt: '2026-06-13T15:30:00.000Z',
|
|
29
|
+
updatedAt: '2026-06-13T15:30:00.000Z',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: '2',
|
|
33
|
+
title: 'Theming & tokens',
|
|
34
|
+
groupId: 'today',
|
|
35
|
+
scope: { type: 'document' },
|
|
36
|
+
messageCount: 5,
|
|
37
|
+
lastMessageAt: '2026-06-13T11:20:00.000Z',
|
|
38
|
+
updatedAt: '2026-06-13T11:20:00.000Z',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const sampleModels: ModelOption[] = [
|
|
43
|
+
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
|
|
44
|
+
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const sampleMessages: ChatMessage[] = [
|
|
48
|
+
{ id: 'm1', role: 'user', content: 'How do I drop the whole chat app in with one tag?' },
|
|
49
|
+
{
|
|
50
|
+
id: 'm2',
|
|
51
|
+
role: 'assistant',
|
|
52
|
+
content:
|
|
53
|
+
'Use `<kitn-chat-workspace>` — set `conversations`, `messages`, and `models` as properties and listen for `conversationselect` + `submit`.',
|
|
54
|
+
actions: ['copy', 'like'],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** Live demo of the actual `<kitn-chat-workspace>` custom element (Shadow DOM and all). */
|
|
59
|
+
function WorkspaceElement() {
|
|
60
|
+
let el:
|
|
61
|
+
| (HTMLElement & {
|
|
62
|
+
groups?: ConversationGroup[];
|
|
63
|
+
conversations?: ConversationSummary[];
|
|
64
|
+
activeId?: string;
|
|
65
|
+
messages?: ChatMessage[];
|
|
66
|
+
models?: ModelOption[];
|
|
67
|
+
currentModel?: string;
|
|
68
|
+
chatTitle?: string;
|
|
69
|
+
})
|
|
70
|
+
| undefined;
|
|
71
|
+
onMount(() => {
|
|
72
|
+
if (el) {
|
|
73
|
+
el.groups = sampleGroups;
|
|
74
|
+
el.conversations = sampleConversations;
|
|
75
|
+
el.activeId = '1';
|
|
76
|
+
el.messages = sampleMessages;
|
|
77
|
+
el.models = sampleModels;
|
|
78
|
+
el.currentModel = 'claude-4';
|
|
79
|
+
el.chatTitle = 'Web component architecture';
|
|
80
|
+
el.addEventListener('conversationselect', (e) => console.log('select', (e as CustomEvent).detail));
|
|
81
|
+
el.addEventListener('submit', (e) => console.log('submit', (e as unknown as CustomEvent).detail));
|
|
82
|
+
el.addEventListener('sidebartoggle', (e) => console.log('sidebartoggle', (e as CustomEvent).detail));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return (
|
|
86
|
+
<div style={{ height: '720px', width: '100%' }}>
|
|
87
|
+
<kitn-chat-workspace
|
|
88
|
+
ref={(e) => (el = e as HTMLElement)}
|
|
89
|
+
style={{ display: 'block', height: '100%' }}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
|
|
96
|
+
<kitn-chat-workspace id="workspace" style="display:block; height:100vh;"></kitn-chat-workspace>
|
|
97
|
+
|
|
98
|
+
<script type="module">
|
|
99
|
+
import '@kitnai/chat/elements'; // registers the custom elements
|
|
100
|
+
|
|
101
|
+
const workspace = document.getElementById('workspace');
|
|
102
|
+
workspace.conversations = [
|
|
103
|
+
{
|
|
104
|
+
id: '1', title: 'Web component architecture',
|
|
105
|
+
scope: { type: 'document' }, messageCount: 12,
|
|
106
|
+
lastMessageAt: '2026-06-13T15:30:00Z', updatedAt: '2026-06-13T15:30:00Z',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
workspace.messages = [
|
|
110
|
+
{ id: 'm1', role: 'user', content: 'How do I drop the whole chat app in with one tag?' },
|
|
111
|
+
{ id: 'm2', role: 'assistant', content: 'Use <kitn-chat-workspace> — set conversations, messages, and models as properties.' },
|
|
112
|
+
];
|
|
113
|
+
workspace.models = [
|
|
114
|
+
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// events are CustomEvents on the element (they do not bubble)
|
|
118
|
+
workspace.addEventListener('conversationselect', (e) => console.log('selected conversation:', e.detail.id));
|
|
119
|
+
workspace.addEventListener('submit', (e) => console.log('user sent:', e.detail.value));
|
|
120
|
+
workspace.addEventListener('sidebartoggle', (e) => console.log('sidebar collapsed:', e.detail.collapsed));
|
|
121
|
+
</script>`;
|
|
122
|
+
|
|
123
|
+
const SOLID_SNIPPET = `import '@kitnai/chat/elements'; // registers the custom elements
|
|
124
|
+
import { onMount } from 'solid-js';
|
|
125
|
+
import type { ConversationSummary, ModelOption } from '@kitnai/chat';
|
|
126
|
+
import type { ChatMessage } from '@kitnai/chat/elements';
|
|
127
|
+
|
|
128
|
+
function Workspace() {
|
|
129
|
+
let el: HTMLElement & {
|
|
130
|
+
conversations?: ConversationSummary[];
|
|
131
|
+
messages?: ChatMessage[];
|
|
132
|
+
models?: ModelOption[];
|
|
133
|
+
activeId?: string;
|
|
134
|
+
};
|
|
135
|
+
const conversations: ConversationSummary[] = [
|
|
136
|
+
{
|
|
137
|
+
id: '1', title: 'Web component architecture',
|
|
138
|
+
scope: { type: 'document' }, messageCount: 12,
|
|
139
|
+
lastMessageAt: '2026-06-13T15:30:00Z', updatedAt: '2026-06-13T15:30:00Z',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
const messages: ChatMessage[] = [
|
|
143
|
+
{ id: 'm1', role: 'user', content: 'How do I drop the whole chat app in with one tag?' },
|
|
144
|
+
{ id: 'm2', role: 'assistant', content: 'Use <kitn-chat-workspace> — set conversations, messages, and models as properties.' },
|
|
145
|
+
];
|
|
146
|
+
onMount(() => {
|
|
147
|
+
el.conversations = conversations;
|
|
148
|
+
el.messages = messages;
|
|
149
|
+
el.activeId = '1';
|
|
150
|
+
el.addEventListener('conversationselect', (e) => console.log('selected:', e.detail.id));
|
|
151
|
+
el.addEventListener('submit', (e) => console.log('user sent:', e.detail.value));
|
|
152
|
+
el.addEventListener('sidebartoggle', (e) => console.log('sidebar collapsed:', e.detail.collapsed));
|
|
153
|
+
});
|
|
154
|
+
return (
|
|
155
|
+
<kitn-chat-workspace
|
|
156
|
+
ref={el}
|
|
157
|
+
style={{ display: 'block', height: '100vh' }}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
}`;
|
|
161
|
+
|
|
162
|
+
const meta = {
|
|
163
|
+
title: 'Web Components/kitn-chat-workspace',
|
|
164
|
+
tags: ['autodocs'],
|
|
165
|
+
parameters: {
|
|
166
|
+
layout: 'fullscreen',
|
|
167
|
+
docs: {
|
|
168
|
+
description: {
|
|
169
|
+
component: [
|
|
170
|
+
'`<kitn-chat-workspace>` is the full chat shell as a single **web component** — a resizable split layout with a collapsible conversation list on the left and a full message thread on the right, all isolated in **Shadow DOM**. SolidJS is bundled in, so the host needs nothing.',
|
|
171
|
+
'**When to use:** dropping an entire chat application shell into a non-Solid app (React, Vue, Svelte, plain HTML), or anywhere you want zero style conflicts and a ready-made list+chat layout. If you *are* in SolidJS and want fine-grained control, compose the `ConversationList` and `ChatThread` primitives directly.',
|
|
172
|
+
'**How to use:** register once with `import \'@kitnai/chat/elements\'`, set rich data as JS **properties** (`el.conversations = [...]`, `el.messages = [...]`, `el.models = [...]`), and listen for **CustomEvents** (`conversationselect`, `submit`, `sidebartoggle`, `newchat`) directly on the element.',
|
|
173
|
+
'**Placement:** as a full-page surface or large panel. Give it an explicit height (e.g. `height: 100vh`). The sidebar is drag-resizable and can be collapsed via the toggle button in its header.',
|
|
174
|
+
'See the **Code** tab below for the HTML usage; the *SolidJS* story shows the same element inside a Solid component.',
|
|
175
|
+
].join('\n\n'),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
} satisfies Meta;
|
|
180
|
+
|
|
181
|
+
export default meta;
|
|
182
|
+
type Story = StoryObj;
|
|
183
|
+
|
|
184
|
+
/** The element used the plain-HTML / any-framework way. */
|
|
185
|
+
export const Default: Story = {
|
|
186
|
+
render: () => <WorkspaceElement />,
|
|
187
|
+
parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** The same element used inside a SolidJS component (properties via `ref` + `onMount`, events via `addEventListener`). */
|
|
191
|
+
export const InSolidJS: Story = {
|
|
192
|
+
name: 'In SolidJS',
|
|
193
|
+
render: () => <WorkspaceElement />,
|
|
194
|
+
parameters: { docs: { source: { code: SOLID_SNIPPET, language: 'tsx' } } },
|
|
195
|
+
};
|
package/src/elements/register.ts
CHANGED
package/src/elements/styles.css
CHANGED
|
@@ -9,4 +9,39 @@
|
|
|
9
9
|
@layer base {
|
|
10
10
|
:host { display: block; }
|
|
11
11
|
* { border-color: var(--color-border); }
|
|
12
|
+
|
|
13
|
+
/* One token-driven focus ring for EVERY focusable element in the shadow root.
|
|
14
|
+
Blue by default (via --color-ring), consistent in light + dark, and
|
|
15
|
+
overridable through --kitn-color-ring. Uses `outline` (not a box-shadow
|
|
16
|
+
ring) so it never clips on overflow and follows the element's radius;
|
|
17
|
+
`:focus-visible` so it only appears for keyboard navigation, never on a
|
|
18
|
+
mouse click. Components that render their own ring set
|
|
19
|
+
`focus-visible:outline-none` (a utility in a later cascade layer) and win
|
|
20
|
+
over this — so there's no double ring, just a guaranteed blue fallback for
|
|
21
|
+
the many controls that have no explicit focus style. */
|
|
22
|
+
:focus-visible {
|
|
23
|
+
outline: 2px solid var(--color-ring);
|
|
24
|
+
outline-offset: 2px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Consistent cross-platform scrollbars for EVERY scroll region inside the
|
|
28
|
+
shadow root (message list, conversation list, menus, hover cards, code
|
|
29
|
+
blocks, the prompt textarea …). Thin + rounded + subtle, themed via tokens
|
|
30
|
+
for light/dark, thumb stronger on hover. Track stays transparent and
|
|
31
|
+
overflow is untouched, so no scrollbar is forced where none is needed.
|
|
32
|
+
Scoped to the shadow root, so it never restyles the host page. */
|
|
33
|
+
* {
|
|
34
|
+
scrollbar-width: thin;
|
|
35
|
+
scrollbar-color: var(--color-scrollbar-thumb) transparent;
|
|
36
|
+
}
|
|
37
|
+
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
38
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
39
|
+
*::-webkit-scrollbar-thumb {
|
|
40
|
+
background-color: var(--color-scrollbar-thumb);
|
|
41
|
+
border-radius: 9999px;
|
|
42
|
+
border: 2px solid transparent;
|
|
43
|
+
background-clip: padding-box;
|
|
44
|
+
}
|
|
45
|
+
*::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }
|
|
46
|
+
*::-webkit-scrollbar-corner { background: transparent; }
|
|
12
47
|
}
|
|
@@ -7,7 +7,7 @@ export interface ChatConfigValue {
|
|
|
7
7
|
proseSize: Accessor<ProseSize>;
|
|
8
8
|
/** Shiki theme for code blocks */
|
|
9
9
|
codeTheme: Accessor<string>;
|
|
10
|
-
/** Node
|
|
10
|
+
/** Node the kit's overlays portal into; undefined → document.body */
|
|
11
11
|
portalMount: Accessor<HTMLElement | undefined>;
|
|
12
12
|
/** Whether code blocks are syntax-highlighted; false → plain text, no Shiki loaded */
|
|
13
13
|
codeHighlight: Accessor<boolean>;
|
|
@@ -45,4 +45,31 @@ npm run build # emits dist/kitn-chat.es.js
|
|
|
45
45
|
- **SolidJS is bundled in** — the host page needs nothing else.
|
|
46
46
|
- The kit's CSS is injected into each element's Shadow DOM automatically; importing `theme.css` is optional and only needed if you want to override design tokens.
|
|
47
47
|
|
|
48
|
+
### Via CDN (no build step, no npm)
|
|
49
|
+
|
|
50
|
+
The element bundle is a self-contained ES module, so you can load it straight from a CDN — no install, no bundler, no import map. It's published on both <a href="https://www.jsdelivr.com/package/npm/@kitnai/chat" target="_blank" rel="noreferrer">jsDelivr</a> and <a href="https://unpkg.com/browse/@kitnai/chat/" target="_blank" rel="noreferrer">unpkg</a>:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<!-- jsDelivr -->
|
|
54
|
+
<script type="module">
|
|
55
|
+
import 'https://cdn.jsdelivr.net/npm/@kitnai/chat/dist/kitn-chat.es.js';
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!-- …or unpkg -->
|
|
59
|
+
<script type="module">
|
|
60
|
+
import 'https://unpkg.com/@kitnai/chat/dist/kitn-chat.es.js';
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<kitn-chat></kitn-chat>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- The URLs above track the **latest** release (no version) — they never go stale, so they're ideal for demos and quick starts.
|
|
67
|
+
- **For production, pin an exact version** — e.g. `.../npm/@kitnai/chat@0.4.0/dist/kitn-chat.es.js`. Pinned URLs are immutable and cached far more aggressively (floating "latest" tags get revalidated every few hours), and — since this package is **pre-1.0** — pinning shields you from breaking changes in a future minor release. A version *range* (`@0` / `@0.4`) won't: pre-1.0 it can still resolve to a breaking minor.
|
|
68
|
+
- SolidJS and the kit's CSS are bundled in, and the lazy syntax-highlighting chunks load from the **same CDN** automatically (they're relative to the bundle) — so nothing else is needed.
|
|
69
|
+
- To override design tokens, also pull in `theme.css`:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@kitnai/chat/theme.css">
|
|
73
|
+
```
|
|
74
|
+
|
|
48
75
|
Head to **[Getting Started](?path=/docs/docs-getting-started--docs)** to render your first chat.
|
|
@@ -30,6 +30,8 @@ Scalar values (strings, numbers, booleans) work as attributes: `placeholder`, `l
|
|
|
30
30
|
|
|
31
31
|
Import the element bundle once as a side-effect (it registers all `kitn-*` custom elements), then set properties and listen for events in a `<script type="module">` block.
|
|
32
32
|
|
|
33
|
+
> **Want the whole shell in one tag?** `<kitn-chat-workspace>` bundles the conversation-list sidebar, the drag-to-resize handle, and the full chat thread together. Set `conversations`, `messages`, and optionally `models` as properties; listen for `conversationselect` and `submit`. See the <a href="?path=/docs/web-components-kitn-chat-workspace--docs">kitn-chat-workspace story</a> and the <a href="https://github.com/kitn-ai/kitn-chat/blob/main/docs/web-components.md#kitn-chat-workspace--kitnchatworkspace">web-components.md reference</a> for the full API.
|
|
34
|
+
|
|
33
35
|
```html
|
|
34
36
|
<!DOCTYPE html>
|
|
35
37
|
<html>
|
|
@@ -5,7 +5,7 @@ import * as ChatPanel from '../chat-panel-layout.stories';
|
|
|
5
5
|
|
|
6
6
|
# @kitnai/chat
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Framework-agnostic, Shadow-DOM web components for building AI chat interfaces.**
|
|
9
9
|
|
|
10
10
|
Message threads, prompt inputs, streaming responses, markdown + code rendering, reasoning & tool-call panels, attachments, and a conversation sidebar — composable building blocks you can drop into any app.
|
|
11
11
|
|
|
@@ -13,12 +13,21 @@ Message threads, prompt inputs, streaming responses, markdown + code rendering,
|
|
|
13
13
|
|
|
14
14
|
## Why @kitnai/chat
|
|
15
15
|
|
|
16
|
-
- **
|
|
16
|
+
- **Works in any framework** — drop in the framework-agnostic **web components** (`<kitn-chat>`) and they just work in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS, so SolidJS apps can also import the components natively for full compositional control.
|
|
17
17
|
- **Zero style conflicts** — the web components render in **Shadow DOM**, so the host page's CSS can't leak in and the kit's Tailwind can't leak out.
|
|
18
18
|
- **Lightweight** — a markdown-only `<kitn-chat>` is **~61 KB gzip**, a single file. Syntax highlighting loads **on demand, per language, with no WASM** — and never loads at all if you don't render code.
|
|
19
|
-
- **~50 composable components** across three layers: headless primitives → UI primitives (built
|
|
19
|
+
- **~50 composable components** across three layers: headless primitives → accessible UI primitives (built in-house, WCAG 2.1 AA — no third-party UI dependency) → AI feature components.
|
|
20
20
|
- **Themeable** — restyle everything by overriding a handful of `--color-*` design tokens.
|
|
21
21
|
|
|
22
|
+
## Browsing the sidebar — which group do I copy from?
|
|
23
|
+
|
|
24
|
+
The kit ships at two layers, and the sidebar reflects that. **Use the right one for your stack:**
|
|
25
|
+
|
|
26
|
+
- **Web Components** — the framework-agnostic `<kitn-*>` custom elements. **This is what to copy into a React, Vue, Angular, Svelte, or plain-HTML app.** Data goes on JS properties, interactions come back as events.
|
|
27
|
+
- **Components · SolidJS** and **UI · SolidJS** — the **native SolidJS** components (feature components) and primitives (Button, Dropdown, HoverCard, …) that the web components are *built from*. Their snippets are SolidJS JSX, so **only copy these into a SolidJS app**.
|
|
28
|
+
|
|
29
|
+
In other words: the `<kitn-chat>` web component is a thin facade over the SolidJS `ChatContainer`/`Message`/… components — same UI, different consumption model. When in doubt, reach for **Web Components**.
|
|
30
|
+
|
|
22
31
|
## Where to next
|
|
23
32
|
|
|
24
33
|
- **[Installation](?path=/docs/docs-installation--docs)** — add it to your project
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import {
|
|
4
|
+
ChatConfig, ChatContainer, ChatContainerContent, ChatContainerScrollAnchor,
|
|
5
|
+
Message, MessageContent, MessageActions,
|
|
6
|
+
PromptInput, PromptInputTextarea, PromptInputActions,
|
|
7
|
+
ModelSwitcher, Button,
|
|
8
|
+
} from '../index';
|
|
9
|
+
import type { ModelOption } from '../types';
|
|
10
|
+
import { Copy, ThumbsUp, ArrowUp } from 'lucide-solid';
|
|
11
|
+
|
|
12
|
+
const meta: Meta = {
|
|
13
|
+
title: 'Patterns/Centered Conversation',
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'A single, centered reading column with no sidebar (Claude.ai-style). The messages and composer share one `max-w` measure centered in the viewport — ideal for a focused, full-window chat. Contrast with **Chat Panel Layout** (a compact embedded panel) and the **Full Chat App** example (a resizable workspace).',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj;
|
|
26
|
+
|
|
27
|
+
const models: ModelOption[] = [
|
|
28
|
+
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
|
|
29
|
+
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const answer = `Sure — here's the gist:
|
|
33
|
+
|
|
34
|
+
- **Signals** are fine-grained, so only the DOM that reads a signal updates.
|
|
35
|
+
- **No re-renders** — components run once; reactive expressions do the work.
|
|
36
|
+
- **No dependency arrays** — effects auto-track what they read.
|
|
37
|
+
|
|
38
|
+
Want a code example next?`;
|
|
39
|
+
|
|
40
|
+
export const Focused: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
const [input, setInput] = createSignal('');
|
|
43
|
+
const [model, setModel] = createSignal('claude-4');
|
|
44
|
+
return (
|
|
45
|
+
<ChatConfig proseSize="base">
|
|
46
|
+
<div style={{ width: '860px', height: '680px' }} class="flex flex-col overflow-hidden rounded-xl border border-border bg-background">
|
|
47
|
+
<header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
|
|
48
|
+
<span class="text-sm font-semibold text-foreground">New conversation</span>
|
|
49
|
+
<ModelSwitcher models={models} currentModelId={model()} onModelChange={setModel} />
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<div class="relative flex-1 overflow-y-auto">
|
|
53
|
+
<ChatContainer class="h-full">
|
|
54
|
+
<ChatContainerContent class="space-y-6 px-5 py-6">
|
|
55
|
+
<Message class="mx-auto flex w-full max-w-2xl flex-col items-end">
|
|
56
|
+
<MessageContent class="bg-muted text-primary max-w-[85%] rounded-3xl px-5 py-2.5">
|
|
57
|
+
In one paragraph, why is SolidJS reactivity fast?
|
|
58
|
+
</MessageContent>
|
|
59
|
+
</Message>
|
|
60
|
+
|
|
61
|
+
<Message class="mx-auto flex w-full max-w-2xl flex-col items-start">
|
|
62
|
+
<div class="group flex w-full flex-col">
|
|
63
|
+
<MessageContent markdown class="bg-transparent p-0 text-foreground">
|
|
64
|
+
{answer}
|
|
65
|
+
</MessageContent>
|
|
66
|
+
<MessageActions class="-ml-2.5 flex gap-0 opacity-0 transition-opacity duration-150 group-hover:opacity-100">
|
|
67
|
+
<Button variant="ghost" size="icon-sm" class="rounded-full"><Copy class="size-3.5" /></Button>
|
|
68
|
+
<Button variant="ghost" size="icon-sm" class="rounded-full"><ThumbsUp class="size-3.5" /></Button>
|
|
69
|
+
</MessageActions>
|
|
70
|
+
</div>
|
|
71
|
+
</Message>
|
|
72
|
+
<ChatContainerScrollAnchor />
|
|
73
|
+
</ChatContainerContent>
|
|
74
|
+
</ChatContainer>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="shrink-0 px-5 pb-5">
|
|
78
|
+
<div class="mx-auto max-w-2xl">
|
|
79
|
+
<PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
|
|
80
|
+
<PromptInputTextarea placeholder="Reply…" class="min-h-[44px] pt-3 pl-4" />
|
|
81
|
+
<PromptInputActions class="mt-2 flex w-full items-center justify-end gap-2 px-3 pb-3">
|
|
82
|
+
<Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
|
|
83
|
+
<ArrowUp class="size-4" />
|
|
84
|
+
</Button>
|
|
85
|
+
</PromptInputActions>
|
|
86
|
+
</PromptInput>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</ChatConfig>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import {
|
|
4
|
+
ChatConfig, ChatContainer, ChatContainerContent, ChatContainerScrollAnchor,
|
|
5
|
+
Message, MessageAvatar, MessageContent,
|
|
6
|
+
PromptInput, PromptInputTextarea, PromptInputActions, Button,
|
|
7
|
+
} from '../index';
|
|
8
|
+
import { ArrowUp, X } from 'lucide-solid';
|
|
9
|
+
|
|
10
|
+
const meta: Meta = {
|
|
11
|
+
title: 'Patterns/Docked Widget',
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'A support-assistant widget docked to the bottom-right corner of a host page. The same building blocks (`ChatContainer` + `Message` + `PromptInput`) shrink into a compact floating card — the kit\'s Shadow-DOM isolation means it can sit over any app without CSS bleed.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj;
|
|
24
|
+
|
|
25
|
+
export const SupportAssistant: Story = {
|
|
26
|
+
render: () => {
|
|
27
|
+
const [input, setInput] = createSignal('');
|
|
28
|
+
return (
|
|
29
|
+
<ChatConfig proseSize="sm">
|
|
30
|
+
{/* Faux host page */}
|
|
31
|
+
<div style={{ width: '920px', height: '680px' }} class="relative overflow-hidden rounded-xl border border-border bg-gradient-to-br from-muted/40 to-background">
|
|
32
|
+
<div class="space-y-3 p-8">
|
|
33
|
+
<div class="h-7 w-48 rounded-md bg-muted" />
|
|
34
|
+
<div class="h-4 w-2/3 rounded bg-muted/70" />
|
|
35
|
+
<div class="h-4 w-1/2 rounded bg-muted/70" />
|
|
36
|
+
<div class="mt-6 grid grid-cols-3 gap-4">
|
|
37
|
+
<div class="h-28 rounded-lg bg-muted/60" />
|
|
38
|
+
<div class="h-28 rounded-lg bg-muted/60" />
|
|
39
|
+
<div class="h-28 rounded-lg bg-muted/60" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Docked chat widget */}
|
|
44
|
+
<div style={{ width: '360px', height: '460px' }} class="absolute bottom-5 right-5 flex flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-xl">
|
|
45
|
+
<header class="flex shrink-0 items-center justify-between border-b border-border px-4 py-3">
|
|
46
|
+
<div class="flex items-center gap-2">
|
|
47
|
+
<span class="size-2 rounded-full bg-tool-green" />
|
|
48
|
+
<span class="text-sm font-semibold text-foreground">Support</span>
|
|
49
|
+
</div>
|
|
50
|
+
<Button variant="ghost" size="icon-sm" aria-label="Close"><X class="size-4" /></Button>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<div class="relative flex-1 overflow-y-auto">
|
|
54
|
+
<ChatContainer class="h-full">
|
|
55
|
+
<ChatContainerContent class="space-y-3 px-3 py-3">
|
|
56
|
+
<Message>
|
|
57
|
+
<MessageAvatar src="" alt="AI" fallback="AI" />
|
|
58
|
+
<MessageContent markdown class="bg-transparent p-0 pt-1">
|
|
59
|
+
Hi! 👋 How can I help you today?
|
|
60
|
+
</MessageContent>
|
|
61
|
+
</Message>
|
|
62
|
+
<Message class="flex-col items-end">
|
|
63
|
+
<MessageContent class="bg-muted text-primary max-w-[85%] rounded-xl px-3.5 py-2">
|
|
64
|
+
How do I reset my password?
|
|
65
|
+
</MessageContent>
|
|
66
|
+
</Message>
|
|
67
|
+
<Message>
|
|
68
|
+
<MessageAvatar src="" alt="AI" fallback="AI" />
|
|
69
|
+
<MessageContent markdown class="bg-transparent p-0 pt-1">
|
|
70
|
+
Head to **Settings → Security** and click **Reset password** — you'll get an email link.
|
|
71
|
+
</MessageContent>
|
|
72
|
+
</Message>
|
|
73
|
+
<ChatContainerScrollAnchor />
|
|
74
|
+
</ChatContainerContent>
|
|
75
|
+
</ChatContainer>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="shrink-0 px-3 pb-3">
|
|
79
|
+
<PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
|
|
80
|
+
<PromptInputTextarea placeholder="Message support…" class="min-h-[40px] pt-2.5 pl-3.5" />
|
|
81
|
+
<PromptInputActions class="mt-1.5 flex w-full items-center justify-end gap-2 px-2.5 pb-2.5">
|
|
82
|
+
<Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
|
|
83
|
+
<ArrowUp class="size-4" />
|
|
84
|
+
</Button>
|
|
85
|
+
</PromptInputActions>
|
|
86
|
+
</PromptInput>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</ChatConfig>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import {
|
|
4
|
+
ChatConfig, Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
|
|
5
|
+
PromptSuggestion, PromptInput, PromptInputTextarea, PromptInputActions, Button,
|
|
6
|
+
} from '../index';
|
|
7
|
+
import { Sparkles, Plus, Globe, ArrowUp } from 'lucide-solid';
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: 'Patterns/Empty State',
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'The new-chat zero state: a centered greeting with starter suggestions above the composer. Composed from `Empty` (+ `EmptyMedia`/`EmptyTitle`/`EmptyDescription`), `PromptSuggestion` chips, and `PromptInput`. Clicking a suggestion fills the composer.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj;
|
|
23
|
+
|
|
24
|
+
const SUGGESTIONS = [
|
|
25
|
+
'Summarize a document',
|
|
26
|
+
'Draft a product update',
|
|
27
|
+
'Explain a code snippet',
|
|
28
|
+
'Plan my week',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const NewChat: Story = {
|
|
32
|
+
render: () => {
|
|
33
|
+
const [input, setInput] = createSignal('');
|
|
34
|
+
return (
|
|
35
|
+
<ChatConfig proseSize="base">
|
|
36
|
+
<div style={{ width: '760px', height: '640px' }} class="flex flex-col overflow-hidden rounded-xl border border-border bg-background">
|
|
37
|
+
<Empty class="flex-1">
|
|
38
|
+
<EmptyHeader>
|
|
39
|
+
<EmptyMedia variant="icon">
|
|
40
|
+
<Sparkles class="size-6" />
|
|
41
|
+
</EmptyMedia>
|
|
42
|
+
<EmptyTitle>How can I help today?</EmptyTitle>
|
|
43
|
+
<EmptyDescription>
|
|
44
|
+
Ask anything, or start from one of these.
|
|
45
|
+
</EmptyDescription>
|
|
46
|
+
</EmptyHeader>
|
|
47
|
+
<EmptyContent>
|
|
48
|
+
<div class="flex max-w-md flex-wrap justify-center gap-2">
|
|
49
|
+
{SUGGESTIONS.map((s) => (
|
|
50
|
+
<PromptSuggestion onClick={() => setInput(s)}>{s}</PromptSuggestion>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</EmptyContent>
|
|
54
|
+
</Empty>
|
|
55
|
+
|
|
56
|
+
<div class="shrink-0 px-4 pb-4">
|
|
57
|
+
<div class="mx-auto max-w-2xl">
|
|
58
|
+
<PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
|
|
59
|
+
<PromptInputTextarea placeholder="Ask anything…" class="min-h-[44px] pt-3 pl-4" />
|
|
60
|
+
<PromptInputActions class="mt-2 flex w-full items-center justify-between gap-2 px-3 pb-3">
|
|
61
|
+
<div class="flex items-center gap-2">
|
|
62
|
+
<Button variant="outline" size="icon-sm" class="rounded-full"><Plus class="size-4" /></Button>
|
|
63
|
+
<Button variant="outline" size="sm" class="rounded-full gap-1"><Globe class="size-4" />Search</Button>
|
|
64
|
+
</div>
|
|
65
|
+
<Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
|
|
66
|
+
<ArrowUp class="size-4" />
|
|
67
|
+
</Button>
|
|
68
|
+
</PromptInputActions>
|
|
69
|
+
</PromptInput>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</ChatConfig>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|