@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,93 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import {
4
+ ChatConfig, ChatContainer, ChatContainerContent, ChatContainerScrollAnchor,
5
+ Message, MessageAvatar, MessageContent,
6
+ PromptInput, PromptInputTextarea, PromptInputActions, Button,
7
+ } from '../index';
8
+ import { ArrowUp, X } from 'lucide-solid';
9
+
10
+ const meta: Meta = {
11
+ title: 'Patterns/Docked Widget',
12
+ parameters: {
13
+ layout: 'centered',
14
+ docs: {
15
+ description: {
16
+ component:
17
+ 'A support-assistant widget docked to the bottom-right corner of a host page. The same building blocks (`ChatContainer` + `Message` + `PromptInput`) shrink into a compact floating card — the kit\'s Shadow-DOM isolation means it can sit over any app without CSS bleed.',
18
+ },
19
+ },
20
+ },
21
+ };
22
+ export default meta;
23
+ type Story = StoryObj;
24
+
25
+ export const SupportAssistant: Story = {
26
+ render: () => {
27
+ const [input, setInput] = createSignal('');
28
+ return (
29
+ <ChatConfig proseSize="sm">
30
+ {/* Faux host page */}
31
+ <div style={{ width: '920px', height: '680px' }} class="relative overflow-hidden rounded-xl border border-border bg-gradient-to-br from-muted/40 to-background">
32
+ <div class="space-y-3 p-8">
33
+ <div class="h-7 w-48 rounded-md bg-muted" />
34
+ <div class="h-4 w-2/3 rounded bg-muted/70" />
35
+ <div class="h-4 w-1/2 rounded bg-muted/70" />
36
+ <div class="mt-6 grid grid-cols-3 gap-4">
37
+ <div class="h-28 rounded-lg bg-muted/60" />
38
+ <div class="h-28 rounded-lg bg-muted/60" />
39
+ <div class="h-28 rounded-lg bg-muted/60" />
40
+ </div>
41
+ </div>
42
+
43
+ {/* Docked chat widget */}
44
+ <div style={{ width: '360px', height: '460px' }} class="absolute bottom-5 right-5 flex flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-xl">
45
+ <header class="flex shrink-0 items-center justify-between border-b border-border px-4 py-3">
46
+ <div class="flex items-center gap-2">
47
+ <span class="size-2 rounded-full bg-tool-green" />
48
+ <span class="text-sm font-semibold text-foreground">Support</span>
49
+ </div>
50
+ <Button variant="ghost" size="icon-sm" aria-label="Close"><X class="size-4" /></Button>
51
+ </header>
52
+
53
+ <div class="relative flex-1 overflow-y-auto">
54
+ <ChatContainer class="h-full">
55
+ <ChatContainerContent class="space-y-3 px-3 py-3">
56
+ <Message>
57
+ <MessageAvatar src="" alt="AI" fallback="AI" />
58
+ <MessageContent markdown class="bg-transparent p-0 pt-1">
59
+ Hi! 👋 How can I help you today?
60
+ </MessageContent>
61
+ </Message>
62
+ <Message class="flex-col items-end">
63
+ <MessageContent class="bg-muted text-primary max-w-[85%] rounded-xl px-3.5 py-2">
64
+ How do I reset my password?
65
+ </MessageContent>
66
+ </Message>
67
+ <Message>
68
+ <MessageAvatar src="" alt="AI" fallback="AI" />
69
+ <MessageContent markdown class="bg-transparent p-0 pt-1">
70
+ Head to **Settings → Security** and click **Reset password** — you'll get an email link.
71
+ </MessageContent>
72
+ </Message>
73
+ <ChatContainerScrollAnchor />
74
+ </ChatContainerContent>
75
+ </ChatContainer>
76
+ </div>
77
+
78
+ <div class="shrink-0 px-3 pb-3">
79
+ <PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
80
+ <PromptInputTextarea placeholder="Message support…" class="min-h-[40px] pt-2.5 pl-3.5" />
81
+ <PromptInputActions class="mt-1.5 flex w-full items-center justify-end gap-2 px-2.5 pb-2.5">
82
+ <Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
83
+ <ArrowUp class="size-4" />
84
+ </Button>
85
+ </PromptInputActions>
86
+ </PromptInput>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </ChatConfig>
91
+ );
92
+ },
93
+ };
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import {
4
+ ChatConfig, Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
5
+ PromptSuggestion, PromptInput, PromptInputTextarea, PromptInputActions, Button,
6
+ } from '../index';
7
+ import { Sparkles, Plus, Globe, ArrowUp } from 'lucide-solid';
8
+
9
+ const meta: Meta = {
10
+ title: 'Patterns/Empty State',
11
+ parameters: {
12
+ layout: 'centered',
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'The new-chat zero state: a centered greeting with starter suggestions above the composer. Composed from `Empty` (+ `EmptyMedia`/`EmptyTitle`/`EmptyDescription`), `PromptSuggestion` chips, and `PromptInput`. Clicking a suggestion fills the composer.',
17
+ },
18
+ },
19
+ },
20
+ };
21
+ export default meta;
22
+ type Story = StoryObj;
23
+
24
+ const SUGGESTIONS = [
25
+ 'Summarize a document',
26
+ 'Draft a product update',
27
+ 'Explain a code snippet',
28
+ 'Plan my week',
29
+ ];
30
+
31
+ export const NewChat: Story = {
32
+ render: () => {
33
+ const [input, setInput] = createSignal('');
34
+ return (
35
+ <ChatConfig proseSize="base">
36
+ <div style={{ width: '760px', height: '640px' }} class="flex flex-col overflow-hidden rounded-xl border border-border bg-background">
37
+ <Empty class="flex-1">
38
+ <EmptyHeader>
39
+ <EmptyMedia variant="icon">
40
+ <Sparkles class="size-6" />
41
+ </EmptyMedia>
42
+ <EmptyTitle>How can I help today?</EmptyTitle>
43
+ <EmptyDescription>
44
+ Ask anything, or start from one of these.
45
+ </EmptyDescription>
46
+ </EmptyHeader>
47
+ <EmptyContent>
48
+ <div class="flex max-w-md flex-wrap justify-center gap-2">
49
+ {SUGGESTIONS.map((s) => (
50
+ <PromptSuggestion onClick={() => setInput(s)}>{s}</PromptSuggestion>
51
+ ))}
52
+ </div>
53
+ </EmptyContent>
54
+ </Empty>
55
+
56
+ <div class="shrink-0 px-4 pb-4">
57
+ <div class="mx-auto max-w-2xl">
58
+ <PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
59
+ <PromptInputTextarea placeholder="Ask anything…" class="min-h-[44px] pt-3 pl-4" />
60
+ <PromptInputActions class="mt-2 flex w-full items-center justify-between gap-2 px-3 pb-3">
61
+ <div class="flex items-center gap-2">
62
+ <Button variant="outline" size="icon-sm" class="rounded-full"><Plus class="size-4" /></Button>
63
+ <Button variant="outline" size="sm" class="rounded-full gap-1"><Globe class="size-4" />Search</Button>
64
+ </div>
65
+ <Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
66
+ <ArrowUp class="size-4" />
67
+ </Button>
68
+ </PromptInputActions>
69
+ </PromptInput>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </ChatConfig>
74
+ );
75
+ },
76
+ };
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { For, createSignal, onMount } from 'solid-js';
3
+
4
+ const meta = {
5
+ title: 'Theming/Typography',
6
+ parameters: { layout: 'padded' },
7
+ } satisfies Meta;
8
+
9
+ export default meta;
10
+ type Story = StoryObj;
11
+
12
+ // The semantic type scale (defined in theme.css `@theme`). Each row is a token
13
+ // that generates a Tailwind utility AND is an overridable CSS custom property.
14
+ const SCALE = [
15
+ { token: '--text-caption', cls: 'text-caption', role: 'Micro labels, badges, sub-counts', used: 'token sub-totals · model provider · media-type subtitle' },
16
+ { token: '--text-meta', cls: 'text-meta', role: 'Controls, toggles, switchers, captions', used: 'reasoning / chain-of-thought triggers · model switcher · context · source' },
17
+ { token: '--text-body', cls: 'text-body', role: 'Primary reading text', used: 'messages · input · suggestions · markdown (also scales with the `proseSize` prop)' },
18
+ { token: '--text-title', cls: 'text-title', role: 'Emphasis / headers', used: 'section emphasis' },
19
+ ] as const;
20
+
21
+ function TypographyScale() {
22
+ const [px, setPx] = createSignal<Record<string, string>>({});
23
+ onMount(() => {
24
+ const cs = getComputedStyle(document.documentElement);
25
+ const toPx = (rem: string) => {
26
+ const v = parseFloat(rem);
27
+ return rem.includes('rem') ? `${Math.round(v * 16)}px` : rem.trim();
28
+ };
29
+ setPx(Object.fromEntries(SCALE.map((s) => [s.token, toPx(cs.getPropertyValue(s.token))])));
30
+ });
31
+
32
+ return (
33
+ <div class="text-foreground max-w-3xl">
34
+ <h2 class="mb-1 text-lg font-semibold">Typography scale</h2>
35
+ <p class="text-muted-foreground mb-6 text-sm">
36
+ Defined once in <code class="text-code-foreground">theme.css</code>. Each token generates a Tailwind utility
37
+ (<code class="text-code-foreground">text-meta</code>, …). To restyle the kit's typography globally, override the
38
+ namespaced <code class="text-code-foreground">--kitn-text-*</code> token on <code class="text-code-foreground">:root</code> —
39
+ it pierces the Shadow&nbsp;DOM exactly like the <code class="text-code-foreground">--kitn-color-*</code> tokens. (The bare
40
+ <code class="text-code-foreground"> --text-*</code> names stay internal, so a host's own tokens can't collide.)
41
+ </p>
42
+
43
+ <div class="border-border divide-border divide-y rounded-xl border">
44
+ <For each={SCALE}>
45
+ {(s) => (
46
+ <div class="grid grid-cols-[170px_1fr] items-center gap-4 p-4">
47
+ <div class="min-w-0">
48
+ <div class="text-foreground font-mono text-xs">{s.token}</div>
49
+ <div class="text-muted-foreground mt-1 font-mono text-[11px]">{px()[s.token] ?? '…'}</div>
50
+ <div class="text-muted-foreground mt-2 text-xs">{s.role}</div>
51
+ </div>
52
+ <div class="min-w-0">
53
+ <div class={`${s.cls} text-foreground`}>The quick brown fox jumps over the lazy dog</div>
54
+ <div class="text-muted-foreground mt-1.5 text-[11px]">{s.used}</div>
55
+ </div>
56
+ </div>
57
+ )}
58
+ </For>
59
+ </div>
60
+
61
+ <h3 class="mt-8 mb-2 text-sm font-semibold">Override example</h3>
62
+ <pre class="bg-muted text-foreground overflow-auto rounded-lg p-3 font-mono text-xs">{`:root {
63
+ --kitn-text-body: 0.9375rem; /* bump the reading size to 15px */
64
+ --kitn-text-meta: 0.8125rem; /* and the control size to 13px */
65
+ }`}</pre>
66
+ <p class="text-muted-foreground mt-2 text-xs">
67
+ Reading text in messages / input / markdown additionally scales with the
68
+ <code class="text-code-foreground"> proseSize</code> prop (<code class="text-code-foreground">xs · sm · base · lg</code>);
69
+ these tokens cover the fixed chrome &amp; controls.
70
+ </p>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ /** The kit's semantic type scale — defined in theme.css, used everywhere, overridable. */
76
+ export const Typography: Story = {
77
+ render: () => <TypographyScale />,
78
+ };
package/src/ui/button.tsx CHANGED
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
3
3
  import { cn } from '../utils/cn';
4
4
 
5
5
  const buttonVariants = cva(
6
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
6
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50',
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -0,0 +1,70 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsible';
3
+
4
+ const meta = {
5
+ title: 'UI/Collapsible',
6
+ component: Collapsible,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ description: {
12
+ component: [
13
+ 'A two-state disclosure that expands and collapses a region of content, animating height via a CSS `grid-template-rows` `0fr` → `1fr` transition (no JS measurement, no layout thrash). The trigger carries `aria-expanded`/`aria-controls` and the collapsed content is `inert`, so it is removed from tab order and the accessibility tree. Works controlled (`open` + `onOpenChange`) or uncontrolled (`defaultOpen`).',
14
+ '**When to use:** to hide secondary detail behind a toggle — a reasoning/"chain of thought" panel, a tool-call payload, an expandable conversation group, an FAQ row.',
15
+ '**How to use:** wrap `CollapsibleTrigger` and `CollapsibleContent` in a `Collapsible`. The trigger renders a `<button>` by default; pass `as` to render a custom element.',
16
+ ].join('\n\n'),
17
+ },
18
+ },
19
+ },
20
+ render: () => <CollapsibleDemo defaultOpen />,
21
+ } satisfies Meta<typeof Collapsible>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ const IMPORT = `import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@kitnai/chat';`;
27
+ const src = (code: string) => ({
28
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
29
+ });
30
+
31
+ function CollapsibleDemo(props: { defaultOpen?: boolean }) {
32
+ return (
33
+ <Collapsible defaultOpen={props.defaultOpen} class="w-80 rounded-lg border border-border">
34
+ <CollapsibleTrigger class="group flex w-full items-center justify-between px-3 py-2 text-sm font-medium text-foreground">
35
+ <span>Reasoning</span>
36
+ <svg
37
+ class="size-4 text-muted-foreground transition-transform duration-200 group-data-[expanded]:rotate-180"
38
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
39
+ >
40
+ <polyline points="6 9 12 15 18 9" />
41
+ </svg>
42
+ </CollapsibleTrigger>
43
+ <CollapsibleContent>
44
+ <p class="px-3 pb-3 text-sm text-muted-foreground">
45
+ First I parsed the request, then checked the available tools, and finally
46
+ composed the answer. This panel is removed from the tab order while collapsed.
47
+ </p>
48
+ </CollapsibleContent>
49
+ </Collapsible>
50
+ );
51
+ }
52
+
53
+ /** Toggle the trigger to expand/collapse. The chevron rotates via the `data-expanded` attribute. */
54
+ export const Playground: Story = {
55
+ ...src(`<Collapsible defaultOpen>
56
+ <CollapsibleTrigger>Reasoning</CollapsibleTrigger>
57
+ <CollapsibleContent>
58
+ <p>First I parsed the request, then checked the available tools…</p>
59
+ </CollapsibleContent>
60
+ </Collapsible>`),
61
+ };
62
+
63
+ /** Starts collapsed. */
64
+ export const InitiallyClosed: Story = {
65
+ render: () => <CollapsibleDemo />,
66
+ ...src(`<Collapsible>
67
+ <CollapsibleTrigger>Reasoning</CollapsibleTrigger>
68
+ <CollapsibleContent>…</CollapsibleContent>
69
+ </Collapsible>`),
70
+ };
@@ -1,14 +1,125 @@
1
- import { Collapsible as KCollapsible } from '@kobalte/core/collapsible';
2
- import { type JSX } from 'solid-js';
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ createSignal,
5
+ createUniqueId,
6
+ splitProps,
7
+ type JSX,
8
+ type Accessor,
9
+ } from 'solid-js';
3
10
  import { cn } from '../utils/cn';
4
11
 
5
- export const Collapsible = KCollapsible;
6
- export const CollapsibleTrigger = KCollapsible.Trigger;
12
+ // Extend SolidJS JSX to allow `bool:inert` (forces setAttribute so jsdom reflects it as an attribute).
13
+ declare module 'solid-js' {
14
+ namespace JSX {
15
+ interface ExplicitBoolAttributes {
16
+ inert: boolean;
17
+ }
18
+ }
19
+ }
20
+
21
+ interface CollapsibleCtx {
22
+ open: Accessor<boolean>;
23
+ toggle: () => void;
24
+ contentId: string;
25
+ }
26
+
27
+ const Ctx = createContext<CollapsibleCtx>();
28
+
29
+ const useCollapsible = () => {
30
+ const c = useContext(Ctx);
31
+ if (!c) throw new Error('Collapsible parts must be used within <Collapsible>');
32
+ return c;
33
+ };
34
+
35
+ export function Collapsible(props: {
36
+ open?: boolean;
37
+ defaultOpen?: boolean;
38
+ onOpenChange?: (open: boolean) => void;
39
+ children: JSX.Element;
40
+ class?: string;
41
+ }) {
42
+ const [local, rest] = splitProps(props, ['open', 'defaultOpen', 'onOpenChange', 'children', 'class']);
43
+ const [uncontrolled, setUncontrolled] = createSignal(local.defaultOpen ?? false);
44
+ const isControlled = () => local.open !== undefined;
45
+ const open = () => (isControlled() ? !!local.open : uncontrolled());
46
+ const toggle = () => {
47
+ const next = !open();
48
+ if (!isControlled()) setUncontrolled(next);
49
+ local.onOpenChange?.(next);
50
+ };
51
+ const contentId = createUniqueId();
52
+ return (
53
+ <Ctx.Provider value={{ open, toggle, contentId }}>
54
+ <div
55
+ class={local.class}
56
+ {...rest}
57
+ data-expanded={open() ? '' : undefined}
58
+ data-closed={open() ? undefined : ''}
59
+ data-state={open() ? 'open' : 'closed'}
60
+ >
61
+ {local.children}
62
+ </div>
63
+ </Ctx.Provider>
64
+ );
65
+ }
66
+
67
+ export function CollapsibleTrigger(props: {
68
+ children?: JSX.Element;
69
+ class?: string;
70
+ as?: (props: Record<string, any>) => JSX.Element;
71
+ onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
72
+ [k: string]: any;
73
+ }) {
74
+ const ctx = useCollapsible();
75
+ const [local, rest] = splitProps(props, ['children', 'class', 'as', 'onClick']);
76
+
77
+ const triggerProps = () => ({
78
+ type: 'button' as const,
79
+ 'aria-expanded': ctx.open(),
80
+ 'aria-controls': ctx.contentId,
81
+ 'data-expanded': ctx.open() ? '' : undefined,
82
+ 'data-closed': ctx.open() ? undefined : '',
83
+ 'data-state': ctx.open() ? 'open' : 'closed',
84
+ onClick: (e: MouseEvent) => {
85
+ if (typeof local.onClick === 'function') {
86
+ (local.onClick as (e: MouseEvent) => void)(e);
87
+ }
88
+ ctx.toggle();
89
+ },
90
+ class: local.class,
91
+ ...rest,
92
+ });
93
+
94
+ return (
95
+ <>
96
+ {local.as
97
+ ? local.as(triggerProps() as any)
98
+ : (
99
+ <button {...triggerProps()}>
100
+ {local.children}
101
+ </button>
102
+ )}
103
+ </>
104
+ );
105
+ }
7
106
 
8
- export function CollapsibleContent(props: { children: JSX.Element; class?: string }) {
107
+ export function CollapsibleContent(props: { children?: JSX.Element; class?: string; [k: string]: any }) {
108
+ const ctx = useCollapsible();
109
+ const [local, rest] = splitProps(props, ['children', 'class']);
9
110
  return (
10
- <KCollapsible.Content class={cn('overflow-hidden', props.class)}>
11
- {props.children}
12
- </KCollapsible.Content>
111
+ <div
112
+ {...rest}
113
+ id={ctx.contentId}
114
+ data-expanded={ctx.open() ? '' : undefined}
115
+ data-closed={ctx.open() ? undefined : ''}
116
+ class={cn(
117
+ 'grid transition-[grid-template-rows] duration-200 ease-out',
118
+ ctx.open() ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
119
+ )}
120
+ bool:inert={!ctx.open()}
121
+ >
122
+ <div class={cn('overflow-hidden', local.class)}>{local.children}</div>
123
+ </div>
13
124
  );
14
125
  }
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from './dropdown';
4
+ import { buttonVariants } from './button';
5
+
6
+ const meta = {
7
+ title: 'UI/Dropdown',
8
+ component: Dropdown,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component: [
15
+ 'An accessible menu that opens from a trigger, built on the kit\'s DIY overlay core (no third-party dependency). Implements the WAI-ARIA menu-button pattern: `aria-haspopup`/`aria-expanded` wiring, roving focus with Arrow/Home/End, type-ahead, Escape/outside-click dismissal, and focus return to the trigger. Portals into the active shadow root and resolves focus through `getRootNode()` so roving focus works inside web components.',
16
+ '**When to use:** for a list of *actions* or single-choice options triggered by a button — overflow "⋯" menus, model pickers, scope selectors. For hover-only context use `HoverCard`; for a label use `Tooltip`.',
17
+ '**How to use:** compose `Dropdown` › `DropdownTrigger` (the button) + `DropdownContent` › one `DropdownItem` per action. Give each item an `onSelect` handler — selecting also closes the menu.',
18
+ ].join('\n\n'),
19
+ },
20
+ },
21
+ },
22
+ render: () => <DropdownDemo />,
23
+ } satisfies Meta<typeof Dropdown>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ const IMPORT = `import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@kitnai/chat';`;
29
+ const src = (code: string) => ({
30
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
31
+ });
32
+
33
+ function DropdownDemo() {
34
+ const [last, setLast] = createSignal<string>();
35
+ return (
36
+ <div class="space-y-3">
37
+ <Dropdown>
38
+ <DropdownTrigger class={buttonVariants({ variant: 'outline' })}>Actions ▾</DropdownTrigger>
39
+ <DropdownContent>
40
+ <DropdownItem onSelect={() => setLast('Rename')}>Rename</DropdownItem>
41
+ <DropdownItem onSelect={() => setLast('Duplicate')}>Duplicate</DropdownItem>
42
+ <DropdownItem onSelect={() => setLast('Archive')}>Archive</DropdownItem>
43
+ </DropdownContent>
44
+ </Dropdown>
45
+ <p class="text-xs text-muted-foreground">Last selected: {last() ?? '—'}</p>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ /** Click the trigger (or focus it and press ↓ / Enter) to open the menu; Arrow keys move, Escape closes. */
51
+ export const Playground: Story = {
52
+ ...src(`<Dropdown>
53
+ <DropdownTrigger class={buttonVariants({ variant: 'outline' })}>Actions ▾</DropdownTrigger>
54
+ <DropdownContent>
55
+ <DropdownItem onSelect={() => rename()}>Rename</DropdownItem>
56
+ <DropdownItem onSelect={() => duplicate()}>Duplicate</DropdownItem>
57
+ <DropdownItem onSelect={() => archive()}>Archive</DropdownItem>
58
+ </DropdownContent>
59
+ </Dropdown>`),
60
+ };