@kitnai/chat 0.3.1 → 0.4.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 (106) hide show
  1. package/README.md +11 -0
  2. package/dist/custom-elements.json +2494 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +667 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +133 -0
  7. package/frameworks/react/index.tsx +530 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +667 -0
  10. package/llms.txt +104 -0
  11. package/package.json +34 -5
  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/checkpoint.tsx +7 -3
  16. package/src/components/context.tsx +14 -18
  17. package/src/components/conversation-item.tsx +1 -1
  18. package/src/components/conversation-list.tsx +5 -4
  19. package/src/components/message-skills.tsx +1 -1
  20. package/src/components/message.tsx +1 -0
  21. package/src/components/model-switcher.tsx +3 -3
  22. package/src/components/prompt-input.tsx +15 -2
  23. package/src/components/reasoning.tsx +2 -2
  24. package/src/components/scroll-button.tsx +1 -0
  25. package/src/components/slash-command.tsx +17 -8
  26. package/src/components/source.tsx +2 -2
  27. package/src/components/thinking-bar.tsx +2 -2
  28. package/src/components/tool.tsx +17 -6
  29. package/src/components/voice-input.tsx +5 -1
  30. package/src/elements/attachments.tsx +132 -0
  31. package/src/elements/chain-of-thought.tsx +45 -0
  32. package/src/elements/chat-scope-picker.tsx +36 -0
  33. package/src/elements/chat.tsx +51 -7
  34. package/src/elements/checkpoint.tsx +43 -0
  35. package/src/elements/code-block.tsx +42 -0
  36. package/src/elements/compiled.css +1 -1
  37. package/src/elements/context-meter.tsx +71 -0
  38. package/src/elements/conversation-list.tsx +6 -0
  39. package/src/elements/default-input.tsx +22 -1
  40. package/src/elements/define.tsx +97 -11
  41. package/src/elements/element-types.d.ts +404 -0
  42. package/src/elements/empty.tsx +29 -0
  43. package/src/elements/feedback-bar.tsx +33 -0
  44. package/src/elements/file-upload.tsx +44 -0
  45. package/src/elements/image.tsx +32 -0
  46. package/src/elements/kitn-attachments.stories.tsx +181 -0
  47. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  48. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  49. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  50. package/src/elements/kitn-code-block.stories.tsx +82 -0
  51. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  52. package/src/elements/kitn-empty.stories.tsx +110 -0
  53. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  54. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  55. package/src/elements/kitn-image.stories.tsx +70 -0
  56. package/src/elements/kitn-loader.stories.tsx +87 -0
  57. package/src/elements/kitn-markdown.stories.tsx +75 -0
  58. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  59. package/src/elements/kitn-message.stories.tsx +105 -0
  60. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  61. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  62. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  63. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  64. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  65. package/src/elements/kitn-source-list.stories.tsx +77 -0
  66. package/src/elements/kitn-source.stories.tsx +87 -0
  67. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  68. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  69. package/src/elements/kitn-tool.stories.tsx +88 -0
  70. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  71. package/src/elements/loader.tsx +25 -0
  72. package/src/elements/markdown.tsx +38 -0
  73. package/src/elements/message-skills.tsx +22 -0
  74. package/src/elements/message.tsx +125 -0
  75. package/src/elements/model-switcher.tsx +35 -0
  76. package/src/elements/prompt-input.tsx +83 -7
  77. package/src/elements/prompt-suggestions.tsx +58 -0
  78. package/src/elements/reasoning.tsx +50 -0
  79. package/src/elements/register.ts +31 -0
  80. package/src/elements/response-stream.tsx +40 -0
  81. package/src/elements/source.tsx +67 -0
  82. package/src/elements/text-shimmer.tsx +28 -0
  83. package/src/elements/thinking-bar.tsx +34 -0
  84. package/src/elements/tool.tsx +23 -0
  85. package/src/elements/voice-input.tsx +41 -0
  86. package/src/index.ts +0 -1
  87. package/src/primitives/chat-config.tsx +2 -2
  88. package/src/stories/docs/Accessibility.mdx +119 -0
  89. package/src/stories/docs/ForAIAgents.mdx +93 -0
  90. package/src/stories/docs/GettingStarted.mdx +2 -2
  91. package/src/stories/docs/Installation.mdx +2 -2
  92. package/src/stories/docs/Integrations.mdx +415 -15
  93. package/src/stories/docs/Introduction.mdx +5 -5
  94. package/src/stories/docs/Theming.mdx +1 -1
  95. package/src/stories/typography.stories.tsx +78 -0
  96. package/src/ui/button.tsx +1 -1
  97. package/src/ui/collapsible.tsx +119 -8
  98. package/src/ui/dropdown.tsx +177 -12
  99. package/src/ui/hover-card.tsx +147 -26
  100. package/src/ui/overlay.tsx +151 -0
  101. package/src/ui/textarea.tsx +1 -1
  102. package/src/ui/tooltip.stories.tsx +1 -1
  103. package/src/ui/tooltip.tsx +59 -13
  104. package/src/utils/cn.ts +19 -1
  105. package/theme.css +72 -43
  106. package/src/ui/dialog.tsx +0 -21
@@ -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,37 @@
3
3
  import './conversation-list';
4
4
  import './prompt-input';
5
5
  import './chat';
6
+ // Composable leaf elements (spike — see docs/handoff + examples/composable)
7
+ import './thinking-bar';
8
+ import './model-switcher';
9
+ import './attachments';
10
+ // Phase 1 — message-rendering core
11
+ import './message';
12
+ import './markdown';
13
+ import './code-block';
14
+ import './reasoning';
15
+ import './tool';
16
+ // Phase 2 — header / meta
17
+ import './context-meter';
18
+ import './feedback-bar';
19
+ import './chat-scope-picker';
20
+ // Phase 3 — input ecosystem
21
+ // (NB: SlashCommand is context-bound to PromptInput — it observes the input
22
+ // value via usePromptInput() — so it is NOT a standalone element. It will fold
23
+ // into <kitn-prompt-input> as a `slash-commands` property in a later pass.)
24
+ import './prompt-suggestions';
25
+ import './file-upload';
26
+ import './voice-input';
27
+ // Phase 4 — indicators & leaves
28
+ import './loader';
29
+ import './text-shimmer';
30
+ import './image';
31
+ import './checkpoint';
32
+ import './message-skills';
33
+ import './source';
34
+ import './response-stream';
35
+ import './empty';
36
+ import './chain-of-thought';
6
37
 
7
38
  export type { ChatMessage, ChatMessageAction } from './chat-types';
8
39
  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
+ ));
@@ -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
+ ));
@@ -0,0 +1,34 @@
1
+ import { defineKitnElement } from './define';
2
+ import { ThinkingBar } from '../components/thinking-bar';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** The shimmering label, e.g. "Thinking…". */
6
+ text?: string;
7
+ /** When true, show a "stop" affordance that fires a `stop` event. */
8
+ stoppable?: boolean;
9
+ /** Label for the stop affordance. */
10
+ stopLabel?: string;
11
+ }
12
+
13
+ /** Events fired by `<kitn-thinking-bar>`. */
14
+ interface Events {
15
+ /** The "stop / answer now" affordance was clicked. */
16
+ stop: void;
17
+ }
18
+
19
+ /**
20
+ * `<kitn-thinking-bar>` — a pure leaf element: an animated "thinking" indicator
21
+ * (one of the primitives the batteries-included `<kitn-chat>` does NOT surface).
22
+ * Config via attributes, the only interaction (`stop`) comes back as an event.
23
+ */
24
+ defineKitnElement<Props, Events>('kitn-thinking-bar', {
25
+ text: 'Thinking',
26
+ stoppable: false,
27
+ stopLabel: 'Answer now',
28
+ }, (props, { dispatch, flag }) => (
29
+ <ThinkingBar
30
+ text={props.text}
31
+ stopLabel={props.stopLabel}
32
+ onStop={flag('stoppable') ? () => dispatch('stop') : undefined}
33
+ />
34
+ ));
@@ -0,0 +1,23 @@
1
+ import { Show } from 'solid-js';
2
+ import { defineKitnElement } from './define';
3
+ import { Tool, type ToolPart } from '../components/tool';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The tool-call to display. Set as a JS property. */
7
+ tool?: ToolPart;
8
+ /** Start expanded. */
9
+ open?: boolean;
10
+ }
11
+
12
+ /**
13
+ * `<kitn-tool>` — a collapsible tool-call panel (input/output inspection with a
14
+ * state badge). Data via the `tool` property; `open` flag starts it expanded.
15
+ */
16
+ defineKitnElement<Props>('kitn-tool', {
17
+ tool: undefined,
18
+ open: false,
19
+ }, (props, { flag }) => (
20
+ <Show when={props.tool}>
21
+ <Tool toolPart={props.tool!} defaultOpen={flag('open')} />
22
+ </Show>
23
+ ));
@@ -0,0 +1,41 @@
1
+ import { defineKitnElement } from './define';
2
+ import { VoiceInput } from '../components/voice-input';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /**
6
+ * Transcriber the host supplies — records audio, returns the text. This is a
7
+ * **function-valued property** (`el.transcribe = async blob => '...'`) because
8
+ * a value-returning callback can't be modelled as a fire-and-forget event.
9
+ */
10
+ transcribe?: (audio: Blob) => Promise<string>;
11
+ /** Disable the mic button (non-interactive). */
12
+ disabled?: boolean;
13
+ }
14
+
15
+ /** Events fired by `<kitn-voice-input>`. */
16
+ interface Events {
17
+ /** Raw audio captured (before transcription) — for hosts that prefer to
18
+ * handle transcription themselves instead of via the `transcribe` property. */
19
+ audiocaptured: { blob: Blob };
20
+ /** Transcription completed (the `transcribe` property resolved). */
21
+ transcription: { text: string };
22
+ }
23
+
24
+ /**
25
+ * `<kitn-voice-input>` — a mic button that records and transcribes. The
26
+ * canonical **function-property** element: set `el.transcribe` to your async
27
+ * transcriber. Also emits `audiocaptured` (raw blob) and `transcription` (text).
28
+ */
29
+ defineKitnElement<Props, Events>('kitn-voice-input', {
30
+ transcribe: undefined,
31
+ disabled: false,
32
+ }, (props, { dispatch, flag }) => (
33
+ <VoiceInput
34
+ disabled={flag('disabled')}
35
+ onTranscribe={async (blob) => {
36
+ dispatch('audiocaptured', { blob });
37
+ return props.transcribe ? props.transcribe(blob) : '';
38
+ }}
39
+ onTranscription={(text) => dispatch('transcription', { text })}
40
+ />
41
+ ));
package/src/index.ts CHANGED
@@ -34,7 +34,6 @@ export { Separator } from './ui/separator';
34
34
  export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './ui/resizable';
35
35
  export type { ResizablePanelGroupProps, ResizablePanelProps, ResizableHandleProps } from './ui/resizable';
36
36
  export { Skeleton } from './ui/skeleton';
37
- export { Dialog, DialogTrigger, DialogContent } from './ui/dialog';
38
37
 
39
38
  // Layer 3: AI/Feature Components
40
39
  export {
@@ -14,7 +14,7 @@ export interface ChatConfigValue {
14
14
  }
15
15
 
16
16
  const defaultConfig: ChatConfigValue = {
17
- proseSize: () => 'base' as ProseSize,
17
+ proseSize: () => 'sm' as ProseSize,
18
18
  codeTheme: () => 'github-dark-dimmed',
19
19
  portalMount: () => undefined,
20
20
  codeHighlight: () => true,
@@ -37,7 +37,7 @@ export interface ChatConfigProps {
37
37
  */
38
38
  export function ChatConfig(props: ChatConfigProps) {
39
39
  const value: ChatConfigValue = {
40
- proseSize: () => props.proseSize ?? 'base',
40
+ proseSize: () => props.proseSize ?? 'sm',
41
41
  codeTheme: () => props.codeTheme ?? 'github-dark-dimmed',
42
42
  portalMount: () => props.portalMount,
43
43
  codeHighlight: () => props.codeHighlight ?? true,
@@ -0,0 +1,119 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/Accessibility" />
4
+
5
+ # Accessibility
6
+
7
+ `@kitnai/chat` targets **WCAG 2.1 Level AA**. The kit is audited with [axe-core](https://github.com/dequelabs/axe-core) (via `scripts/audit-a11y.mjs`) and ships with **0 axe violations** in both light and dark modes. Keyboard navigation is verified with a Playwright browser test suite.
8
+
9
+ This page describes the accessibility posture, the keyboard model, known limitations, and what you need to do as an integrator.
10
+
11
+ ---
12
+
13
+ ## Color contrast
14
+
15
+ All text and UI chrome meet WCAG 2.1 AA contrast requirements (4.5:1 for body text, 3:1 for large text and UI components) in the default light and dark palettes.
16
+
17
+ The `theme` attribute on every element (`light | dark | auto`) controls which token set loads. `auto` (the default) follows the OS `prefers-color-scheme` media query, so users who configure dark mode at the OS level get it automatically. Override either set with `--kitn-color-*` tokens — if you do, verify contrast yourself; the kit cannot audit tokens it does not control.
18
+
19
+ ---
20
+
21
+ ## Keyboard operability
22
+
23
+ Every interactive control is reachable and operable without a mouse.
24
+
25
+ ### Focus order
26
+
27
+ Tab moves through all focusable controls in DOM order. The prompt textarea, send button, attachment button, toolbar controls (search, mic), header controls (model switcher, context meter), message action buttons, suggestion chips, and conversation list items are all included. Focus rings are always visible — they are not hidden on `:focus`, only on `:focus-visible`.
28
+
29
+ ### Dropdowns and select menus (ModelSwitcher, ChatScopePicker, …)
30
+
31
+ | Key | Action |
32
+ |---|---|
33
+ | Enter / Space / ↓ | Open the menu |
34
+ | ↑ / ↓ | Move between items |
35
+ | Home / End | Jump to first / last item |
36
+ | Typeahead (letter keys) | Jump to the first item starting with that letter |
37
+ | Enter / Space | Select the focused item |
38
+ | Esc | Close without selecting; focus returns to the trigger |
39
+ | Tab | Close and move focus past the trigger |
40
+
41
+ Focus always returns to the trigger element after the menu closes, whether closed by Esc, selection, or a click outside.
42
+
43
+ ### Collapsible panels (Reasoning, Tool calls, …)
44
+
45
+ | Key | Action |
46
+ |---|---|
47
+ | Enter / Space | Toggle expanded / collapsed |
48
+ | Tab | Move into the expanded content |
49
+ | Esc | Collapse (if focused inside) |
50
+
51
+ ### Slash command palette
52
+
53
+ Typing `/` in the prompt input opens the command palette.
54
+
55
+ | Key | Action |
56
+ |---|---|
57
+ | ↑ / ↓ | Navigate items |
58
+ | Home / End | Jump to first / last item |
59
+ | Enter / Tab | Select the focused command (inserts `/label ` into the input with caret at end) |
60
+ | Esc | Close the palette; focus stays in the input |
61
+
62
+ ### Conversation list
63
+
64
+ | Key | Action |
65
+ |---|---|
66
+ | ↑ / ↓ | Move between conversations |
67
+ | Enter | Select the focused conversation |
68
+ | Tab | Move through action buttons (if any) |
69
+
70
+ ---
71
+
72
+ ## Accessible names on icon buttons
73
+
74
+ Every icon-only button (attach, send, mic, search, copy, like, dislike, regenerate, close) carries an accessible name via `aria-label`. Screen readers announce the button's purpose rather than "button" or nothing. Labels use plain language: "Send message", "Attach file", "Copy message", "Thumbs up", and so on.
75
+
76
+ ---
77
+
78
+ ## Tooltips and hover cards
79
+
80
+ Tooltips and hover cards (on sources, context meter, etc.) are dismissable and persistent per WCAG 2.1 SC 1.4.13 (Content on Hover or Focus):
81
+
82
+ - They appear on both hover and keyboard focus.
83
+ - Moving the pointer over the tooltip/card content keeps it visible (it does not vanish when the pointer leaves the trigger).
84
+ - They can be dismissed with **Esc** without moving focus.
85
+
86
+ ---
87
+
88
+ ## ARIA landmarks and live regions
89
+
90
+ - The message list is marked as a log region (`role="log"`, `aria-live="polite"`) so streaming tokens are announced to screen readers without being intrusive.
91
+ - The prompt area is labelled; the textarea carries its placeholder as an accessible name fallback.
92
+ - The conversation sidebar is a navigation landmark (`<nav>`).
93
+
94
+ ---
95
+
96
+ ## Shadow DOM and screen readers
97
+
98
+ All elements render inside Shadow DOM. Modern screen readers (NVDA + Chrome, JAWS + Chrome, VoiceOver + Safari/Chrome) pierce Shadow DOM correctly. Accessible names, roles, and live regions work as expected.
99
+
100
+ ---
101
+
102
+ ## Audit tooling
103
+
104
+ Accessibility is verified with:
105
+
106
+ - **axe-core** (`scripts/audit-a11y.mjs`) — automated rule checks in both light and dark themes. Zero violations at the time of build.
107
+ - **Playwright keyboard tests** — 21-scenario browser test suite covering focus order, dropdown/collapsible/palette keyboard nav, ArrowUp/Down/Home/End/typeahead/Enter/Escape/Tab, and focus-return after close.
108
+
109
+ The kit does not carry a "WCAG AA certified" certification — no automated tool can certify that. What it does carry is a targeted AA design, an axe-core clean bill, and a passing Playwright keyboard suite.
110
+
111
+ ---
112
+
113
+ ## What integrators need to do
114
+
115
+ 1. **Do not hide focus rings.** The kit renders visible focus rings via `:focus-visible`. Do not add `outline: none` or `outline: 0` in your host-page CSS targeting `*` or the element host.
116
+ 2. **Verify contrast if you override tokens.** Use a contrast checker after overriding any `--kitn-color-*` token.
117
+ 3. **Provide meaningful conversation titles.** The conversation list announces the `title` field to screen readers. "Chat 1" is less useful than "React integration help".
118
+ 4. **Set a meaningful `placeholder`.** The textarea placeholder doubles as an accessible name when no label is associated.
119
+ 5. **Check your theme choice.** Using `theme="light"` in a page the user has set to dark mode (or vice versa) can create contrast mismatches between kit components and host-page content. `theme="auto"` (the default) avoids this.