@kitnai/chat 0.1.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/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/bash-InADTalH.js +6 -0
- package/dist/core-AYMC6_lb.js +5874 -0
- package/dist/engine-javascript-vq0WuIJl.js +2643 -0
- package/dist/github-dark-dimmed-DUshB20C.js +4 -0
- package/dist/github-light-JYsPkUQd.js +4 -0
- package/dist/javascript-C25yR2R2.js +6 -0
- package/dist/json-DxJze_jm.js +6 -0
- package/dist/kitn-chat.es.js +6632 -0
- package/dist/tsx-B8rCNbgL.js +6 -0
- package/dist/typescript-RycA9KXf.js +6 -0
- package/package.json +80 -0
- package/src/components/attachments.stories.tsx +304 -0
- package/src/components/attachments.tsx +394 -0
- package/src/components/chain-of-thought.stories.tsx +212 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +188 -0
- package/src/components/chat-container.tsx +78 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/checkpoint.stories.tsx +103 -0
- package/src/components/checkpoint.tsx +81 -0
- package/src/components/code-block.stories.tsx +151 -0
- package/src/components/code-block.tsx +99 -0
- package/src/components/context.stories.tsx +180 -0
- package/src/components/context.tsx +323 -0
- package/src/components/conversation-item.stories.tsx +126 -0
- package/src/components/conversation-item.tsx +18 -0
- package/src/components/conversation-list.stories.tsx +134 -0
- package/src/components/conversation-list.tsx +100 -0
- package/src/components/empty.stories.tsx +435 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +101 -0
- package/src/components/feedback-bar.tsx +58 -0
- package/src/components/file-upload.stories.tsx +157 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/image.stories.tsx +90 -0
- package/src/components/image.tsx +67 -0
- package/src/components/loader.stories.tsx +182 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +181 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-narrow.stories.tsx +330 -0
- package/src/components/message-skills.stories.tsx +212 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +282 -0
- package/src/components/message.tsx +149 -0
- package/src/components/model-switcher.stories.tsx +98 -0
- package/src/components/model-switcher.tsx +36 -0
- package/src/components/prompt-input.stories.tsx +223 -0
- package/src/components/prompt-input.tsx +190 -0
- package/src/components/prompt-suggestion.stories.tsx +143 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +141 -0
- package/src/components/reasoning.tsx +157 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +101 -0
- package/src/components/scroll-button.tsx +33 -0
- package/src/components/slash-command.stories.tsx +164 -0
- package/src/components/slash-command.tsx +223 -0
- package/src/components/source.stories.tsx +125 -0
- package/src/components/source.tsx +129 -0
- package/src/components/text-shimmer.stories.tsx +88 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +88 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +154 -0
- package/src/components/tool.tsx +173 -0
- package/src/components/voice-input.stories.tsx +84 -0
- package/src/components/voice-input.tsx +103 -0
- package/src/elements/chat-types.ts +14 -0
- package/src/elements/chat.tsx +111 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/conversation-list.tsx +26 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +53 -0
- package/src/elements/define.tsx +54 -0
- package/src/elements/kitn-chat.stories.tsx +105 -0
- package/src/elements/kitn-conversation-list.stories.tsx +177 -0
- package/src/elements/kitn-prompt-input.stories.tsx +123 -0
- package/src/elements/prompt-input.tsx +39 -0
- package/src/elements/register.ts +9 -0
- package/src/elements/styles.css +12 -0
- package/src/index.ts +128 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/highlighter.ts +150 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-stick-to-bottom.ts +43 -0
- package/src/primitives/use-text-stream.ts +112 -0
- package/src/primitives/use-voice-recorder.ts +50 -0
- package/src/stories/chat-panel-layout.stories.tsx +144 -0
- package/src/stories/chat-scene.tsx +570 -0
- package/src/stories/checkpoint-restore.stories.tsx +224 -0
- package/src/stories/context-usage.stories.tsx +155 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +151 -0
- package/src/stories/conversation-with-sources.stories.tsx +165 -0
- package/src/stories/docs/GettingStarted.mdx +76 -0
- package/src/stories/docs/Installation.mdx +48 -0
- package/src/stories/docs/Integrations.mdx +110 -0
- package/src/stories/docs/Introduction.mdx +29 -0
- package/src/stories/docs/Theming.mdx +87 -0
- package/src/stories/docs/theme-editor/canvas.tsx +32 -0
- package/src/stories/docs/theme-editor/inspector.tsx +66 -0
- package/src/stories/docs/theme-editor/presets.test.ts +32 -0
- package/src/stories/docs/theme-editor/presets.ts +64 -0
- package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
- package/src/stories/docs/theme-editor/theme-css.ts +15 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +145 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +167 -0
- package/src/stories/prompt-input-variants.stories.tsx +179 -0
- package/src/stories/streaming-response.stories.tsx +234 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/types.ts +41 -0
- package/src/ui/avatar.stories.tsx +104 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +87 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +146 -0
- package/src/ui/button.tsx +37 -0
- package/src/ui/collapsible.tsx +14 -0
- package/src/ui/dialog.tsx +21 -0
- package/src/ui/dropdown.tsx +26 -0
- package/src/ui/hover-card.tsx +48 -0
- package/src/ui/resizable.stories.tsx +171 -0
- package/src/ui/resizable.tsx +219 -0
- package/src/ui/scroll-area.tsx +13 -0
- package/src/ui/separator.stories.tsx +82 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +338 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +75 -0
- package/src/ui/tooltip.tsx +22 -0
- package/src/utils/cn.ts +6 -0
- package/theme.css +115 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { Image } from './image';
|
|
3
|
+
|
|
4
|
+
// Compact SVG chat typing icon as base64
|
|
5
|
+
const chatIconBase64 =
|
|
6
|
+
'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDQ4IDQ4Ij48cmVjdCB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHJ4PSIxMCIgZmlsbD0iIzdjM2FlZCIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMjQiIHI9IjQiIGZpbGw9IiNmZmYiLz48Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI0IiBmaWxsPSIjZmZmIi8+PGNpcmNsZSBjeD0iMzIiIGN5PSIyNCIgcj0iNCIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Image',
|
|
10
|
+
component: Image,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'padded',
|
|
14
|
+
docs: {
|
|
15
|
+
controls: { exclude: ['use:eventListener'] },
|
|
16
|
+
description: {
|
|
17
|
+
component: [
|
|
18
|
+
'Renders an image from a `base64` string or a `uint8Array`, building a data URL or object URL automatically; shows a pulsing placeholder until a source is available.',
|
|
19
|
+
'**When to use:** to display model-generated or attached images supplied as raw bytes / base64 (the `GeneratedImageLike` shape) rather than a remote URL.',
|
|
20
|
+
'**How to use:** pass `base64` + `mediaType` (or `uint8Array` + `mediaType`) and an `alt` description; size and style via `class`.',
|
|
21
|
+
'**Placement:** inside assistant messages, attachment previews, or anywhere a generated image needs to be shown.',
|
|
22
|
+
].join('\n\n'),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
argTypes: {
|
|
27
|
+
base64: {
|
|
28
|
+
control: 'text',
|
|
29
|
+
description: 'Base64-encoded image data. Combined with `mediaType` to form a data URL.',
|
|
30
|
+
},
|
|
31
|
+
uint8Array: {
|
|
32
|
+
control: 'object',
|
|
33
|
+
description: 'Raw image bytes. Combined with `mediaType` to form an object URL.',
|
|
34
|
+
},
|
|
35
|
+
mediaType: {
|
|
36
|
+
control: 'text',
|
|
37
|
+
description: 'MIME type of the image data, e.g. `image/png` or `image/svg+xml`.',
|
|
38
|
+
table: { defaultValue: { summary: 'image/png' } },
|
|
39
|
+
},
|
|
40
|
+
alt: {
|
|
41
|
+
control: 'text',
|
|
42
|
+
description: 'Alternative text describing the image (also used on the placeholder).',
|
|
43
|
+
},
|
|
44
|
+
class: {
|
|
45
|
+
control: 'text',
|
|
46
|
+
description: 'Additional CSS classes for the image / placeholder element.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
args: {
|
|
50
|
+
base64: chatIconBase64,
|
|
51
|
+
mediaType: 'image/svg+xml',
|
|
52
|
+
alt: 'Compact gradient chat icon',
|
|
53
|
+
class: 'h-24 w-24 rounded-md',
|
|
54
|
+
},
|
|
55
|
+
render: (args) => <Image {...args} />,
|
|
56
|
+
} satisfies Meta<typeof Image>;
|
|
57
|
+
|
|
58
|
+
export default meta;
|
|
59
|
+
type Story = StoryObj<typeof meta>;
|
|
60
|
+
|
|
61
|
+
const IMPORT = `import { Image } from '@kitnai/chat';`;
|
|
62
|
+
const src = (code: string) => ({
|
|
63
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** Interactive playground — swap the base64 data, media type, and sizing classes. */
|
|
67
|
+
export const Playground: Story = {
|
|
68
|
+
...src(`<Image
|
|
69
|
+
base64={chatIconBase64}
|
|
70
|
+
mediaType="image/svg+xml"
|
|
71
|
+
alt="Compact gradient chat icon"
|
|
72
|
+
class="h-24 w-24 rounded-md"
|
|
73
|
+
/>`),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Basic: Story = {
|
|
77
|
+
args: { class: 'h-24 w-24 rounded-md' },
|
|
78
|
+
...src(`<Image base64={chatIconBase64} mediaType="image/svg+xml" alt="Compact gradient chat icon" class="h-24 w-24 rounded-md" />`),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const CustomSize: Story = {
|
|
82
|
+
args: { alt: 'Large preview', class: 'h-64 w-64 rounded-lg' },
|
|
83
|
+
...src(`<Image base64={chatIconBase64} mediaType="image/svg+xml" alt="Large preview" class="h-64 w-64 rounded-lg" />`),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Placeholder state shown while no `base64`/`uint8Array` source is available (showcase). */
|
|
87
|
+
export const Placeholder: Story = {
|
|
88
|
+
render: () => <Image alt="Loading image" class="h-24 w-24 rounded-md" />,
|
|
89
|
+
...src(`<Image alt="Loading image" class="h-24 w-24 rounded-md" />`),
|
|
90
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type JSX, splitProps, createSignal, createEffect, onCleanup, Show } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface GeneratedImageLike {
|
|
5
|
+
base64?: string;
|
|
6
|
+
uint8Array?: Uint8Array;
|
|
7
|
+
mediaType?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImageProps extends GeneratedImageLike {
|
|
11
|
+
alt: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getImageSrc(base64?: string, mediaType?: string): string | undefined {
|
|
16
|
+
if (base64 && mediaType) {
|
|
17
|
+
return `data:${mediaType};base64,${base64}`;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Image(props: ImageProps) {
|
|
23
|
+
const [local, rest] = splitProps(props, ['base64', 'uint8Array', 'mediaType', 'class', 'alt']);
|
|
24
|
+
const [objectUrl, setObjectUrl] = createSignal<string | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
const mediaType = () => local.mediaType ?? 'image/png';
|
|
27
|
+
|
|
28
|
+
createEffect(() => {
|
|
29
|
+
const arr = local.uint8Array;
|
|
30
|
+
const mt = mediaType();
|
|
31
|
+
if (arr && mt) {
|
|
32
|
+
const blob = new Blob([arr as BlobPart], { type: mt });
|
|
33
|
+
const url = URL.createObjectURL(blob);
|
|
34
|
+
setObjectUrl(url);
|
|
35
|
+
onCleanup(() => URL.revokeObjectURL(url));
|
|
36
|
+
} else {
|
|
37
|
+
setObjectUrl(undefined);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const src = () => getImageSrc(local.base64, mediaType()) ?? objectUrl();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Show
|
|
45
|
+
when={src()}
|
|
46
|
+
fallback={
|
|
47
|
+
<div
|
|
48
|
+
aria-label={local.alt}
|
|
49
|
+
role="img"
|
|
50
|
+
class={cn(
|
|
51
|
+
'h-auto max-w-full animate-pulse overflow-hidden rounded-md bg-muted',
|
|
52
|
+
local.class
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
<img
|
|
58
|
+
src={src()}
|
|
59
|
+
alt={local.alt}
|
|
60
|
+
class={cn('h-auto max-w-full overflow-hidden rounded-md', local.class)}
|
|
61
|
+
role="img"
|
|
62
|
+
/>
|
|
63
|
+
</Show>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Image };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { For } from 'solid-js';
|
|
3
|
+
import { Loader, type LoaderVariant, type LoaderSize } from './loader';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Loader',
|
|
7
|
+
component: Loader,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
controls: { exclude: ['use:eventListener'] },
|
|
13
|
+
description: {
|
|
14
|
+
component: [
|
|
15
|
+
'A loading indicator with twelve animated **variants** (spinners, dots, bars, terminal, and text-based) in three **sizes**.',
|
|
16
|
+
'**When to use:** while waiting on an async operation — a streaming response, a tool call, or any pending state.',
|
|
17
|
+
'**How to use:** pick a `variant` and `size`. Text variants (`text-blink`, `text-shimmer`, `loading-dots`) display the `text` prop (defaults to "Thinking").',
|
|
18
|
+
'**Placement:** inside assistant message bubbles, send-button states, empty states, or anywhere an in-progress signal is needed.',
|
|
19
|
+
].join('\n\n'),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
variant: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: [
|
|
27
|
+
'circular', 'classic', 'pulse', 'pulse-dot', 'dots', 'typing',
|
|
28
|
+
'wave', 'bars', 'terminal', 'text-blink', 'text-shimmer', 'loading-dots',
|
|
29
|
+
],
|
|
30
|
+
description: 'Animation style of the loader.',
|
|
31
|
+
table: { defaultValue: { summary: 'circular' } },
|
|
32
|
+
},
|
|
33
|
+
size: {
|
|
34
|
+
control: 'select',
|
|
35
|
+
options: ['sm', 'md', 'lg'],
|
|
36
|
+
description: 'Loader size preset.',
|
|
37
|
+
table: { defaultValue: { summary: 'md' } },
|
|
38
|
+
},
|
|
39
|
+
text: {
|
|
40
|
+
control: 'text',
|
|
41
|
+
description: 'Label shown by the text variants (`text-blink`, `text-shimmer`, `loading-dots`).',
|
|
42
|
+
table: { defaultValue: { summary: 'Thinking' } },
|
|
43
|
+
},
|
|
44
|
+
class: {
|
|
45
|
+
control: 'text',
|
|
46
|
+
description: 'Additional CSS classes for the loader element.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
args: {
|
|
50
|
+
variant: 'circular',
|
|
51
|
+
size: 'md',
|
|
52
|
+
text: 'Thinking',
|
|
53
|
+
},
|
|
54
|
+
render: (args) => <Loader {...args} />,
|
|
55
|
+
} satisfies Meta<typeof Loader>;
|
|
56
|
+
|
|
57
|
+
export default meta;
|
|
58
|
+
type Story = StoryObj<typeof meta>;
|
|
59
|
+
|
|
60
|
+
const IMPORT = `import { Loader } from '@kitnai/chat';`;
|
|
61
|
+
const src = (code: string) => ({
|
|
62
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** Interactive playground — switch variant, size, and text to explore every loader. */
|
|
66
|
+
export const Playground: Story = {
|
|
67
|
+
...src(`<Loader variant="circular" size="md" />`),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Classic: Story = {
|
|
71
|
+
args: { variant: 'classic' },
|
|
72
|
+
...src(`<Loader variant="classic" size="md" />`),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Pulse: Story = {
|
|
76
|
+
args: { variant: 'pulse' },
|
|
77
|
+
...src(`<Loader variant="pulse" size="md" />`),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const PulseDot: Story = {
|
|
81
|
+
args: { variant: 'pulse-dot' },
|
|
82
|
+
...src(`<Loader variant="pulse-dot" size="md" />`),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Dots: Story = {
|
|
86
|
+
args: { variant: 'dots' },
|
|
87
|
+
...src(`<Loader variant="dots" size="md" />`),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const Typing: Story = {
|
|
91
|
+
args: { variant: 'typing' },
|
|
92
|
+
...src(`<Loader variant="typing" size="md" />`),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const Wave: Story = {
|
|
96
|
+
args: { variant: 'wave' },
|
|
97
|
+
...src(`<Loader variant="wave" size="md" />`),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Bars: Story = {
|
|
101
|
+
args: { variant: 'bars' },
|
|
102
|
+
...src(`<Loader variant="bars" size="md" />`),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Terminal: Story = {
|
|
106
|
+
args: { variant: 'terminal' },
|
|
107
|
+
...src(`<Loader variant="terminal" size="md" />`),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const TextBlink: Story = {
|
|
111
|
+
args: { variant: 'text-blink', text: 'Thinking' },
|
|
112
|
+
...src(`<Loader variant="text-blink" text="Thinking" size="md" />`),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const TextShimmer: Story = {
|
|
116
|
+
args: { variant: 'text-shimmer', text: 'Analyzing' },
|
|
117
|
+
...src(`<Loader variant="text-shimmer" text="Analyzing" size="md" />`),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const LoadingDots: Story = {
|
|
121
|
+
args: { variant: 'loading-dots', text: 'Processing' },
|
|
122
|
+
...src(`<Loader variant="loading-dots" text="Processing" size="md" />`),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const allVariants: LoaderVariant[] = [
|
|
126
|
+
'circular', 'classic', 'pulse', 'pulse-dot', 'dots', 'typing',
|
|
127
|
+
'wave', 'bars', 'terminal', 'text-blink', 'text-shimmer', 'loading-dots',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const allSizes: LoaderSize[] = ['sm', 'md', 'lg'];
|
|
131
|
+
|
|
132
|
+
/** Every variant across every size (showcase — not driven by controls). */
|
|
133
|
+
export const AllVariantsGrid: Story = {
|
|
134
|
+
render: () => (
|
|
135
|
+
<div class="space-y-6">
|
|
136
|
+
<For each={allVariants}>
|
|
137
|
+
{(variant) => (
|
|
138
|
+
<div class="flex items-center gap-6">
|
|
139
|
+
<span class="w-28 text-sm text-muted-foreground font-mono">{variant}</span>
|
|
140
|
+
<For each={allSizes}>
|
|
141
|
+
{(size) => (
|
|
142
|
+
<div class="flex items-center justify-center w-24 h-10">
|
|
143
|
+
<Loader
|
|
144
|
+
variant={variant}
|
|
145
|
+
size={size}
|
|
146
|
+
text={['text-blink', 'text-shimmer', 'loading-dots'].includes(variant) ? 'Loading' : undefined}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</For>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</For>
|
|
154
|
+
</div>
|
|
155
|
+
),
|
|
156
|
+
...src(`<Loader variant="circular" size="sm" />
|
|
157
|
+
<Loader variant="circular" size="md" />
|
|
158
|
+
<Loader variant="circular" size="lg" />`),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** A single variant rendered at all three sizes (showcase). */
|
|
162
|
+
export const AllSizes: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<div class="flex items-end gap-6">
|
|
165
|
+
<div class="text-center space-y-2">
|
|
166
|
+
<Loader variant="circular" size="sm" />
|
|
167
|
+
<p class="text-xs text-muted-foreground">Small</p>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="text-center space-y-2">
|
|
170
|
+
<Loader variant="circular" size="md" />
|
|
171
|
+
<p class="text-xs text-muted-foreground">Medium</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="text-center space-y-2">
|
|
174
|
+
<Loader variant="circular" size="lg" />
|
|
175
|
+
<p class="text-xs text-muted-foreground">Large</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
),
|
|
179
|
+
...src(`<Loader variant="circular" size="sm" />
|
|
180
|
+
<Loader variant="circular" size="md" />
|
|
181
|
+
<Loader variant="circular" size="lg" />`),
|
|
182
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { Switch, Match, For, Show } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export type LoaderVariant =
|
|
5
|
+
| 'circular'
|
|
6
|
+
| 'classic'
|
|
7
|
+
| 'pulse'
|
|
8
|
+
| 'pulse-dot'
|
|
9
|
+
| 'dots'
|
|
10
|
+
| 'typing'
|
|
11
|
+
| 'wave'
|
|
12
|
+
| 'bars'
|
|
13
|
+
| 'terminal'
|
|
14
|
+
| 'text-blink'
|
|
15
|
+
| 'text-shimmer'
|
|
16
|
+
| 'loading-dots';
|
|
17
|
+
|
|
18
|
+
export type LoaderSize = 'sm' | 'md' | 'lg';
|
|
19
|
+
|
|
20
|
+
export interface LoaderProps {
|
|
21
|
+
variant?: LoaderVariant;
|
|
22
|
+
size?: LoaderSize;
|
|
23
|
+
text?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- CircularLoader ---
|
|
28
|
+
|
|
29
|
+
export function CircularLoader(props: { class?: string; size?: LoaderSize }) {
|
|
30
|
+
const size = () => props.size ?? 'md';
|
|
31
|
+
const sizeClasses = { sm: 'size-4', md: 'size-5', lg: 'size-6' };
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
class={cn(
|
|
35
|
+
'border-primary animate-spin rounded-full border-2 border-t-transparent',
|
|
36
|
+
sizeClasses[size()],
|
|
37
|
+
props.class
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<span class="sr-only">Loading</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- ClassicLoader ---
|
|
46
|
+
|
|
47
|
+
export function ClassicLoader(props: { class?: string; size?: LoaderSize }) {
|
|
48
|
+
const size = () => props.size ?? 'md';
|
|
49
|
+
const sizeClasses = { sm: 'size-4', md: 'size-5', lg: 'size-6' };
|
|
50
|
+
const barSizes = {
|
|
51
|
+
sm: { height: '6px', width: '1.5px' },
|
|
52
|
+
md: { height: '8px', width: '2px' },
|
|
53
|
+
lg: { height: '10px', width: '2.5px' },
|
|
54
|
+
};
|
|
55
|
+
return (
|
|
56
|
+
<div class={cn('relative', sizeClasses[size()], props.class)}>
|
|
57
|
+
<div class="absolute h-full w-full">
|
|
58
|
+
<For each={Array.from({ length: 12 }, (_, i) => i)}>
|
|
59
|
+
{(i) => (
|
|
60
|
+
<div
|
|
61
|
+
class="bg-primary absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full"
|
|
62
|
+
style={{
|
|
63
|
+
top: '0',
|
|
64
|
+
left: '50%',
|
|
65
|
+
'margin-left': size() === 'sm' ? '-0.75px' : size() === 'lg' ? '-1.25px' : '-1px',
|
|
66
|
+
'transform-origin': `${size() === 'sm' ? '0.75px' : size() === 'lg' ? '1.25px' : '1px'} ${size() === 'sm' ? '10px' : size() === 'lg' ? '14px' : '12px'}`,
|
|
67
|
+
transform: `rotate(${i * 30}deg)`,
|
|
68
|
+
opacity: 0,
|
|
69
|
+
'animation-delay': `${i * 0.1}s`,
|
|
70
|
+
height: barSizes[size()].height,
|
|
71
|
+
width: barSizes[size()].width,
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
</For>
|
|
76
|
+
</div>
|
|
77
|
+
<span class="sr-only">Loading</span>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- PulseLoader ---
|
|
83
|
+
|
|
84
|
+
export function PulseLoader(props: { class?: string; size?: LoaderSize }) {
|
|
85
|
+
const size = () => props.size ?? 'md';
|
|
86
|
+
const sizeClasses = { sm: 'size-4', md: 'size-5', lg: 'size-6' };
|
|
87
|
+
return (
|
|
88
|
+
<div class={cn('relative', sizeClasses[size()], props.class)}>
|
|
89
|
+
<div class="border-primary absolute inset-0 animate-[thin-pulse_1.5s_ease-in-out_infinite] rounded-full border-2" />
|
|
90
|
+
<span class="sr-only">Loading</span>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- PulseDotLoader ---
|
|
96
|
+
|
|
97
|
+
export function PulseDotLoader(props: { class?: string; size?: LoaderSize }) {
|
|
98
|
+
const size = () => props.size ?? 'md';
|
|
99
|
+
const sizeClasses = { sm: 'size-1', md: 'size-2', lg: 'size-3' };
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
class={cn(
|
|
103
|
+
'bg-primary animate-[pulse-dot_1.2s_ease-in-out_infinite] rounded-full',
|
|
104
|
+
sizeClasses[size()],
|
|
105
|
+
props.class
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<span class="sr-only">Loading</span>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- DotsLoader ---
|
|
114
|
+
|
|
115
|
+
export function DotsLoader(props: { class?: string; size?: LoaderSize }) {
|
|
116
|
+
const size = () => props.size ?? 'md';
|
|
117
|
+
const dotSizes = { sm: 'h-1.5 w-1.5', md: 'h-2 w-2', lg: 'h-2.5 w-2.5' };
|
|
118
|
+
const containerSizes = { sm: 'h-4', md: 'h-5', lg: 'h-6' };
|
|
119
|
+
return (
|
|
120
|
+
<div class={cn('flex items-center space-x-1', containerSizes[size()], props.class)}>
|
|
121
|
+
<For each={[0, 1, 2]}>
|
|
122
|
+
{(i) => (
|
|
123
|
+
<div
|
|
124
|
+
class={cn(
|
|
125
|
+
'bg-primary animate-[bounce-dots_1.4s_ease-in-out_infinite] rounded-full',
|
|
126
|
+
dotSizes[size()]
|
|
127
|
+
)}
|
|
128
|
+
style={{ 'animation-delay': `${i * 160}ms` }}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</For>
|
|
132
|
+
<span class="sr-only">Loading</span>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- TypingLoader ---
|
|
138
|
+
|
|
139
|
+
export function TypingLoader(props: { class?: string; size?: LoaderSize }) {
|
|
140
|
+
const size = () => props.size ?? 'md';
|
|
141
|
+
const dotSizes = { sm: 'h-1 w-1', md: 'h-1.5 w-1.5', lg: 'h-2 w-2' };
|
|
142
|
+
const containerSizes = { sm: 'h-4', md: 'h-5', lg: 'h-6' };
|
|
143
|
+
return (
|
|
144
|
+
<div class={cn('flex items-center space-x-1', containerSizes[size()], props.class)}>
|
|
145
|
+
<For each={[0, 1, 2]}>
|
|
146
|
+
{(i) => (
|
|
147
|
+
<div
|
|
148
|
+
class={cn(
|
|
149
|
+
'bg-primary animate-[typing_1s_infinite] rounded-full',
|
|
150
|
+
dotSizes[size()]
|
|
151
|
+
)}
|
|
152
|
+
style={{ 'animation-delay': `${i * 250}ms` }}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</For>
|
|
156
|
+
<span class="sr-only">Loading</span>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- WaveLoader ---
|
|
162
|
+
|
|
163
|
+
export function WaveLoader(props: { class?: string; size?: LoaderSize }) {
|
|
164
|
+
const size = () => props.size ?? 'md';
|
|
165
|
+
const barWidths = { sm: 'w-0.5', md: 'w-0.5', lg: 'w-1' };
|
|
166
|
+
const containerSizes = { sm: 'h-4', md: 'h-5', lg: 'h-6' };
|
|
167
|
+
const heights: Record<LoaderSize, string[]> = {
|
|
168
|
+
sm: ['6px', '9px', '12px', '9px', '6px'],
|
|
169
|
+
md: ['8px', '12px', '16px', '12px', '8px'],
|
|
170
|
+
lg: ['10px', '15px', '20px', '15px', '10px'],
|
|
171
|
+
};
|
|
172
|
+
return (
|
|
173
|
+
<div class={cn('flex items-center gap-0.5', containerSizes[size()], props.class)}>
|
|
174
|
+
<For each={[0, 1, 2, 3, 4]}>
|
|
175
|
+
{(i) => (
|
|
176
|
+
<div
|
|
177
|
+
class={cn(
|
|
178
|
+
'bg-primary animate-[wave_1s_ease-in-out_infinite] rounded-full',
|
|
179
|
+
barWidths[size()]
|
|
180
|
+
)}
|
|
181
|
+
style={{
|
|
182
|
+
'animation-delay': `${i * 100}ms`,
|
|
183
|
+
height: heights[size()][i],
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
</For>
|
|
188
|
+
<span class="sr-only">Loading</span>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- BarsLoader ---
|
|
194
|
+
|
|
195
|
+
export function BarsLoader(props: { class?: string; size?: LoaderSize }) {
|
|
196
|
+
const size = () => props.size ?? 'md';
|
|
197
|
+
const barWidths = { sm: 'w-1', md: 'w-1.5', lg: 'w-2' };
|
|
198
|
+
const containerSizes = { sm: 'h-4 gap-1', md: 'h-5 gap-1.5', lg: 'h-6 gap-2' };
|
|
199
|
+
return (
|
|
200
|
+
<div class={cn('flex', containerSizes[size()], props.class)}>
|
|
201
|
+
<For each={[0, 1, 2]}>
|
|
202
|
+
{(i) => (
|
|
203
|
+
<div
|
|
204
|
+
class={cn(
|
|
205
|
+
'bg-primary h-full animate-[wave-bars_1.2s_ease-in-out_infinite]',
|
|
206
|
+
barWidths[size()]
|
|
207
|
+
)}
|
|
208
|
+
style={{ 'animation-delay': `${i * 0.2}s` }}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
</For>
|
|
212
|
+
<span class="sr-only">Loading</span>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- TerminalLoader ---
|
|
218
|
+
|
|
219
|
+
export function TerminalLoader(props: { class?: string; size?: LoaderSize }) {
|
|
220
|
+
const size = () => props.size ?? 'md';
|
|
221
|
+
const cursorSizes = { sm: 'h-3 w-1.5', md: 'h-4 w-2', lg: 'h-5 w-2.5' };
|
|
222
|
+
const textSizes = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
|
|
223
|
+
const containerSizes = { sm: 'h-4', md: 'h-5', lg: 'h-6' };
|
|
224
|
+
return (
|
|
225
|
+
<div class={cn('flex items-center space-x-1', containerSizes[size()], props.class)}>
|
|
226
|
+
<span class={cn('text-primary font-mono', textSizes[size()])}>{'>'}</span>
|
|
227
|
+
<div class={cn('bg-primary animate-[blink_1s_step-end_infinite]', cursorSizes[size()])} />
|
|
228
|
+
<span class="sr-only">Loading</span>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- TextBlinkLoader ---
|
|
234
|
+
|
|
235
|
+
export function TextBlinkLoader(props: { text?: string; class?: string; size?: LoaderSize }) {
|
|
236
|
+
const size = () => props.size ?? 'md';
|
|
237
|
+
const textSizes = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
class={cn(
|
|
241
|
+
'animate-[text-blink_2s_ease-in-out_infinite] font-medium',
|
|
242
|
+
textSizes[size()],
|
|
243
|
+
props.class
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
{props.text ?? 'Thinking'}
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- TextShimmerLoader ---
|
|
252
|
+
|
|
253
|
+
export function TextShimmerLoader(props: { text?: string; class?: string; size?: LoaderSize }) {
|
|
254
|
+
const size = () => props.size ?? 'md';
|
|
255
|
+
const textSizes = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
|
|
256
|
+
return (
|
|
257
|
+
<div
|
|
258
|
+
class={cn(
|
|
259
|
+
'bg-[linear-gradient(to_right,var(--color-muted-foreground)_40%,var(--color-foreground)_60%,var(--color-muted-foreground)_80%)]',
|
|
260
|
+
'bg-size-[200%_auto] bg-clip-text font-medium text-transparent',
|
|
261
|
+
'animate-[shimmer_4s_infinite_linear]',
|
|
262
|
+
textSizes[size()],
|
|
263
|
+
props.class
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
{props.text ?? 'Thinking'}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- TextDotsLoader ---
|
|
272
|
+
|
|
273
|
+
export function TextDotsLoader(props: { text?: string; class?: string; size?: LoaderSize }) {
|
|
274
|
+
const size = () => props.size ?? 'md';
|
|
275
|
+
const textSizes = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
|
|
276
|
+
return (
|
|
277
|
+
<div class={cn('inline-flex items-center', props.class)}>
|
|
278
|
+
<span class={cn('text-primary font-medium', textSizes[size()])}>
|
|
279
|
+
{props.text ?? 'Thinking'}
|
|
280
|
+
</span>
|
|
281
|
+
<span class="inline-flex">
|
|
282
|
+
<span class="text-primary animate-[loading-dots_1.4s_infinite_0.2s]">.</span>
|
|
283
|
+
<span class="text-primary animate-[loading-dots_1.4s_infinite_0.4s]">.</span>
|
|
284
|
+
<span class="text-primary animate-[loading-dots_1.4s_infinite_0.6s]">.</span>
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Loader (variant dispatcher) ---
|
|
291
|
+
|
|
292
|
+
export function Loader(props: LoaderProps) {
|
|
293
|
+
return (
|
|
294
|
+
<Switch fallback={<CircularLoader size={props.size} class={props.class} />}>
|
|
295
|
+
<Match when={props.variant === 'circular'}>
|
|
296
|
+
<CircularLoader size={props.size} class={props.class} />
|
|
297
|
+
</Match>
|
|
298
|
+
<Match when={props.variant === 'classic'}>
|
|
299
|
+
<ClassicLoader size={props.size} class={props.class} />
|
|
300
|
+
</Match>
|
|
301
|
+
<Match when={props.variant === 'pulse'}>
|
|
302
|
+
<PulseLoader size={props.size} class={props.class} />
|
|
303
|
+
</Match>
|
|
304
|
+
<Match when={props.variant === 'pulse-dot'}>
|
|
305
|
+
<PulseDotLoader size={props.size} class={props.class} />
|
|
306
|
+
</Match>
|
|
307
|
+
<Match when={props.variant === 'dots'}>
|
|
308
|
+
<DotsLoader size={props.size} class={props.class} />
|
|
309
|
+
</Match>
|
|
310
|
+
<Match when={props.variant === 'typing'}>
|
|
311
|
+
<TypingLoader size={props.size} class={props.class} />
|
|
312
|
+
</Match>
|
|
313
|
+
<Match when={props.variant === 'wave'}>
|
|
314
|
+
<WaveLoader size={props.size} class={props.class} />
|
|
315
|
+
</Match>
|
|
316
|
+
<Match when={props.variant === 'bars'}>
|
|
317
|
+
<BarsLoader size={props.size} class={props.class} />
|
|
318
|
+
</Match>
|
|
319
|
+
<Match when={props.variant === 'terminal'}>
|
|
320
|
+
<TerminalLoader size={props.size} class={props.class} />
|
|
321
|
+
</Match>
|
|
322
|
+
<Match when={props.variant === 'text-blink'}>
|
|
323
|
+
<TextBlinkLoader text={props.text} size={props.size} class={props.class} />
|
|
324
|
+
</Match>
|
|
325
|
+
<Match when={props.variant === 'text-shimmer'}>
|
|
326
|
+
<TextShimmerLoader text={props.text} size={props.size} class={props.class} />
|
|
327
|
+
</Match>
|
|
328
|
+
<Match when={props.variant === 'loading-dots'}>
|
|
329
|
+
<TextDotsLoader text={props.text} size={props.size} class={props.class} />
|
|
330
|
+
</Match>
|
|
331
|
+
</Switch>
|
|
332
|
+
);
|
|
333
|
+
}
|