@kitn.ai/ui 0.14.1
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 +394 -0
- package/dist/bash-InADTalH.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -0
- package/dist/css-M7EaDHN_.js +1 -0
- package/dist/custom-elements.json +4739 -0
- package/dist/engine-javascript-vq0WuIJl.js +141 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -0
- package/dist/github-light-JYsPkUQd.js +1 -0
- package/dist/host-embed.d.ts +33 -0
- package/dist/html-CPZ3oZQ7.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/javascript-C25yR2R2.js +1 -0
- package/dist/json-DxJze_jm.js +1 -0
- package/dist/kitn-chat-provider.d.ts +2 -0
- package/dist/kitn-chat-provider.es.js +240 -0
- package/dist/kitn-chat.es.js +77 -0
- package/dist/llms/llms-full.txt +1018 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/origin.d.ts +5 -0
- package/dist/provider-runtime.d.ts +27 -0
- package/dist/schemas/card-envelope.schema.json +14 -0
- package/dist/schemas/card-event.schema.json +12 -0
- package/dist/schemas/choice.schema.json +70 -0
- package/dist/schemas/confirm.schema.json +65 -0
- package/dist/schemas/embed.schema.json +65 -0
- package/dist/schemas/form.result.schema.json +7 -0
- package/dist/schemas/form.schema.json +33 -0
- package/dist/schemas/link.schema.json +56 -0
- package/dist/schemas/tasks.result.schema.json +16 -0
- package/dist/schemas/tasks.schema.json +78 -0
- package/dist/svelte--5p79yCD.js +1 -0
- package/dist/theme.tokens.css +176 -0
- package/dist/tsx-B8rCNbgL.js +1 -0
- package/dist/typescript-RycA9KXf.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/version.d.ts +4 -0
- package/dist/vue-BmIZj4XD.js +1 -0
- package/dist/wire.d.ts +53 -0
- package/frameworks/react/index.tsx +948 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +1018 -0
- package/llms.txt +104 -0
- package/package.json +141 -0
- package/src/components/artifact.stories.tsx +138 -0
- package/src/components/artifact.tsx +743 -0
- package/src/components/attachments.stories.tsx +303 -0
- package/src/components/attachments.test.tsx +64 -0
- package/src/components/attachments.tsx +399 -0
- package/src/components/card-fallback.tsx +28 -0
- package/src/components/card-renderer.tsx +52 -0
- package/src/components/card.tsx +110 -0
- package/src/components/chain-of-thought.stories.tsx +211 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +187 -0
- package/src/components/chat-container.tsx +82 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/chat-thread.test.tsx +58 -0
- package/src/components/chat-thread.tsx +249 -0
- package/src/components/checkpoint.stories.tsx +102 -0
- package/src/components/checkpoint.tsx +88 -0
- package/src/components/choice-card.tsx +569 -0
- package/src/components/code-block.stories.tsx +150 -0
- package/src/components/code-block.tsx +105 -0
- package/src/components/component-meta.json +3735 -0
- package/src/components/confirm-card.stories.tsx +74 -0
- package/src/components/confirm-card.tsx +315 -0
- package/src/components/context.stories.tsx +229 -0
- package/src/components/context.test.tsx +133 -0
- package/src/components/context.tsx +363 -0
- package/src/components/conversation-item.stories.tsx +125 -0
- package/src/components/conversation-item.tsx +16 -0
- package/src/components/conversation-list.stories.tsx +133 -0
- package/src/components/conversation-list.tsx +101 -0
- package/src/components/default-input.test.tsx +111 -0
- package/src/components/embed.tsx +196 -0
- package/src/components/empty.stories.tsx +434 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +138 -0
- package/src/components/feedback-bar.test.tsx +103 -0
- package/src/components/feedback-bar.tsx +166 -0
- package/src/components/file-tree.stories.tsx +73 -0
- package/src/components/file-tree.tsx +383 -0
- package/src/components/file-upload.stories.tsx +156 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/form-widgets.tsx +461 -0
- package/src/components/form.tsx +873 -0
- package/src/components/image.stories.tsx +89 -0
- package/src/components/image.tsx +67 -0
- package/src/components/link-preview.tsx +194 -0
- package/src/components/loader.stories.tsx +181 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +180 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-action-bar.test.tsx +128 -0
- package/src/components/message-narrow.stories.tsx +329 -0
- package/src/components/message-skills.stories.tsx +211 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +281 -0
- package/src/components/message.tsx +227 -0
- package/src/components/model-switcher.stories.tsx +123 -0
- package/src/components/model-switcher.test.tsx +65 -0
- package/src/components/model-switcher.tsx +87 -0
- package/src/components/prompt-input.stories.tsx +222 -0
- package/src/components/prompt-input.tsx +211 -0
- package/src/components/prompt-suggestion.stories.tsx +142 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +140 -0
- package/src/components/reasoning.tsx +156 -0
- package/src/components/resizable.test.tsx +226 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +100 -0
- package/src/components/scroll-button.tsx +34 -0
- package/src/components/slash-command.stories.tsx +163 -0
- package/src/components/slash-command.tsx +232 -0
- package/src/components/source-list.test.tsx +152 -0
- package/src/components/source.stories.tsx +124 -0
- package/src/components/source.tsx +129 -0
- package/src/components/tasks-card.stories.tsx +78 -0
- package/src/components/tasks-card.tsx +432 -0
- package/src/components/text-shimmer.stories.tsx +87 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +87 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +153 -0
- package/src/components/tool.tsx +184 -0
- package/src/components/use-card-resolution.ts +28 -0
- package/src/components/voice-input.stories.tsx +83 -0
- package/src/components/voice-input.tsx +107 -0
- package/src/elements/artifact.stories.tsx +506 -0
- package/src/elements/artifact.tsx +137 -0
- package/src/elements/attachments.stories.tsx +190 -0
- package/src/elements/attachments.tsx +136 -0
- package/src/elements/card.stories.tsx +113 -0
- package/src/elements/card.tsx +40 -0
- package/src/elements/cards.stories.tsx +59 -0
- package/src/elements/cards.tsx +97 -0
- package/src/elements/catalog.stories.tsx +491 -0
- package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
- package/src/elements/chain-of-thought.stories.tsx +113 -0
- package/src/elements/chain-of-thought.tsx +92 -0
- package/src/elements/chat-scope-picker.stories.tsx +73 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-types.ts +42 -0
- package/src/elements/chat-workspace.stories.tsx +288 -0
- package/src/elements/chat-workspace.tsx +161 -0
- package/src/elements/chat.stories.tsx +151 -0
- package/src/elements/chat.tsx +80 -0
- package/src/elements/checkpoint.stories.tsx +72 -0
- package/src/elements/checkpoint.tsx +47 -0
- package/src/elements/choice.stories.tsx +240 -0
- package/src/elements/choice.tsx +48 -0
- package/src/elements/code-block.stories.tsx +83 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/composed-shell.stories.tsx +316 -0
- package/src/elements/confirm-card.stories.tsx +227 -0
- package/src/elements/confirm-card.tsx +51 -0
- package/src/elements/context-meter.stories.tsx +151 -0
- package/src/elements/context-meter.tsx +121 -0
- package/src/elements/conversation-list.declarative.test.tsx +137 -0
- package/src/elements/conversation-list.stories.tsx +243 -0
- package/src/elements/conversation-list.tsx +80 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +247 -0
- package/src/elements/define.tsx +171 -0
- package/src/elements/element-meta.json +3314 -0
- package/src/elements/element-types.d.ts +710 -0
- package/src/elements/embed.stories.tsx +213 -0
- package/src/elements/embed.tsx +35 -0
- package/src/elements/empty.stories.tsx +111 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.stories.tsx +114 -0
- package/src/elements/feedback-bar.tsx +61 -0
- package/src/elements/file-tree.stories.tsx +134 -0
- package/src/elements/file-tree.tsx +52 -0
- package/src/elements/file-upload.stories.tsx +82 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/form.stories.tsx +248 -0
- package/src/elements/form.tsx +43 -0
- package/src/elements/framework-usage.json +563 -0
- package/src/elements/image.stories.tsx +71 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/link-preview.stories.tsx +209 -0
- package/src/elements/link-preview.tsx +34 -0
- package/src/elements/loader.stories.tsx +88 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.stories.tsx +76 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.declarative.test.tsx +104 -0
- package/src/elements/message-skills.stories.tsx +114 -0
- package/src/elements/message-skills.tsx +85 -0
- package/src/elements/message.stories.tsx +218 -0
- package/src/elements/message.tsx +162 -0
- package/src/elements/model-switcher.declarative.test.tsx +130 -0
- package/src/elements/model-switcher.stories.tsx +198 -0
- package/src/elements/model-switcher.tsx +93 -0
- package/src/elements/popover.stories.tsx +80 -0
- package/src/elements/popover.tsx +59 -0
- package/src/elements/prompt-input-slash-command.test.tsx +128 -0
- package/src/elements/prompt-input-stoppable.test.tsx +88 -0
- package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
- package/src/elements/prompt-input.stories.tsx +331 -0
- package/src/elements/prompt-input.tsx +185 -0
- package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
- package/src/elements/prompt-suggestions.stories.tsx +213 -0
- package/src/elements/prompt-suggestions.tsx +94 -0
- package/src/elements/reasoning.stories.tsx +77 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +56 -0
- package/src/elements/remote.stories.tsx +314 -0
- package/src/elements/remote.tsx +221 -0
- package/src/elements/resizable.d.ts +27 -0
- package/src/elements/resizable.stories.tsx +426 -0
- package/src/elements/resizable.tsx +553 -0
- package/src/elements/response-stream.stories.tsx +79 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/scroll-button.stories.tsx +202 -0
- package/src/elements/scroll-button.test.tsx +148 -0
- package/src/elements/scroll-button.tsx +126 -0
- package/src/elements/source-list.stories.tsx +143 -0
- package/src/elements/source.stories.tsx +88 -0
- package/src/elements/source.tsx +120 -0
- package/src/elements/styles.css +199 -0
- package/src/elements/switch.stories.tsx +56 -0
- package/src/elements/switch.tsx +43 -0
- package/src/elements/tasks.stories.tsx +237 -0
- package/src/elements/tasks.tsx +46 -0
- package/src/elements/text-shimmer.stories.tsx +64 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.stories.tsx +72 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.stories.tsx +89 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.stories.tsx +88 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +253 -0
- package/src/primitives/card-contract.ts +71 -0
- package/src/primitives/card-host.tsx +35 -0
- package/src/primitives/card-registry.tsx +67 -0
- package/src/primitives/card-resolution.ts +36 -0
- package/src/primitives/card-routing.ts +79 -0
- package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
- package/src/primitives/card-schemas/card-event.schema.json +12 -0
- package/src/primitives/card-schemas/choice.schema.json +70 -0
- package/src/primitives/card-schemas/confirm.schema.json +65 -0
- package/src/primitives/card-schemas/embed.schema.json +65 -0
- package/src/primitives/card-schemas/form.result.schema.json +7 -0
- package/src/primitives/card-schemas/form.schema.json +33 -0
- package/src/primitives/card-schemas/link.schema.json +56 -0
- package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
- package/src/primitives/card-schemas/tasks.schema.json +78 -0
- package/src/primitives/card-validate.ts +95 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/controllable.test.ts +47 -0
- package/src/primitives/controllable.ts +21 -0
- package/src/primitives/embed-providers.ts +254 -0
- package/src/primitives/highlighter.ts +157 -0
- package/src/primitives/link-preview.ts +87 -0
- package/src/primitives/pdf-preview.ts +121 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-resize-observer.ts +12 -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/remote/host-embed.ts +345 -0
- package/src/remote/index.ts +2 -0
- package/src/remote/origin.ts +30 -0
- package/src/remote/provider-runtime.ts +259 -0
- package/src/remote/provider.ts +2 -0
- package/src/remote/validate.ts +22 -0
- package/src/remote/version.ts +12 -0
- package/src/remote/wire.ts +48 -0
- package/src/stories/chat-panel-layout.stories.tsx +145 -0
- package/src/stories/chat-scene.tsx +571 -0
- package/src/stories/checkpoint-restore.stories.tsx +256 -0
- package/src/stories/context-usage.stories.tsx +212 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
- package/src/stories/conversation-with-sources.stories.tsx +471 -0
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ChildElements.mdx +269 -0
- package/src/stories/docs/ForAIAgents.mdx +162 -0
- package/src/stories/docs/GettingStarted.mdx +65 -0
- package/src/stories/docs/Installation.mdx +83 -0
- package/src/stories/docs/Introduction.mdx +50 -0
- package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
- package/src/stories/docs/Theming.mdx +85 -0
- package/src/stories/docs/element-controls.ts +121 -0
- package/src/stories/docs/frameworks/Angular.mdx +245 -0
- package/src/stories/docs/frameworks/Html.mdx +227 -0
- package/src/stories/docs/frameworks/Overview.mdx +47 -0
- package/src/stories/docs/frameworks/React.mdx +227 -0
- package/src/stories/docs/frameworks/Solid.mdx +173 -0
- package/src/stories/docs/frameworks/Svelte.mdx +194 -0
- package/src/stories/docs/frameworks/Vue.mdx +238 -0
- package/src/stories/docs/generative-ui-overview.mdx +186 -0
- package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
- package/src/stories/docs/recipes/Streaming.mdx +68 -0
- package/src/stories/docs/recipes/TextToSpeech.mdx +35 -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 +146 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/examples/ChoosingComponents.mdx +106 -0
- package/src/stories/examples/sample-data.ts +79 -0
- package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
- package/src/stories/examples/usage/context-usage.ts +591 -0
- package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
- package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
- package/src/stories/examples/usage/empty-state.ts +144 -0
- package/src/stories/examples/usage/full-chat-app.ts +277 -0
- package/src/stories/examples/usage/index.ts +63 -0
- package/src/stories/examples/usage/message-actions.ts +704 -0
- package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
- package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
- package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
- package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
- package/src/stories/examples/usage/streaming-response.ts +687 -0
- package/src/stories/examples/usage/types.ts +33 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +230 -0
- 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/prompt-input-variants.stories.tsx +456 -0
- package/src/stories/streaming-response.stories.tsx +438 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/types.ts +47 -0
- package/src/ui/action-icons.ts +52 -0
- package/src/ui/avatar.stories.tsx +103 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +86 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +145 -0
- package/src/ui/button.tsx +38 -0
- package/src/ui/collapsible.stories.tsx +69 -0
- package/src/ui/collapsible.tsx +125 -0
- package/src/ui/dropdown.stories.tsx +59 -0
- package/src/ui/dropdown.tsx +196 -0
- package/src/ui/hover-card.stories.tsx +77 -0
- package/src/ui/hover-card.test.tsx +30 -0
- package/src/ui/hover-card.tsx +172 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +158 -0
- package/src/ui/popover.stories.tsx +81 -0
- package/src/ui/popover.test.tsx +99 -0
- package/src/ui/popover.tsx +92 -0
- package/src/ui/resizable.stories.tsx +236 -0
- package/src/ui/resizable.tsx +576 -0
- package/src/ui/scroll-area.stories.tsx +50 -0
- package/src/ui/scroll-area.tsx +15 -0
- package/src/ui/separator.stories.tsx +81 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +337 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/switch.stories.tsx +51 -0
- package/src/ui/switch.test.tsx +59 -0
- package/src/ui/switch.tsx +62 -0
- package/src/ui/textarea.stories.tsx +76 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +74 -0
- package/src/ui/tooltip.tsx +68 -0
- package/src/utils/cn.ts +24 -0
- package/theme.css +187 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import '@testing-library/jest-dom/vitest';
|
|
3
|
+
import { render, cleanup } from '@solidjs/testing-library';
|
|
4
|
+
import { ChatThread } from './chat-thread';
|
|
5
|
+
|
|
6
|
+
// jsdom doesn't implement Element.scrollTo; the auto-scroll container calls it.
|
|
7
|
+
if (!Element.prototype.scrollTo) (Element.prototype as unknown as { scrollTo: () => void }).scrollTo = () => {};
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
describe('ChatThread header composition', () => {
|
|
12
|
+
it('hides the header with no title, models, context, or header slot', () => {
|
|
13
|
+
const { container } = render(() => <ChatThread messages={[]} />);
|
|
14
|
+
expect(container.querySelector('header')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('shows the header when only header-start content is present', () => {
|
|
18
|
+
const { container } = render(() => <ChatThread messages={[]} headerStart />);
|
|
19
|
+
expect(container.querySelector('header')).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders header-start and header-end slots inside the header', () => {
|
|
23
|
+
const { container } = render(() => <ChatThread messages={[]} headerStart headerEnd />);
|
|
24
|
+
expect(container.querySelector('header slot[name="header-start"]')).toBeTruthy();
|
|
25
|
+
expect(container.querySelector('header slot[name="header-end"]')).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('still shows the header for a chat title (back-compat)', () => {
|
|
29
|
+
const { container, getByText } = render(() => <ChatThread messages={[]} chatTitle="Assistant" />);
|
|
30
|
+
expect(container.querySelector('header')).toBeTruthy();
|
|
31
|
+
expect(getByText('Assistant')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('ChatThread suggestions gating', () => {
|
|
36
|
+
const SUGGESTIONS = ['What can you do?', 'Tell me a joke'];
|
|
37
|
+
const oneMessage = [{ id: '1', role: 'user' as const, content: 'hi' }];
|
|
38
|
+
|
|
39
|
+
it('renders suggestions when the thread is empty', () => {
|
|
40
|
+
const { getByText } = render(() => <ChatThread messages={[]} suggestions={SUGGESTIONS} />);
|
|
41
|
+
expect(getByText('What can you do?')).toBeInTheDocument();
|
|
42
|
+
expect(getByText('Tell me a joke')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('hides suggestions once the conversation has messages (default)', () => {
|
|
46
|
+
const { queryByText } = render(() => <ChatThread messages={oneMessage} suggestions={SUGGESTIONS} />);
|
|
47
|
+
expect(queryByText('What can you do?')).toBeNull();
|
|
48
|
+
expect(queryByText('Tell me a joke')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps suggestions visible with messages when persistSuggestions is set', () => {
|
|
52
|
+
const { getByText } = render(() => (
|
|
53
|
+
<ChatThread messages={oneMessage} suggestions={SUGGESTIONS} persistSuggestions />
|
|
54
|
+
));
|
|
55
|
+
expect(getByText('What can you do?')).toBeInTheDocument();
|
|
56
|
+
expect(getByText('Tell me a joke')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createSignal, For, Show } from 'solid-js';
|
|
2
|
+
import { ChatConfig, useChatConfig } from '../primitives/chat-config';
|
|
3
|
+
import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container';
|
|
4
|
+
import { Message, MessageAvatar, MessageContent, MessageActionBar } from './message';
|
|
5
|
+
import { Reasoning, ReasoningTrigger, ReasoningContent } from './reasoning';
|
|
6
|
+
import { Tool } from './tool';
|
|
7
|
+
import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from './attachments';
|
|
8
|
+
import { ModelSwitcher } from './model-switcher';
|
|
9
|
+
import { ScrollButton } from './scroll-button';
|
|
10
|
+
import {
|
|
11
|
+
Context, ContextTrigger, ContextContent, ContextContentHeader,
|
|
12
|
+
ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage,
|
|
13
|
+
} from './context';
|
|
14
|
+
import { DefaultPromptInput } from '../elements/default-input';
|
|
15
|
+
import type { SlashCommandItem } from './slash-command';
|
|
16
|
+
import type { ChatMessage } from '../elements/chat-types';
|
|
17
|
+
import type { ProseSize } from '../primitives/chat-config';
|
|
18
|
+
import type { ModelOption } from '../types';
|
|
19
|
+
|
|
20
|
+
export interface ChatThreadContextUsage {
|
|
21
|
+
usedTokens: number;
|
|
22
|
+
maxTokens: number;
|
|
23
|
+
inputTokens?: number;
|
|
24
|
+
outputTokens?: number;
|
|
25
|
+
estimatedCost?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ChatThreadProps {
|
|
29
|
+
/** Extra classes for the thread root (e.g. `h-full`). */
|
|
30
|
+
class?: string;
|
|
31
|
+
/** The full message thread to render, newest last. Each entry carries its role,
|
|
32
|
+
* content, and optional reasoning/tools/attachments/actions. Set as a JS
|
|
33
|
+
* property (`el.messages = [...]`). */
|
|
34
|
+
messages: ChatMessage[];
|
|
35
|
+
/** Controlled value of the input. When set, the host owns the input text and
|
|
36
|
+
* must update it on `kai-value-change`; leave unset for uncontrolled behavior. */
|
|
37
|
+
value?: string;
|
|
38
|
+
/** Placeholder text shown in the empty input. */
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
/** When true, shows the loading/streaming state and disables submit (use while
|
|
41
|
+
* awaiting the assistant's reply). */
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
/** Starter prompts shown above the input when the thread is empty. Clicking one
|
|
44
|
+
* follows `suggestionMode`. Set as a JS property. */
|
|
45
|
+
suggestions?: string[];
|
|
46
|
+
/** What clicking a suggestion does: `'submit'` (default) sends it immediately
|
|
47
|
+
* as if typed and submitted; `'fill'` just places it in the input. */
|
|
48
|
+
suggestionMode?: 'submit' | 'fill';
|
|
49
|
+
/** Keep suggestions visible after the conversation starts. By default
|
|
50
|
+
* suggestions are conversation starters and hide once `messages` is
|
|
51
|
+
* non-empty; set this to keep them always shown. Default false. */
|
|
52
|
+
persistSuggestions?: boolean;
|
|
53
|
+
/** Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`).
|
|
54
|
+
* Defaults to `'sm'`. */
|
|
55
|
+
proseSize?: ProseSize;
|
|
56
|
+
/** Shiki theme name for syntax-highlighted code blocks (e.g.
|
|
57
|
+
* `'github-dark-dimmed'`). */
|
|
58
|
+
codeTheme?: string;
|
|
59
|
+
/** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
|
|
60
|
+
* `<pre>` blocks (lighter, no highlighter load). Default true. */
|
|
61
|
+
codeHighlight?: boolean;
|
|
62
|
+
/** Optional header title shown on the left of the header. */
|
|
63
|
+
chatTitle?: string;
|
|
64
|
+
/** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
|
|
65
|
+
* header and a `kai-model-change` event fires on selection. */
|
|
66
|
+
models?: ModelOption[];
|
|
67
|
+
/** The currently selected model id (pairs with `models`). */
|
|
68
|
+
currentModel?: string;
|
|
69
|
+
/** Optional context-window token usage. When set, a Context token meter is
|
|
70
|
+
* shown in the header. */
|
|
71
|
+
context?: ChatThreadContextUsage;
|
|
72
|
+
/** Show the scroll-to-bottom button inside the scroll area. Default true. */
|
|
73
|
+
scrollButton?: boolean;
|
|
74
|
+
/** Whether the host has `slot="header-start"` content (left of the title) —
|
|
75
|
+
* set by the `<kai-chat>` facade so a custom control forces the header open. */
|
|
76
|
+
headerStart?: boolean;
|
|
77
|
+
/** Whether the host has `slot="header-end"` content (right of the controls). */
|
|
78
|
+
headerEnd?: boolean;
|
|
79
|
+
/** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
|
|
80
|
+
search?: boolean;
|
|
81
|
+
/** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
|
|
82
|
+
voice?: boolean;
|
|
83
|
+
/** Slash commands — when set, typing `/` in the input opens the command
|
|
84
|
+
* palette and fires `kai-slash-select`. Set as a JS property. */
|
|
85
|
+
slashCommands?: SlashCommandItem[];
|
|
86
|
+
/** Command ids to highlight as active in the palette. */
|
|
87
|
+
slashActiveIds?: string[];
|
|
88
|
+
/** Single-line palette rows. */
|
|
89
|
+
slashCompact?: boolean;
|
|
90
|
+
/** Whether each message's action bar is always visible (`'always'`, default)
|
|
91
|
+
* or only revealed on hover of that message row (`'hover'`). */
|
|
92
|
+
actionsReveal?: 'always' | 'hover';
|
|
93
|
+
// callbacks (the facade maps these to dispatch())
|
|
94
|
+
onValueChange?: (value: string) => void;
|
|
95
|
+
onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void;
|
|
96
|
+
onSuggestionClick?: (value: string) => void;
|
|
97
|
+
onModelChange?: (modelId: string) => void;
|
|
98
|
+
onMessageAction?: (detail: { messageId: string; action: string }) => void;
|
|
99
|
+
onSearch?: () => void;
|
|
100
|
+
onVoice?: () => void;
|
|
101
|
+
onSlashSelect?: (command: SlashCommandItem) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function ChatThread(props: ChatThreadProps) {
|
|
105
|
+
const outer = useChatConfig();
|
|
106
|
+
const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
|
|
107
|
+
const [internal, setInternal] = createSignal(props.value ?? '');
|
|
108
|
+
const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
|
|
109
|
+
const current = () => props.value ?? internal();
|
|
110
|
+
const handleChange = (v: string) => { setInternal(v); props.onValueChange?.(v); };
|
|
111
|
+
const handleSubmit = () => { props.onSubmit?.({ value: current(), attachments: attachments() }); setAttachments([]); };
|
|
112
|
+
const handleSuggestionClick = (v: string) => {
|
|
113
|
+
if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
|
|
114
|
+
else { props.onSubmit?.({ value: v, attachments: attachments() }); setAttachments([]); }
|
|
115
|
+
};
|
|
116
|
+
const showHeader = () => !!(props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd);
|
|
117
|
+
// Suggestions are conversation starters: show only on an empty thread unless
|
|
118
|
+
// the host opts into persisting them.
|
|
119
|
+
const visibleSuggestions = () =>
|
|
120
|
+
props.persistSuggestions || props.messages.length === 0 ? props.suggestions : undefined;
|
|
121
|
+
const showScrollButton = () => props.scrollButton !== false;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ChatConfig proseSize={props.proseSize} codeTheme={props.codeTheme} codeHighlight={props.codeHighlight !== false} portalMount={outer.portalMount()}>
|
|
125
|
+
<div class={`flex h-full flex-col bg-background ${props.class ?? ''}`}>
|
|
126
|
+
<Show when={showHeader()}>
|
|
127
|
+
<header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
|
|
128
|
+
<div class="flex items-center gap-2">
|
|
129
|
+
{/* Consumer-injected leading controls (sidebar-toggle, compose, a
|
|
130
|
+
popover title-button). Projects light-DOM `slot="header-start"`
|
|
131
|
+
children of <kai-chat>; inert outside a shadow root. */}
|
|
132
|
+
<slot name="header-start" />
|
|
133
|
+
<Show when={props.chatTitle}>
|
|
134
|
+
<div class="text-sm font-semibold text-foreground">{props.chatTitle}</div>
|
|
135
|
+
</Show>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="flex items-center gap-2">
|
|
138
|
+
<Show when={props.models}>
|
|
139
|
+
<ModelSwitcher
|
|
140
|
+
models={props.models!}
|
|
141
|
+
currentModelId={props.currentModel ?? props.models![0]?.id ?? ''}
|
|
142
|
+
onModelChange={(modelId) => props.onModelChange?.(modelId)}
|
|
143
|
+
/>
|
|
144
|
+
</Show>
|
|
145
|
+
<Show when={props.context}>
|
|
146
|
+
<Context
|
|
147
|
+
usedTokens={props.context!.usedTokens} maxTokens={props.context!.maxTokens}
|
|
148
|
+
inputTokens={props.context!.inputTokens} outputTokens={props.context!.outputTokens}
|
|
149
|
+
estimatedCost={props.context!.estimatedCost}
|
|
150
|
+
>
|
|
151
|
+
<ContextTrigger />
|
|
152
|
+
<ContextContent>
|
|
153
|
+
<ContextContentHeader />
|
|
154
|
+
<ContextContentBody><div class="space-y-1.5"><ContextInputUsage /><ContextOutputUsage /></div></ContextContentBody>
|
|
155
|
+
<ContextContentFooter />
|
|
156
|
+
</ContextContent>
|
|
157
|
+
</Context>
|
|
158
|
+
</Show>
|
|
159
|
+
{/* Consumer-injected trailing controls (share, settings, …).
|
|
160
|
+
Projects light-DOM `slot="header-end"` children of <kai-chat>. */}
|
|
161
|
+
<slot name="header-end" />
|
|
162
|
+
</div>
|
|
163
|
+
</header>
|
|
164
|
+
</Show>
|
|
165
|
+
<div class="relative flex-1 overflow-hidden">
|
|
166
|
+
<ChatContainer class="h-full px-4 py-3">
|
|
167
|
+
<ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
|
|
168
|
+
<For each={props.messages}>
|
|
169
|
+
{(m) => {
|
|
170
|
+
const body = (
|
|
171
|
+
<>
|
|
172
|
+
<Show when={m.reasoning}>
|
|
173
|
+
<Reasoning class="mb-2 w-full">
|
|
174
|
+
<ReasoningTrigger>{m.reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
|
|
175
|
+
<ReasoningContent markdown>{m.reasoning!.text}</ReasoningContent>
|
|
176
|
+
</Reasoning>
|
|
177
|
+
</Show>
|
|
178
|
+
<For each={m.tools ?? []}>{(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}</For>
|
|
179
|
+
<Show when={m.attachments?.length}>
|
|
180
|
+
<Attachments variant="inline" class={m.role === 'user' ? 'mb-2 justify-end' : 'mb-2'}>
|
|
181
|
+
<For each={m.attachments!}>
|
|
182
|
+
{(att) => (<Attachment data={att}><AttachmentPreview /><AttachmentInfo /></Attachment>)}
|
|
183
|
+
</For>
|
|
184
|
+
</Attachments>
|
|
185
|
+
</Show>
|
|
186
|
+
<MessageContent
|
|
187
|
+
markdown={m.role === 'assistant'}
|
|
188
|
+
class={m.role === 'user' ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2' : 'bg-transparent p-0'}
|
|
189
|
+
>
|
|
190
|
+
{m.content}
|
|
191
|
+
</MessageContent>
|
|
192
|
+
<Show when={m.actions?.length}>
|
|
193
|
+
<MessageActionBar
|
|
194
|
+
actions={m.actions!}
|
|
195
|
+
reveal={reveal()}
|
|
196
|
+
onAction={(action) => props.onMessageAction?.({ messageId: m.id, action })}
|
|
197
|
+
/>
|
|
198
|
+
</Show>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
const rowGroup = reveal() === 'hover' ? 'group ' : '';
|
|
202
|
+
return (
|
|
203
|
+
<Show
|
|
204
|
+
when={m.avatar}
|
|
205
|
+
fallback={
|
|
206
|
+
<Message class={`${rowGroup}${m.role === 'user' ? 'flex-col items-end' : 'flex-col items-start'}`}>
|
|
207
|
+
{body}
|
|
208
|
+
</Message>
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
{(av) => (
|
|
212
|
+
<Message class={rowGroup}>
|
|
213
|
+
<MessageAvatar src={av().src ?? ''} alt={av().alt ?? ''} fallback={av().fallback} />
|
|
214
|
+
<div class={`flex min-w-0 flex-1 flex-col ${m.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
215
|
+
{body}
|
|
216
|
+
</div>
|
|
217
|
+
</Message>
|
|
218
|
+
)}
|
|
219
|
+
</Show>
|
|
220
|
+
);
|
|
221
|
+
}}
|
|
222
|
+
</For>
|
|
223
|
+
<ChatContainerScrollAnchor />
|
|
224
|
+
</ChatContainerContent>
|
|
225
|
+
<Show when={showScrollButton()}>
|
|
226
|
+
<div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
|
|
227
|
+
<ScrollButton class="shadow-sm" />
|
|
228
|
+
</div>
|
|
229
|
+
</Show>
|
|
230
|
+
</ChatContainer>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="shrink-0 px-4 pb-4">
|
|
233
|
+
<div class="mx-auto max-w-3xl">
|
|
234
|
+
<DefaultPromptInput
|
|
235
|
+
value={current()} placeholder={props.placeholder} loading={props.loading === true}
|
|
236
|
+
suggestions={visibleSuggestions()} attachments={attachments()}
|
|
237
|
+
search={props.search === true} voice={props.voice === true}
|
|
238
|
+
slashCommands={props.slashCommands} slashActiveIds={props.slashActiveIds} slashCompact={props.slashCompact === true}
|
|
239
|
+
onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
|
|
240
|
+
onAttachmentsChange={setAttachments}
|
|
241
|
+
onSearch={() => props.onSearch?.()} onVoice={() => props.onVoice?.()}
|
|
242
|
+
onSlashSelect={(command) => props.onSlashSelect?.(command)}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</ChatConfig>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
import { Checkpoint, CheckpointIcon, CheckpointTrigger } from './checkpoint';
|
|
4
|
+
import { componentDescription } from '../stories/docs/element-controls';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Solid (Advanced)/Elements/Checkpoint',
|
|
8
|
+
component: Checkpoint,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
description: componentDescription([
|
|
14
|
+
'An inline marker that lets a user restore the conversation to a saved point. Composed of `Checkpoint` (row + separator) wrapping a `CheckpointIcon` and a `CheckpointTrigger` button.',
|
|
15
|
+
'**When to use:** to mark a restorable state in a transcript — e.g. before an edit or a branching action — so the user can revert to it.',
|
|
16
|
+
'**How to use:** place a `CheckpointIcon` and a `CheckpointTrigger` inside `Checkpoint`. Give the trigger an `onClick` handler and an optional `tooltip`; pass custom SVG children to `CheckpointIcon` to override the default flag.',
|
|
17
|
+
'**Placement:** between messages in a chat transcript, as a thin separator-style row.',
|
|
18
|
+
]),
|
|
19
|
+
controls: { exclude: ['use:eventListener'] },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
children: {
|
|
24
|
+
control: false,
|
|
25
|
+
description: 'The `CheckpointIcon` and `CheckpointTrigger` contents.',
|
|
26
|
+
},
|
|
27
|
+
class: {
|
|
28
|
+
control: 'text',
|
|
29
|
+
description: 'Extra classes for the checkpoint row.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
args: {},
|
|
33
|
+
render: (args) => (
|
|
34
|
+
<div class="max-w-md">
|
|
35
|
+
<Checkpoint {...args}>
|
|
36
|
+
<CheckpointIcon />
|
|
37
|
+
<CheckpointTrigger tooltip="Restore to this point" onClick={fn()}>
|
|
38
|
+
Restore
|
|
39
|
+
</CheckpointTrigger>
|
|
40
|
+
</Checkpoint>
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
} satisfies Meta<typeof Checkpoint>;
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
const IMPORT = `import { Checkpoint, CheckpointIcon, CheckpointTrigger } from '@kitn.ai/ui';`;
|
|
49
|
+
const src = (code: string) => ({
|
|
50
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** Interactive playground — the default flag icon with a restore trigger. */
|
|
54
|
+
export const Playground: Story = {
|
|
55
|
+
...src(`<Checkpoint>
|
|
56
|
+
<CheckpointIcon />
|
|
57
|
+
<CheckpointTrigger tooltip="Restore to this point" onClick={handleRestore}>
|
|
58
|
+
Restore
|
|
59
|
+
</CheckpointTrigger>
|
|
60
|
+
</Checkpoint>`),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const WithCustomIcon: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<div class="max-w-md">
|
|
66
|
+
<Checkpoint>
|
|
67
|
+
<CheckpointIcon>
|
|
68
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-4">
|
|
69
|
+
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
|
|
70
|
+
<path d="M12 6v6l4 2" />
|
|
71
|
+
</svg>
|
|
72
|
+
</CheckpointIcon>
|
|
73
|
+
<CheckpointTrigger tooltip="Go back to this checkpoint" onClick={fn()}>
|
|
74
|
+
Revert to checkpoint
|
|
75
|
+
</CheckpointTrigger>
|
|
76
|
+
</Checkpoint>
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
...src(`<Checkpoint>
|
|
80
|
+
<CheckpointIcon>
|
|
81
|
+
<ClockIcon class="size-4" />
|
|
82
|
+
</CheckpointIcon>
|
|
83
|
+
<CheckpointTrigger tooltip="Go back to this checkpoint" onClick={handleRevert}>
|
|
84
|
+
Revert to checkpoint
|
|
85
|
+
</CheckpointTrigger>
|
|
86
|
+
</Checkpoint>`),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const NoTooltip: Story = {
|
|
90
|
+
render: () => (
|
|
91
|
+
<div class="max-w-md">
|
|
92
|
+
<Checkpoint>
|
|
93
|
+
<CheckpointIcon />
|
|
94
|
+
<CheckpointTrigger onClick={fn()}>Restore</CheckpointTrigger>
|
|
95
|
+
</Checkpoint>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
...src(`<Checkpoint>
|
|
99
|
+
<CheckpointIcon />
|
|
100
|
+
<CheckpointTrigger onClick={handleRestore}>Restore</CheckpointTrigger>
|
|
101
|
+
</Checkpoint>`),
|
|
102
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type JSX, Show, splitProps } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
import { Tooltip } from '../ui/tooltip';
|
|
5
|
+
import { Separator } from '../ui/separator';
|
|
6
|
+
|
|
7
|
+
export interface CheckpointProps extends JSX.HTMLAttributes<HTMLDivElement> {}
|
|
8
|
+
|
|
9
|
+
export function Checkpoint(props: CheckpointProps) {
|
|
10
|
+
const [local, rest] = splitProps(props, ['class', 'children']);
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
class={cn(
|
|
14
|
+
'flex items-center gap-0.5 overflow-hidden text-muted-foreground',
|
|
15
|
+
local.class
|
|
16
|
+
)}
|
|
17
|
+
{...rest}
|
|
18
|
+
>
|
|
19
|
+
{local.children}
|
|
20
|
+
<Separator />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CheckpointIconProps {
|
|
26
|
+
class?: string;
|
|
27
|
+
children?: JSX.Element;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CheckpointIcon(props: CheckpointIconProps) {
|
|
31
|
+
return (
|
|
32
|
+
<Show
|
|
33
|
+
when={!props.children}
|
|
34
|
+
fallback={props.children}
|
|
35
|
+
>
|
|
36
|
+
<svg
|
|
37
|
+
width="16"
|
|
38
|
+
height="16"
|
|
39
|
+
viewBox="0 0 24 24"
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
stroke-width="2"
|
|
43
|
+
class={cn('size-4 shrink-0', props.class)}
|
|
44
|
+
>
|
|
45
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z" />
|
|
46
|
+
</svg>
|
|
47
|
+
</Show>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CheckpointTriggerProps {
|
|
52
|
+
tooltip?: string;
|
|
53
|
+
/** Accessible name for the button — required when it has no visible text (icon-only). */
|
|
54
|
+
'aria-label'?: string;
|
|
55
|
+
onClick?: () => void;
|
|
56
|
+
children?: JSX.Element;
|
|
57
|
+
class?: string;
|
|
58
|
+
variant?: 'ghost' | 'default' | 'outline';
|
|
59
|
+
size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function CheckpointTrigger(props: CheckpointTriggerProps) {
|
|
63
|
+
const variant = () => props.variant ?? 'ghost';
|
|
64
|
+
const size = () => props.size ?? 'sm';
|
|
65
|
+
|
|
66
|
+
// A factory (not a single shared node): the Show fallback and the Tooltip
|
|
67
|
+
// child each need their OWN element instance, otherwise Solid reuses one DOM
|
|
68
|
+
// node across both branches and throws HierarchyRequestError when the tooltip
|
|
69
|
+
// branch mounts it.
|
|
70
|
+
const renderButton = () => (
|
|
71
|
+
<Button
|
|
72
|
+
variant={variant()}
|
|
73
|
+
size={size()}
|
|
74
|
+
type="button"
|
|
75
|
+
aria-label={props['aria-label']}
|
|
76
|
+
onClick={props.onClick}
|
|
77
|
+
class={props.class}
|
|
78
|
+
>
|
|
79
|
+
{props.children}
|
|
80
|
+
</Button>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Show when={props.tooltip} fallback={renderButton()}>
|
|
85
|
+
<Tooltip content={props.tooltip!}>{renderButton()}</Tooltip>
|
|
86
|
+
</Show>
|
|
87
|
+
);
|
|
88
|
+
}
|