@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,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import { TasksCard, type TasksCardData } from './tasks-card';
|
|
4
|
+
import { componentDescription } from '../stories/docs/element-controls';
|
|
5
|
+
import type { CardEvent, CardHost, CardContext } from '../primitives/card-contract';
|
|
6
|
+
|
|
7
|
+
const ctx: CardContext = { theme: { mode: 'light' }, locale: 'en' };
|
|
8
|
+
|
|
9
|
+
/** Renders the Solid <TasksCard> with a capturing `host`, logging emitted events. */
|
|
10
|
+
function Demo(props: { def: TasksCardData; heading?: string; cardId: string }) {
|
|
11
|
+
const [log, setLog] = createSignal<CardEvent[]>([]);
|
|
12
|
+
const host: CardHost = { context: () => ctx, emit: (e) => setLog((p) => [...p, e]) };
|
|
13
|
+
return (
|
|
14
|
+
<div style={{ 'max-width': '460px', display: 'flex', 'flex-direction': 'column', gap: '12px' }}>
|
|
15
|
+
<TasksCard host={host} data={props.def} heading={props.heading} cardId={props.cardId} />
|
|
16
|
+
<pre
|
|
17
|
+
style={{
|
|
18
|
+
margin: 0,
|
|
19
|
+
'max-height': '180px',
|
|
20
|
+
overflow: 'auto',
|
|
21
|
+
background: 'var(--color-muted, #f4f4f5)',
|
|
22
|
+
'border-radius': '8px',
|
|
23
|
+
padding: '8px',
|
|
24
|
+
'font-size': '12px',
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{log().length === 0 ? '// emitted CardEvents appear here' : JSON.stringify(log(), null, 2)}
|
|
28
|
+
</pre>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PLAN: TasksCardData = {
|
|
34
|
+
selectAll: true,
|
|
35
|
+
confirmLabel: 'Run selected',
|
|
36
|
+
tasks: [
|
|
37
|
+
{ id: 'lint', label: 'Run linter', checked: true },
|
|
38
|
+
{ id: 'test', label: 'Run unit tests', checked: true },
|
|
39
|
+
{ id: 'build', label: 'Build production bundle' },
|
|
40
|
+
{ id: 'deploy', label: 'Deploy to staging', description: 'Reversible; staging only' },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const BOUNDED: TasksCardData = {
|
|
45
|
+
confirmLabel: 'Request review',
|
|
46
|
+
min: 1,
|
|
47
|
+
max: 2,
|
|
48
|
+
tasks: [
|
|
49
|
+
{ id: 'ana', label: 'Ana' },
|
|
50
|
+
{ id: 'ben', label: 'Ben' },
|
|
51
|
+
{ id: 'cat', label: 'Cat' },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const meta = {
|
|
56
|
+
title: 'Solid (Advanced)/Elements/TasksCard',
|
|
57
|
+
component: TasksCard,
|
|
58
|
+
tags: ['autodocs'],
|
|
59
|
+
parameters: {
|
|
60
|
+
layout: 'padded',
|
|
61
|
+
docs: {
|
|
62
|
+
description: componentDescription([
|
|
63
|
+
'The SolidJS layer behind `<kai-tasks>`. Pass a `host` (a `CardHost`) to receive the emitted `CardEvent`s directly (the native-host path), or wrap in a `CardProvider`. Toggling rows is local; only confirm emits `submit` with `{ selected }` in input order.',
|
|
64
|
+
]),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
} satisfies Meta<typeof TasksCard>;
|
|
68
|
+
|
|
69
|
+
export default meta;
|
|
70
|
+
type Story = StoryObj<typeof TasksCard>;
|
|
71
|
+
|
|
72
|
+
export const SelectAPlan: Story = {
|
|
73
|
+
render: () => <Demo def={PLAN} heading="Approve the plan steps" cardId="card-plan" />,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Bounded: Story = {
|
|
77
|
+
render: () => <Demo def={BOUNDED} heading="Pick up to 2 reviewers" cardId="card-bounded" />,
|
|
78
|
+
};
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
For,
|
|
4
|
+
Show,
|
|
5
|
+
splitProps,
|
|
6
|
+
mergeProps,
|
|
7
|
+
createSignal,
|
|
8
|
+
createMemo,
|
|
9
|
+
createEffect,
|
|
10
|
+
on,
|
|
11
|
+
ErrorBoundary,
|
|
12
|
+
createUniqueId,
|
|
13
|
+
} from 'solid-js';
|
|
14
|
+
import { cn } from '../utils/cn';
|
|
15
|
+
import { Button } from '../ui/button';
|
|
16
|
+
import { Card } from './card';
|
|
17
|
+
import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
|
|
18
|
+
import { useCardResolution } from './use-card-resolution';
|
|
19
|
+
import { emitCardEvent } from '../primitives/card-routing';
|
|
20
|
+
import { useCardHost } from '../primitives/card-host';
|
|
21
|
+
import { Check } from 'lucide-solid';
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Types (tasks.schema.json) — see src/primitives/card-schemas/tasks.schema.json
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface TasksTask {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
checked?: boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TasksCardData {
|
|
36
|
+
mode?: 'select'; // future: 'select' | 'progress'
|
|
37
|
+
heading?: string;
|
|
38
|
+
tasks: TasksTask[]; // >=1
|
|
39
|
+
selectAll?: boolean;
|
|
40
|
+
confirmLabel?: string;
|
|
41
|
+
allowEmpty?: boolean;
|
|
42
|
+
min?: number;
|
|
43
|
+
max?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TasksCardResult {
|
|
47
|
+
selected: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type TasksCardEnvelope = CardEnvelope<'tasks', TasksCardData>;
|
|
51
|
+
|
|
52
|
+
export const TASKS_CARD_TYPE = 'tasks' as const;
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Pure helpers (unit-tested in isolation).
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** De-dupe tasks by id (first wins). Returns the usable list + an optional error. */
|
|
59
|
+
export function normalizeTasks(tasks: unknown): { tasks: TasksTask[]; error?: string } {
|
|
60
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
61
|
+
return { tasks: [], error: "This card couldn't be displayed." };
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const out: TasksTask[] = [];
|
|
65
|
+
for (const t of tasks) {
|
|
66
|
+
if (!t || typeof t !== 'object') continue;
|
|
67
|
+
const task = t as Partial<TasksTask>;
|
|
68
|
+
if (typeof task.id !== 'string' || task.id.length === 0) continue;
|
|
69
|
+
if (typeof task.label !== 'string' || task.label.length === 0) continue;
|
|
70
|
+
if (seen.has(task.id)) {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.warn(`[kai-tasks] duplicate task id "${task.id}" ignored`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
seen.add(task.id);
|
|
76
|
+
out.push({
|
|
77
|
+
id: task.id,
|
|
78
|
+
label: task.label,
|
|
79
|
+
description: typeof task.description === 'string' ? task.description : undefined,
|
|
80
|
+
checked: task.checked === true,
|
|
81
|
+
disabled: task.disabled === true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (out.length === 0) return { tasks: [], error: "This card couldn't be displayed." };
|
|
85
|
+
return { tasks: out };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The ids of the initially-checked tasks (in input order). */
|
|
89
|
+
export function initialSelected(tasks: TasksTask[]): string[] {
|
|
90
|
+
return tasks.filter((t) => t.checked).map((t) => t.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The selected ids in input order (selection set ∩ tasks, preserving task order). */
|
|
94
|
+
export function selectedInOrder(tasks: TasksTask[], selected: Set<string>): string[] {
|
|
95
|
+
return tasks.filter((t) => selected.has(t.id)).map((t) => t.id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The toggleable (non-disabled) task ids. */
|
|
99
|
+
export function toggleableIds(tasks: TasksTask[]): string[] {
|
|
100
|
+
return tasks.filter((t) => !t.disabled).map((t) => t.id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type SelectAllState = 'checked' | 'unchecked' | 'indeterminate';
|
|
104
|
+
|
|
105
|
+
/** Select-all tri-state over the toggleable rows. */
|
|
106
|
+
export function selectAllState(tasks: TasksTask[], selected: Set<string>): SelectAllState {
|
|
107
|
+
const ids = toggleableIds(tasks);
|
|
108
|
+
if (ids.length === 0) return 'unchecked';
|
|
109
|
+
const n = ids.filter((id) => selected.has(id)).length;
|
|
110
|
+
if (n === 0) return 'unchecked';
|
|
111
|
+
if (n === ids.length) return 'checked';
|
|
112
|
+
return 'indeterminate';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether select-all should be shown: requested AND not blocked by `max` (since
|
|
116
|
+
* "all" would violate max). */
|
|
117
|
+
export function showSelectAll(data: TasksCardData, tasks: TasksTask[]): boolean {
|
|
118
|
+
if (data.selectAll !== true) return false;
|
|
119
|
+
const count = toggleableIds(tasks).length;
|
|
120
|
+
if (data.max !== undefined && count > data.max) return false;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether confirm is enabled for the current selection count. */
|
|
125
|
+
export function canConfirm(data: TasksCardData, count: number): boolean {
|
|
126
|
+
const min = data.min ?? (data.allowEmpty ? 0 : 1);
|
|
127
|
+
if (count < min) return false;
|
|
128
|
+
if (data.max !== undefined && count > data.max) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether an unchecked row is blocked because `max` is reached. */
|
|
133
|
+
export function isMaxReached(data: TasksCardData, count: number): boolean {
|
|
134
|
+
return data.max !== undefined && count >= data.max;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The disabled-reason text for confirm (for aria-describedby), or undefined. */
|
|
138
|
+
export function confirmReason(data: TasksCardData, count: number): string | undefined {
|
|
139
|
+
if (canConfirm(data, count)) return undefined;
|
|
140
|
+
const min = data.min ?? (data.allowEmpty ? 0 : 1);
|
|
141
|
+
if (count < min) return `Select at least ${min}.`;
|
|
142
|
+
if (data.max !== undefined && count > data.max) return `Select at most ${data.max}.`;
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// The <TasksCard> component.
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export interface TasksCardProps {
|
|
151
|
+
/** The tasks definition (CardEnvelope.data). */
|
|
152
|
+
data?: TasksCardData;
|
|
153
|
+
/** The card id used to correlate every emitted CardEvent. */
|
|
154
|
+
cardId?: string;
|
|
155
|
+
/** The envelope title rendered in the card chrome. */
|
|
156
|
+
heading?: string;
|
|
157
|
+
/** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
|
|
158
|
+
* bubbling `kai-card` CustomEvent off `hostElement`). */
|
|
159
|
+
host?: CardHost;
|
|
160
|
+
/** The custom-element host node, for the bubbling `kai-card` fallback emit. */
|
|
161
|
+
hostElement?: HTMLElement;
|
|
162
|
+
class?: string;
|
|
163
|
+
/** When set, render the chromed read-only summary instead of the interactive controls. */
|
|
164
|
+
resolution?: CardResolution;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const DEFAULT_DATA: TasksCardData = { tasks: [] };
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `TasksCard` — a selectable task/plan list (checkbox rows + optional select-all
|
|
171
|
+
* + a confirm button) inside `Card` chrome. Row toggling and select-all are local
|
|
172
|
+
* UI state; only the final confirm emits the Card contract's `submit` verb
|
|
173
|
+
* (`{ kind:'submit', cardId, data:{ selected } }`) with the checked ids in
|
|
174
|
+
* input order. Emits `ready` on mount and `error` for an unusable definition.
|
|
175
|
+
*/
|
|
176
|
+
export function TasksCard(props: TasksCardProps): JSX.Element {
|
|
177
|
+
const merged = mergeProps({ cardId: 'kai-tasks' }, props);
|
|
178
|
+
const [local] = splitProps(merged, ['data', 'cardId', 'heading', 'host', 'hostElement', 'class', 'resolution']);
|
|
179
|
+
|
|
180
|
+
const ctxHost = useCardHost();
|
|
181
|
+
const uid = createUniqueId();
|
|
182
|
+
|
|
183
|
+
const emit = (event: CardEvent): void => {
|
|
184
|
+
const h = local.host ?? ctxHost;
|
|
185
|
+
if (h) h.emit(event);
|
|
186
|
+
else if (local.hostElement) emitCardEvent(local.hostElement, event);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const normalized = createMemo(() => normalizeTasks(local.data?.tasks));
|
|
190
|
+
const valid = createMemo(() => normalized().error === undefined);
|
|
191
|
+
const errorMessage = createMemo(() => normalized().error ?? '');
|
|
192
|
+
const tasks = createMemo(() => normalized().tasks);
|
|
193
|
+
const data = createMemo<TasksCardData>(() => local.data ?? DEFAULT_DATA);
|
|
194
|
+
|
|
195
|
+
const [selected, setSelected] = createSignal<Set<string>>(new Set());
|
|
196
|
+
|
|
197
|
+
const res = useCardResolution({ prop: () => local.resolution, data: () => local.data });
|
|
198
|
+
|
|
199
|
+
// Seed selection from the tasks' initial checked state when a NEW definition arrives.
|
|
200
|
+
createEffect(
|
|
201
|
+
on(
|
|
202
|
+
() => local.data,
|
|
203
|
+
() => {
|
|
204
|
+
setSelected(new Set(initialSelected(tasks())));
|
|
205
|
+
},
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ready / error lifecycle emits.
|
|
210
|
+
createEffect(
|
|
211
|
+
on(valid, (ok) => {
|
|
212
|
+
if (ok) emit({ kind: 'ready', cardId: local.cardId });
|
|
213
|
+
else emit({ kind: 'error', cardId: local.cardId, message: errorMessage() });
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const count = createMemo(() => selected().size);
|
|
218
|
+
const confirmLabel = () => data().confirmLabel ?? 'Confirm';
|
|
219
|
+
const masterState = createMemo(() => selectAllState(tasks(), selected()));
|
|
220
|
+
const showMaster = createMemo(() => showSelectAll(data(), tasks()));
|
|
221
|
+
const reason = createMemo(() => confirmReason(data(), count()));
|
|
222
|
+
const confirmEnabled = createMemo(() => canConfirm(data(), count()) && !res.isResolved());
|
|
223
|
+
|
|
224
|
+
const toggle = (id: string, on: boolean): void => {
|
|
225
|
+
if (res.isResolved()) return;
|
|
226
|
+
const next = new Set(selected());
|
|
227
|
+
if (on) {
|
|
228
|
+
// Respect max: block adding past max.
|
|
229
|
+
if (isMaxReached(data(), next.size) && !next.has(id)) return;
|
|
230
|
+
next.add(id);
|
|
231
|
+
} else {
|
|
232
|
+
next.delete(id);
|
|
233
|
+
}
|
|
234
|
+
setSelected(next);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const toggleAll = (on: boolean): void => {
|
|
238
|
+
if (res.isResolved()) return;
|
|
239
|
+
const next = new Set(selected());
|
|
240
|
+
for (const id of toggleableIds(tasks())) {
|
|
241
|
+
if (on) next.add(id);
|
|
242
|
+
else next.delete(id);
|
|
243
|
+
}
|
|
244
|
+
setSelected(next);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onConfirm = (): void => {
|
|
248
|
+
if (!confirmEnabled()) return;
|
|
249
|
+
const result: TasksCardResult = { selected: selectedInOrder(tasks(), selected()) };
|
|
250
|
+
emit({ kind: 'submit', cardId: local.cardId, data: result });
|
|
251
|
+
res.setLocal({ kind: 'submit', data: result });
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Surface the resolved state for host styling.
|
|
255
|
+
createEffect(() => {
|
|
256
|
+
const el = local.hostElement;
|
|
257
|
+
if (!el) return;
|
|
258
|
+
if (res.isResolved()) el.setAttribute('data-kai-resolved', 'submitted');
|
|
259
|
+
else el.removeAttribute('data-kai-resolved');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const resolvedSummary = createMemo(() => {
|
|
263
|
+
const r = res.resolution();
|
|
264
|
+
if (!r || r.kind !== 'submit') return undefined;
|
|
265
|
+
const sel = (r.data as { selected?: unknown })?.selected;
|
|
266
|
+
const ids: string[] = Array.isArray(sel) ? (sel as string[]) : [];
|
|
267
|
+
const all = tasks();
|
|
268
|
+
const chosen = all.filter((t) => ids.includes(t.id)).map((t) => t.label);
|
|
269
|
+
return { count: chosen.length, total: all.length, labels: chosen };
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const reasonId = `kai-tl-reason-${uid}`;
|
|
273
|
+
const countId = `kai-tl-count-${uid}`;
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
|
|
277
|
+
<ErrorBoundary
|
|
278
|
+
fallback={() => {
|
|
279
|
+
emit({ kind: 'error', cardId: local.cardId, message: 'The card failed to render.' });
|
|
280
|
+
return <Card heading={local.heading} errorMessage="The card failed to render." />;
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<Card
|
|
284
|
+
heading={local.heading ?? local.data?.heading}
|
|
285
|
+
actions={
|
|
286
|
+
res.isResolved() ? undefined : (
|
|
287
|
+
<div class="flex w-full flex-wrap items-center justify-between gap-2">
|
|
288
|
+
<span id={countId} aria-live="polite" class="text-xs text-muted-foreground">
|
|
289
|
+
{count()} selected
|
|
290
|
+
</span>
|
|
291
|
+
<div class="ml-auto flex items-center gap-2">
|
|
292
|
+
<Show when={reason()}>
|
|
293
|
+
<span id={reasonId} class="sr-only">
|
|
294
|
+
{reason()}
|
|
295
|
+
</span>
|
|
296
|
+
</Show>
|
|
297
|
+
<Button
|
|
298
|
+
type="button"
|
|
299
|
+
disabled={!confirmEnabled()}
|
|
300
|
+
aria-describedby={reason() ? reasonId : undefined}
|
|
301
|
+
onClick={onConfirm}
|
|
302
|
+
>
|
|
303
|
+
{confirmLabel()}
|
|
304
|
+
</Button>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
<Show
|
|
311
|
+
when={!res.isResolved()}
|
|
312
|
+
fallback={<TasksResolved summary={resolvedSummary()!} optimistic={res.isOptimistic()} />}
|
|
313
|
+
>
|
|
314
|
+
<div
|
|
315
|
+
role="group"
|
|
316
|
+
aria-label={local.heading ?? local.data?.heading ?? 'Tasks'}
|
|
317
|
+
class={cn('flex flex-col', local.class)}
|
|
318
|
+
onKeyDown={(e) => {
|
|
319
|
+
// Enter anywhere in the card (off a checkbox) confirms when enabled.
|
|
320
|
+
if (e.key !== 'Enter') return;
|
|
321
|
+
const target = e.target as HTMLElement;
|
|
322
|
+
if (target.tagName === 'INPUT') return;
|
|
323
|
+
if (confirmEnabled()) onConfirm();
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
<div class="divide-y divide-border overflow-hidden rounded-lg border border-input">
|
|
327
|
+
<Show when={showMaster()}>
|
|
328
|
+
{(() => {
|
|
329
|
+
const indeterminate = () => masterState() === 'indeterminate';
|
|
330
|
+
return (
|
|
331
|
+
<label
|
|
332
|
+
class={cn(
|
|
333
|
+
'flex cursor-pointer items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors',
|
|
334
|
+
masterState() === 'checked'
|
|
335
|
+
? 'bg-accent text-accent-foreground'
|
|
336
|
+
: 'text-foreground hover:bg-muted/50',
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
339
|
+
<input
|
|
340
|
+
type="checkbox"
|
|
341
|
+
class="kai-checkbox"
|
|
342
|
+
checked={masterState() === 'checked'}
|
|
343
|
+
aria-checked={indeterminate() ? 'mixed' : masterState() === 'checked'}
|
|
344
|
+
ref={(el) => {
|
|
345
|
+
createEffect(() => {
|
|
346
|
+
el.indeterminate = indeterminate();
|
|
347
|
+
});
|
|
348
|
+
}}
|
|
349
|
+
onChange={(e) => toggleAll(e.currentTarget.checked)}
|
|
350
|
+
/>
|
|
351
|
+
<span>Select all</span>
|
|
352
|
+
</label>
|
|
353
|
+
);
|
|
354
|
+
})()}
|
|
355
|
+
</Show>
|
|
356
|
+
|
|
357
|
+
<For each={tasks()}>
|
|
358
|
+
{(task) => {
|
|
359
|
+
const checked = () => selected().has(task.id);
|
|
360
|
+
const blocked = () =>
|
|
361
|
+
task.disabled || (!checked() && isMaxReached(data(), count()));
|
|
362
|
+
const descId = `kai-tl-desc-${uid}-${task.id}`;
|
|
363
|
+
return (
|
|
364
|
+
<label
|
|
365
|
+
class={cn(
|
|
366
|
+
'flex items-start gap-3 px-3 py-2.5 text-sm transition-colors',
|
|
367
|
+
blocked()
|
|
368
|
+
? 'cursor-not-allowed opacity-60'
|
|
369
|
+
: 'cursor-pointer hover:bg-muted/50',
|
|
370
|
+
checked() && !blocked()
|
|
371
|
+
? 'bg-accent font-medium text-accent-foreground'
|
|
372
|
+
: 'text-foreground',
|
|
373
|
+
)}
|
|
374
|
+
data-task-id={task.id}
|
|
375
|
+
>
|
|
376
|
+
<input
|
|
377
|
+
type="checkbox"
|
|
378
|
+
class="kai-checkbox mt-0.5"
|
|
379
|
+
checked={checked()}
|
|
380
|
+
disabled={blocked()}
|
|
381
|
+
aria-disabled={blocked() ? 'true' : undefined}
|
|
382
|
+
aria-describedby={task.description ? descId : undefined}
|
|
383
|
+
onChange={(e) => toggle(task.id, e.currentTarget.checked)}
|
|
384
|
+
/>
|
|
385
|
+
<span class="flex flex-col gap-0.5">
|
|
386
|
+
<span>{task.label}</span>
|
|
387
|
+
<Show when={task.description}>
|
|
388
|
+
<span id={descId} class="text-xs font-normal text-muted-foreground">
|
|
389
|
+
{task.description}
|
|
390
|
+
</span>
|
|
391
|
+
</Show>
|
|
392
|
+
</span>
|
|
393
|
+
</label>
|
|
394
|
+
);
|
|
395
|
+
}}
|
|
396
|
+
</For>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<Show when={data().max !== undefined}>
|
|
400
|
+
<p class="pt-1 text-xs text-muted-foreground">Up to {data().max} selected.</p>
|
|
401
|
+
</Show>
|
|
402
|
+
</div>
|
|
403
|
+
</Show>
|
|
404
|
+
</Card>
|
|
405
|
+
</ErrorBoundary>
|
|
406
|
+
</Show>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
411
|
+
// Read-only resolved summary presenter.
|
|
412
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
function TasksResolved(props: {
|
|
415
|
+
summary: { count: number; total: number; labels: string[] };
|
|
416
|
+
optimistic: boolean;
|
|
417
|
+
}): JSX.Element {
|
|
418
|
+
return (
|
|
419
|
+
<div class="flex flex-col gap-2" role={props.optimistic ? 'status' : undefined}>
|
|
420
|
+
<p class="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
421
|
+
<Check size={16} aria-hidden="true" />
|
|
422
|
+
<span>Selected {props.summary.count} of {props.summary.total}</span>
|
|
423
|
+
</p>
|
|
424
|
+
<Show when={props.summary.labels.length > 0}
|
|
425
|
+
fallback={<p class="text-sm text-muted-foreground">None selected</p>}>
|
|
426
|
+
<ul class="list-disc pl-5 text-sm text-foreground">
|
|
427
|
+
<For each={props.summary.labels}>{(l) => <li>{l}</li>}</For>
|
|
428
|
+
</ul>
|
|
429
|
+
</Show>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { TextShimmer } from './text-shimmer';
|
|
3
|
+
import { componentDescription } from '../stories/docs/element-controls';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Solid (Advanced)/Elements/TextShimmer',
|
|
7
|
+
component: TextShimmer,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
controls: { exclude: ['use:eventListener'] },
|
|
13
|
+
description: componentDescription([
|
|
14
|
+
'An animated text effect that sweeps a light gradient across its content, rendered into any element tag via `as`.',
|
|
15
|
+
'**When to use:** to signal an in-progress / loading state with a label — e.g. "Thinking", "Generating", "Searching" — or to draw subtle attention to a transient status line.',
|
|
16
|
+
'**How to use:** wrap text in `<TextShimmer>`; tune `duration` (seconds per sweep) and `spread` (gradient width, clamped 5–45). Use `as` to change the wrapping tag (e.g. `"h2"`) and pass any standard HTML attributes.',
|
|
17
|
+
'**Placement:** loading indicators, streaming status lines, thinking bars, and placeholder labels.',
|
|
18
|
+
]),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
as: {
|
|
23
|
+
control: 'text',
|
|
24
|
+
description: 'HTML tag to render the shimmer into.',
|
|
25
|
+
table: { defaultValue: { summary: 'span' } },
|
|
26
|
+
},
|
|
27
|
+
duration: {
|
|
28
|
+
control: 'number',
|
|
29
|
+
description: 'Seconds for one full shimmer sweep.',
|
|
30
|
+
table: { defaultValue: { summary: '4' } },
|
|
31
|
+
},
|
|
32
|
+
spread: {
|
|
33
|
+
control: 'number',
|
|
34
|
+
description: 'Width of the gradient highlight (clamped to 5–45).',
|
|
35
|
+
table: { defaultValue: { summary: '20' } },
|
|
36
|
+
},
|
|
37
|
+
children: {
|
|
38
|
+
control: 'text',
|
|
39
|
+
description: 'Text to apply the shimmer effect to.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
args: {
|
|
43
|
+
as: 'span',
|
|
44
|
+
duration: 4,
|
|
45
|
+
spread: 20,
|
|
46
|
+
children: 'Shimmering text effect',
|
|
47
|
+
},
|
|
48
|
+
render: (args) => <TextShimmer {...args} />,
|
|
49
|
+
} satisfies Meta<typeof TextShimmer>;
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
const IMPORT = `import { TextShimmer } from '@kitn.ai/ui';`;
|
|
55
|
+
const src = (code: string) => ({
|
|
56
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Interactive playground — tweak the controls to explore the shimmer effect. */
|
|
60
|
+
export const Playground: Story = {
|
|
61
|
+
...src(`<TextShimmer duration={4} spread={20}>Shimmering text effect</TextShimmer>`),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const FastShimmer: Story = {
|
|
65
|
+
args: { duration: 1.5, children: 'Fast shimmer animation' },
|
|
66
|
+
...src(`<TextShimmer duration={1.5}>Fast shimmer animation</TextShimmer>`),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const SlowShimmer: Story = {
|
|
70
|
+
args: { duration: 8, children: 'Slow shimmer animation' },
|
|
71
|
+
...src(`<TextShimmer duration={8}>Slow shimmer animation</TextShimmer>`),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const WideSpread: Story = {
|
|
75
|
+
args: { spread: 40, children: 'Wide spread shimmer' },
|
|
76
|
+
...src(`<TextShimmer spread={40}>Wide spread shimmer</TextShimmer>`),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const NarrowSpread: Story = {
|
|
80
|
+
args: { spread: 5, children: 'Narrow spread shimmer' },
|
|
81
|
+
...src(`<TextShimmer spread={5}>Narrow spread shimmer</TextShimmer>`),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const AsHeading: Story = {
|
|
85
|
+
args: { as: 'h2', class: 'text-2xl', children: 'Shimmer Heading' } as never,
|
|
86
|
+
...src(`<TextShimmer as="h2" class="text-2xl">Shimmer Heading</TextShimmer>`),
|
|
87
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type JSX, splitProps } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Dynamic } from 'solid-js/web';
|
|
4
|
+
|
|
5
|
+
export interface TextShimmerProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
6
|
+
as?: string;
|
|
7
|
+
duration?: number;
|
|
8
|
+
spread?: number;
|
|
9
|
+
children: JSX.Element;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function TextShimmer(props: TextShimmerProps) {
|
|
13
|
+
const [local, rest] = splitProps(props, ['as', 'class', 'duration', 'spread', 'children']);
|
|
14
|
+
|
|
15
|
+
const dynamicSpread = () => Math.min(Math.max(local.spread ?? 20, 5), 45);
|
|
16
|
+
const tag = () => local.as ?? 'span';
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Dynamic
|
|
20
|
+
component={tag()}
|
|
21
|
+
class={cn(
|
|
22
|
+
'bg-size-[200%_auto] bg-clip-text font-medium text-transparent',
|
|
23
|
+
'animate-[shimmer_4s_infinite_linear]',
|
|
24
|
+
local.class
|
|
25
|
+
)}
|
|
26
|
+
style={{
|
|
27
|
+
'background-image': `linear-gradient(to right, var(--color-muted-foreground) ${50 - dynamicSpread()}%, var(--color-foreground) 50%, var(--color-muted-foreground) ${50 + dynamicSpread()}%)`,
|
|
28
|
+
'animation-duration': `${local.duration ?? 4}s`,
|
|
29
|
+
}}
|
|
30
|
+
{...rest}
|
|
31
|
+
>
|
|
32
|
+
{local.children}
|
|
33
|
+
</Dynamic>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { TextShimmer };
|