@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.
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/bash-InADTalH.js +6 -0
- package/dist/core-AYMC6_lb.js +5874 -0
- package/dist/engine-javascript-vq0WuIJl.js +2643 -0
- package/dist/github-dark-dimmed-DUshB20C.js +4 -0
- package/dist/github-light-JYsPkUQd.js +4 -0
- package/dist/javascript-C25yR2R2.js +6 -0
- package/dist/json-DxJze_jm.js +6 -0
- package/dist/kitn-chat.es.js +6632 -0
- package/dist/tsx-B8rCNbgL.js +6 -0
- package/dist/typescript-RycA9KXf.js +6 -0
- package/package.json +80 -0
- package/src/components/attachments.stories.tsx +304 -0
- package/src/components/attachments.tsx +394 -0
- package/src/components/chain-of-thought.stories.tsx +212 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +188 -0
- package/src/components/chat-container.tsx +78 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/checkpoint.stories.tsx +103 -0
- package/src/components/checkpoint.tsx +81 -0
- package/src/components/code-block.stories.tsx +151 -0
- package/src/components/code-block.tsx +99 -0
- package/src/components/context.stories.tsx +180 -0
- package/src/components/context.tsx +323 -0
- package/src/components/conversation-item.stories.tsx +126 -0
- package/src/components/conversation-item.tsx +18 -0
- package/src/components/conversation-list.stories.tsx +134 -0
- package/src/components/conversation-list.tsx +100 -0
- package/src/components/empty.stories.tsx +435 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +101 -0
- package/src/components/feedback-bar.tsx +58 -0
- package/src/components/file-upload.stories.tsx +157 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/image.stories.tsx +90 -0
- package/src/components/image.tsx +67 -0
- package/src/components/loader.stories.tsx +182 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +181 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-narrow.stories.tsx +330 -0
- package/src/components/message-skills.stories.tsx +212 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +282 -0
- package/src/components/message.tsx +149 -0
- package/src/components/model-switcher.stories.tsx +98 -0
- package/src/components/model-switcher.tsx +36 -0
- package/src/components/prompt-input.stories.tsx +223 -0
- package/src/components/prompt-input.tsx +190 -0
- package/src/components/prompt-suggestion.stories.tsx +143 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +141 -0
- package/src/components/reasoning.tsx +157 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +101 -0
- package/src/components/scroll-button.tsx +33 -0
- package/src/components/slash-command.stories.tsx +164 -0
- package/src/components/slash-command.tsx +223 -0
- package/src/components/source.stories.tsx +125 -0
- package/src/components/source.tsx +129 -0
- package/src/components/text-shimmer.stories.tsx +88 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +88 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +154 -0
- package/src/components/tool.tsx +173 -0
- package/src/components/voice-input.stories.tsx +84 -0
- package/src/components/voice-input.tsx +103 -0
- package/src/elements/chat-types.ts +14 -0
- package/src/elements/chat.tsx +111 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/conversation-list.tsx +26 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +53 -0
- package/src/elements/define.tsx +54 -0
- package/src/elements/kitn-chat.stories.tsx +105 -0
- package/src/elements/kitn-conversation-list.stories.tsx +177 -0
- package/src/elements/kitn-prompt-input.stories.tsx +123 -0
- package/src/elements/prompt-input.tsx +39 -0
- package/src/elements/register.ts +9 -0
- package/src/elements/styles.css +12 -0
- package/src/index.ts +128 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/highlighter.ts +150 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-stick-to-bottom.ts +43 -0
- package/src/primitives/use-text-stream.ts +112 -0
- package/src/primitives/use-voice-recorder.ts +50 -0
- package/src/stories/chat-panel-layout.stories.tsx +144 -0
- package/src/stories/chat-scene.tsx +570 -0
- package/src/stories/checkpoint-restore.stories.tsx +224 -0
- package/src/stories/context-usage.stories.tsx +155 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +151 -0
- package/src/stories/conversation-with-sources.stories.tsx +165 -0
- package/src/stories/docs/GettingStarted.mdx +76 -0
- package/src/stories/docs/Installation.mdx +48 -0
- package/src/stories/docs/Integrations.mdx +110 -0
- package/src/stories/docs/Introduction.mdx +29 -0
- package/src/stories/docs/Theming.mdx +87 -0
- package/src/stories/docs/theme-editor/canvas.tsx +32 -0
- package/src/stories/docs/theme-editor/inspector.tsx +66 -0
- package/src/stories/docs/theme-editor/presets.test.ts +32 -0
- package/src/stories/docs/theme-editor/presets.ts +64 -0
- package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
- package/src/stories/docs/theme-editor/theme-css.ts +15 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +145 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +167 -0
- package/src/stories/prompt-input-variants.stories.tsx +179 -0
- package/src/stories/streaming-response.stories.tsx +234 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/types.ts +41 -0
- package/src/ui/avatar.stories.tsx +104 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +87 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +146 -0
- package/src/ui/button.tsx +37 -0
- package/src/ui/collapsible.tsx +14 -0
- package/src/ui/dialog.tsx +21 -0
- package/src/ui/dropdown.tsx +26 -0
- package/src/ui/hover-card.tsx +48 -0
- package/src/ui/resizable.stories.tsx +171 -0
- package/src/ui/resizable.tsx +219 -0
- package/src/ui/scroll-area.tsx +13 -0
- package/src/ui/separator.stories.tsx +82 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +338 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +75 -0
- package/src/ui/tooltip.tsx +22 -0
- package/src/utils/cn.ts +6 -0
- package/theme.css +115 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Palette } from './theme-css';
|
|
2
|
+
|
|
3
|
+
export type Preset = { name: string; light: Palette; dark: Palette };
|
|
4
|
+
export type BrandOverride = { light: Palette; dark: Palette };
|
|
5
|
+
|
|
6
|
+
/** Per-preset brand-token overrides (light + dark). Neutrals come from the live base. */
|
|
7
|
+
export const BRAND_OVERRIDES: Record<string, BrandOverride> = {
|
|
8
|
+
Violet: {
|
|
9
|
+
light: {
|
|
10
|
+
'--color-primary': 'hsl(262 83% 58%)',
|
|
11
|
+
'--color-primary-foreground': 'hsl(0 0% 100%)',
|
|
12
|
+
'--color-ring': 'hsl(262 83% 58%)',
|
|
13
|
+
'--color-code-foreground': 'hsl(262 83% 58%)',
|
|
14
|
+
},
|
|
15
|
+
dark: {
|
|
16
|
+
'--color-primary': 'hsl(263 70% 65%)',
|
|
17
|
+
'--color-primary-foreground': 'hsl(0 0% 100%)',
|
|
18
|
+
'--color-ring': 'hsl(263 70% 65%)',
|
|
19
|
+
'--color-code-foreground': 'hsl(263 90% 80%)',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
Emerald: {
|
|
23
|
+
light: {
|
|
24
|
+
'--color-primary': 'hsl(160 84% 39%)',
|
|
25
|
+
'--color-primary-foreground': 'hsl(0 0% 100%)',
|
|
26
|
+
'--color-ring': 'hsl(160 84% 39%)',
|
|
27
|
+
'--color-code-foreground': 'hsl(160 84% 32%)',
|
|
28
|
+
},
|
|
29
|
+
dark: {
|
|
30
|
+
'--color-primary': 'hsl(158 64% 52%)',
|
|
31
|
+
'--color-primary-foreground': 'hsl(160 30% 8%)',
|
|
32
|
+
'--color-ring': 'hsl(158 64% 52%)',
|
|
33
|
+
'--color-code-foreground': 'hsl(158 70% 65%)',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
Mono: {
|
|
37
|
+
light: {
|
|
38
|
+
'--color-primary': 'hsl(0 0% 20%)',
|
|
39
|
+
'--color-primary-foreground': 'hsl(0 0% 98%)',
|
|
40
|
+
'--color-ring': 'hsl(0 0% 40%)',
|
|
41
|
+
'--color-code-foreground': 'hsl(0 0% 30%)',
|
|
42
|
+
},
|
|
43
|
+
dark: {
|
|
44
|
+
'--color-primary': 'hsl(0 0% 92%)',
|
|
45
|
+
'--color-primary-foreground': 'hsl(0 0% 12%)',
|
|
46
|
+
'--color-ring': 'hsl(0 0% 70%)',
|
|
47
|
+
'--color-code-foreground': 'hsl(0 0% 75%)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ORDER = ['Default', 'Violet', 'Emerald', 'Mono'];
|
|
53
|
+
|
|
54
|
+
/** Build full presets by layering brand overrides onto the live-discovered base palette. */
|
|
55
|
+
export function buildPresets(base: { light: Palette; dark: Palette }): Preset[] {
|
|
56
|
+
return ORDER.map((name) => {
|
|
57
|
+
const ov = BRAND_OVERRIDES[name];
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
light: ov ? { ...base.light, ...ov.light } : { ...base.light },
|
|
61
|
+
dark: ov ? { ...base.dark, ...ov.dark } : { ...base.dark },
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildThemeCss } from './theme-css';
|
|
3
|
+
|
|
4
|
+
describe('buildThemeCss', () => {
|
|
5
|
+
it('emits sorted :root and .dark blocks', () => {
|
|
6
|
+
const css = buildThemeCss(
|
|
7
|
+
{ '--color-primary': '#ffffff', '--radius': '0.6rem' },
|
|
8
|
+
{ '--color-primary': '#000000' },
|
|
9
|
+
);
|
|
10
|
+
expect(css).toBe(
|
|
11
|
+
':root {\n --color-primary: #ffffff;\n --radius: 0.6rem;\n}\n\n.dark {\n --color-primary: #000000;\n}',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('keys are sorted within each block', () => {
|
|
16
|
+
const css = buildThemeCss({ '--b': '2', '--a': '1' }, {});
|
|
17
|
+
expect(css).toBe(':root {\n --a: 1;\n --b: 2;\n}\n\n.dark {\n\n}');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A map of CSS custom-property name → value, e.g. { '--color-primary': '#fff' }. */
|
|
2
|
+
export type Palette = Record<string, string>;
|
|
3
|
+
|
|
4
|
+
function block(selector: string, palette: Palette): string {
|
|
5
|
+
const body = Object.keys(palette)
|
|
6
|
+
.sort()
|
|
7
|
+
.map((k) => ` ${k}: ${palette[k]};`)
|
|
8
|
+
.join('\n');
|
|
9
|
+
return `${selector} {\n${body}\n}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Build a paste-ready theme override block: full light set on :root, dark set on .dark. */
|
|
13
|
+
export function buildThemeCss(light: Palette, dark: Palette): string {
|
|
14
|
+
return `${block(':root', light)}\n\n${block('.dark', dark)}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/stories/docs/theme-editor/theme-editor.tsx
|
|
2
|
+
import { createSignal, createEffect, onMount, onCleanup, For } from 'solid-js';
|
|
3
|
+
import { Button } from '../../../ui/button';
|
|
4
|
+
import { discoverPalettes } from '../theme-tokens';
|
|
5
|
+
import { buildThemeCss, type Palette } from './theme-css';
|
|
6
|
+
import { buildPresets, type Preset } from './presets';
|
|
7
|
+
import { Inspector } from './inspector';
|
|
8
|
+
import { Canvas, CANVAS_CLASS } from './canvas';
|
|
9
|
+
|
|
10
|
+
const STYLE_ID = 'kitn-theme-editor-overrides';
|
|
11
|
+
|
|
12
|
+
/** Full-screen live theme editor: edit light/dark tokens, preview a real chat, export CSS. */
|
|
13
|
+
export function ThemeEditor() {
|
|
14
|
+
const [presets, setPresets] = createSignal<Preset[]>([]);
|
|
15
|
+
const [mode, setMode] = createSignal<'light' | 'dark'>('light');
|
|
16
|
+
const [light, setLight] = createSignal<Palette>({});
|
|
17
|
+
const [dark, setDark] = createSignal<Palette>({});
|
|
18
|
+
const [presetName, setPresetName] = createSignal('Default');
|
|
19
|
+
const [copied, setCopied] = createSignal(false);
|
|
20
|
+
|
|
21
|
+
onMount(() => {
|
|
22
|
+
const ps = buildPresets(discoverPalettes());
|
|
23
|
+
setPresets(ps);
|
|
24
|
+
const def = ps.find((p) => p.name === 'Default')!;
|
|
25
|
+
setLight({ ...def.light });
|
|
26
|
+
setDark({ ...def.dark });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Apply the ACTIVE mode's palette directly onto the canvas wrapper. Scoping to
|
|
30
|
+
// CANVAS_CLASS (rather than :root/.dark) means the preview reflects the editor's
|
|
31
|
+
// mode independently of any ancestor `.dark` (e.g. Storybook's dark theme), and
|
|
32
|
+
// only the canvas reskins — not the editor chrome. Export (Copy CSS) still emits
|
|
33
|
+
// the full :root + .dark theme separately.
|
|
34
|
+
let styleEl: HTMLStyleElement | undefined;
|
|
35
|
+
createEffect(() => {
|
|
36
|
+
if (!Object.keys(light()).length) return; // not seeded yet
|
|
37
|
+
const colors: Palette = mode() === 'light' ? light() : dark();
|
|
38
|
+
const radius = light()['--radius'] ?? '0.6rem';
|
|
39
|
+
const colorBody = Object.keys(colors)
|
|
40
|
+
.filter((k) => k.startsWith('--color-'))
|
|
41
|
+
.sort()
|
|
42
|
+
.map((k) => ` ${k}: ${colors[k]};`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
// Re-express the radius scale on the wrapper. The kit's --radius-sm/md/lg/xl are
|
|
45
|
+
// calc(var(--radius) …) declared at :root, so their var(--radius) resolves once
|
|
46
|
+
// at :root and inherits as a fixed length — overriding --radius lower in the tree
|
|
47
|
+
// doesn't move them. Re-declaring them here (against the wrapper's --radius) does.
|
|
48
|
+
const radiusBody = [
|
|
49
|
+
` --radius: ${radius};`,
|
|
50
|
+
` --radius-sm: calc(var(--radius) - 4px);`,
|
|
51
|
+
` --radius-md: calc(var(--radius) - 2px);`,
|
|
52
|
+
` --radius-lg: var(--radius);`,
|
|
53
|
+
` --radius-xl: calc(var(--radius) + 4px);`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
// Re-establish the inherited `color` and native `color-scheme` on the wrapper:
|
|
56
|
+
// tokens alone don't fix text whose color is inherited from outside the canvas
|
|
57
|
+
// (e.g. elements with no explicit text-color class), nor native form controls.
|
|
58
|
+
const css = `.${CANVAS_CLASS} {\n${colorBody}\n${radiusBody}\n color: var(--color-foreground);\n color-scheme: ${mode()};\n}`;
|
|
59
|
+
if (!styleEl) {
|
|
60
|
+
styleEl = document.createElement('style');
|
|
61
|
+
styleEl.id = STYLE_ID;
|
|
62
|
+
document.head.appendChild(styleEl);
|
|
63
|
+
}
|
|
64
|
+
styleEl.textContent = css;
|
|
65
|
+
});
|
|
66
|
+
onCleanup(() => styleEl?.remove());
|
|
67
|
+
|
|
68
|
+
const colorTokens = () => Object.keys(light()).filter((n) => n.startsWith('--color-')).sort();
|
|
69
|
+
const activeValues = () => (mode() === 'light' ? light() : dark());
|
|
70
|
+
|
|
71
|
+
const setColor = (token: string, hex: string) => {
|
|
72
|
+
if (mode() === 'light') setLight((v) => ({ ...v, [token]: hex }));
|
|
73
|
+
else setDark((v) => ({ ...v, [token]: hex }));
|
|
74
|
+
};
|
|
75
|
+
const setRadius = (rem: string) => setLight((v) => ({ ...v, '--radius': rem }));
|
|
76
|
+
|
|
77
|
+
const loadPreset = (name: string) => {
|
|
78
|
+
const p = presets().find((x) => x.name === name);
|
|
79
|
+
if (!p) return;
|
|
80
|
+
setLight({ ...p.light });
|
|
81
|
+
setDark({ ...p.dark });
|
|
82
|
+
setPresetName(name);
|
|
83
|
+
};
|
|
84
|
+
const reset = () => loadPreset('Default');
|
|
85
|
+
|
|
86
|
+
const copyCss = async () => {
|
|
87
|
+
try {
|
|
88
|
+
await navigator.clipboard.writeText(buildThemeCss(light(), dark()));
|
|
89
|
+
setCopied(true);
|
|
90
|
+
setTimeout(() => setCopied(false), 1500);
|
|
91
|
+
} catch {
|
|
92
|
+
/* clipboard blocked */
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div class="h-screen w-full flex flex-col bg-background text-foreground">
|
|
98
|
+
{/* Top bar */}
|
|
99
|
+
<div class="flex items-center justify-between gap-3 border-b border-border px-4 h-12 shrink-0">
|
|
100
|
+
<div class="flex items-center gap-3">
|
|
101
|
+
<strong class="text-sm">Theme editor</strong>
|
|
102
|
+
<div class="flex rounded-md border border-border overflow-hidden text-xs">
|
|
103
|
+
<button class="px-2.5 py-1" classList={{ 'bg-secondary': mode() === 'light' }} onClick={() => setMode('light')}>
|
|
104
|
+
Light
|
|
105
|
+
</button>
|
|
106
|
+
<button class="px-2.5 py-1" classList={{ 'bg-secondary': mode() === 'dark' }} onClick={() => setMode('dark')}>
|
|
107
|
+
Dark
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="flex items-center gap-2">
|
|
112
|
+
<select
|
|
113
|
+
class="bg-input border border-border rounded-md text-xs px-2 h-8"
|
|
114
|
+
value={presetName()}
|
|
115
|
+
onChange={(e) => loadPreset(e.currentTarget.value)}
|
|
116
|
+
>
|
|
117
|
+
<For each={presets()}>{(p) => <option value={p.name}>{p.name}</option>}</For>
|
|
118
|
+
</select>
|
|
119
|
+
<Button size="sm" variant="outline" onClick={copyCss}>
|
|
120
|
+
{copied() ? 'Copied!' : 'Copy CSS'}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button size="sm" variant="outline" onClick={reset}>
|
|
123
|
+
Reset
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Body */}
|
|
129
|
+
<div class="flex-1 flex min-h-0">
|
|
130
|
+
<div class="w-[300px] shrink-0 border-r border-border overflow-auto">
|
|
131
|
+
<Inspector
|
|
132
|
+
tokens={colorTokens()}
|
|
133
|
+
values={activeValues()}
|
|
134
|
+
radius={light()['--radius'] ?? '0.6rem'}
|
|
135
|
+
onColorChange={setColor}
|
|
136
|
+
onRadiusChange={setRadius}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex-1 min-w-0 p-3">
|
|
140
|
+
<Canvas mode={mode()} />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createSignal, For, onMount, type JSX } from 'solid-js';
|
|
2
|
+
import type { Palette } from './theme-editor/theme-css';
|
|
3
|
+
|
|
4
|
+
// Docs-only helpers (not part of the kit's public API). They auto-discover the
|
|
5
|
+
// kit's design tokens straight from the loaded CSS, so the reference + editor
|
|
6
|
+
// can never drift from theme.css as tokens are added or changed.
|
|
7
|
+
|
|
8
|
+
type ColorToken = { name: string; light: string; dark: string };
|
|
9
|
+
type RadiusToken = { name: string; value: string };
|
|
10
|
+
|
|
11
|
+
export const PURPOSE: Record<string, string> = {
|
|
12
|
+
'--color-background': 'App / page background',
|
|
13
|
+
'--color-foreground': 'Default text',
|
|
14
|
+
'--color-card': 'Card surface',
|
|
15
|
+
'--color-card-foreground': 'Text on cards',
|
|
16
|
+
'--color-popover': 'Popover / menu surface',
|
|
17
|
+
'--color-popover-foreground': 'Text in popovers',
|
|
18
|
+
'--color-primary': 'Primary action background',
|
|
19
|
+
'--color-primary-foreground': 'Text on primary',
|
|
20
|
+
'--color-secondary': 'Secondary surface',
|
|
21
|
+
'--color-secondary-foreground': 'Text on secondary',
|
|
22
|
+
'--color-muted': 'Muted surface (subtle fills)',
|
|
23
|
+
'--color-muted-foreground': 'Muted / secondary text',
|
|
24
|
+
'--color-accent': 'Accent / hover surface',
|
|
25
|
+
'--color-accent-foreground': 'Text on accent',
|
|
26
|
+
'--color-destructive': 'Destructive / danger',
|
|
27
|
+
'--color-destructive-foreground': 'Text on destructive',
|
|
28
|
+
'--color-border': 'Borders / dividers',
|
|
29
|
+
'--color-input': 'Input field background',
|
|
30
|
+
'--color-ring': 'Focus ring',
|
|
31
|
+
'--color-sidebar': 'Sidebar background',
|
|
32
|
+
'--color-code-foreground': 'Inline code text / accent',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Raw walk of loaded stylesheets → light/dark custom-property maps (colors + radius).
|
|
36
|
+
* Recurses into grouping rules (@layer, @media, @supports) because Tailwind v4
|
|
37
|
+
* emits the `:root` theme tokens inside an `@layer`, which a flat walk would miss. */
|
|
38
|
+
function collectTokens(): { light: Record<string, string>; dark: Record<string, string> } {
|
|
39
|
+
const light: Record<string, string> = {};
|
|
40
|
+
const dark: Record<string, string> = {};
|
|
41
|
+
const visit = (rules: CSSRuleList) => {
|
|
42
|
+
for (const rule of Array.from(rules)) {
|
|
43
|
+
const r = rule as CSSStyleRule;
|
|
44
|
+
if (r.selectorText && r.style) {
|
|
45
|
+
const isRoot = /(^|,)\s*:root\b/.test(r.selectorText);
|
|
46
|
+
const isDark = /(^|,)\s*\.dark\b/.test(r.selectorText);
|
|
47
|
+
if (isRoot || isDark) {
|
|
48
|
+
for (const prop of Array.from(r.style)) {
|
|
49
|
+
if (!prop.startsWith('--color-') && !prop.startsWith('--radius')) continue;
|
|
50
|
+
const val = r.style.getPropertyValue(prop).trim();
|
|
51
|
+
if (isRoot) light[prop] = val;
|
|
52
|
+
if (isDark) dark[prop] = val;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Grouping rules (@layer/@media/@supports) carry nested rules — recurse.
|
|
57
|
+
const nested = (rule as CSSGroupingRule).cssRules;
|
|
58
|
+
if (nested) visit(nested);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
62
|
+
try {
|
|
63
|
+
visit(sheet.cssRules);
|
|
64
|
+
} catch {
|
|
65
|
+
// cross-origin sheet
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { light, dark };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Reference data for the token table: a color token is one with a `.dark` override. */
|
|
72
|
+
function discover(): { colors: ColorToken[]; radii: RadiusToken[] } {
|
|
73
|
+
const { light, dark } = collectTokens();
|
|
74
|
+
const colors = Object.keys(dark)
|
|
75
|
+
.filter((n) => n.startsWith('--color-'))
|
|
76
|
+
.sort()
|
|
77
|
+
.map((name) => ({ name, light: light[name] || dark[name], dark: dark[name] }));
|
|
78
|
+
const radii = Object.keys(light)
|
|
79
|
+
.filter((n) => n.startsWith('--radius'))
|
|
80
|
+
.sort()
|
|
81
|
+
.map((name) => ({ name, value: light[name] }));
|
|
82
|
+
return { colors, radii };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Light/dark palettes for the theme editor: light = colors + --radius, dark = colors.
|
|
86
|
+
* The token set is keyed off the `.dark` overrides — that's what defines a kit token
|
|
87
|
+
* and excludes Tailwind's default palette (which has no `.dark` entry). Light values
|
|
88
|
+
* come from `:root`, falling back to the dark value if a token only exists in dark. */
|
|
89
|
+
export function discoverPalettes(): { light: Palette; dark: Palette } {
|
|
90
|
+
const { light, dark } = collectTokens();
|
|
91
|
+
const names = Object.keys(dark).filter((n) => n.startsWith('--color-')).sort();
|
|
92
|
+
const lightPalette: Palette = { '--radius': light['--radius'] ?? '0.6rem' };
|
|
93
|
+
const darkPalette: Palette = {};
|
|
94
|
+
for (const name of names) {
|
|
95
|
+
lightPalette[name] = light[name] || dark[name];
|
|
96
|
+
darkPalette[name] = dark[name];
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
light: lightPalette,
|
|
100
|
+
dark: darkPalette,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve any CSS color string (e.g. hsl(...)) to #rrggbb for a color input. */
|
|
105
|
+
export function toHex(css: string): string {
|
|
106
|
+
const el = document.createElement('div');
|
|
107
|
+
el.style.color = css;
|
|
108
|
+
el.style.display = 'none';
|
|
109
|
+
document.body.appendChild(el);
|
|
110
|
+
const rgb = getComputedStyle(el).color;
|
|
111
|
+
el.remove();
|
|
112
|
+
const m = rgb.match(/\d+(\.\d+)?/g);
|
|
113
|
+
if (!m) return '#000000';
|
|
114
|
+
return '#' + m.slice(0, 3).map((x) => Math.round(+x).toString(16).padStart(2, '0')).join('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cellHead: JSX.CSSProperties = { 'text-align': 'left', padding: '6px 10px', 'border-bottom': '1px solid var(--color-border)', 'font-weight': '600' };
|
|
118
|
+
const cell: JSX.CSSProperties = { padding: '6px 10px', 'border-bottom': '1px solid var(--color-border)', 'vertical-align': 'middle' };
|
|
119
|
+
|
|
120
|
+
function Swatch(props: { color: string }) {
|
|
121
|
+
return (
|
|
122
|
+
<span
|
|
123
|
+
style={{
|
|
124
|
+
display: 'inline-block', width: '1.15rem', height: '1.15rem', 'border-radius': '4px',
|
|
125
|
+
background: props.color, border: '1px solid var(--color-border)', 'vertical-align': 'middle',
|
|
126
|
+
'margin-right': '.4rem',
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Auto-generated reference of every overridable token (light + dark values). */
|
|
133
|
+
export function TokenTable() {
|
|
134
|
+
const [data, setData] = createSignal<{ colors: ColorToken[]; radii: RadiusToken[] }>({ colors: [], radii: [] });
|
|
135
|
+
onMount(() => setData(discover()));
|
|
136
|
+
return (
|
|
137
|
+
<div style={{ 'font-size': '13px', color: 'var(--color-foreground)' }}>
|
|
138
|
+
<table style={{ width: '100%', 'border-collapse': 'collapse' }}>
|
|
139
|
+
<thead>
|
|
140
|
+
<tr>
|
|
141
|
+
<th style={cellHead}>Token</th>
|
|
142
|
+
<th style={cellHead}>Purpose</th>
|
|
143
|
+
<th style={cellHead}>Light</th>
|
|
144
|
+
<th style={cellHead}>Dark</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
148
|
+
<For each={data().colors}>
|
|
149
|
+
{(t) => (
|
|
150
|
+
<tr>
|
|
151
|
+
<td style={cell}><code>{t.name}</code></td>
|
|
152
|
+
<td style={{ ...cell, color: 'var(--color-muted-foreground)' }}>{PURPOSE[t.name] || ''}</td>
|
|
153
|
+
<td style={cell}><Swatch color={t.light} /><small>{t.light}</small></td>
|
|
154
|
+
<td style={cell}><Swatch color={t.dark} /><small>{t.dark}</small></td>
|
|
155
|
+
</tr>
|
|
156
|
+
)}
|
|
157
|
+
</For>
|
|
158
|
+
<For each={data().radii}>
|
|
159
|
+
{(t) => (
|
|
160
|
+
<tr>
|
|
161
|
+
<td style={cell}><code>{t.name}</code></td>
|
|
162
|
+
<td style={{ ...cell, color: 'var(--color-muted-foreground)' }}>Corner radius</td>
|
|
163
|
+
<td style={{ ...cell }} colspan={2}><small>{t.value}</small></td>
|
|
164
|
+
</tr>
|
|
165
|
+
)}
|
|
166
|
+
</For>
|
|
167
|
+
</tbody>
|
|
168
|
+
</table>
|
|
169
|
+
<p style={{ 'margin-top': '.75rem', color: 'var(--color-muted-foreground)', 'font-size': '12px' }}>
|
|
170
|
+
This table is generated live from the loaded CSS — it always reflects the current tokens in <code>theme.css</code>.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
|
2
|
+
import { ChatScene } from "./chat-scene";
|
|
3
|
+
|
|
4
|
+
const meta: Meta = {
|
|
5
|
+
title: "Examples/Full Chat App",
|
|
6
|
+
parameters: {
|
|
7
|
+
// Render inline in docs so the embedded example inherits the dark-mode
|
|
8
|
+
// class (a non-inline iframe would stay light). Bounded to a sensible height.
|
|
9
|
+
docs: { story: { inline: true, height: "640px" } },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => <ChatScene />,
|
|
18
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import {
|
|
4
|
+
Message, MessageAvatar, MessageContent, MessageActions,
|
|
5
|
+
FeedbackBar, Button,
|
|
6
|
+
} from '../index';
|
|
7
|
+
import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Share, Bookmark, Check } from 'lucide-solid';
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: 'Examples/Message Actions',
|
|
11
|
+
parameters: { layout: 'padded' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj;
|
|
16
|
+
|
|
17
|
+
export const HoverActions: Story = {
|
|
18
|
+
name: 'Actions on Hover',
|
|
19
|
+
render: () => (
|
|
20
|
+
<div class="space-y-6 max-w-2xl p-4">
|
|
21
|
+
<p class="text-sm text-muted-foreground">Hover over the assistant message to see action buttons appear.</p>
|
|
22
|
+
|
|
23
|
+
<Message>
|
|
24
|
+
<MessageAvatar src="" fallback="U" alt="User" />
|
|
25
|
+
<MessageContent>How do I handle errors in async Rust functions?</MessageContent>
|
|
26
|
+
</Message>
|
|
27
|
+
|
|
28
|
+
<Message>
|
|
29
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
30
|
+
<div class="group flex-1 space-y-2">
|
|
31
|
+
<MessageContent markdown>
|
|
32
|
+
{`In Rust, async functions return \`Result\` types just like sync functions. The \`?\` operator works seamlessly:
|
|
33
|
+
|
|
34
|
+
\`\`\`rust
|
|
35
|
+
async fn fetch_user(id: u64) -> Result<User, AppError> {
|
|
36
|
+
let response = client.get(&format!("/users/{}", id))
|
|
37
|
+
.send()
|
|
38
|
+
.await?; // propagates reqwest::Error
|
|
39
|
+
let user = response
|
|
40
|
+
.json::<User>()
|
|
41
|
+
.await?; // propagates deserialization error
|
|
42
|
+
Ok(user)
|
|
43
|
+
}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
Use \`anyhow::Result\` for applications and \`thiserror\` for libraries to define custom error types.`}
|
|
47
|
+
</MessageContent>
|
|
48
|
+
<MessageActions class="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
49
|
+
<Button variant="ghost" size="icon-sm"><Copy class="size-3.5" /></Button>
|
|
50
|
+
<Button variant="ghost" size="icon-sm"><ThumbsUp class="size-3.5" /></Button>
|
|
51
|
+
<Button variant="ghost" size="icon-sm"><ThumbsDown class="size-3.5" /></Button>
|
|
52
|
+
<Button variant="ghost" size="icon-sm"><RefreshCw class="size-3.5" /></Button>
|
|
53
|
+
</MessageActions>
|
|
54
|
+
</div>
|
|
55
|
+
</Message>
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const AlwaysVisible: Story = {
|
|
61
|
+
name: 'Always Visible Actions',
|
|
62
|
+
render: () => (
|
|
63
|
+
<div class="space-y-6 max-w-2xl p-4">
|
|
64
|
+
<Message>
|
|
65
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
66
|
+
<div class="flex-1 space-y-2">
|
|
67
|
+
<MessageContent markdown>
|
|
68
|
+
{`To install Tailwind CSS v4 in a Vite project:
|
|
69
|
+
|
|
70
|
+
\`\`\`bash
|
|
71
|
+
pnpm add tailwindcss @tailwindcss/vite
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
Then add the plugin to your \`vite.config.ts\`:
|
|
75
|
+
|
|
76
|
+
\`\`\`typescript
|
|
77
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
plugins: [tailwindcss()],
|
|
81
|
+
});
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
Import Tailwind in your main CSS file:
|
|
85
|
+
|
|
86
|
+
\`\`\`css
|
|
87
|
+
@import "tailwindcss";
|
|
88
|
+
\`\`\``}
|
|
89
|
+
</MessageContent>
|
|
90
|
+
<MessageActions>
|
|
91
|
+
<Button variant="ghost" size="icon-sm"><Copy class="size-3.5" /></Button>
|
|
92
|
+
<Button variant="ghost" size="icon-sm"><ThumbsUp class="size-3.5" /></Button>
|
|
93
|
+
<Button variant="ghost" size="icon-sm"><ThumbsDown class="size-3.5" /></Button>
|
|
94
|
+
<Button variant="ghost" size="icon-sm"><RefreshCw class="size-3.5" /></Button>
|
|
95
|
+
<Button variant="ghost" size="icon-sm"><Share class="size-3.5" /></Button>
|
|
96
|
+
<Button variant="ghost" size="icon-sm"><Bookmark class="size-3.5" /></Button>
|
|
97
|
+
</MessageActions>
|
|
98
|
+
</div>
|
|
99
|
+
</Message>
|
|
100
|
+
</div>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const WithCopyConfirmation: Story = {
|
|
105
|
+
name: 'Copy with Confirmation',
|
|
106
|
+
render: () => {
|
|
107
|
+
const [copied, setCopied] = createSignal(false);
|
|
108
|
+
|
|
109
|
+
const handleCopy = () => {
|
|
110
|
+
setCopied(true);
|
|
111
|
+
setTimeout(() => setCopied(false), 2000);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div class="space-y-6 max-w-2xl p-4">
|
|
116
|
+
<Message>
|
|
117
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
118
|
+
<div class="flex-1 space-y-2">
|
|
119
|
+
<MessageContent>
|
|
120
|
+
The current LTS version of Node.js is 22.x, which includes built-in support for the fetch API, test runner, and watch mode.
|
|
121
|
+
</MessageContent>
|
|
122
|
+
<MessageActions>
|
|
123
|
+
<Button variant="ghost" size="icon-sm" onClick={handleCopy}>
|
|
124
|
+
{copied() ? <Check class="size-3.5 text-green-500" /> : <Copy class="size-3.5" />}
|
|
125
|
+
</Button>
|
|
126
|
+
<Button variant="ghost" size="icon-sm"><ThumbsUp class="size-3.5" /></Button>
|
|
127
|
+
<Button variant="ghost" size="icon-sm"><ThumbsDown class="size-3.5" /></Button>
|
|
128
|
+
</MessageActions>
|
|
129
|
+
</div>
|
|
130
|
+
</Message>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const WithFeedbackBar: Story = {
|
|
137
|
+
name: 'Feedback Bar',
|
|
138
|
+
render: () => {
|
|
139
|
+
const [showFeedback, setShowFeedback] = createSignal(true);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div class="space-y-6 max-w-2xl p-4">
|
|
143
|
+
<Message>
|
|
144
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
145
|
+
<div class="flex-1 space-y-3">
|
|
146
|
+
<MessageContent markdown>
|
|
147
|
+
{`Here are 3 ways to debounce in JavaScript:
|
|
148
|
+
|
|
149
|
+
1. **setTimeout approach** -- simple but manual cleanup
|
|
150
|
+
2. **lodash.debounce** -- battle-tested, configurable leading/trailing
|
|
151
|
+
3. **AbortController** -- modern, cancellable, works with fetch`}
|
|
152
|
+
</MessageContent>
|
|
153
|
+
|
|
154
|
+
{showFeedback() && (
|
|
155
|
+
<FeedbackBar
|
|
156
|
+
title="Was this response helpful?"
|
|
157
|
+
onHelpful={() => setShowFeedback(false)}
|
|
158
|
+
onNotHelpful={() => setShowFeedback(false)}
|
|
159
|
+
onClose={() => setShowFeedback(false)}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</Message>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
};
|