@kitnai/chat 0.3.1 → 0.5.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 (119) hide show
  1. package/README.md +35 -5
  2. package/dist/custom-elements.json +2969 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +718 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +137 -0
  7. package/frameworks/react/index.tsx +584 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +718 -0
  10. package/llms.txt +104 -0
  11. package/package.json +53 -6
  12. package/src/components/attachments.tsx +4 -2
  13. package/src/components/chain-of-thought.tsx +1 -1
  14. package/src/components/chat-scope-picker.tsx +2 -2
  15. package/src/components/chat-thread.tsx +217 -0
  16. package/src/components/checkpoint.tsx +7 -3
  17. package/src/components/context.tsx +14 -18
  18. package/src/components/conversation-item.tsx +1 -1
  19. package/src/components/conversation-list.tsx +5 -4
  20. package/src/components/message-skills.tsx +1 -1
  21. package/src/components/message.tsx +1 -0
  22. package/src/components/model-switcher.tsx +3 -3
  23. package/src/components/prompt-input.tsx +20 -2
  24. package/src/components/reasoning.tsx +2 -2
  25. package/src/components/scroll-button.tsx +1 -0
  26. package/src/components/slash-command.tsx +17 -8
  27. package/src/components/source.tsx +2 -2
  28. package/src/components/thinking-bar.tsx +2 -2
  29. package/src/components/tool.tsx +17 -6
  30. package/src/components/voice-input.tsx +5 -1
  31. package/src/elements/attachments.tsx +132 -0
  32. package/src/elements/chain-of-thought.tsx +45 -0
  33. package/src/elements/chat-scope-picker.tsx +36 -0
  34. package/src/elements/chat-workspace.tsx +122 -0
  35. package/src/elements/chat.tsx +31 -228
  36. package/src/elements/checkpoint.tsx +43 -0
  37. package/src/elements/code-block.tsx +42 -0
  38. package/src/elements/compiled.css +1 -1
  39. package/src/elements/context-meter.tsx +71 -0
  40. package/src/elements/conversation-list.tsx +6 -0
  41. package/src/elements/default-input.tsx +22 -1
  42. package/src/elements/define.tsx +98 -12
  43. package/src/elements/element-types.d.ts +444 -0
  44. package/src/elements/empty.tsx +29 -0
  45. package/src/elements/feedback-bar.tsx +33 -0
  46. package/src/elements/file-upload.tsx +44 -0
  47. package/src/elements/image.tsx +32 -0
  48. package/src/elements/kitn-attachments.stories.tsx +181 -0
  49. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  50. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  51. package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
  52. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  53. package/src/elements/kitn-code-block.stories.tsx +82 -0
  54. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  55. package/src/elements/kitn-empty.stories.tsx +110 -0
  56. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  57. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  58. package/src/elements/kitn-image.stories.tsx +70 -0
  59. package/src/elements/kitn-loader.stories.tsx +87 -0
  60. package/src/elements/kitn-markdown.stories.tsx +75 -0
  61. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  62. package/src/elements/kitn-message.stories.tsx +105 -0
  63. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  64. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  65. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  66. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  67. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  68. package/src/elements/kitn-source-list.stories.tsx +77 -0
  69. package/src/elements/kitn-source.stories.tsx +87 -0
  70. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  71. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  72. package/src/elements/kitn-tool.stories.tsx +88 -0
  73. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  74. package/src/elements/loader.tsx +25 -0
  75. package/src/elements/markdown.tsx +38 -0
  76. package/src/elements/message-skills.tsx +22 -0
  77. package/src/elements/message.tsx +125 -0
  78. package/src/elements/model-switcher.tsx +35 -0
  79. package/src/elements/prompt-input.tsx +83 -7
  80. package/src/elements/prompt-suggestions.tsx +58 -0
  81. package/src/elements/reasoning.tsx +50 -0
  82. package/src/elements/register.ts +32 -0
  83. package/src/elements/response-stream.tsx +40 -0
  84. package/src/elements/source.tsx +67 -0
  85. package/src/elements/styles.css +14 -0
  86. package/src/elements/text-shimmer.tsx +28 -0
  87. package/src/elements/thinking-bar.tsx +34 -0
  88. package/src/elements/tool.tsx +23 -0
  89. package/src/elements/voice-input.tsx +41 -0
  90. package/src/index.ts +0 -1
  91. package/src/primitives/chat-config.tsx +3 -3
  92. package/src/stories/docs/Accessibility.mdx +119 -0
  93. package/src/stories/docs/ForAIAgents.mdx +93 -0
  94. package/src/stories/docs/GettingStarted.mdx +2 -2
  95. package/src/stories/docs/Installation.mdx +29 -2
  96. package/src/stories/docs/Integrations.mdx +417 -15
  97. package/src/stories/docs/Introduction.mdx +17 -8
  98. package/src/stories/docs/Theming.mdx +1 -1
  99. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  100. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  101. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  102. package/src/stories/typography.stories.tsx +78 -0
  103. package/src/ui/button.tsx +1 -1
  104. package/src/ui/collapsible.stories.tsx +70 -0
  105. package/src/ui/collapsible.tsx +119 -8
  106. package/src/ui/dropdown.stories.tsx +60 -0
  107. package/src/ui/dropdown.tsx +177 -12
  108. package/src/ui/hover-card.stories.tsx +78 -0
  109. package/src/ui/hover-card.tsx +147 -26
  110. package/src/ui/overlay.stories.tsx +115 -0
  111. package/src/ui/overlay.tsx +151 -0
  112. package/src/ui/scroll-area.stories.tsx +51 -0
  113. package/src/ui/textarea.stories.tsx +77 -0
  114. package/src/ui/textarea.tsx +1 -1
  115. package/src/ui/tooltip.stories.tsx +1 -1
  116. package/src/ui/tooltip.tsx +59 -13
  117. package/src/utils/cn.ts +19 -1
  118. package/theme.css +76 -43
  119. package/src/ui/dialog.tsx +0 -21
@@ -0,0 +1,38 @@
1
+ import { defineKitnElement } from './define';
2
+ import { Markdown } from '../components/markdown';
3
+ import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The markdown source to render. */
7
+ content: string;
8
+ /** Text/markdown sizing. */
9
+ proseSize?: ProseSize;
10
+ /** Shiki theme for fenced code blocks. */
11
+ codeTheme?: string;
12
+ /** Disable syntax highlighting (no Shiki loads). */
13
+ codeHighlight?: boolean;
14
+ }
15
+
16
+ /**
17
+ * `<kitn-markdown>` — renders markdown (with fenced-code syntax highlighting) as
18
+ * a standalone element. Content via the `content` property; sizing/highlighting
19
+ * via attributes.
20
+ */
21
+ defineKitnElement<Props>('kitn-markdown', {
22
+ content: '',
23
+ proseSize: 'sm',
24
+ codeTheme: 'github-dark-dimmed',
25
+ codeHighlight: true,
26
+ }, (props, { flag }) => {
27
+ const outer = useChatConfig();
28
+ return (
29
+ <ChatConfig
30
+ proseSize={props.proseSize}
31
+ codeTheme={props.codeTheme}
32
+ codeHighlight={flag('codeHighlight')}
33
+ portalMount={outer.portalMount()}
34
+ >
35
+ <Markdown content={props.content} />
36
+ </ChatConfig>
37
+ );
38
+ });
@@ -0,0 +1,22 @@
1
+ import { defineKitnElement } from './define';
2
+ import { MessageSkills } from '../components/message-skills';
3
+
4
+ interface Skill {
5
+ /** Stable identifier for the skill. */
6
+ id: string;
7
+ /** Human-readable skill name shown on the badge. */
8
+ name: string;
9
+ }
10
+
11
+ interface Props extends Record<string, unknown> {
12
+ /** The active skills to badge. Set as a JS property. */
13
+ skills: Skill[];
14
+ }
15
+
16
+ /**
17
+ * `<kitn-message-skills>` — badges showing which skills were active for a
18
+ * message. Data via the `skills` property.
19
+ */
20
+ defineKitnElement<Props>('kitn-message-skills', {
21
+ skills: [],
22
+ }, (props) => <MessageSkills skills={props.skills} />);
@@ -0,0 +1,125 @@
1
+ import { For, Show, type Component } from 'solid-js';
2
+ import { defineKitnElement } from './define';
3
+ import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config';
4
+ import { Message, MessageContent, MessageActions } from '../components/message';
5
+ import { Reasoning, ReasoningTrigger, ReasoningContent } from '../components/reasoning';
6
+ import { Tool } from '../components/tool';
7
+ import { Attachments, Attachment, AttachmentPreview, AttachmentInfo } from '../components/attachments';
8
+ import { Button } from '../ui/button';
9
+ import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Pencil } from 'lucide-solid';
10
+ import type { ChatMessage, ChatMessageAction } from './chat-types';
11
+
12
+ interface Props extends Record<string, unknown> {
13
+ /** The full message object. Set as a JS property. */
14
+ message?: ChatMessage;
15
+ /** Convenience for simple cases when not passing a `message` object. */
16
+ role?: 'user' | 'assistant';
17
+ /** Convenience content (used when `message` is not set). */
18
+ content?: string;
19
+ /** Force markdown on/off. Defaults to on for assistant, off for user. */
20
+ markdown?: boolean;
21
+ /** Text/markdown sizing for the message body. */
22
+ proseSize?: ProseSize;
23
+ /** Shiki theme name used for fenced code blocks in the content. */
24
+ codeTheme?: string;
25
+ /** Disable syntax highlighting for code blocks (no Shiki loads). */
26
+ codeHighlight?: boolean;
27
+ }
28
+
29
+ /** Events fired by `<kitn-message>`. */
30
+ interface Events {
31
+ /** An action button was clicked. */
32
+ messageaction: { messageId: string; action: ChatMessageAction };
33
+ }
34
+
35
+ const ACTION_LABEL: Record<ChatMessageAction, string> = {
36
+ copy: 'Copy', like: 'Like', dislike: 'Dislike', regenerate: 'Regenerate', edit: 'Edit',
37
+ };
38
+ const ACTION_ICON: Record<ChatMessageAction, Component<{ class?: string }>> = {
39
+ copy: Copy, like: ThumbsUp, dislike: ThumbsDown, regenerate: RefreshCw, edit: Pencil,
40
+ };
41
+
42
+ /**
43
+ * `<kitn-message>` — a single message row: markdown/plain content, reasoning,
44
+ * tool calls, attachments, and action buttons, rendered from one `message`
45
+ * object (the same shape `<kitn-chat>` uses per message). The keystone of the
46
+ * "compose your own message list" pattern. Emits `messageaction`.
47
+ */
48
+ defineKitnElement<Props, Events>('kitn-message', {
49
+ message: undefined,
50
+ role: 'assistant',
51
+ content: undefined,
52
+ markdown: undefined,
53
+ proseSize: 'sm',
54
+ codeTheme: 'github-dark-dimmed',
55
+ codeHighlight: true,
56
+ }, (props, { dispatch, flag, element }) => {
57
+ const outer = useChatConfig();
58
+ const msg = (): ChatMessage =>
59
+ props.message ?? { id: 'message', role: props.role ?? 'assistant', content: props.content ?? '' };
60
+ const isUser = () => msg().role === 'user';
61
+ // markdown: explicit prop/attribute wins; otherwise default by role.
62
+ const markdownExplicit = () =>
63
+ element.hasAttribute('markdown') || props.markdown === true || props.markdown === false;
64
+ const useMarkdown = () => (markdownExplicit() ? flag('markdown') : !isUser());
65
+
66
+ return (
67
+ <ChatConfig
68
+ proseSize={props.proseSize}
69
+ codeTheme={props.codeTheme}
70
+ codeHighlight={flag('codeHighlight')}
71
+ portalMount={outer.portalMount()}
72
+ >
73
+ <Message class={isUser() ? 'flex-col items-end' : 'flex-col items-start'}>
74
+ <Show when={msg().reasoning}>
75
+ <Reasoning class="mb-2 w-full">
76
+ <ReasoningTrigger>{msg().reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
77
+ <ReasoningContent markdown>{msg().reasoning!.text}</ReasoningContent>
78
+ </Reasoning>
79
+ </Show>
80
+ <For each={msg().tools ?? []}>
81
+ {(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}
82
+ </For>
83
+ <Show when={msg().attachments?.length}>
84
+ <Attachments variant="inline" class={isUser() ? 'mb-2 justify-end' : 'mb-2'}>
85
+ <For each={msg().attachments!}>
86
+ {(att) => (
87
+ <Attachment data={att}>
88
+ <AttachmentPreview />
89
+ <AttachmentInfo />
90
+ </Attachment>
91
+ )}
92
+ </For>
93
+ </Attachments>
94
+ </Show>
95
+ <MessageContent
96
+ markdown={useMarkdown()}
97
+ class={isUser()
98
+ ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2'
99
+ : 'bg-transparent p-0'}
100
+ >
101
+ {msg().content}
102
+ </MessageContent>
103
+ <Show when={msg().actions?.length}>
104
+ <MessageActions class="mt-1 flex gap-0">
105
+ <For each={msg().actions!}>
106
+ {(a) => (
107
+ <Button
108
+ variant="ghost" size="icon-sm" class="rounded-full"
109
+ data-action={a}
110
+ aria-label={ACTION_LABEL[a]}
111
+ onClick={() => dispatch('messageaction', { messageId: msg().id, action: a })}
112
+ >
113
+ {(() => {
114
+ const Icon = ACTION_ICON[a];
115
+ return <Icon class="size-3.5" />;
116
+ })()}
117
+ </Button>
118
+ )}
119
+ </For>
120
+ </MessageActions>
121
+ </Show>
122
+ </Message>
123
+ </ChatConfig>
124
+ );
125
+ });
@@ -0,0 +1,35 @@
1
+ import { defineKitnElement } from './define';
2
+ import { ModelSwitcher } from '../components/model-switcher';
3
+ import type { ModelOption } from '../types';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The selectable models. Set as a JS property (array). */
7
+ models: ModelOption[];
8
+ /** The currently-selected model id. Defaults to the first model. */
9
+ currentModel?: string;
10
+ }
11
+
12
+ /** Events fired by `<kitn-model-switcher>`. */
13
+ interface Events {
14
+ /** A model was selected. */
15
+ modelchange: { modelId: string };
16
+ }
17
+
18
+ /**
19
+ * `<kitn-model-switcher>` — an event-emitting leaf element. Data in via the
20
+ * `models` property, selection out via a `modelchange` event. Mirrors the
21
+ * header switcher inside `<kitn-chat>` as a standalone, composable piece.
22
+ *
23
+ * Note: like the underlying primitive, this only renders when more than one
24
+ * model is provided.
25
+ */
26
+ defineKitnElement<Props, Events>('kitn-model-switcher', {
27
+ models: [],
28
+ currentModel: undefined,
29
+ }, (props, { dispatch }) => (
30
+ <ModelSwitcher
31
+ models={props.models}
32
+ currentModelId={props.currentModel ?? props.models[0]?.id ?? ''}
33
+ onModelChange={(modelId) => dispatch('modelchange', { modelId })}
34
+ />
35
+ ));
@@ -1,25 +1,84 @@
1
- import { createSignal } from 'solid-js';
1
+ import { createEffect, createSignal } from 'solid-js';
2
2
  import { defineKitnElement } from './define';
3
3
  import { DefaultPromptInput } from './default-input';
4
4
  import type { AttachmentData } from '../components/attachments';
5
+ import type { SlashCommandItem } from '../components/slash-command';
5
6
 
6
7
  interface Props extends Record<string, unknown> {
8
+ /** Controlled value of the input. When set, the host owns the text and must
9
+ * update it on `valuechange`; leave unset for uncontrolled behavior. */
7
10
  value?: string;
11
+ /** Placeholder text shown in the empty input. */
8
12
  placeholder?: string;
13
+ /** Disable the input and submit button entirely (non-interactive). */
9
14
  disabled?: boolean;
15
+ /** Show the loading/streaming state and block submit (use while awaiting a
16
+ * reply). */
10
17
  loading?: boolean;
18
+ /** Starter prompts shown above the input. Clicking one follows
19
+ * `suggestionMode`. Set as a JS property. */
11
20
  suggestions?: string[];
21
+ /** What clicking a suggestion does: `'submit'` (default) sends it immediately
22
+ * as if typed and submitted; `'fill'` just places it in the input. */
23
+ suggestionMode?: 'submit' | 'fill';
24
+ /** Slash commands — when set, typing `/` opens the command palette. Set as a
25
+ * JS property. */
26
+ slashCommands?: SlashCommandItem[];
27
+ /** Command ids to highlight as active. */
28
+ slashActiveIds?: string[];
29
+ /** Single-line palette rows. */
30
+ slashCompact?: boolean;
31
+ /** Show a Search (Globe) button in the left toolbar; clicking it fires a
32
+ * `search` event. */
33
+ search?: boolean;
34
+ /** Show a Voice (Mic) button in the left toolbar; clicking it fires a `voice`
35
+ * event. */
36
+ voice?: boolean;
37
+ /** Attachments to seed the input with (so a consumer can pre-populate staged
38
+ * files without an upload). Set as a JS property; the element then manages its
39
+ * own attachment state from there (add via the paperclip, remove per chip). */
40
+ attachments?: AttachmentData[];
12
41
  }
13
42
 
14
- defineKitnElement<Props>('kitn-prompt-input', {
43
+ /** Events fired by `<kitn-prompt-input>`. */
44
+ interface Events {
45
+ /** The user submitted the prompt (Enter or send button) with its attachments. */
46
+ submit: { value: string; attachments: AttachmentData[] };
47
+ /** The input text changed (fires on every keystroke). */
48
+ valuechange: { value: string };
49
+ /** A suggestion was clicked while `suggestion-mode="fill"`. */
50
+ suggestionclick: { value: string };
51
+ /** A slash command was chosen from the palette. */
52
+ slashselect: { command: SlashCommandItem };
53
+ /** The Search (Globe) toolbar button was clicked. */
54
+ search: undefined;
55
+ /** The Voice (Mic) toolbar button was clicked. */
56
+ voice: undefined;
57
+ }
58
+
59
+ defineKitnElement<Props, Events>('kitn-prompt-input', {
15
60
  value: undefined,
16
61
  placeholder: 'Send a message...',
17
62
  disabled: false,
18
63
  loading: false,
19
64
  suggestions: undefined,
20
- }, (props, { dispatch }) => {
65
+ suggestionMode: 'submit',
66
+ slashCommands: undefined,
67
+ slashActiveIds: undefined,
68
+ slashCompact: false,
69
+ search: false,
70
+ voice: false,
71
+ attachments: undefined,
72
+ }, (props, { dispatch, flag }) => {
21
73
  const [internal, setInternal] = createSignal(props.value ?? '');
22
- const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
74
+ // Seed staged attachments from the `attachments` property; the element manages
75
+ // its own state from there (paperclip adds, per-chip remove deletes).
76
+ const [attachments, setAttachments] = createSignal<AttachmentData[]>(props.attachments ?? []);
77
+ // Re-seed when the `attachments` property is (re)assigned by the consumer
78
+ // (e.g. set via a `ref` after mount). Subsequent in-element edits stay local.
79
+ createEffect(() => {
80
+ if (props.attachments) setAttachments(props.attachments);
81
+ });
23
82
  const current = () => props.value ?? internal();
24
83
 
25
84
  const handleChange = (v: string) => { setInternal(v); dispatch('valuechange', { value: v }); };
@@ -27,20 +86,37 @@ defineKitnElement<Props>('kitn-prompt-input', {
27
86
  dispatch('submit', { value: current(), attachments: attachments() });
28
87
  setAttachments([]);
29
88
  };
30
- const handleSuggestionClick = (v: string) => { handleChange(v); dispatch('suggestionclick', { value: v }); };
89
+ const handleSuggestionClick = (v: string) => {
90
+ if ((props.suggestionMode ?? 'submit') === 'fill') {
91
+ handleChange(v);
92
+ dispatch('suggestionclick', { value: v });
93
+ } else {
94
+ // Default: behave as if the user typed the suggestion and pressed submit.
95
+ dispatch('submit', { value: v, attachments: attachments() });
96
+ setAttachments([]);
97
+ }
98
+ };
31
99
 
32
100
  return (
33
101
  <DefaultPromptInput
34
102
  value={current()}
35
103
  placeholder={props.placeholder}
36
- disabled={props.disabled}
37
- loading={props.loading}
104
+ disabled={flag('disabled')}
105
+ loading={flag('loading')}
38
106
  suggestions={props.suggestions}
39
107
  attachments={attachments()}
108
+ slashCommands={props.slashCommands}
109
+ slashActiveIds={props.slashActiveIds}
110
+ slashCompact={flag('slashCompact')}
111
+ search={flag('search')}
112
+ voice={flag('voice')}
40
113
  onValueChange={handleChange}
41
114
  onSubmit={handleSubmit}
42
115
  onSuggestionClick={handleSuggestionClick}
43
116
  onAttachmentsChange={setAttachments}
117
+ onSearch={() => dispatch('search')}
118
+ onVoice={() => dispatch('voice')}
119
+ onSlashSelect={(command) => dispatch('slashselect', { command })}
44
120
  />
45
121
  );
46
122
  });
@@ -0,0 +1,58 @@
1
+ import { For } from 'solid-js';
2
+ import { defineKitnElement } from './define';
3
+ import { PromptSuggestion } from '../components/prompt-suggestion';
4
+
5
+ type Item = string | { label: string; value?: string };
6
+
7
+ interface Props extends Record<string, unknown> {
8
+ /** The suggestions. Strings, or `{ label, value }` when the displayed text
9
+ * and the emitted value differ. Set as a JS property. */
10
+ suggestions: Item[];
11
+ /** Chip style: `'outline'` (default), `'ghost'`, or `'default'` (filled). */
12
+ variant?: 'outline' | 'ghost' | 'default';
13
+ /** Size preset for each chip. Defaults to the pill default (`'lg'`); pass
14
+ * `'sm'` for smaller pills (or `'md'`). */
15
+ size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm';
16
+ /** Full-width left-aligned rows instead of pills. */
17
+ block?: boolean;
18
+ /** Substring to highlight within each suggestion. */
19
+ highlight?: string;
20
+ }
21
+
22
+ /** Events fired by `<kitn-prompt-suggestions>`. */
23
+ interface Events {
24
+ /** A suggestion was clicked. */
25
+ select: { value: string };
26
+ }
27
+
28
+ const labelOf = (s: Item) => (typeof s === 'string' ? s : s.label);
29
+ const valueOf = (s: Item) => (typeof s === 'string' ? s : s.value ?? s.label);
30
+
31
+ /**
32
+ * `<kitn-prompt-suggestions>` — a row/list of suggestion chips. Data via the
33
+ * `suggestions` property; `variant`/`block`/`highlight` attributes; emits
34
+ * `select`.
35
+ */
36
+ defineKitnElement<Props, Events>('kitn-prompt-suggestions', {
37
+ suggestions: [],
38
+ variant: 'outline',
39
+ size: undefined,
40
+ block: false,
41
+ highlight: undefined,
42
+ }, (props, { dispatch, flag }) => (
43
+ <div class={flag('block') ? 'flex flex-col gap-2' : 'flex flex-wrap gap-2'}>
44
+ <For each={props.suggestions}>
45
+ {(s) => (
46
+ <PromptSuggestion
47
+ variant={props.variant}
48
+ size={props.size}
49
+ block={flag('block')}
50
+ highlight={props.highlight}
51
+ onClick={() => dispatch('select', { value: valueOf(s) })}
52
+ >
53
+ {labelOf(s)}
54
+ </PromptSuggestion>
55
+ )}
56
+ </For>
57
+ </div>
58
+ ));
@@ -0,0 +1,50 @@
1
+ import { defineKitnElement } from './define';
2
+ import { Reasoning, ReasoningTrigger, ReasoningContent } from '../components/reasoning';
3
+ import { ChatConfig, useChatConfig } from '../primitives/chat-config';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The reasoning text to display. */
7
+ text: string;
8
+ /** Trigger label. */
9
+ label?: string;
10
+ /** Controlled open state — set as a property (`el.open = true`). Omit for
11
+ * uncontrolled (the trigger toggles it). */
12
+ open?: boolean;
13
+ /** While true, auto-expands (and re-collapses when it flips false). */
14
+ streaming?: boolean;
15
+ /** Render `text` as markdown. */
16
+ markdown?: boolean;
17
+ }
18
+
19
+ /** Events fired by `<kitn-reasoning>`. */
20
+ interface Events {
21
+ /** Open state changed (via the trigger or streaming auto-open). */
22
+ openchange: { open: boolean };
23
+ }
24
+
25
+ /**
26
+ * `<kitn-reasoning>` — a collapsible reasoning/thinking block that auto-expands
27
+ * while `streaming`. Text via the `text` property; `markdown`/`streaming` flags;
28
+ * `open` is a controlled property; emits `openchange`.
29
+ */
30
+ defineKitnElement<Props, Events>('kitn-reasoning', {
31
+ text: '',
32
+ label: 'Reasoning',
33
+ open: undefined,
34
+ streaming: false,
35
+ markdown: true,
36
+ }, (props, { dispatch, flag }) => {
37
+ const outer = useChatConfig();
38
+ return (
39
+ <ChatConfig portalMount={outer.portalMount()}>
40
+ <Reasoning
41
+ open={props.open}
42
+ isStreaming={flag('streaming')}
43
+ onOpenChange={(open) => dispatch('openchange', { open })}
44
+ >
45
+ <ReasoningTrigger>{props.label}</ReasoningTrigger>
46
+ <ReasoningContent markdown={flag('markdown')}>{props.text}</ReasoningContent>
47
+ </Reasoning>
48
+ </ChatConfig>
49
+ );
50
+ });
@@ -3,6 +3,38 @@
3
3
  import './conversation-list';
4
4
  import './prompt-input';
5
5
  import './chat';
6
+ import './chat-workspace';
7
+ // Composable leaf elements (spike — see docs/handoff + examples/composable)
8
+ import './thinking-bar';
9
+ import './model-switcher';
10
+ import './attachments';
11
+ // Phase 1 — message-rendering core
12
+ import './message';
13
+ import './markdown';
14
+ import './code-block';
15
+ import './reasoning';
16
+ import './tool';
17
+ // Phase 2 — header / meta
18
+ import './context-meter';
19
+ import './feedback-bar';
20
+ import './chat-scope-picker';
21
+ // Phase 3 — input ecosystem
22
+ // (NB: SlashCommand is context-bound to PromptInput — it observes the input
23
+ // value via usePromptInput() — so it is NOT a standalone element. It will fold
24
+ // into <kitn-prompt-input> as a `slash-commands` property in a later pass.)
25
+ import './prompt-suggestions';
26
+ import './file-upload';
27
+ import './voice-input';
28
+ // Phase 4 — indicators & leaves
29
+ import './loader';
30
+ import './text-shimmer';
31
+ import './image';
32
+ import './checkpoint';
33
+ import './message-skills';
34
+ import './source';
35
+ import './response-stream';
36
+ import './empty';
37
+ import './chain-of-thought';
6
38
 
7
39
  export type { ChatMessage, ChatMessageAction } from './chat-types';
8
40
  export { configureCodeHighlighting, isCodeHighlightingEnabled } from '../primitives/highlighter';
@@ -0,0 +1,40 @@
1
+ import { defineKitnElement } from './define';
2
+ import { ResponseStream, type Mode } from '../components/response-stream';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** Text to stream. A string, or an `AsyncIterable<string>` (set as a JS
6
+ * property — async iterables can't be HTML attributes). */
7
+ text?: string | AsyncIterable<string>;
8
+ /** Reveal animation. */
9
+ mode?: Mode;
10
+ /** Characters/segments per tick. */
11
+ speed?: number;
12
+ /** Element tag to render as. */
13
+ as?: string;
14
+ }
15
+
16
+ /** Events fired by `<kitn-response-stream>`. */
17
+ interface Events {
18
+ /** Streaming finished. */
19
+ complete: void;
20
+ }
21
+
22
+ /**
23
+ * `<kitn-response-stream>` — reveals text with a typewriter or fade animation.
24
+ * Text via the `text` property; `mode`/`speed` attributes; emits `complete`.
25
+ */
26
+ defineKitnElement<Props, Events>('kitn-response-stream', {
27
+ text: '',
28
+ mode: 'typewriter',
29
+ speed: 20,
30
+ as: undefined,
31
+ }, (props, { dispatch }) => (
32
+ <ResponseStream
33
+ textStream={props.text ?? ''}
34
+ mode={props.mode}
35
+ speed={props.speed}
36
+ as={props.as}
37
+ class="text-body"
38
+ onComplete={() => dispatch('complete')}
39
+ />
40
+ ));
@@ -0,0 +1,67 @@
1
+ import { For, Show } from 'solid-js';
2
+ import { defineKitnElement } from './define';
3
+ import { Source, SourceTrigger, SourceContent, SourceList } from '../components/source';
4
+
5
+ // --- <kitn-source> — a single citation link with hover preview ---
6
+
7
+ interface SourceProps extends Record<string, unknown> {
8
+ /** The URL this citation links to (the domain also seeds the default label/favicon). */
9
+ href?: string;
10
+ /** Trigger label (defaults to the domain). */
11
+ label?: string;
12
+ /** Hover-card headline. Attribute: `headline` (`title` is avoided — it's a
13
+ * global HTML attribute that reflects in a CE constructor and breaks it). */
14
+ headline?: string;
15
+ /** Hover-card body text describing the source. */
16
+ description?: string;
17
+ /** Show the source's favicon next to the trigger label. */
18
+ showFavicon?: boolean;
19
+ }
20
+
21
+ defineKitnElement<SourceProps>('kitn-source', {
22
+ href: '',
23
+ label: undefined,
24
+ headline: '',
25
+ description: '',
26
+ showFavicon: false,
27
+ }, (props, { flag }) => (
28
+ <Show when={props.href}>
29
+ <Source href={props.href!}>
30
+ <SourceTrigger label={props.label} showFavicon={flag('showFavicon')} />
31
+ <SourceContent title={props.headline ?? ''} description={props.description ?? ''} />
32
+ </Source>
33
+ </Show>
34
+ ));
35
+
36
+ // --- <kitn-source-list> — a wrapped list of citation links ---
37
+
38
+ interface SourceItem {
39
+ href: string;
40
+ title?: string;
41
+ description?: string;
42
+ label?: string;
43
+ showFavicon?: boolean;
44
+ }
45
+
46
+ interface SourceListProps extends Record<string, unknown> {
47
+ /** The sources to render. Set as a JS property. */
48
+ sources: SourceItem[];
49
+ /** Show favicons on all items (per-item `showFavicon` overrides). */
50
+ showFavicon?: boolean;
51
+ }
52
+
53
+ defineKitnElement<SourceListProps>('kitn-source-list', {
54
+ sources: [],
55
+ showFavicon: false,
56
+ }, (props, { flag }) => (
57
+ <SourceList>
58
+ <For each={props.sources}>
59
+ {(s) => (
60
+ <Source href={s.href}>
61
+ <SourceTrigger label={s.label} showFavicon={s.showFavicon ?? flag('showFavicon')} />
62
+ <SourceContent title={s.title ?? ''} description={s.description ?? ''} />
63
+ </Source>
64
+ )}
65
+ </For>
66
+ </SourceList>
67
+ ));
@@ -9,4 +9,18 @@
9
9
  @layer base {
10
10
  :host { display: block; }
11
11
  * { border-color: var(--color-border); }
12
+
13
+ /* One token-driven focus ring for EVERY focusable element in the shadow root.
14
+ Blue by default (via --color-ring), consistent in light + dark, and
15
+ overridable through --kitn-color-ring. Uses `outline` (not a box-shadow
16
+ ring) so it never clips on overflow and follows the element's radius;
17
+ `:focus-visible` so it only appears for keyboard navigation, never on a
18
+ mouse click. Components that render their own ring set
19
+ `focus-visible:outline-none` (a utility in a later cascade layer) and win
20
+ over this — so there's no double ring, just a guaranteed blue fallback for
21
+ the many controls that have no explicit focus style. */
22
+ :focus-visible {
23
+ outline: 2px solid var(--color-ring);
24
+ outline-offset: 2px;
25
+ }
12
26
  }
@@ -0,0 +1,28 @@
1
+ import { defineKitnElement } from './define';
2
+ import { TextShimmer } from '../components/text-shimmer';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** The text to shimmer. */
6
+ text?: string;
7
+ /** Element tag to render as (default `span`). */
8
+ as?: string;
9
+ /** Animation duration in seconds. */
10
+ duration?: number;
11
+ /** Gradient spread (5–45). */
12
+ spread?: number;
13
+ }
14
+
15
+ /**
16
+ * `<kitn-text-shimmer>` — animated shimmering text. Text via the `text`
17
+ * attribute; `duration`/`spread` tune the effect.
18
+ */
19
+ defineKitnElement<Props>('kitn-text-shimmer', {
20
+ text: '',
21
+ as: 'span',
22
+ duration: 4,
23
+ spread: 20,
24
+ }, (props) => (
25
+ <TextShimmer as={props.as} duration={props.duration} spread={props.spread} class="text-body">
26
+ {props.text}
27
+ </TextShimmer>
28
+ ));