create-kai 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/README.md +311 -0
- package/dist/index.js +4433 -0
- package/dist/templates/angular/_gitignore +7 -0
- package/dist/templates/angular/angular.json +53 -0
- package/dist/templates/angular/package.json +30 -0
- package/dist/templates/angular/src/app/app.config.ts +11 -0
- package/dist/templates/angular/src/app/app.css +36 -0
- package/dist/templates/angular/src/app/app.html +51 -0
- package/dist/templates/angular/src/app/app.ts +78 -0
- package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
- package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
- package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
- package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
- package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
- package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
- package/dist/templates/angular/src/app/types.ts +2 -0
- package/dist/templates/angular/src/chat-data.ts +106 -0
- package/dist/templates/angular/src/index.html +16 -0
- package/dist/templates/angular/src/main.ts +16 -0
- package/dist/templates/angular/src/styles.css +10 -0
- package/dist/templates/angular/tsconfig.app.json +9 -0
- package/dist/templates/angular/tsconfig.json +22 -0
- package/dist/templates/nextjs/_gitignore +11 -0
- package/dist/templates/nextjs/app/chat-data.ts +125 -0
- package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
- package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
- package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
- package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
- package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
- package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
- package/dist/templates/nextjs/app/globals.css +153 -0
- package/dist/templates/nextjs/app/hooks/index.ts +1 -0
- package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
- package/dist/templates/nextjs/app/layout.tsx +50 -0
- package/dist/templates/nextjs/app/page.tsx +17 -0
- package/dist/templates/nextjs/app/theme.ts +10 -0
- package/dist/templates/nextjs/app/workspace.tsx +148 -0
- package/dist/templates/nextjs/next.config.mjs +22 -0
- package/dist/templates/nextjs/package.json +23 -0
- package/dist/templates/nextjs/postcss.config.mjs +16 -0
- package/dist/templates/nextjs/tsconfig.json +23 -0
- package/dist/templates/react/_gitignore +6 -0
- package/dist/templates/react/index.html +16 -0
- package/dist/templates/react/package.json +25 -0
- package/dist/templates/react/src/App.tsx +108 -0
- package/dist/templates/react/src/chat-data.ts +106 -0
- package/dist/templates/react/src/components/Composer.tsx +87 -0
- package/dist/templates/react/src/components/Sidebar.tsx +38 -0
- package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
- package/dist/templates/react/src/components/ThreadView.tsx +40 -0
- package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/index.ts +2 -0
- package/dist/templates/react/src/hooks/index.ts +1 -0
- package/dist/templates/react/src/hooks/useConversations.ts +37 -0
- package/dist/templates/react/src/index.css +111 -0
- package/dist/templates/react/src/main.tsx +15 -0
- package/dist/templates/react/src/vite-env.d.ts +1 -0
- package/dist/templates/react/tsconfig.app.json +28 -0
- package/dist/templates/react/tsconfig.json +7 -0
- package/dist/templates/react/tsconfig.node.json +25 -0
- package/dist/templates/react/vite.config.ts +11 -0
- package/dist/templates/solid/_gitignore +6 -0
- package/dist/templates/solid/index.html +13 -0
- package/dist/templates/solid/package.json +23 -0
- package/dist/templates/solid/src/App.tsx +151 -0
- package/dist/templates/solid/src/chat-data.ts +234 -0
- package/dist/templates/solid/src/components/Composer.tsx +90 -0
- package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
- package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
- package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
- package/dist/templates/solid/src/components/icons/index.tsx +84 -0
- package/dist/templates/solid/src/index.tsx +5 -0
- package/dist/templates/solid/src/lib/chat.ts +64 -0
- package/dist/templates/solid/src/lib/conversations.ts +54 -0
- package/dist/templates/solid/src/lib/types.ts +2 -0
- package/dist/templates/solid/src/styles.css +11 -0
- package/dist/templates/solid/tsconfig.json +18 -0
- package/dist/templates/solid/vite.config.ts +10 -0
- package/dist/templates/svelte/_gitignore +6 -0
- package/dist/templates/svelte/index.html +16 -0
- package/dist/templates/svelte/package.json +23 -0
- package/dist/templates/svelte/src/App.svelte +116 -0
- package/dist/templates/svelte/src/app.d.ts +20 -0
- package/dist/templates/svelte/src/chat-data.ts +106 -0
- package/dist/templates/svelte/src/components/Composer.svelte +102 -0
- package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
- package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
- package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
- package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
- package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
- package/dist/templates/svelte/src/index.css +111 -0
- package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
- package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
- package/dist/templates/svelte/src/lib/types.ts +2 -0
- package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
- package/dist/templates/svelte/src/main.ts +18 -0
- package/dist/templates/svelte/svelte.config.js +8 -0
- package/dist/templates/svelte/tsconfig.app.json +28 -0
- package/dist/templates/svelte/tsconfig.json +7 -0
- package/dist/templates/svelte/tsconfig.node.json +25 -0
- package/dist/templates/svelte/vite.config.ts +15 -0
- package/dist/templates/tanstack-start/_gitignore +14 -0
- package/dist/templates/tanstack-start/package.json +28 -0
- package/dist/templates/tanstack-start/serve.mjs +129 -0
- package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
- package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
- package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
- package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
- package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
- package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
- package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
- package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
- package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
- package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
- package/dist/templates/tanstack-start/src/router.tsx +16 -0
- package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
- package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
- package/dist/templates/tanstack-start/src/styles.css +153 -0
- package/dist/templates/tanstack-start/src/theme.ts +10 -0
- package/dist/templates/tanstack-start/tsconfig.json +16 -0
- package/dist/templates/tanstack-start/vite.config.ts +28 -0
- package/dist/templates/vanilla/_gitignore +6 -0
- package/dist/templates/vanilla/index.html +16 -0
- package/dist/templates/vanilla/package.json +19 -0
- package/dist/templates/vanilla/src/chat-data.ts +106 -0
- package/dist/templates/vanilla/src/index.css +111 -0
- package/dist/templates/vanilla/src/main.ts +53 -0
- package/dist/templates/vanilla/src/state.ts +119 -0
- package/dist/templates/vanilla/src/view.ts +222 -0
- package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
- package/dist/templates/vanilla/src/voice-input.ts +55 -0
- package/dist/templates/vanilla/tsconfig.json +24 -0
- package/dist/templates/vanilla/vite.config.ts +14 -0
- package/dist/templates/vue/_gitignore +6 -0
- package/dist/templates/vue/index.html +16 -0
- package/dist/templates/vue/package.json +23 -0
- package/dist/templates/vue/src/App.vue +104 -0
- package/dist/templates/vue/src/chat-data.ts +106 -0
- package/dist/templates/vue/src/components/Composer.vue +96 -0
- package/dist/templates/vue/src/components/Sidebar.vue +48 -0
- package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
- package/dist/templates/vue/src/components/ThreadView.vue +50 -0
- package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
- package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
- package/dist/templates/vue/src/composables/index.ts +3 -0
- package/dist/templates/vue/src/composables/useChat.ts +47 -0
- package/dist/templates/vue/src/composables/useConversations.ts +42 -0
- package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
- package/dist/templates/vue/src/index.css +111 -0
- package/dist/templates/vue/src/main.ts +18 -0
- package/dist/templates/vue/src/types.ts +4 -0
- package/dist/templates/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/vue/tsconfig.app.json +29 -0
- package/dist/templates/vue/tsconfig.json +7 -0
- package/dist/templates/vue/tsconfig.node.json +25 -0
- package/dist/templates/vue/vite.config.ts +24 -0
- package/package.json +63 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE SERVER-RENDERING PROOF, ON THE PAGE.
|
|
5
|
+
*
|
|
6
|
+
* This starter's original job was to show that a `kai-*` element survives SSR +
|
|
7
|
+
* hydration, and this badge is what makes that visible without opening devtools.
|
|
8
|
+
* It is worth keeping alongside the working chat because HYDRATION FAILURE LOOKS
|
|
9
|
+
* LIKE SUCCESS: TanStack Start can emit perfectly correct HTML and then throw
|
|
10
|
+
* during hydration, leaving a page that reads exactly right and does nothing. A
|
|
11
|
+
* screenshot cannot tell those apart. This badge can, because the only way it
|
|
12
|
+
* turns green is for client JavaScript to have run.
|
|
13
|
+
*
|
|
14
|
+
* HOW IT AVOIDS BEING THE BUG IT REPORTS. The state starts `null` — the same value
|
|
15
|
+
* on the server and on React's first client render — so the two trees agree and
|
|
16
|
+
* hydration has nothing to reconcile. Only afterwards does the effect (which never
|
|
17
|
+
* runs on the server) look at `customElements` and re-render. Reading
|
|
18
|
+
* `customElements` during render instead would produce "none" in the server HTML
|
|
19
|
+
* and "5" in the client tree, i.e. a hydration mismatch inside the hydration
|
|
20
|
+
* detector.
|
|
21
|
+
*
|
|
22
|
+
* The tags are polled on a rAF loop rather than checked once: each React wrapper
|
|
23
|
+
* registers its own element from a layout effect and the element modules load as
|
|
24
|
+
* dynamic imports, so the definitions land over the next few frames rather than
|
|
25
|
+
* all at once.
|
|
26
|
+
*/
|
|
27
|
+
const TAGS = ['kai-button', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-resizable'] as const;
|
|
28
|
+
|
|
29
|
+
export function HydrationBadge() {
|
|
30
|
+
const [defined, setDefined] = useState<number | null>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let raf = 0;
|
|
34
|
+
const check = () => {
|
|
35
|
+
const n = TAGS.filter((t) => !!customElements.get(t)).length;
|
|
36
|
+
setDefined(n);
|
|
37
|
+
if (n < TAGS.length) raf = requestAnimationFrame(check);
|
|
38
|
+
};
|
|
39
|
+
check();
|
|
40
|
+
return () => cancelAnimationFrame(raf);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const hydrated = defined === TAGS.length;
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
className="hydration"
|
|
47
|
+
data-testid="registration-status"
|
|
48
|
+
data-state={hydrated ? 'hydrated' : 'server'}
|
|
49
|
+
title={
|
|
50
|
+
hydrated
|
|
51
|
+
? `Client JS ran: all ${TAGS.length} kai-* elements are defined in customElements.`
|
|
52
|
+
: 'Server-rendered markup. The kai-* tags are still bare — no client JS has registered them yet.'
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<span className="hydration-dot" aria-hidden />
|
|
56
|
+
{defined === null
|
|
57
|
+
? 'server-rendered · elements not yet registered'
|
|
58
|
+
: hydrated
|
|
59
|
+
? `hydrated · ${defined}/${TAGS.length} kai-* elements registered`
|
|
60
|
+
: `hydrating · ${defined}/${TAGS.length} kai-* elements registered`}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Conversations } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { Theme } from '../theme';
|
|
3
|
+
import type { Conversation } from '../chat-data';
|
|
4
|
+
|
|
5
|
+
interface SidebarProps {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
conversations: Conversation[];
|
|
8
|
+
activeId: string;
|
|
9
|
+
collapsed: boolean;
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
onNewChat: () => void;
|
|
12
|
+
onToggle: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The conversation rail — a thin wrapper over `<kai-conversations>`.
|
|
17
|
+
*
|
|
18
|
+
* `conversations` is an ARRAY, so it crosses as a JS property rather than an HTML
|
|
19
|
+
* attribute. That is the half of the contract server rendering makes visible: the
|
|
20
|
+
* SSR pass emits `<kai-conversations></kai-conversations>` with no `conversations`
|
|
21
|
+
* attribute anywhere, because the wrapper assigns the property from a layout
|
|
22
|
+
* effect that only ever runs in the browser.
|
|
23
|
+
*
|
|
24
|
+
* The `.sidebar` div owns the shell's right border (kept OFF the element so it follows
|
|
25
|
+
* the shell's light/dark tokens, not the light-mode ones the element pins on itself at
|
|
26
|
+
* `:host`).
|
|
27
|
+
*/
|
|
28
|
+
export function Sidebar({ theme, conversations, activeId, collapsed, onSelect, onNewChat, onToggle }: SidebarProps) {
|
|
29
|
+
return (
|
|
30
|
+
<aside className="sidebar">
|
|
31
|
+
<Conversations
|
|
32
|
+
theme={theme}
|
|
33
|
+
groups={[]} /* flat list: the element buckets `conversations` by recency itself */
|
|
34
|
+
conversations={conversations}
|
|
35
|
+
activeId={activeId}
|
|
36
|
+
collapsed={collapsed}
|
|
37
|
+
onConversationSelect={(e) => onSelect(e.detail.id)}
|
|
38
|
+
onNewChat={onNewChat}
|
|
39
|
+
onToggleSidebar={onToggle}
|
|
40
|
+
/>
|
|
41
|
+
</aside>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Button } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { Theme } from '../theme';
|
|
3
|
+
import { MoonIcon, SunIcon } from './icons';
|
|
4
|
+
|
|
5
|
+
interface ThemeToggleProps {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
onToggle: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Light/dark switch for the top bar. Shows the moon in light mode (tap → dark) and
|
|
12
|
+
* the sun in dark mode. The `<Button>` is icon-only, so it carries the accessible
|
|
13
|
+
* label; the glyph is decorative (`aria-hidden`) and sits in the `icon` slot.
|
|
14
|
+
*
|
|
15
|
+
* The glyph is chosen from `theme`, which is route state seeded to the same value
|
|
16
|
+
* on both sides of hydration — NOT from `prefers-color-scheme`, which the server
|
|
17
|
+
* cannot read and which would therefore render a different icon than the client.
|
|
18
|
+
*/
|
|
19
|
+
export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
|
|
20
|
+
const Glyph = theme === 'light' ? MoonIcon : SunIcon;
|
|
21
|
+
return (
|
|
22
|
+
<Button theme={theme} variant="ghost" size="icon" label="Toggle light/dark theme" onClick={onToggle}>
|
|
23
|
+
<Glyph slot="icon" aria-hidden />
|
|
24
|
+
</Button>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Thread } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { ChatMessage } from '@kitn.ai/ui/react';
|
|
3
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
4
|
+
import type { Theme } from '../theme';
|
|
5
|
+
|
|
6
|
+
interface ThreadViewProps {
|
|
7
|
+
theme: Theme;
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The scrolling message list, and the piece that carries the server-rendering
|
|
13
|
+
* proof.
|
|
14
|
+
*
|
|
15
|
+
* `<kai-thread>` (React `<Thread>`) owns the message rendering, the centered
|
|
16
|
+
* fixed-width column and stick-to-bottom scroll. The server emits it as an EMPTY
|
|
17
|
+
* tag — no `messages` attribute, no serialized thread — and the array only arrives
|
|
18
|
+
* once the wrapper's layout effect assigns `el.messages` in the browser. Streaming
|
|
19
|
+
* is that same channel repeated: a NEW array reference per chunk, which is why
|
|
20
|
+
* mutating the existing array in place would render nothing.
|
|
21
|
+
*
|
|
22
|
+
* This component just bakes the per-message actions onto the assistant turns and
|
|
23
|
+
* wires the custom `speak` action to the browser's speech synthesis. `copy` (and
|
|
24
|
+
* the feedback votes) are handled inside the element.
|
|
25
|
+
*/
|
|
26
|
+
export function ThreadView({ theme, messages }: ThreadViewProps) {
|
|
27
|
+
// <Thread> reads `actions` off each message; only assistant replies get them.
|
|
28
|
+
const withActions: ChatMessage[] = messages.map((m) =>
|
|
29
|
+
m.role === 'assistant'
|
|
30
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
31
|
+
: m,
|
|
32
|
+
);
|
|
33
|
+
return (
|
|
34
|
+
<Thread
|
|
35
|
+
className="thread"
|
|
36
|
+
theme={theme}
|
|
37
|
+
messages={withActions}
|
|
38
|
+
onMessageAction={(e) => {
|
|
39
|
+
if (e.detail.action === 'speak') {
|
|
40
|
+
const m = messages.find((x) => x.id === e.detail.messageId);
|
|
41
|
+
if (!m) return;
|
|
42
|
+
window.speechSynthesis.cancel();
|
|
43
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moon glyph — shown in light mode (tap the toggle → dark). moon/sun aren't in the
|
|
5
|
+
* kit's `<Icon>` set, so the example owns them. Props spread straight onto the
|
|
6
|
+
* `<svg>`, so `slot="icon"`, `className`, and `aria-hidden` pass through and it
|
|
7
|
+
* drops into `<Button>`'s icon slot.
|
|
8
|
+
*/
|
|
9
|
+
export function MoonIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
width={size}
|
|
14
|
+
height={size}
|
|
15
|
+
fill="none"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
strokeWidth={2}
|
|
18
|
+
strokeLinecap="round"
|
|
19
|
+
strokeLinejoin="round"
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sun glyph — shown in dark mode (tap the toggle → light). See `MoonIcon` for why
|
|
5
|
+
* the example owns these icons. Props spread straight onto the `<svg>`.
|
|
6
|
+
*/
|
|
7
|
+
export function SunIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
width={size}
|
|
12
|
+
height={size}
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
strokeWidth={2}
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<circle cx="12" cy="12" r="4" />
|
|
21
|
+
<path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
|
|
22
|
+
<path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useConversations } from './useConversations';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import type { KaiChatController } from '@kitn.ai/ui/react';
|
|
3
|
+
import { THREADS, newId, type Conversation } from '../chat-data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Owns the conversation list + which one is active, plus the in-memory `THREADS`
|
|
7
|
+
* stash: on select/new we swap the OPEN thread out to `THREADS` and swap the
|
|
8
|
+
* picked one in. Takes the `useKaiChat` controller so it can read the live
|
|
9
|
+
* messages when stashing and reset them when loading another thread.
|
|
10
|
+
*
|
|
11
|
+
* In-memory on purpose — this is where a real app would read and write a database
|
|
12
|
+
* from a TanStack server function. Nothing here runs on the server: the hook's
|
|
13
|
+
* initial state is the same on both sides of hydration, and every mutation is
|
|
14
|
+
* driven by a user event.
|
|
15
|
+
*/
|
|
16
|
+
export function useConversations(chat: KaiChatController, initial: Conversation[]) {
|
|
17
|
+
const [conversations, setConversations] = useState<Conversation[]>(initial);
|
|
18
|
+
const [activeId, setActiveId] = useState(initial[0].id);
|
|
19
|
+
|
|
20
|
+
const selectConversation = useCallback(
|
|
21
|
+
(id: string) => {
|
|
22
|
+
THREADS[activeId] = chat.messages; // stash the open thread
|
|
23
|
+
setActiveId(id);
|
|
24
|
+
chat.setMessages(THREADS[id] ?? []); // load the picked one
|
|
25
|
+
},
|
|
26
|
+
[activeId, chat],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const newChat = useCallback(() => {
|
|
30
|
+
const id = newId();
|
|
31
|
+
THREADS[activeId] = chat.messages;
|
|
32
|
+
THREADS[id] = [];
|
|
33
|
+
setConversations((cs) => [
|
|
34
|
+
{ id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
35
|
+
...cs,
|
|
36
|
+
]);
|
|
37
|
+
setActiveId(id);
|
|
38
|
+
chat.setMessages([]);
|
|
39
|
+
}, [activeId, chat]);
|
|
40
|
+
|
|
41
|
+
return { conversations, activeId, selectConversation, newChat };
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRouter } from '@tanstack/react-router';
|
|
2
|
+
import { routeTree } from './routeTree.gen';
|
|
3
|
+
|
|
4
|
+
export function getRouter() {
|
|
5
|
+
const router = createRouter({
|
|
6
|
+
routeTree,
|
|
7
|
+
scrollRestoration: true,
|
|
8
|
+
});
|
|
9
|
+
return router;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module '@tanstack/react-router' {
|
|
13
|
+
interface Register {
|
|
14
|
+
router: ReturnType<typeof getRouter>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Outlet,
|
|
4
|
+
createRootRoute,
|
|
5
|
+
HeadContent,
|
|
6
|
+
Scripts,
|
|
7
|
+
} from '@tanstack/react-router';
|
|
8
|
+
|
|
9
|
+
// The kit's design tokens, as PLAIN COMPILED CSS.
|
|
10
|
+
//
|
|
11
|
+
// IT MUST BE `theme.tokens.css`, NOT `theme.css`. They are not two spellings of
|
|
12
|
+
// one file: `@kitn.ai/ui/theme.css` is Tailwind v4 SOURCE — its light tokens live
|
|
13
|
+
// inside an `@theme { … }` block, which is a Tailwind at-rule, not CSS. The test
|
|
14
|
+
// is whether TAILWIND PROCESSES THE FILE, not which specifier you import: this app
|
|
15
|
+
// runs none, so `theme.css` would reach the browser verbatim, where an unknown
|
|
16
|
+
// at-rule is DISCARDED WHOLE. (The Solid starter imports that same file and is
|
|
17
|
+
// right to — it runs Tailwind, so the block is compiled rather than discarded.)
|
|
18
|
+
//
|
|
19
|
+
// The damage is narrow, which is why it survives review. Dark mode is unaffected
|
|
20
|
+
// (those tokens are a plain `.dark` rule), and anything inside a `kai-*` element
|
|
21
|
+
// still resolves. What loses its colours is the chrome OUTSIDE every element —
|
|
22
|
+
// here `html`/`body` and `.app`. It builds green either way.
|
|
23
|
+
//
|
|
24
|
+
// Importing it from the root route is what puts it in the SERVER-rendered <head>:
|
|
25
|
+
// React 19 hoists the stylesheet link out of the module graph and emits it with
|
|
26
|
+
// `data-precedence`, so the SSR'd document already carries
|
|
27
|
+
// `<link rel="stylesheet">` and there is no flash of unstyled custom elements.
|
|
28
|
+
import '@kitn.ai/ui/theme.tokens.css';
|
|
29
|
+
import '../styles.css';
|
|
30
|
+
|
|
31
|
+
export const Route = createRootRoute({
|
|
32
|
+
head: () => ({
|
|
33
|
+
meta: [
|
|
34
|
+
{ charSet: 'utf-8' },
|
|
35
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
36
|
+
{ title: '@kitn.ai/ui — TanStack Start example' },
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
component: RootComponent,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function RootComponent() {
|
|
43
|
+
return (
|
|
44
|
+
<RootDocument>
|
|
45
|
+
<Outlet />
|
|
46
|
+
</RootDocument>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|
51
|
+
return (
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<HeadContent />
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
{children}
|
|
58
|
+
<Scripts />
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
3
|
+
import { Button, Resizable, ResizableItem, useKaiChat } from '@kitn.ai/ui/react';
|
|
4
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
5
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from '../chat-data';
|
|
6
|
+
import { Sidebar } from '../components/Sidebar';
|
|
7
|
+
import { ThreadView } from '../components/ThreadView';
|
|
8
|
+
import { Composer } from '../components/Composer';
|
|
9
|
+
import { ThemeToggle } from '../components/ThemeToggle';
|
|
10
|
+
import { HydrationBadge } from '../components/HydrationBadge';
|
|
11
|
+
import { useConversations } from '../hooks';
|
|
12
|
+
import type { Theme } from '../theme';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A mini chat **workspace composed by hand** from @kitn.ai/ui's individual
|
|
16
|
+
* elements — server-rendered by TanStack Start, then hydrated and streamed into.
|
|
17
|
+
*
|
|
18
|
+
* <Resizable>/<ResizableItem> — the draggable sidebar | main split
|
|
19
|
+
* <Conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
20
|
+
* <Thread> — the scrolling message list (kai-thread; stick-to-bottom built in)
|
|
21
|
+
* <PromptInput> — the composer at the bottom
|
|
22
|
+
*
|
|
23
|
+
* `useKaiChat` owns the message array + streaming; everything else is plain React
|
|
24
|
+
* state. Swap `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS ROUTE PROVES ABOUT SERVER RENDERING
|
|
27
|
+
* ---------------------------------------------
|
|
28
|
+
* This starter exists to show that the `kai-*` web components survive SSR. It used
|
|
29
|
+
* to show that with a static `<Chat>` fed a frozen array. A composed thread that
|
|
30
|
+
* hydrates and then STREAMS proves strictly more, so the demonstration moved here
|
|
31
|
+
* rather than being kept alongside:
|
|
32
|
+
*
|
|
33
|
+
* 1. The server emits BARE TAGS. `@kitn.ai/ui/react` registers each element from
|
|
34
|
+
* a layout effect, and layout effects do not run on the server, so the SSR'd
|
|
35
|
+
* HTML contains `<kai-thread></kai-thread>` and nothing inside it. View source
|
|
36
|
+
* and you will find empty elements — that is correct, not a bug.
|
|
37
|
+
* 2. Array and object props NEVER become attributes. `messages`, `conversations`
|
|
38
|
+
* and `triggers` are assigned as live DOM properties after hydration, so
|
|
39
|
+
* there is no serialize/parse step and no `messages="[{...}]"` in the markup.
|
|
40
|
+
* The static version proved this ONCE, at mount.
|
|
41
|
+
* 3. Streaming proves the channel KEEPS working. Every chunk assigns a new
|
|
42
|
+
* `messages` array reference to the same element that the server rendered
|
|
43
|
+
* empty. A page that hydrated badly can still look right; it cannot stream.
|
|
44
|
+
* The badge in the top bar reports which of those you are looking at.
|
|
45
|
+
*
|
|
46
|
+
* Nothing on this page is fetched from the server: no loader, no server function.
|
|
47
|
+
* The reply comes from the kit's mock responder in the browser. That keeps the SSR
|
|
48
|
+
* question — does the ELEMENT survive hydration — separate from a data-loading
|
|
49
|
+
* question this starter is not about.
|
|
50
|
+
*/
|
|
51
|
+
export const Route = createFileRoute('/')({
|
|
52
|
+
component: Home,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function Home() {
|
|
56
|
+
const [theme, setTheme] = useState<Theme>('dark');
|
|
57
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
58
|
+
const chat = useKaiChat({ initialMessages: THREADS[CONVERSATIONS[0].id] ?? [] });
|
|
59
|
+
const { conversations, activeId, selectConversation, newChat } = useConversations(chat, CONVERSATIONS);
|
|
60
|
+
|
|
61
|
+
const send = useCallback(
|
|
62
|
+
async (raw: string) => {
|
|
63
|
+
const text = raw.trim();
|
|
64
|
+
if (!text) return;
|
|
65
|
+
// The Composer already cleared its own input; here we just append the user
|
|
66
|
+
// message and stream the (mock) assistant reply.
|
|
67
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
68
|
+
const stream = chat.streamAssistant();
|
|
69
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that
|
|
70
|
+
// go through the SAME reader a real model's response would, so this preview
|
|
71
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
72
|
+
// `mockResponse(text)` becomes a POST to a TanStack server route, with
|
|
73
|
+
// toOpenAIMessages(chat.messages) as the body. The line below stays.
|
|
74
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
75
|
+
stream.done();
|
|
76
|
+
},
|
|
77
|
+
[chat],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={`app${theme === 'dark' ? ' dark' : ''}`}>
|
|
82
|
+
{/* <kai-resizable> owns the sidebar width + the divider. The handle defaults
|
|
83
|
+
to the `line` hairline (transparent at rest, tinting on hover/drag);
|
|
84
|
+
collapsing the sidebar maps to <ResizableItem collapsed>. */}
|
|
85
|
+
<Resizable theme={theme} orientation="horizontal">
|
|
86
|
+
<ResizableItem theme={theme} size="280px" min="220px" max="420px" collapsed={collapsed}>
|
|
87
|
+
<Sidebar
|
|
88
|
+
theme={theme}
|
|
89
|
+
conversations={conversations}
|
|
90
|
+
activeId={activeId}
|
|
91
|
+
collapsed={collapsed}
|
|
92
|
+
onSelect={selectConversation}
|
|
93
|
+
onNewChat={newChat}
|
|
94
|
+
onToggle={() => setCollapsed((c) => !c)}
|
|
95
|
+
/>
|
|
96
|
+
</ResizableItem>
|
|
97
|
+
|
|
98
|
+
<ResizableItem theme={theme}>
|
|
99
|
+
<main className="main">
|
|
100
|
+
<header className="bar">
|
|
101
|
+
<div className="bar-left">
|
|
102
|
+
{collapsed && (
|
|
103
|
+
<Button
|
|
104
|
+
theme={theme}
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="icon"
|
|
107
|
+
icon="panel-left"
|
|
108
|
+
label="Show sidebar"
|
|
109
|
+
onClick={() => setCollapsed(false)}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
<span className="brand">@kitn.ai/ui · composed chat on TanStack Start</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="bar-right">
|
|
115
|
+
<HydrationBadge />
|
|
116
|
+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
|
|
120
|
+
<ThreadView theme={theme} messages={chat.messages} />
|
|
121
|
+
|
|
122
|
+
<Composer
|
|
123
|
+
theme={theme}
|
|
124
|
+
loading={chat.loading}
|
|
125
|
+
suggestions={chat.messages.length <= 2 ? SUGGESTIONS : []}
|
|
126
|
+
triggers={TRIGGERS}
|
|
127
|
+
onSubmit={send}
|
|
128
|
+
onSuggestionClick={send}
|
|
129
|
+
/>
|
|
130
|
+
</main>
|
|
131
|
+
</ResizableItem>
|
|
132
|
+
</Resizable>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* The app shell. Colors come from the kit's `--color-*` tokens, imported in
|
|
2
|
+
`routes/__root.tsx` as `@kitn.ai/ui/theme.tokens.css`.
|
|
3
|
+
|
|
4
|
+
READ THE TOKEN IMPORT NOTE IN `__root.tsx` BEFORE CHANGING IT. The tokens have
|
|
5
|
+
to be the COMPILED stylesheet, not `@kitn.ai/ui/theme.css` — that one is
|
|
6
|
+
Tailwind v4 source, this app runs no Tailwind, and `html`/`body`/`.app` would
|
|
7
|
+
silently lose their light-mode colours with a completely green build. The rules
|
|
8
|
+
that sit INSIDE <kai-resizable> (`.sidebar`, `.bar`, `.composer`) would survive
|
|
9
|
+
it, which is what makes the failure hard to see. */
|
|
10
|
+
|
|
11
|
+
html,
|
|
12
|
+
body {
|
|
13
|
+
height: 100%;
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
16
|
+
background: var(--color-background);
|
|
17
|
+
color: var(--color-foreground);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Full-height workspace shell. `.dark` is toggled on THIS element rather than on
|
|
21
|
+
<html>, which is what keeps the theme out of the server-rendered document: the
|
|
22
|
+
theme is route state, so the server and the first client render agree on it and
|
|
23
|
+
there is nothing for hydration to disagree about. <kai-resizable> fills this box
|
|
24
|
+
(its `:host` is `height:100%`) and owns the sidebar | main split. */
|
|
25
|
+
.app {
|
|
26
|
+
height: 100dvh;
|
|
27
|
+
background: var(--color-background);
|
|
28
|
+
color: var(--color-foreground);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* The sidebar is its own surface: `--color-muted` sits a step off
|
|
32
|
+
`--color-background` in both themes. That step is subtle in light mode, so a 1px
|
|
33
|
+
right border marks the at-rest division — <kai-resizable>'s `line` handle is
|
|
34
|
+
transparent until hover/drag, so the boundary has to come from the panel itself.
|
|
35
|
+
`box-sizing: border-box` keeps the border inside the 280px item so it doesn't
|
|
36
|
+
shift the content width. */
|
|
37
|
+
.sidebar {
|
|
38
|
+
background: var(--color-muted);
|
|
39
|
+
border-right: 1px solid var(--color-border);
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
height: 100%;
|
|
42
|
+
min-height: 0;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.sidebar > * {
|
|
47
|
+
display: block;
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.main {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
min-width: 0;
|
|
56
|
+
height: 100%;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.bar {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
gap: 0.75rem;
|
|
64
|
+
padding: 0.75rem 1rem;
|
|
65
|
+
border-bottom: 1px solid var(--color-border);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.bar-left,
|
|
69
|
+
.bar-right {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 0.5rem;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.brand {
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
color: var(--color-muted-foreground);
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* The hydration badge (see components/HydrationBadge.tsx). Two states, and the
|
|
86
|
+
colours are the point: server-rendered HTML shows the neutral one, and it only
|
|
87
|
+
goes green once a client effect has run. */
|
|
88
|
+
.hydration {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.375rem;
|
|
92
|
+
font-size: 0.75rem;
|
|
93
|
+
font-variant-numeric: tabular-nums;
|
|
94
|
+
padding: 0.2rem 0.5rem;
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
border: 1px solid var(--color-border);
|
|
97
|
+
background: var(--color-muted);
|
|
98
|
+
color: var(--color-muted-foreground);
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.hydration[data-state='hydrated'] {
|
|
103
|
+
color: var(--color-foreground);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.hydration-dot {
|
|
107
|
+
width: 0.5rem;
|
|
108
|
+
height: 0.5rem;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
background: var(--color-muted-foreground);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.hydration[data-state='hydrated'] .hydration-dot {
|
|
114
|
+
background: #22c55e;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* <kai-thread> scrolls internally and centers its own fixed-width column, so the
|
|
118
|
+
host just needs to fill the remaining height of the main flex column. */
|
|
119
|
+
.thread {
|
|
120
|
+
flex: 1;
|
|
121
|
+
min-height: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* No top divider above the input — the composer floats against the thread. Extra
|
|
125
|
+
bottom padding lifts the input + hint off the viewport edge. */
|
|
126
|
+
.composer {
|
|
127
|
+
padding: 0.75rem 1rem 1.5rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Match the composer to the centered message column above it. */
|
|
131
|
+
.composer > * {
|
|
132
|
+
display: block;
|
|
133
|
+
width: 100%;
|
|
134
|
+
max-width: 48rem;
|
|
135
|
+
margin: 0 auto;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.composer-hint {
|
|
139
|
+
margin: 0.5rem auto 0;
|
|
140
|
+
font-size: 0.75rem;
|
|
141
|
+
text-align: center;
|
|
142
|
+
color: var(--color-muted-foreground);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.composer-hint kbd {
|
|
146
|
+
font-family: inherit;
|
|
147
|
+
font-size: 0.6875rem;
|
|
148
|
+
padding: 0.05rem 0.3rem;
|
|
149
|
+
border-radius: 0.25rem;
|
|
150
|
+
border: 1px solid var(--color-border);
|
|
151
|
+
background: var(--color-muted);
|
|
152
|
+
color: var(--color-foreground);
|
|
153
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's color mode.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than an export from the route: `src/routes/*` is compiled
|
|
5
|
+
* by the TanStack Router plugin, which rewrites those files to build the route
|
|
6
|
+
* tree, so components in `src/components/` importing a type back out of a route
|
|
7
|
+
* would point the dependency the wrong way round. Everything shared between the
|
|
8
|
+
* route and its components lives outside `routes/`.
|
|
9
|
+
*/
|
|
10
|
+
export type Theme = 'light' | 'dark';
|