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.
Files changed (176) hide show
  1. package/README.md +311 -0
  2. package/dist/index.js +4433 -0
  3. package/dist/templates/angular/_gitignore +7 -0
  4. package/dist/templates/angular/angular.json +53 -0
  5. package/dist/templates/angular/package.json +30 -0
  6. package/dist/templates/angular/src/app/app.config.ts +11 -0
  7. package/dist/templates/angular/src/app/app.css +36 -0
  8. package/dist/templates/angular/src/app/app.html +51 -0
  9. package/dist/templates/angular/src/app/app.ts +78 -0
  10. package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
  11. package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
  12. package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
  13. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
  14. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
  15. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
  16. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
  17. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
  18. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
  19. package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
  20. package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
  21. package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
  22. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
  23. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
  24. package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
  25. package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
  26. package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
  27. package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
  28. package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
  29. package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
  30. package/dist/templates/angular/src/app/types.ts +2 -0
  31. package/dist/templates/angular/src/chat-data.ts +106 -0
  32. package/dist/templates/angular/src/index.html +16 -0
  33. package/dist/templates/angular/src/main.ts +16 -0
  34. package/dist/templates/angular/src/styles.css +10 -0
  35. package/dist/templates/angular/tsconfig.app.json +9 -0
  36. package/dist/templates/angular/tsconfig.json +22 -0
  37. package/dist/templates/nextjs/_gitignore +11 -0
  38. package/dist/templates/nextjs/app/chat-data.ts +125 -0
  39. package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
  40. package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
  41. package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
  42. package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
  43. package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
  44. package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
  45. package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
  46. package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
  47. package/dist/templates/nextjs/app/globals.css +153 -0
  48. package/dist/templates/nextjs/app/hooks/index.ts +1 -0
  49. package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
  50. package/dist/templates/nextjs/app/layout.tsx +50 -0
  51. package/dist/templates/nextjs/app/page.tsx +17 -0
  52. package/dist/templates/nextjs/app/theme.ts +10 -0
  53. package/dist/templates/nextjs/app/workspace.tsx +148 -0
  54. package/dist/templates/nextjs/next.config.mjs +22 -0
  55. package/dist/templates/nextjs/package.json +23 -0
  56. package/dist/templates/nextjs/postcss.config.mjs +16 -0
  57. package/dist/templates/nextjs/tsconfig.json +23 -0
  58. package/dist/templates/react/_gitignore +6 -0
  59. package/dist/templates/react/index.html +16 -0
  60. package/dist/templates/react/package.json +25 -0
  61. package/dist/templates/react/src/App.tsx +108 -0
  62. package/dist/templates/react/src/chat-data.ts +106 -0
  63. package/dist/templates/react/src/components/Composer.tsx +87 -0
  64. package/dist/templates/react/src/components/Sidebar.tsx +38 -0
  65. package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
  66. package/dist/templates/react/src/components/ThreadView.tsx +40 -0
  67. package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
  68. package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
  69. package/dist/templates/react/src/components/icons/index.ts +2 -0
  70. package/dist/templates/react/src/hooks/index.ts +1 -0
  71. package/dist/templates/react/src/hooks/useConversations.ts +37 -0
  72. package/dist/templates/react/src/index.css +111 -0
  73. package/dist/templates/react/src/main.tsx +15 -0
  74. package/dist/templates/react/src/vite-env.d.ts +1 -0
  75. package/dist/templates/react/tsconfig.app.json +28 -0
  76. package/dist/templates/react/tsconfig.json +7 -0
  77. package/dist/templates/react/tsconfig.node.json +25 -0
  78. package/dist/templates/react/vite.config.ts +11 -0
  79. package/dist/templates/solid/_gitignore +6 -0
  80. package/dist/templates/solid/index.html +13 -0
  81. package/dist/templates/solid/package.json +23 -0
  82. package/dist/templates/solid/src/App.tsx +151 -0
  83. package/dist/templates/solid/src/chat-data.ts +234 -0
  84. package/dist/templates/solid/src/components/Composer.tsx +90 -0
  85. package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
  86. package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
  87. package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
  88. package/dist/templates/solid/src/components/icons/index.tsx +84 -0
  89. package/dist/templates/solid/src/index.tsx +5 -0
  90. package/dist/templates/solid/src/lib/chat.ts +64 -0
  91. package/dist/templates/solid/src/lib/conversations.ts +54 -0
  92. package/dist/templates/solid/src/lib/types.ts +2 -0
  93. package/dist/templates/solid/src/styles.css +11 -0
  94. package/dist/templates/solid/tsconfig.json +18 -0
  95. package/dist/templates/solid/vite.config.ts +10 -0
  96. package/dist/templates/svelte/_gitignore +6 -0
  97. package/dist/templates/svelte/index.html +16 -0
  98. package/dist/templates/svelte/package.json +23 -0
  99. package/dist/templates/svelte/src/App.svelte +116 -0
  100. package/dist/templates/svelte/src/app.d.ts +20 -0
  101. package/dist/templates/svelte/src/chat-data.ts +106 -0
  102. package/dist/templates/svelte/src/components/Composer.svelte +102 -0
  103. package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
  104. package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
  105. package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
  106. package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
  107. package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
  108. package/dist/templates/svelte/src/index.css +111 -0
  109. package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
  110. package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
  111. package/dist/templates/svelte/src/lib/types.ts +2 -0
  112. package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
  113. package/dist/templates/svelte/src/main.ts +18 -0
  114. package/dist/templates/svelte/svelte.config.js +8 -0
  115. package/dist/templates/svelte/tsconfig.app.json +28 -0
  116. package/dist/templates/svelte/tsconfig.json +7 -0
  117. package/dist/templates/svelte/tsconfig.node.json +25 -0
  118. package/dist/templates/svelte/vite.config.ts +15 -0
  119. package/dist/templates/tanstack-start/_gitignore +14 -0
  120. package/dist/templates/tanstack-start/package.json +28 -0
  121. package/dist/templates/tanstack-start/serve.mjs +129 -0
  122. package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
  123. package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
  124. package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
  125. package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
  126. package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
  127. package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
  128. package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
  129. package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
  130. package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
  131. package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
  132. package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
  133. package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
  134. package/dist/templates/tanstack-start/src/router.tsx +16 -0
  135. package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
  136. package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
  137. package/dist/templates/tanstack-start/src/styles.css +153 -0
  138. package/dist/templates/tanstack-start/src/theme.ts +10 -0
  139. package/dist/templates/tanstack-start/tsconfig.json +16 -0
  140. package/dist/templates/tanstack-start/vite.config.ts +28 -0
  141. package/dist/templates/vanilla/_gitignore +6 -0
  142. package/dist/templates/vanilla/index.html +16 -0
  143. package/dist/templates/vanilla/package.json +19 -0
  144. package/dist/templates/vanilla/src/chat-data.ts +106 -0
  145. package/dist/templates/vanilla/src/index.css +111 -0
  146. package/dist/templates/vanilla/src/main.ts +53 -0
  147. package/dist/templates/vanilla/src/state.ts +119 -0
  148. package/dist/templates/vanilla/src/view.ts +222 -0
  149. package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
  150. package/dist/templates/vanilla/src/voice-input.ts +55 -0
  151. package/dist/templates/vanilla/tsconfig.json +24 -0
  152. package/dist/templates/vanilla/vite.config.ts +14 -0
  153. package/dist/templates/vue/_gitignore +6 -0
  154. package/dist/templates/vue/index.html +16 -0
  155. package/dist/templates/vue/package.json +23 -0
  156. package/dist/templates/vue/src/App.vue +104 -0
  157. package/dist/templates/vue/src/chat-data.ts +106 -0
  158. package/dist/templates/vue/src/components/Composer.vue +96 -0
  159. package/dist/templates/vue/src/components/Sidebar.vue +48 -0
  160. package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
  161. package/dist/templates/vue/src/components/ThreadView.vue +50 -0
  162. package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
  163. package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
  164. package/dist/templates/vue/src/composables/index.ts +3 -0
  165. package/dist/templates/vue/src/composables/useChat.ts +47 -0
  166. package/dist/templates/vue/src/composables/useConversations.ts +42 -0
  167. package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
  168. package/dist/templates/vue/src/index.css +111 -0
  169. package/dist/templates/vue/src/main.ts +18 -0
  170. package/dist/templates/vue/src/types.ts +4 -0
  171. package/dist/templates/vue/src/vite-env.d.ts +1 -0
  172. package/dist/templates/vue/tsconfig.app.json +29 -0
  173. package/dist/templates/vue/tsconfig.json +7 -0
  174. package/dist/templates/vue/tsconfig.node.json +25 -0
  175. package/dist/templates/vue/vite.config.ts +24 -0
  176. 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,2 @@
1
+ export { MoonIcon } from './MoonIcon';
2
+ export { SunIcon } from './SunIcon';
@@ -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
+ }