@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,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
+ };