@kitnai/chat 0.4.0 → 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.
- package/README.md +24 -5
- package/dist/custom-elements.json +475 -0
- package/dist/kitn-chat.es.js +18 -18
- package/dist/llms/llms-full.txt +55 -4
- package/dist/llms/llms.txt +3 -3
- package/dist/theme.tokens.css +6 -2
- package/frameworks/react/index.tsx +54 -0
- package/llms-full.txt +55 -4
- package/llms.txt +3 -3
- package/package.json +20 -2
- package/src/components/chat-thread.tsx +217 -0
- package/src/components/prompt-input.tsx +5 -0
- package/src/elements/chat-workspace.tsx +122 -0
- package/src/elements/chat.tsx +30 -271
- package/src/elements/compiled.css +1 -1
- package/src/elements/define.tsx +1 -1
- package/src/elements/element-types.d.ts +40 -0
- package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
- package/src/elements/register.ts +1 -0
- package/src/elements/styles.css +14 -0
- package/src/primitives/chat-config.tsx +1 -1
- package/src/stories/docs/Installation.mdx +27 -0
- package/src/stories/docs/Integrations.mdx +2 -0
- package/src/stories/docs/Introduction.mdx +12 -3
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/ui/collapsible.stories.tsx +70 -0
- package/src/ui/dropdown.stories.tsx +60 -0
- package/src/ui/hover-card.stories.tsx +78 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +1 -1
- package/src/ui/scroll-area.stories.tsx +51 -0
- package/src/ui/textarea.stories.tsx +77 -0
- package/theme.css +6 -2
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { HoverCard } from './hover-card';
|
|
3
|
+
import { Button } from './button';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'UI/HoverCard',
|
|
7
|
+
component: HoverCard,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: [
|
|
14
|
+
'A floating card that opens when its trigger is hovered or focused, built on the kit\'s DIY overlay core (positioning + dismiss + presence — no third-party dependency). Portals into the active shadow root so it never clips, and a transparent "safe bridge" keeps it open while the pointer travels from trigger to card.',
|
|
15
|
+
'**When to use:** to reveal supplementary, non-essential context on hover — a user/profile preview, a link or citation preview, an attachment summary. For an actionable menu use `Dropdown`; for a one-line label use `Tooltip`.',
|
|
16
|
+
'**How to use:** pass the trigger element as `trigger` and the card body as `children`. Tune `openDelay` / `closeDelay` (ms) to taste.',
|
|
17
|
+
].join('\n\n'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
trigger: { control: false, description: 'The element that opens the card on hover/focus.' },
|
|
23
|
+
children: { control: false, description: 'The card contents.' },
|
|
24
|
+
openDelay: { control: 'number', description: 'Delay (ms) before the card opens. Default 0.' },
|
|
25
|
+
closeDelay: { control: 'number', description: 'Delay (ms) before the card closes after the pointer leaves. Default 300.' },
|
|
26
|
+
class: { control: 'text', description: 'Extra classes applied to the card body.' },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
trigger: <Button variant="outline">@ada</Button>,
|
|
30
|
+
children: (
|
|
31
|
+
<div class="flex gap-3">
|
|
32
|
+
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-medium text-primary-foreground">AL</div>
|
|
33
|
+
<div class="space-y-1">
|
|
34
|
+
<p class="text-sm font-medium text-foreground">Ada Lovelace</p>
|
|
35
|
+
<p class="text-xs text-muted-foreground">Wrote the first algorithm intended for a machine. Joined in 1843.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
render: (args) => <HoverCard {...args} />,
|
|
41
|
+
} satisfies Meta<typeof HoverCard>;
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
const IMPORT = `import { HoverCard } from '@kitnai/chat';`;
|
|
47
|
+
const src = (code: string) => ({
|
|
48
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** Hover (or focus) the trigger to reveal a profile preview card. */
|
|
52
|
+
export const Playground: Story = {
|
|
53
|
+
...src(`<HoverCard trigger={<Button variant="outline">@ada</Button>}>
|
|
54
|
+
<ProfilePreview name="Ada Lovelace" />
|
|
55
|
+
</HoverCard>`),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A link-preview card, the way it might appear inline in an assistant message. */
|
|
59
|
+
export const LinkPreview: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<p class="max-w-md text-sm text-foreground">
|
|
62
|
+
See the{' '}
|
|
63
|
+
<HoverCard
|
|
64
|
+
trigger={<a href="#" class="font-medium text-primary underline underline-offset-2">MDN reference</a>}
|
|
65
|
+
>
|
|
66
|
+
<div class="space-y-1">
|
|
67
|
+
<p class="text-sm font-medium text-foreground">Custom elements — MDN</p>
|
|
68
|
+
<p class="text-xs text-muted-foreground">developer.mozilla.org</p>
|
|
69
|
+
<p class="text-xs text-muted-foreground">Define your own HTML elements with the CustomElementRegistry.</p>
|
|
70
|
+
</div>
|
|
71
|
+
</HoverCard>{' '}
|
|
72
|
+
for the full custom-elements API.
|
|
73
|
+
</p>
|
|
74
|
+
),
|
|
75
|
+
...src(`<HoverCard trigger={<a href="...">MDN reference</a>}>
|
|
76
|
+
<LinkCard title="Custom elements — MDN" host="developer.mozilla.org" />
|
|
77
|
+
</HoverCard>`),
|
|
78
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal, Show } from 'solid-js';
|
|
3
|
+
import { Portal } from 'solid-js/web';
|
|
4
|
+
import { createPresence, usePosition, useDismiss } from './overlay';
|
|
5
|
+
import { buttonVariants } from './button';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `overlay.tsx` is not a component — it's the small DIY toolkit that every
|
|
9
|
+
* floating surface in the kit (Tooltip, HoverCard, Dropdown) is built from,
|
|
10
|
+
* replacing the former third-party UI dependency. Three primitives:
|
|
11
|
+
*
|
|
12
|
+
* - `usePosition(ref, floating, opts)` — anchors `floating` to `ref` via
|
|
13
|
+
* @floating-ui/dom with flip/shift, tracking it on scroll/resize.
|
|
14
|
+
* - `createPresence(open)` — keeps a node mounted through its CSS exit
|
|
15
|
+
* animation, then unmounts on `animationend`.
|
|
16
|
+
* - `useDismiss({ enabled, onDismiss, refs })` — Escape + outside-pointerdown
|
|
17
|
+
* dismissal (no scroll lock).
|
|
18
|
+
*
|
|
19
|
+
* Plus an `As` polymorphic helper for trigger elements.
|
|
20
|
+
*/
|
|
21
|
+
const meta: Meta = {
|
|
22
|
+
title: 'UI/Overlay Core',
|
|
23
|
+
tags: ['autodocs'],
|
|
24
|
+
parameters: {
|
|
25
|
+
layout: 'padded',
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component: [
|
|
29
|
+
"The shared, dependency-free foundation behind every floating surface in the kit — `Tooltip`, `HoverCard`, and `Dropdown` are all assembled from these primitives. It's a toolkit of hooks, not a renderable component.",
|
|
30
|
+
'**`usePosition(ref, floating, opts)`** anchors a floating node to a trigger via `@floating-ui/dom` (flip/shift, fixed strategy, tracks on scroll/resize). **`createPresence(open)`** keeps the node mounted through its CSS exit animation. **`useDismiss({ enabled, onDismiss, refs })`** handles Escape + outside-click. An **`As`** helper renders a polymorphic trigger.',
|
|
31
|
+
'**When to use:** only when you need a floating surface the prebuilt components don\'t cover. Reach for `Tooltip` / `HoverCard` / `Dropdown` first — they already wire these together with the correct ARIA and focus behavior.',
|
|
32
|
+
'The demo below composes all three into a minimal popover; everything portals into the active shadow root via `ChatConfig`.',
|
|
33
|
+
].join('\n\n'),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof meta>;
|
|
41
|
+
|
|
42
|
+
function PopoverDemo() {
|
|
43
|
+
const [open, setOpen] = createSignal(false);
|
|
44
|
+
const [trigger, setTrigger] = createSignal<HTMLElement>();
|
|
45
|
+
const [content, setContent] = createSignal<HTMLElement>();
|
|
46
|
+
const presence = createPresence(open);
|
|
47
|
+
const position = usePosition(trigger, content, { placement: 'bottom-start', gutter: 6 });
|
|
48
|
+
useDismiss({ enabled: open, onDismiss: () => setOpen(false), refs: () => [trigger(), content()] });
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<button
|
|
53
|
+
ref={setTrigger}
|
|
54
|
+
type="button"
|
|
55
|
+
class={buttonVariants({ variant: 'outline' })}
|
|
56
|
+
onClick={() => setOpen(!open())}
|
|
57
|
+
>
|
|
58
|
+
{open() ? 'Close' : 'Open'} popover
|
|
59
|
+
</button>
|
|
60
|
+
<Show when={presence.present()}>
|
|
61
|
+
<Portal>
|
|
62
|
+
<div
|
|
63
|
+
ref={(el) => { setContent(el); presence.setRef(el); }}
|
|
64
|
+
data-expanded={presence.state() === 'open' ? '' : undefined}
|
|
65
|
+
data-closed={presence.state() === 'closed' ? '' : undefined}
|
|
66
|
+
style={{ position: 'fixed', left: `${position.pos().x}px`, top: `${position.pos().y}px` }}
|
|
67
|
+
class="z-50 w-64 rounded-lg bg-card p-3 text-sm text-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95"
|
|
68
|
+
>
|
|
69
|
+
Positioned by <code class="text-xs">usePosition</code>, kept mounted through its
|
|
70
|
+
exit animation by <code class="text-xs">createPresence</code>, and closed on
|
|
71
|
+
Escape / outside-click by <code class="text-xs">useDismiss</code>.
|
|
72
|
+
</div>
|
|
73
|
+
</Portal>
|
|
74
|
+
</Show>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A minimal popover hand-built from the three overlay primitives. Click to toggle; click outside or press Escape to dismiss. */
|
|
80
|
+
export const MinimalPopover: Story = {
|
|
81
|
+
render: () => <PopoverDemo />,
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
source: {
|
|
85
|
+
code: `import { createPresence, usePosition, useDismiss } from '@kitnai/chat';
|
|
86
|
+
|
|
87
|
+
function PopoverDemo() {
|
|
88
|
+
const [open, setOpen] = createSignal(false);
|
|
89
|
+
const [trigger, setTrigger] = createSignal<HTMLElement>();
|
|
90
|
+
const [content, setContent] = createSignal<HTMLElement>();
|
|
91
|
+
const presence = createPresence(open);
|
|
92
|
+
const position = usePosition(trigger, content, { placement: 'bottom-start', gutter: 6 });
|
|
93
|
+
useDismiss({ enabled: open, onDismiss: () => setOpen(false), refs: () => [trigger(), content()] });
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<button ref={setTrigger} onClick={() => setOpen(!open())}>Toggle</button>
|
|
98
|
+
<Show when={presence.present()}>
|
|
99
|
+
<Portal>
|
|
100
|
+
<div
|
|
101
|
+
ref={(el) => { setContent(el); presence.setRef(el); }}
|
|
102
|
+
style={{ position: 'fixed', left: \`\${position.pos().x}px\`, top: \`\${position.pos().y}px\` }}
|
|
103
|
+
>
|
|
104
|
+
…content…
|
|
105
|
+
</div>
|
|
106
|
+
</Portal>
|
|
107
|
+
</Show>
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}`,
|
|
111
|
+
language: 'tsx',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
package/src/ui/overlay.tsx
CHANGED
|
@@ -57,7 +57,7 @@ export type AsTag = string | ((props: Record<string, any>) => JSX.Element);
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Polymorphic element. `as` may be a tag name (default 'span') or a render
|
|
60
|
-
* function that receives the forwarded props (
|
|
60
|
+
* function that receives the forwarded props (render-prop style `as={fn}`).
|
|
61
61
|
* Uses splitProps (NOT destructuring) so reactive forwarded props such as
|
|
62
62
|
* aria-expanded stay reactive. All extra props (incl. `ref`, event handlers,
|
|
63
63
|
* aria-*) are forwarded. `children` is left in `rest` so it forwards naturally.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { For } from 'solid-js';
|
|
3
|
+
import { ScrollArea } from './scroll-area';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'UI/ScrollArea',
|
|
7
|
+
component: ScrollArea,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: [
|
|
14
|
+
'A vertically scrollable container with thin, themed scrollbars (`scrollbar-thin` + muted thumb, transparent track). A thin styling layer over native overflow — no custom scroll hijacking, so momentum, keyboard, and accessibility behave exactly like the platform expects. Constrain it with a height (or let a flex parent bound it) and overflow content scrolls.',
|
|
15
|
+
'**When to use:** any bounded region whose content can exceed its height — the conversation/history sidebar, a long menu, a tall card body.',
|
|
16
|
+
'**How to use:** set a height via `class` and drop the scrollable content inside. All other div props (e.g. `aria-label`) are forwarded.',
|
|
17
|
+
].join('\n\n'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
render: () => (
|
|
22
|
+
<ScrollArea class="h-56 w-72 rounded-lg border border-border p-2">
|
|
23
|
+
<ul class="space-y-1">
|
|
24
|
+
<For each={Array.from({ length: 24 }, (_, i) => i + 1)}>
|
|
25
|
+
{(n) => (
|
|
26
|
+
<li class="rounded-md px-3 py-2 text-sm text-foreground hover:bg-muted">
|
|
27
|
+
Conversation {n}
|
|
28
|
+
</li>
|
|
29
|
+
)}
|
|
30
|
+
</For>
|
|
31
|
+
</ul>
|
|
32
|
+
</ScrollArea>
|
|
33
|
+
),
|
|
34
|
+
} satisfies Meta<typeof ScrollArea>;
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
const IMPORT = `import { ScrollArea } from '@kitnai/chat';`;
|
|
40
|
+
|
|
41
|
+
/** A bounded list that scrolls. Note macOS hides overlay scrollbars until you scroll. */
|
|
42
|
+
export const Playground: Story = {
|
|
43
|
+
parameters: {
|
|
44
|
+
docs: {
|
|
45
|
+
source: {
|
|
46
|
+
code: `${IMPORT}\n\n<ScrollArea class="h-56 w-72 rounded-lg border p-2">\n <For each={conversations}>{(c) => <Row {...c} />}</For>\n</ScrollArea>`,
|
|
47
|
+
language: 'tsx',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import type { JSX } from 'solid-js';
|
|
3
|
+
import { Textarea } from './textarea';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'UI/Textarea',
|
|
7
|
+
component: Textarea,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: [
|
|
14
|
+
'A single-line-by-default textarea that auto-grows with its content up to an optional `maxHeight`, after which it scrolls. It is **transparent and borderless by design** — it is meant to drop into a composed input frame that owns the visual boundary and focus ring (the demos below wrap it exactly the way `PromptInput` / `<kitn-prompt-input>` does, with `focus-within` on the frame). This is the editable surface behind `PromptInput`.',
|
|
15
|
+
'**When to use:** free-text entry that may span multiple lines — a chat composer, a comment box, an editable note.',
|
|
16
|
+
'**How to use:** drop it inside a framed container and use it like a native `<textarea>` (`value`, `placeholder`, `onInput`, …). Auto-resize is on by default; set `maxHeight` (px) to cap growth, or `autoResize={false}` for a fixed-height field.',
|
|
17
|
+
].join('\n\n'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
placeholder: { control: 'text', description: 'Placeholder text.' },
|
|
23
|
+
maxHeight: { control: 'number', description: 'Max height (px) before the field scrolls instead of growing.' },
|
|
24
|
+
autoResize: { control: 'boolean', description: 'Grow with content. Default true.' },
|
|
25
|
+
class: { control: 'text', description: 'Extra classes.' },
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
placeholder: 'Ask anything… (Shift+Enter for a newline)',
|
|
29
|
+
maxHeight: 200,
|
|
30
|
+
},
|
|
31
|
+
render: (args) => (
|
|
32
|
+
<Frame>
|
|
33
|
+
<Textarea {...args} class="focus-visible:ring-0" />
|
|
34
|
+
</Frame>
|
|
35
|
+
),
|
|
36
|
+
} satisfies Meta<typeof Textarea>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The composed input frame, mirroring how `PromptInput` wraps the textarea: the
|
|
40
|
+
* FRAME owns the boundary + focus ring (`focus-within`), and the transparent
|
|
41
|
+
* textarea inside has its own ring neutralized so there's no nested box.
|
|
42
|
+
*/
|
|
43
|
+
function Frame(props: { children: JSX.Element }) {
|
|
44
|
+
return (
|
|
45
|
+
<div class="w-96 cursor-text rounded-xl border border-border bg-muted/40 p-3 shadow-xs transition-shadow focus-within:border-ring focus-within:ring-2 focus-within:ring-ring">
|
|
46
|
+
{props.children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
const IMPORT = `import { Textarea } from '@kitnai/chat';`;
|
|
55
|
+
const src = (code: string) => ({
|
|
56
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Type several lines — the field grows until `maxHeight`, then scrolls. Focus the field: the ring is on the frame, not a nested box. */
|
|
60
|
+
export const Playground: Story = {
|
|
61
|
+
...src(`{/* The frame owns the focus ring; the textarea's own ring is neutralized. */}
|
|
62
|
+
<div class="cursor-text rounded-xl border bg-muted/40 p-3 focus-within:ring-2 focus-within:ring-ring">
|
|
63
|
+
<Textarea placeholder="Ask anything…" maxHeight={200} class="focus-visible:ring-0" />
|
|
64
|
+
</div>`),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** A fixed-height field (auto-resize disabled). */
|
|
68
|
+
export const FixedHeight: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<Frame>
|
|
71
|
+
<Textarea autoResize={false} rows={4} placeholder="Fixed at 4 rows…" class="focus-visible:ring-0" />
|
|
72
|
+
</Frame>
|
|
73
|
+
),
|
|
74
|
+
...src(`<div class="cursor-text rounded-xl border bg-muted/40 p-3 focus-within:ring-2 focus-within:ring-ring">
|
|
75
|
+
<Textarea autoResize={false} rows={4} placeholder="Fixed at 4 rows…" class="focus-visible:ring-0" />
|
|
76
|
+
</div>`),
|
|
77
|
+
};
|
package/theme.css
CHANGED
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
--color-destructive-foreground: var(--kitn-color-destructive-foreground, hsl(0 0% 98%));
|
|
28
28
|
--color-border: var(--kitn-color-border, hsl(240 5.9% 90%));
|
|
29
29
|
--color-input: var(--kitn-color-input, hsl(240 5.9% 90%));
|
|
30
|
-
|
|
30
|
+
/* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always
|
|
31
|
+
obvious and on-brand in both modes. Light uses a strong blue; dark uses a
|
|
32
|
+
brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text
|
|
33
|
+
contrast (≥3:1) against the kit's backgrounds. Override via --kitn-color-ring. */
|
|
34
|
+
--color-ring: var(--kitn-color-ring, hsl(217 91% 53%));
|
|
31
35
|
--color-sidebar: var(--kitn-color-sidebar, hsl(0 0% 100%));
|
|
32
36
|
|
|
33
37
|
/* Inline `code` accent. Blue text on a translucent blue chip. */
|
|
@@ -97,7 +101,7 @@
|
|
|
97
101
|
--color-destructive-foreground: var(--kitn-color-destructive-foreground, hsl(0 0% 98%));
|
|
98
102
|
--color-border: var(--kitn-color-border, hsl(45 4% 17%));
|
|
99
103
|
--color-input: var(--kitn-color-input, hsl(45 4% 17%));
|
|
100
|
-
--color-ring: var(--kitn-color-ring, hsl(
|
|
104
|
+
--color-ring: var(--kitn-color-ring, hsl(217 91% 68%));
|
|
101
105
|
--color-sidebar: var(--kitn-color-sidebar, hsl(50 2% 7%));
|
|
102
106
|
--color-code-foreground: var(--kitn-color-code-foreground, hsl(213 94% 78%));
|
|
103
107
|
|