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,48 @@
|
|
|
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
|
+
* prerender emits `<kai-conversations></kai-conversations>` with no `conversations`
|
|
21
|
+
* attribute anywhere, because the wrapper assigns the property from a layout effect
|
|
22
|
+
* that only ever runs in the browser.
|
|
23
|
+
*
|
|
24
|
+
* NO `'use client'` HERE, and that is not an oversight. The directive marks the
|
|
25
|
+
* BOUNDARY, not every file past it: `app/workspace.tsx` carries it, so everything
|
|
26
|
+
* it imports is already in the client graph. Repeating it on each leaf would be
|
|
27
|
+
* harmless noise that teaches the wrong rule.
|
|
28
|
+
*
|
|
29
|
+
* The `.sidebar` div owns the shell's right border (kept OFF the element so it follows
|
|
30
|
+
* the shell's light/dark tokens, not the light-mode ones the element pins on itself at
|
|
31
|
+
* `:host`).
|
|
32
|
+
*/
|
|
33
|
+
export function Sidebar({ theme, conversations, activeId, collapsed, onSelect, onNewChat, onToggle }: SidebarProps) {
|
|
34
|
+
return (
|
|
35
|
+
<aside className="sidebar">
|
|
36
|
+
<Conversations
|
|
37
|
+
theme={theme}
|
|
38
|
+
groups={[]} /* flat list: the element buckets `conversations` by recency itself */
|
|
39
|
+
conversations={conversations}
|
|
40
|
+
activeId={activeId}
|
|
41
|
+
collapsed={collapsed}
|
|
42
|
+
onConversationSelect={(e) => onSelect(e.detail.id)}
|
|
43
|
+
onNewChat={onNewChat}
|
|
44
|
+
onToggleSidebar={onToggle}
|
|
45
|
+
/>
|
|
46
|
+
</aside>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 client 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 prerender a different icon than the client
|
|
18
|
+
* builds on hydration.
|
|
19
|
+
*/
|
|
20
|
+
export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
|
|
21
|
+
const Glyph = theme === 'light' ? MoonIcon : SunIcon;
|
|
22
|
+
return (
|
|
23
|
+
<Button theme={theme} variant="ghost" size="icon" label="Toggle light/dark theme" onClick={onToggle}>
|
|
24
|
+
<Glyph slot="icon" aria-hidden />
|
|
25
|
+
</Button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -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,153 @@
|
|
|
1
|
+
/* The app shell. Colors come from the kit's `--color-*` tokens, imported in
|
|
2
|
+
`app/layout.tsx` as `@kitn.ai/ui/theme.tokens.css`.
|
|
3
|
+
|
|
4
|
+
READ THE TOKEN IMPORT NOTE IN `layout.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 the `.app` rule below would
|
|
7
|
+
silently fall back to a transparent background in light mode with a completely
|
|
8
|
+
green build. The rules that sit INSIDE a kai-* element (`.sidebar`, `.bar`)
|
|
9
|
+
would survive 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 prerendered document: the theme
|
|
22
|
+
is client state, so the server and the first client render agree on it and there
|
|
23
|
+
is nothing for hydration to disagree about. <kai-resizable> fills this box (its
|
|
24
|
+
`: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 app/components/HydrationBadge.tsx). Two states, and the
|
|
86
|
+
colours are the point: prerendered HTML shows the neutral one, and it only goes
|
|
87
|
+
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 @@
|
|
|
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 Next server action or a route handler. Nothing here runs on the server:
|
|
13
|
+
* the hook's initial state is the same on both sides of hydration, and every
|
|
14
|
+
* mutation is 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,50 @@
|
|
|
1
|
+
// Root layout — a React Server Component (no 'use client' here).
|
|
2
|
+
//
|
|
3
|
+
// The kit's design tokens, as PLAIN COMPILED CSS.
|
|
4
|
+
//
|
|
5
|
+
// IT MUST BE `theme.tokens.css`, NOT `theme.css`. They are not two spellings of
|
|
6
|
+
// one file: `@kitn.ai/ui/theme.css` is Tailwind v4 SOURCE — its light tokens live
|
|
7
|
+
// inside an `@theme { … }` block, which is a Tailwind at-rule, not CSS. This app
|
|
8
|
+
// runs no Tailwind (`postcss.config.mjs` declares no plugins), so that file would
|
|
9
|
+
// reach the browser verbatim, where an unknown at-rule is DISCARDED WHOLE, and
|
|
10
|
+
// `next build` would stay green throughout. That is what this starter shipped
|
|
11
|
+
// until it was measured on the emitted asset: `.next/static/css/*.css` carried
|
|
12
|
+
// one raw `@theme {`, and `--color-background` was defined nowhere else.
|
|
13
|
+
//
|
|
14
|
+
// WHAT THAT ACTUALLY BREAKS, stated at its real size, because "the app renders on
|
|
15
|
+
// fallbacks" is the easy version and it is wrong. Dark mode is fine — those tokens
|
|
16
|
+
// are a plain `.dark` rule. In light mode `:root` defines no `--color-*` at all,
|
|
17
|
+
// but anything nested inside a `kai-*` element still resolves — nothing re-scopes
|
|
18
|
+
// anything: Tailwind emits `@theme` to `:root, :host`, so the elements' own
|
|
19
|
+
// compiled CSS pins the tokens on every host and children inherit off it, slotted
|
|
20
|
+
// or not. What loses its colours is the chrome OUTSIDE every element — this app's
|
|
21
|
+
// `.app` shell and `html`/`body`, measured computing
|
|
22
|
+
// `background-color: rgba(0, 0, 0, 0)`. Theming host-page chrome is exactly what
|
|
23
|
+
// this stylesheet is for, and the narrowness is why the bug survived: the page
|
|
24
|
+
// looks nearly right.
|
|
25
|
+
//
|
|
26
|
+
// THE RULE IS "DOES TAILWIND PROCESS THIS FILE", NOT "NEVER IMPORT theme.css".
|
|
27
|
+
// The Solid starter imports the very same `theme.css` and is correct, because its
|
|
28
|
+
// stylesheet says `@import "tailwindcss"` above it and `@tailwindcss/vite` is in
|
|
29
|
+
// the pipeline, so `@theme` is COMPILED rather than discarded. Add Tailwind here
|
|
30
|
+
// and you would switch back.
|
|
31
|
+
//
|
|
32
|
+
// A CSS import works from a Server Component because it has no client runtime,
|
|
33
|
+
// and importing it HERE is what puts the stylesheet in the prerendered document,
|
|
34
|
+
// so there is no flash of unstyled custom elements.
|
|
35
|
+
import '@kitn.ai/ui/theme.tokens.css';
|
|
36
|
+
import './globals.css';
|
|
37
|
+
import type { ReactNode } from 'react';
|
|
38
|
+
|
|
39
|
+
export const metadata = {
|
|
40
|
+
title: '@kitn.ai/ui — Next.js App Router example',
|
|
41
|
+
description: 'A chat workspace composed by hand from the kai-* web components, on Next.js 15 / React 19.',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
45
|
+
return (
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<body>{children}</body>
|
|
48
|
+
</html>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// This page is a React Server Component (NO 'use client' here) — the App Router
|
|
2
|
+
// default, and the shape the kit is meant to be consumed in.
|
|
3
|
+
//
|
|
4
|
+
// The chat workspace itself is the client island next door, `app/workspace.tsx`.
|
|
5
|
+
// It carries the directive because IT uses hooks and event-handler props — the
|
|
6
|
+
// standard RSC rule. Rendering a kai wrapper does NOT need one: the wrappers in
|
|
7
|
+
// `@kitn.ai/ui/react` ship their own `'use client'` banner, so they are already
|
|
8
|
+
// client components and a Server Component can render them like any other.
|
|
9
|
+
//
|
|
10
|
+
// So the whole page still prerenders on the server: this component runs there,
|
|
11
|
+
// the island is prerendered to HTML there too, and the browser gets a document
|
|
12
|
+
// with bare `<kai-*>` tags in it. `app/workspace.tsx` explains what that proves.
|
|
13
|
+
import { Workspace } from './workspace';
|
|
14
|
+
|
|
15
|
+
export default function HomePage() {
|
|
16
|
+
return <Workspace />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's color mode.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than an export from `workspace.tsx`. That file carries
|
|
5
|
+
* `'use client'`, and a Server Component importing a type back out of it would
|
|
6
|
+
* point the dependency the wrong way round — types erase, so it would compile,
|
|
7
|
+
* but the import graph would say the server depends on a client module. Anything
|
|
8
|
+
* shared between the two sides lives in a file that declares neither.
|
|
9
|
+
*/
|
|
10
|
+
export type Theme = 'light' | 'dark';
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { Button, Resizable, ResizableItem, useKaiChat } from '@kitn.ai/ui/react';
|
|
5
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
6
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
|
|
7
|
+
import { Sidebar } from './components/Sidebar';
|
|
8
|
+
import { ThreadView } from './components/ThreadView';
|
|
9
|
+
import { Composer } from './components/Composer';
|
|
10
|
+
import { ThemeToggle } from './components/ThemeToggle';
|
|
11
|
+
import { HydrationBadge } from './components/HydrationBadge';
|
|
12
|
+
import { useConversations } from './hooks';
|
|
13
|
+
import type { Theme } from './theme';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A mini chat **workspace composed by hand** from @kitn.ai/ui's individual
|
|
17
|
+
* elements — prerendered by the App Router, then hydrated and streamed into.
|
|
18
|
+
*
|
|
19
|
+
* <Resizable>/<ResizableItem> — the draggable sidebar | main split
|
|
20
|
+
* <Conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
21
|
+
* <Thread> — the scrolling message list (kai-thread; stick-to-bottom built in)
|
|
22
|
+
* <PromptInput> — the composer at the bottom
|
|
23
|
+
*
|
|
24
|
+
* `useKaiChat` owns the message array + streaming; everything else is plain React
|
|
25
|
+
* state. Swap `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
26
|
+
*
|
|
27
|
+
* WHY THE `'use client'` IS ON THIS FILE AND NOT ON app/page.tsx
|
|
28
|
+
* -------------------------------------------------------------
|
|
29
|
+
* It is here because THIS component uses hooks and passes event handlers, which is
|
|
30
|
+
* the standard React Server Component rule — NOT a requirement of the kit. The
|
|
31
|
+
* `@kitn.ai/ui/react` wrappers ship their own `'use client'` banner, so a Server
|
|
32
|
+
* Component can render one directly with no directive of its own; `app/page.tsx`
|
|
33
|
+
* stays a Server Component and simply renders this island. The directive marks a
|
|
34
|
+
* boundary, so everything imported below is already in the client graph and none
|
|
35
|
+
* of those files repeats it.
|
|
36
|
+
*
|
|
37
|
+
* WHAT THIS PAGE PROVES ABOUT SERVER RENDERING
|
|
38
|
+
* --------------------------------------------
|
|
39
|
+
* This starter exists to show that the `kai-*` web components survive SSR/RSC. It
|
|
40
|
+
* used to show that with a `<Button>` rendered from a Server Component and a
|
|
41
|
+
* static `<Conversations>` fed a frozen array. A composed thread that hydrates and
|
|
42
|
+
* then STREAMS proves strictly more, so the demonstration moved here rather than
|
|
43
|
+
* being kept alongside — `create-kai` copies this whole starter into every
|
|
44
|
+
* scaffolded project, so a second demo route would be dead code in each one:
|
|
45
|
+
*
|
|
46
|
+
* 1. The server emits BARE TAGS. `@kitn.ai/ui/react` registers each element from
|
|
47
|
+
* a layout effect, and layout effects do not run during a prerender, so the
|
|
48
|
+
* HTML Next ships contains `<kai-thread></kai-thread>` and nothing inside it.
|
|
49
|
+
* View source and you will find empty elements — that is correct, not a bug.
|
|
50
|
+
* 2. Array and object props NEVER become attributes. `messages`, `conversations`
|
|
51
|
+
* and `triggers` are assigned as live DOM properties after hydration, so
|
|
52
|
+
* there is no serialize/parse step and no `messages="[{...}]"` in the markup.
|
|
53
|
+
* The static version proved this ONCE, at mount.
|
|
54
|
+
* 3. Streaming proves the channel KEEPS working. Every chunk assigns a new
|
|
55
|
+
* `messages` array reference to the same element the server rendered empty. A
|
|
56
|
+
* page that hydrated badly can still look right; it cannot stream. The badge
|
|
57
|
+
* in the top bar reports which of those you are looking at — and on Next that
|
|
58
|
+
* badge earns its keep twice over, because a PRODUCTION React build minifies
|
|
59
|
+
* hydration errors into a numbered link. Run `next dev` when you want them in
|
|
60
|
+
* words.
|
|
61
|
+
*
|
|
62
|
+
* Nothing on this page is fetched from the server: no server action, no route
|
|
63
|
+
* handler, no `fetch` in a Server Component. The reply comes from the kit's mock
|
|
64
|
+
* responder in the browser. That keeps the SSR question — does the ELEMENT survive
|
|
65
|
+
* hydration — separate from a data-loading question this starter is not about.
|
|
66
|
+
*/
|
|
67
|
+
export function Workspace() {
|
|
68
|
+
const [theme, setTheme] = useState<Theme>('dark');
|
|
69
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
70
|
+
const chat = useKaiChat({ initialMessages: THREADS[CONVERSATIONS[0].id] ?? [] });
|
|
71
|
+
const { conversations, activeId, selectConversation, newChat } = useConversations(chat, CONVERSATIONS);
|
|
72
|
+
|
|
73
|
+
const send = useCallback(
|
|
74
|
+
async (raw: string) => {
|
|
75
|
+
const text = raw.trim();
|
|
76
|
+
if (!text) return;
|
|
77
|
+
// The Composer already cleared its own input; here we just append the user
|
|
78
|
+
// message and stream the (mock) assistant reply.
|
|
79
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
80
|
+
const stream = chat.streamAssistant();
|
|
81
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that
|
|
82
|
+
// go through the SAME reader a real model's response would, so this preview
|
|
83
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
84
|
+
// `mockResponse(text)` becomes a POST to a Next route handler
|
|
85
|
+
// (`app/api/chat/route.ts`), with toOpenAIMessages(chat.messages) as the
|
|
86
|
+
// body. The line below stays.
|
|
87
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
88
|
+
stream.done();
|
|
89
|
+
},
|
|
90
|
+
[chat],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={`app${theme === 'dark' ? ' dark' : ''}`}>
|
|
95
|
+
{/* <kai-resizable> owns the sidebar width + the divider. The handle defaults
|
|
96
|
+
to the `line` hairline (transparent at rest, tinting on hover/drag);
|
|
97
|
+
collapsing the sidebar maps to <ResizableItem collapsed>. */}
|
|
98
|
+
<Resizable theme={theme} orientation="horizontal">
|
|
99
|
+
<ResizableItem theme={theme} size="280px" min="220px" max="420px" collapsed={collapsed}>
|
|
100
|
+
<Sidebar
|
|
101
|
+
theme={theme}
|
|
102
|
+
conversations={conversations}
|
|
103
|
+
activeId={activeId}
|
|
104
|
+
collapsed={collapsed}
|
|
105
|
+
onSelect={selectConversation}
|
|
106
|
+
onNewChat={newChat}
|
|
107
|
+
onToggle={() => setCollapsed((c) => !c)}
|
|
108
|
+
/>
|
|
109
|
+
</ResizableItem>
|
|
110
|
+
|
|
111
|
+
<ResizableItem theme={theme}>
|
|
112
|
+
<main className="main">
|
|
113
|
+
<header className="bar">
|
|
114
|
+
<div className="bar-left">
|
|
115
|
+
{collapsed && (
|
|
116
|
+
<Button
|
|
117
|
+
theme={theme}
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="icon"
|
|
120
|
+
icon="panel-left"
|
|
121
|
+
label="Show sidebar"
|
|
122
|
+
onClick={() => setCollapsed(false)}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
<span className="brand">@kitn.ai/ui · composed chat on Next.js</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="bar-right">
|
|
128
|
+
<HydrationBadge />
|
|
129
|
+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
|
|
130
|
+
</div>
|
|
131
|
+
</header>
|
|
132
|
+
|
|
133
|
+
<ThreadView theme={theme} messages={chat.messages} />
|
|
134
|
+
|
|
135
|
+
<Composer
|
|
136
|
+
theme={theme}
|
|
137
|
+
loading={chat.loading}
|
|
138
|
+
suggestions={chat.messages.length <= 2 ? SUGGESTIONS : []}
|
|
139
|
+
triggers={TRIGGERS}
|
|
140
|
+
onSubmit={send}
|
|
141
|
+
onSuggestionClick={send}
|
|
142
|
+
/>
|
|
143
|
+
</main>
|
|
144
|
+
</ResizableItem>
|
|
145
|
+
</Resizable>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
/** @type {import('next').NextConfig} */
|
|
7
|
+
const nextConfig = {
|
|
8
|
+
// No special config is required to consume @kitn.ai/ui in a STANDALONE app:
|
|
9
|
+
// - It ships pre-compiled ESM in `dist/`, so `transpilePackages` is NOT needed.
|
|
10
|
+
// - The register-all entry (`@kitn.ai/ui/elements`) is SSR-safe — it touches
|
|
11
|
+
// no `window`/`customElements` at import time, so importing it never throws
|
|
12
|
+
// during server rendering; it only registers in the browser.
|
|
13
|
+
//
|
|
14
|
+
// `outputFileTracingRoot` is only here because this example is NESTED inside
|
|
15
|
+
// the library's monorepo: it pins the workspace root so Next doesn't walk up
|
|
16
|
+
// and inherit the repo root's lockfile / Tailwind PostCSS config. A standalone
|
|
17
|
+
// app does not need this (nor the local postcss.config.mjs).
|
|
18
|
+
outputFileTracingRoot: __dirname,
|
|
19
|
+
reactStrictMode: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default nextConfig;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-nextjs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@kitn.ai/ui": "file:../../../packages/ui",
|
|
13
|
+
"next": "^15.5.19",
|
|
14
|
+
"react": "^19.2.7",
|
|
15
|
+
"react-dom": "^19.2.7"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.20.0",
|
|
19
|
+
"@types/react": "^19.2.17",
|
|
20
|
+
"@types/react-dom": "^19.2.3",
|
|
21
|
+
"typescript": "^5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|