@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.
- package/README.md +35 -5
- package/dist/custom-elements.json +2969 -0
- package/dist/kitn-chat.es.js +52 -39
- package/dist/llms/llms-full.txt +718 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/theme.tokens.css +137 -0
- package/frameworks/react/index.tsx +584 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +718 -0
- package/llms.txt +104 -0
- package/package.json +53 -6
- package/src/components/attachments.tsx +4 -2
- package/src/components/chain-of-thought.tsx +1 -1
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +217 -0
- package/src/components/checkpoint.tsx +7 -3
- package/src/components/context.tsx +14 -18
- package/src/components/conversation-item.tsx +1 -1
- package/src/components/conversation-list.tsx +5 -4
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +1 -0
- package/src/components/model-switcher.tsx +3 -3
- package/src/components/prompt-input.tsx +20 -2
- package/src/components/reasoning.tsx +2 -2
- package/src/components/scroll-button.tsx +1 -0
- package/src/components/slash-command.tsx +17 -8
- package/src/components/source.tsx +2 -2
- package/src/components/thinking-bar.tsx +2 -2
- package/src/components/tool.tsx +17 -6
- package/src/components/voice-input.tsx +5 -1
- package/src/elements/attachments.tsx +132 -0
- package/src/elements/chain-of-thought.tsx +45 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-workspace.tsx +122 -0
- package/src/elements/chat.tsx +31 -228
- package/src/elements/checkpoint.tsx +43 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/context-meter.tsx +71 -0
- package/src/elements/conversation-list.tsx +6 -0
- package/src/elements/default-input.tsx +22 -1
- package/src/elements/define.tsx +98 -12
- package/src/elements/element-types.d.ts +444 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.tsx +33 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/kitn-attachments.stories.tsx +181 -0
- package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
- package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
- package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
- package/src/elements/kitn-checkpoint.stories.tsx +71 -0
- package/src/elements/kitn-code-block.stories.tsx +82 -0
- package/src/elements/kitn-context-meter.stories.tsx +85 -0
- package/src/elements/kitn-empty.stories.tsx +110 -0
- package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
- package/src/elements/kitn-file-upload.stories.tsx +81 -0
- package/src/elements/kitn-image.stories.tsx +70 -0
- package/src/elements/kitn-loader.stories.tsx +87 -0
- package/src/elements/kitn-markdown.stories.tsx +75 -0
- package/src/elements/kitn-message-skills.stories.tsx +74 -0
- package/src/elements/kitn-message.stories.tsx +105 -0
- package/src/elements/kitn-model-switcher.stories.tsx +80 -0
- package/src/elements/kitn-prompt-input.stories.tsx +74 -16
- package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
- package/src/elements/kitn-reasoning.stories.tsx +76 -0
- package/src/elements/kitn-response-stream.stories.tsx +79 -0
- package/src/elements/kitn-source-list.stories.tsx +77 -0
- package/src/elements/kitn-source.stories.tsx +87 -0
- package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
- package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
- package/src/elements/kitn-tool.stories.tsx +88 -0
- package/src/elements/kitn-voice-input.stories.tsx +87 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.tsx +22 -0
- package/src/elements/message.tsx +125 -0
- package/src/elements/model-switcher.tsx +35 -0
- package/src/elements/prompt-input.tsx +83 -7
- package/src/elements/prompt-suggestions.tsx +58 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +32 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/source.tsx +67 -0
- package/src/elements/styles.css +14 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +0 -1
- package/src/primitives/chat-config.tsx +3 -3
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ForAIAgents.mdx +93 -0
- package/src/stories/docs/GettingStarted.mdx +2 -2
- package/src/stories/docs/Installation.mdx +29 -2
- package/src/stories/docs/Integrations.mdx +417 -15
- package/src/stories/docs/Introduction.mdx +17 -8
- package/src/stories/docs/Theming.mdx +1 -1
- 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/stories/typography.stories.tsx +78 -0
- package/src/ui/button.tsx +1 -1
- package/src/ui/collapsible.stories.tsx +70 -0
- package/src/ui/collapsible.tsx +119 -8
- package/src/ui/dropdown.stories.tsx +60 -0
- package/src/ui/dropdown.tsx +177 -12
- package/src/ui/hover-card.stories.tsx +78 -0
- package/src/ui/hover-card.tsx +147 -26
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +151 -0
- package/src/ui/scroll-area.stories.tsx +51 -0
- package/src/ui/textarea.stories.tsx +77 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/tooltip.stories.tsx +1 -1
- package/src/ui/tooltip.tsx +59 -13
- package/src/utils/cn.ts +19 -1
- package/theme.css +76 -43
- package/src/ui/dialog.tsx +0 -21
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal, createEffect, onCleanup, splitProps, type Accessor, type JSX,
|
|
3
|
+
} from 'solid-js';
|
|
4
|
+
import { Dynamic } from 'solid-js/web';
|
|
5
|
+
import {
|
|
6
|
+
computePosition, autoUpdate, offset, flip, shift, arrow, type Placement,
|
|
7
|
+
} from '@floating-ui/dom';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keep a node mounted through its CSS exit animation.
|
|
11
|
+
* Open -> present=true, state='open' (enter animation via base classes)
|
|
12
|
+
* Close -> state='closed' (data-closed triggers tw-animate-css animate-out),
|
|
13
|
+
* then unmount on `animationend`. If no animation is defined
|
|
14
|
+
* (e.g. jsdom), unmount on the next microtask.
|
|
15
|
+
*/
|
|
16
|
+
export function createPresence(show: Accessor<boolean>) {
|
|
17
|
+
const [present, setPresent] = createSignal(show());
|
|
18
|
+
const [state, setState] = createSignal<'open' | 'closed'>(show() ? 'open' : 'closed');
|
|
19
|
+
let node: Element | undefined;
|
|
20
|
+
let generation = 0;
|
|
21
|
+
const setRef = (el: Element) => { node = el; };
|
|
22
|
+
|
|
23
|
+
createEffect((prev: boolean | undefined) => {
|
|
24
|
+
const visible = show();
|
|
25
|
+
if (visible) {
|
|
26
|
+
generation++;
|
|
27
|
+
setPresent(true);
|
|
28
|
+
setState('open');
|
|
29
|
+
} else if (prev) {
|
|
30
|
+
setState('closed');
|
|
31
|
+
const el = node;
|
|
32
|
+
const hasAnim = el && (() => {
|
|
33
|
+
const cs = getComputedStyle(el);
|
|
34
|
+
return cs.animationName !== 'none' && parseFloat(cs.animationDuration || '0') > 0;
|
|
35
|
+
})();
|
|
36
|
+
if (!el || !hasAnim) {
|
|
37
|
+
const gen = ++generation;
|
|
38
|
+
queueMicrotask(() => { if (gen === generation) setPresent(false); });
|
|
39
|
+
return visible;
|
|
40
|
+
}
|
|
41
|
+
const animEl = el as HTMLElement;
|
|
42
|
+
const onEnd = (e: AnimationEvent) => {
|
|
43
|
+
if (e.target !== animEl) return;
|
|
44
|
+
animEl.removeEventListener('animationend', onEnd);
|
|
45
|
+
setPresent(false);
|
|
46
|
+
};
|
|
47
|
+
animEl.addEventListener('animationend', onEnd);
|
|
48
|
+
onCleanup(() => animEl.removeEventListener('animationend', onEnd));
|
|
49
|
+
}
|
|
50
|
+
return visible;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return { present, state, setRef };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AsTag = string | ((props: Record<string, any>) => JSX.Element);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Polymorphic element. `as` may be a tag name (default 'span') or a render
|
|
60
|
+
* function that receives the forwarded props (render-prop style `as={fn}`).
|
|
61
|
+
* Uses splitProps (NOT destructuring) so reactive forwarded props such as
|
|
62
|
+
* aria-expanded stay reactive. All extra props (incl. `ref`, event handlers,
|
|
63
|
+
* aria-*) are forwarded. `children` is left in `rest` so it forwards naturally.
|
|
64
|
+
*/
|
|
65
|
+
export function As(props: { as?: AsTag; children?: JSX.Element; [k: string]: any }) {
|
|
66
|
+
const [local, rest] = splitProps(props, ['as']);
|
|
67
|
+
if (typeof local.as === 'function') return local.as(rest);
|
|
68
|
+
return <Dynamic component={local.as ?? 'span'} {...rest} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface UsePositionOptions {
|
|
72
|
+
placement?: Placement;
|
|
73
|
+
gutter?: number;
|
|
74
|
+
arrowEl?: Accessor<HTMLElement | undefined>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Position `floating` relative to `reference` using fixed strategy + autoUpdate,
|
|
79
|
+
* so the element tracks the trigger on scroll/resize (fix DD-2). Writes
|
|
80
|
+
* position into the returned `pos` signal; caller applies it as inline style.
|
|
81
|
+
*
|
|
82
|
+
* `options` (placement/gutter) are read at setup time — pass static values;
|
|
83
|
+
* reactive option changes won't reposition until the next autoUpdate tick.
|
|
84
|
+
*/
|
|
85
|
+
export function usePosition(
|
|
86
|
+
reference: Accessor<HTMLElement | undefined>,
|
|
87
|
+
floating: Accessor<HTMLElement | undefined>,
|
|
88
|
+
options: UsePositionOptions = {},
|
|
89
|
+
) {
|
|
90
|
+
const [pos, setPos] = createSignal<{ x: number; y: number; placement: Placement }>(
|
|
91
|
+
{ x: 0, y: 0, placement: options.placement ?? 'bottom' },
|
|
92
|
+
);
|
|
93
|
+
const [arrowPos, setArrowPos] = createSignal<{ x?: number; y?: number }>({});
|
|
94
|
+
|
|
95
|
+
createEffect(() => {
|
|
96
|
+
const ref = reference();
|
|
97
|
+
const float = floating();
|
|
98
|
+
if (!ref || !float) return;
|
|
99
|
+
const update = () => {
|
|
100
|
+
const middleware = [offset(options.gutter ?? 8), flip(), shift({ padding: 8 })];
|
|
101
|
+
const aEl = options.arrowEl?.();
|
|
102
|
+
if (aEl) middleware.push(arrow({ element: aEl }));
|
|
103
|
+
computePosition(ref, float, {
|
|
104
|
+
placement: options.placement ?? 'bottom',
|
|
105
|
+
strategy: 'fixed',
|
|
106
|
+
middleware,
|
|
107
|
+
}).then(({ x, y, placement, middlewareData }) => {
|
|
108
|
+
setPos({ x, y, placement });
|
|
109
|
+
if (middlewareData.arrow) setArrowPos({ x: middlewareData.arrow.x, y: middlewareData.arrow.y });
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const cleanup = autoUpdate(ref, float, update);
|
|
113
|
+
onCleanup(cleanup);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { pos, arrowPos };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type DismissReason = 'escape' | 'outside';
|
|
120
|
+
|
|
121
|
+
export interface UseDismissOptions {
|
|
122
|
+
enabled: Accessor<boolean>;
|
|
123
|
+
onDismiss: (reason: DismissReason) => void;
|
|
124
|
+
/** Elements considered "inside" (trigger + content). Pointerdown outside all of them dismisses. */
|
|
125
|
+
refs: () => (HTMLElement | undefined)[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Escape key + outside-pointerdown dismissal. Does NOT lock page scroll (fix DD-1).
|
|
130
|
+
*
|
|
131
|
+
* `onDismiss` and `refs` are captured at call time (component setup), which is
|
|
132
|
+
* fine in SolidJS since components don't re-run — ensure they close over mutable
|
|
133
|
+
* variables, not stale values.
|
|
134
|
+
*/
|
|
135
|
+
export function useDismiss(opts: UseDismissOptions) {
|
|
136
|
+
createEffect(() => {
|
|
137
|
+
if (!opts.enabled()) return;
|
|
138
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') opts.onDismiss('escape'); };
|
|
139
|
+
const onPointer = (e: PointerEvent) => {
|
|
140
|
+
const target = e.target as Node;
|
|
141
|
+
const inside = opts.refs().some((el) => el && el.contains(target));
|
|
142
|
+
if (!inside) opts.onDismiss('outside');
|
|
143
|
+
};
|
|
144
|
+
document.addEventListener('keydown', onKey);
|
|
145
|
+
document.addEventListener('pointerdown', onPointer, true);
|
|
146
|
+
onCleanup(() => {
|
|
147
|
+
document.removeEventListener('keydown', onKey);
|
|
148
|
+
document.removeEventListener('pointerdown', onPointer, true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -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/src/ui/textarea.tsx
CHANGED
|
@@ -13,7 +13,7 @@ export function Textarea(props: TextareaProps) {
|
|
|
13
13
|
return (
|
|
14
14
|
<textarea
|
|
15
15
|
ref={local.autoResize !== false ? ref : undefined}
|
|
16
|
-
class={cn('w-full resize-none bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none', local.class)}
|
|
16
|
+
class={cn('w-full resize-none rounded-md bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', local.class)}
|
|
17
17
|
rows={1}
|
|
18
18
|
{...rest}
|
|
19
19
|
/>
|
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
docs: {
|
|
12
12
|
description: {
|
|
13
13
|
component: [
|
|
14
|
-
'A small floating label that appears on hover/focus of its trigger element, built
|
|
14
|
+
'A small floating label that appears on hover/focus of its trigger element, built with a DIY overlay-core implementation (no third-party dependency, no arrow).',
|
|
15
15
|
'**When to use:** to clarify the purpose of icon-only buttons or terse controls — short, supplementary hints that are not essential to complete the action.',
|
|
16
16
|
'**How to use:** wrap a single interactive `children` element and set `content` to the hint text. The child becomes the trigger.',
|
|
17
17
|
'**Placement:** toolbars, message action rows, and any compact icon control where a label would not otherwise fit.',
|
package/src/ui/tooltip.tsx
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createSignal, createUniqueId, onCleanup, Show, type JSX, splitProps } from 'solid-js';
|
|
2
|
+
import { Portal } from 'solid-js/web';
|
|
3
3
|
import { cn } from '../utils/cn';
|
|
4
4
|
import { useChatConfig } from '../primitives/chat-config';
|
|
5
|
+
import { createPresence, usePosition, useDismiss, As } from './overlay';
|
|
5
6
|
|
|
6
|
-
export interface TooltipProps { content: string; children: JSX.Element; class?: string; }
|
|
7
|
+
export interface TooltipProps { content: string; children: JSX.Element; class?: string; openDelay?: number; }
|
|
7
8
|
|
|
8
9
|
export function Tooltip(props: TooltipProps) {
|
|
9
|
-
const [local] = splitProps(props, ['content', 'children', 'class']);
|
|
10
|
+
const [local] = splitProps(props, ['content', 'children', 'class', 'openDelay']);
|
|
10
11
|
const config = useChatConfig();
|
|
12
|
+
const id = createUniqueId();
|
|
13
|
+
const [open, setOpen] = createSignal(false);
|
|
14
|
+
const [triggerEl, setTriggerEl] = createSignal<HTMLElement>();
|
|
15
|
+
const [contentEl, setContentEl] = createSignal<HTMLElement>();
|
|
16
|
+
let timer: number | undefined;
|
|
17
|
+
|
|
18
|
+
const [pointerInside, setPointerInside] = createSignal(false);
|
|
19
|
+
const [focusInside, setFocusInside] = createSignal(false);
|
|
20
|
+
|
|
21
|
+
const show = (delay = 0) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
if (delay) timer = window.setTimeout(() => setOpen(true), delay);
|
|
24
|
+
else setOpen(true);
|
|
25
|
+
};
|
|
26
|
+
const hide = () => { clearTimeout(timer); setOpen(false); };
|
|
27
|
+
const maybeHide = () => { if (!pointerInside() && !focusInside()) hide(); };
|
|
28
|
+
onCleanup(() => clearTimeout(timer));
|
|
29
|
+
|
|
30
|
+
const presence = createPresence(open);
|
|
31
|
+
const position = usePosition(triggerEl, contentEl, { placement: 'top', gutter: 6 });
|
|
32
|
+
useDismiss({ enabled: open, onDismiss: hide, refs: () => [triggerEl(), contentEl()] });
|
|
33
|
+
|
|
11
34
|
return (
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
<>
|
|
36
|
+
<As
|
|
37
|
+
as="span"
|
|
38
|
+
ref={setTriggerEl}
|
|
39
|
+
aria-describedby={open() ? id : undefined}
|
|
40
|
+
onPointerEnter={() => { setPointerInside(true); show(local.openDelay ?? 600); }}
|
|
41
|
+
onPointerLeave={() => { setPointerInside(false); maybeHide(); }}
|
|
42
|
+
onFocusIn={() => { setFocusInside(true); show(); }}
|
|
43
|
+
onFocusOut={(e: FocusEvent) => { const t = triggerEl(); if (t && t.contains(e.relatedTarget as Node)) return; setFocusInside(false); maybeHide(); }}
|
|
44
|
+
>
|
|
45
|
+
{local.children}
|
|
46
|
+
</As>
|
|
47
|
+
<Show when={presence.present()}>
|
|
48
|
+
<Portal mount={config.portalMount()}>
|
|
49
|
+
<div
|
|
50
|
+
ref={(el) => { setContentEl(el); presence.setRef(el); }}
|
|
51
|
+
id={id}
|
|
52
|
+
role="tooltip"
|
|
53
|
+
data-expanded={presence.state() === 'open' ? '' : undefined}
|
|
54
|
+
data-closed={presence.state() === 'closed' ? '' : undefined}
|
|
55
|
+
style={{ position: 'fixed', left: `${position.pos().x}px`, top: `${position.pos().y}px`, 'pointer-events': 'none' }}
|
|
56
|
+
class={cn(
|
|
57
|
+
'z-50 rounded-md bg-foreground px-2.5 py-1 text-xs text-background shadow-md',
|
|
58
|
+
'animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95',
|
|
59
|
+
local.class,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{local.content}
|
|
63
|
+
</div>
|
|
64
|
+
</Portal>
|
|
65
|
+
</Show>
|
|
66
|
+
</>
|
|
21
67
|
);
|
|
22
68
|
}
|
package/src/utils/cn.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
// The kit defines custom font-size utilities via @theme tokens in theme.css:
|
|
5
|
+
// text-caption / text-meta / text-body / text-title. tailwind-merge has no way
|
|
6
|
+
// to know these are font sizes, so by default it buckets e.g. `text-body` with
|
|
7
|
+
// text COLORS and drops a real color (`text-transparent`, `text-foreground`, …)
|
|
8
|
+
// whenever both appear in the same cn() call — which silently broke TextShimmer
|
|
9
|
+
// inside the web components (the element adds `text-body`, dropping
|
|
10
|
+
// `text-transparent`, so the gradient stayed hidden behind opaque text).
|
|
11
|
+
//
|
|
12
|
+
// Register them in the `font-size` group so they conflict only with other font
|
|
13
|
+
// sizes (text-xs/sm/base/lg/…) and never with text colors.
|
|
14
|
+
const twMerge = extendTailwindMerge({
|
|
15
|
+
extend: {
|
|
16
|
+
classGroups: {
|
|
17
|
+
'font-size': [{ text: ['caption', 'meta', 'body', 'title'] }],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
3
21
|
|
|
4
22
|
export function cn(...inputs: ClassValue[]) {
|
|
5
23
|
return twMerge(clsx(inputs));
|
package/theme.css
CHANGED
|
@@ -9,36 +9,63 @@
|
|
|
9
9
|
@custom-variant dark (&:is(.dark *));
|
|
10
10
|
|
|
11
11
|
@theme {
|
|
12
|
-
--color-background: hsl(0 0% 100%);
|
|
13
|
-
--color-foreground: hsl(240 10% 3.9%);
|
|
14
|
-
--color-card: hsl(0 0% 100%);
|
|
15
|
-
--color-card-foreground: hsl(240 10% 3.9%);
|
|
16
|
-
--color-popover: hsl(0 0% 100%);
|
|
17
|
-
--color-popover-foreground: hsl(240 10% 3.9%);
|
|
18
|
-
--color-primary: hsl(240 5.9% 10%);
|
|
19
|
-
--color-primary-foreground: hsl(0 0% 98%);
|
|
20
|
-
--color-secondary: hsl(240 4.8% 95.9%);
|
|
21
|
-
--color-secondary-foreground: hsl(240 5.9% 10%);
|
|
22
|
-
--color-muted: hsl(240 4.8% 95.9%);
|
|
23
|
-
--color-muted-foreground: hsl(240 3.8%
|
|
24
|
-
--color-accent: hsl(240 4.8% 95.9%);
|
|
25
|
-
--color-accent-foreground: hsl(240 5.9% 10%);
|
|
26
|
-
--color-destructive: hsl(0 84.2% 60.2%);
|
|
27
|
-
--color-destructive-foreground: hsl(0 0% 98%);
|
|
28
|
-
--color-border: hsl(240 5.9% 90%);
|
|
29
|
-
--color-input: hsl(240 5.9% 90%);
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
--color-background: var(--kitn-color-background, hsl(0 0% 100%));
|
|
13
|
+
--color-foreground: var(--kitn-color-foreground, hsl(240 10% 3.9%));
|
|
14
|
+
--color-card: var(--kitn-color-card, hsl(0 0% 100%));
|
|
15
|
+
--color-card-foreground: var(--kitn-color-card-foreground, hsl(240 10% 3.9%));
|
|
16
|
+
--color-popover: var(--kitn-color-popover, hsl(0 0% 100%));
|
|
17
|
+
--color-popover-foreground: var(--kitn-color-popover-foreground, hsl(240 10% 3.9%));
|
|
18
|
+
--color-primary: var(--kitn-color-primary, hsl(240 5.9% 10%));
|
|
19
|
+
--color-primary-foreground: var(--kitn-color-primary-foreground, hsl(0 0% 98%));
|
|
20
|
+
--color-secondary: var(--kitn-color-secondary, hsl(240 4.8% 95.9%));
|
|
21
|
+
--color-secondary-foreground: var(--kitn-color-secondary-foreground, hsl(240 5.9% 10%));
|
|
22
|
+
--color-muted: var(--kitn-color-muted, hsl(240 4.8% 95.9%));
|
|
23
|
+
--color-muted-foreground: var(--kitn-color-muted-foreground, hsl(240 3.8% 43%));
|
|
24
|
+
--color-accent: var(--kitn-color-accent, hsl(240 4.8% 95.9%));
|
|
25
|
+
--color-accent-foreground: var(--kitn-color-accent-foreground, hsl(240 5.9% 10%));
|
|
26
|
+
--color-destructive: var(--kitn-color-destructive, hsl(0 84.2% 60.2%));
|
|
27
|
+
--color-destructive-foreground: var(--kitn-color-destructive-foreground, hsl(0 0% 98%));
|
|
28
|
+
--color-border: var(--kitn-color-border, hsl(240 5.9% 90%));
|
|
29
|
+
--color-input: var(--kitn-color-input, hsl(240 5.9% 90%));
|
|
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%));
|
|
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. */
|
|
34
|
-
--color-code-foreground: hsl(224.3 76.3% 48%);
|
|
38
|
+
--color-code-foreground: var(--kitn-color-code-foreground, hsl(224.3 76.3% 48%));
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
/* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of
|
|
41
|
+
the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA
|
|
42
|
+
(4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;
|
|
43
|
+
dark mode keeps brighter hues for AA on the dark fill. Override via
|
|
44
|
+
--kitn-color-tool-* to retheme. */
|
|
45
|
+
--color-tool-blue: var(--kitn-color-tool-blue, hsl(217 91% 38%));
|
|
46
|
+
--color-tool-amber: var(--kitn-color-tool-amber, hsl(38 92% 28%));
|
|
47
|
+
--color-tool-green: var(--kitn-color-tool-green, hsl(142 71% 26%));
|
|
48
|
+
--color-tool-red: var(--kitn-color-tool-red, hsl(0 72% 42%));
|
|
49
|
+
|
|
50
|
+
--radius: var(--kitn-radius, 0.6rem);
|
|
37
51
|
--radius-sm: calc(var(--radius) - 4px);
|
|
38
52
|
--radius-md: calc(var(--radius) - 2px);
|
|
39
53
|
--radius-lg: var(--radius);
|
|
40
54
|
--radius-xl: calc(var(--radius) + 4px);
|
|
41
55
|
|
|
56
|
+
/* Typography scale — semantic sizes shared by all components. Each generates a
|
|
57
|
+
Tailwind utility (text-caption / text-meta / text-body / text-title). To
|
|
58
|
+
restyle the kit's typography, override the namespaced --kitn-text-* token on
|
|
59
|
+
:root (e.g. `--kitn-text-body: 0.9375rem`) — it pierces the Shadow DOM via the
|
|
60
|
+
var() fallback, exactly like the --kitn-color-* tokens. The bare --text-*
|
|
61
|
+
names stay internal so a host's own --text-* can't collide. (Message/markdown/
|
|
62
|
+
input reading size also scales with the `proseSize` prop; these tokens cover
|
|
63
|
+
the fixed chrome & controls.) */
|
|
64
|
+
--text-caption: var(--kitn-text-caption, 0.6875rem); --text-caption--line-height: 1rem; /* 11px — micro labels, badges, sub-counts */
|
|
65
|
+
--text-meta: var(--kitn-text-meta, 0.75rem); --text-meta--line-height: 1.1rem; /* 12px — controls, toggles, switchers, captions */
|
|
66
|
+
--text-body: var(--kitn-text-body, 0.875rem); --text-body--line-height: 1.45rem; /* 14px — primary reading text */
|
|
67
|
+
--text-title: var(--kitn-text-title, 1rem); --text-title--line-height: 1.5rem; /* 16px — emphasis / headers */
|
|
68
|
+
|
|
42
69
|
@keyframes typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }
|
|
43
70
|
@keyframes loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }
|
|
44
71
|
@keyframes wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }
|
|
@@ -56,27 +83,33 @@
|
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
.dark {
|
|
59
|
-
--color-background: hsl(50 2% 9%);
|
|
60
|
-
--color-foreground: hsl(0 0% 98%);
|
|
61
|
-
--color-card: hsl(
|
|
62
|
-
--color-card-foreground: hsl(0 0% 98%);
|
|
63
|
-
--color-popover: hsl(
|
|
64
|
-
--color-popover-foreground: hsl(0 0% 98%);
|
|
65
|
-
--color-primary: hsl(0 0% 98%);
|
|
66
|
-
--color-primary-foreground: hsl(
|
|
67
|
-
--color-secondary: hsl(
|
|
68
|
-
--color-secondary-foreground: hsl(0 0% 98%);
|
|
69
|
-
--color-muted: hsl(
|
|
70
|
-
--color-muted-foreground: hsl(
|
|
71
|
-
--color-accent: hsl(
|
|
72
|
-
--color-accent-foreground: hsl(0 0% 98%);
|
|
73
|
-
--color-destructive: hsl(0 62.8% 30.6%);
|
|
74
|
-
--color-destructive-foreground: hsl(0 0% 98%);
|
|
75
|
-
--color-border: hsl(
|
|
76
|
-
--color-input: hsl(
|
|
77
|
-
--color-ring: hsl(
|
|
78
|
-
--color-sidebar: hsl(50 2% 7%);
|
|
79
|
-
--color-code-foreground: hsl(213 94% 78%);
|
|
86
|
+
--color-background: var(--kitn-color-background, hsl(50 2% 9%));
|
|
87
|
+
--color-foreground: var(--kitn-color-foreground, hsl(0 0% 98%));
|
|
88
|
+
--color-card: var(--kitn-color-card, hsl(45 4% 12%));
|
|
89
|
+
--color-card-foreground: var(--kitn-color-card-foreground, hsl(0 0% 98%));
|
|
90
|
+
--color-popover: var(--kitn-color-popover, hsl(45 4% 12%));
|
|
91
|
+
--color-popover-foreground: var(--kitn-color-popover-foreground, hsl(0 0% 98%));
|
|
92
|
+
--color-primary: var(--kitn-color-primary, hsl(0 0% 98%));
|
|
93
|
+
--color-primary-foreground: var(--kitn-color-primary-foreground, hsl(45 4% 11%));
|
|
94
|
+
--color-secondary: var(--kitn-color-secondary, hsl(45 4% 17%));
|
|
95
|
+
--color-secondary-foreground: var(--kitn-color-secondary-foreground, hsl(0 0% 98%));
|
|
96
|
+
--color-muted: var(--kitn-color-muted, hsl(45 4% 17%));
|
|
97
|
+
--color-muted-foreground: var(--kitn-color-muted-foreground, hsl(45 4% 64%));
|
|
98
|
+
--color-accent: var(--kitn-color-accent, hsl(45 4% 17%));
|
|
99
|
+
--color-accent-foreground: var(--kitn-color-accent-foreground, hsl(0 0% 98%));
|
|
100
|
+
--color-destructive: var(--kitn-color-destructive, hsl(0 62.8% 30.6%));
|
|
101
|
+
--color-destructive-foreground: var(--kitn-color-destructive-foreground, hsl(0 0% 98%));
|
|
102
|
+
--color-border: var(--kitn-color-border, hsl(45 4% 17%));
|
|
103
|
+
--color-input: var(--kitn-color-input, hsl(45 4% 17%));
|
|
104
|
+
--color-ring: var(--kitn-color-ring, hsl(217 91% 68%));
|
|
105
|
+
--color-sidebar: var(--kitn-color-sidebar, hsl(50 2% 7%));
|
|
106
|
+
--color-code-foreground: var(--kitn-color-code-foreground, hsl(213 94% 78%));
|
|
107
|
+
|
|
108
|
+
/* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */
|
|
109
|
+
--color-tool-blue: var(--kitn-color-tool-blue, hsl(217 91% 70%));
|
|
110
|
+
--color-tool-amber: var(--kitn-color-tool-amber, hsl(38 92% 50%));
|
|
111
|
+
--color-tool-green: var(--kitn-color-tool-green, hsl(142 71% 45%));
|
|
112
|
+
--color-tool-red: var(--kitn-color-tool-red, hsl(0 84% 70%));
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
/* Self-contained markdown styling — replaces the typography plugin's `prose`.
|
package/src/ui/dialog.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Dialog as KDialog } from '@kobalte/core/dialog';
|
|
2
|
-
import { type JSX } from 'solid-js';
|
|
3
|
-
import { cn } from '../utils/cn';
|
|
4
|
-
import { useChatConfig } from '../primitives/chat-config';
|
|
5
|
-
|
|
6
|
-
export const Dialog = KDialog;
|
|
7
|
-
export const DialogTrigger = KDialog.Trigger;
|
|
8
|
-
|
|
9
|
-
export function DialogContent(props: { children: JSX.Element; class?: string; title: string }) {
|
|
10
|
-
const config = useChatConfig();
|
|
11
|
-
return (
|
|
12
|
-
<KDialog.Portal mount={config.portalMount()}>
|
|
13
|
-
<KDialog.Overlay class="fixed inset-0 z-50 bg-black/50 animate-in fade-in-0" />
|
|
14
|
-
<KDialog.Content class={cn('fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-xl bg-card p-6 shadow-xl animate-in fade-in-0 zoom-in-95 w-full max-w-md', props.class)}>
|
|
15
|
-
<KDialog.Title class="text-lg font-semibold">{props.title}</KDialog.Title>
|
|
16
|
-
{props.children}
|
|
17
|
-
<KDialog.CloseButton class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" />
|
|
18
|
-
</KDialog.Content>
|
|
19
|
-
</KDialog.Portal>
|
|
20
|
-
);
|
|
21
|
-
}
|