@kitn.ai/ui 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-tooling/construct/public.d.ts +15 -0
- package/dist/agent-tooling/construct/schema.d.ts +111 -0
- package/dist/construct-cli.es.js +3 -3
- package/dist/construct.d.ts +5 -0
- package/dist/construct.js +327 -0
- package/dist/core-C8fzo39E.js +1 -12
- package/dist/engine-javascript-C1x7zo1_.js +1 -141
- package/dist/mcp.es.js +1 -1
- package/package.json +16 -16
- package/bin/route.test.js +0 -58
- package/src/agent-tooling/README.md +0 -192
- package/src/agent-tooling/archetypes.ts +0 -205
- package/src/agent-tooling/catalog/README.md +0 -676
- package/src/agent-tooling/catalog/catalog-types.ts +0 -231
- package/src/agent-tooling/catalog/fabrications.ts +0 -96
- package/src/agent-tooling/catalog/invariants.ts +0 -284
- package/src/agent-tooling/catalog/labs-titles.ts +0 -114
- package/src/agent-tooling/catalog/scenarios.ts +0 -87
- package/src/agent-tooling/catalog/surfaces.ts +0 -347
- package/src/agent-tooling/construct/cli-entry.ts +0 -8
- package/src/agent-tooling/construct/cli.ts +0 -147
- package/src/agent-tooling/construct/codegen.ts +0 -1635
- package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
- package/src/agent-tooling/construct/dev.ts +0 -148
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
- package/src/agent-tooling/construct/schema.ts +0 -452
- package/src/agent-tooling/integrations/anthropic.ts +0 -442
- package/src/agent-tooling/integrations/cloudflare.ts +0 -218
- package/src/agent-tooling/integrations/langgraph.ts +0 -133
- package/src/agent-tooling/integrations/mastra.ts +0 -182
- package/src/agent-tooling/integrations/mock.ts +0 -148
- package/src/agent-tooling/integrations/ollama.ts +0 -97
- package/src/agent-tooling/integrations/openai.ts +0 -95
- package/src/agent-tooling/integrations/openrouter.ts +0 -82
- package/src/agent-tooling/integrations/pi.ts +0 -176
- package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
- package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
- package/src/agent-tooling/mcp/manifest.ts +0 -422
- package/src/agent-tooling/mcp/server.ts +0 -116
- package/src/agent-tooling/mcp/stdio.ts +0 -18
- package/src/agent-tooling/mcp/tools/construct.ts +0 -130
- package/src/agent-tooling/mcp/tools/debug.ts +0 -621
- package/src/agent-tooling/mcp/tools/reference.ts +0 -980
- package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
- package/src/agent-tooling/mcp/tools/theme.ts +0 -456
- package/src/agent-tooling/mcp/tools/types.ts +0 -14
- package/src/agent-tooling/mcp/validate-args.ts +0 -141
- package/src/agent-tooling/recipes/composed-thread.ts +0 -714
- package/src/agent-tooling/recipes/index.ts +0 -22
- package/src/agent-tooling/recipes/types.ts +0 -32
- package/src/agent-tooling/registry.ts +0 -132
- package/src/agent-tooling/route-emit.ts +0 -305
- package/src/agent-tooling/types.ts +0 -390
- package/src/components/artifact-card.tsx +0 -120
- package/src/components/artifact.tsx +0 -914
- package/src/components/attachment-types.ts +0 -66
- package/src/components/attachments.tsx +0 -536
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
- package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
- package/src/components/audio-visualizer/fit-scale.ts +0 -112
- package/src/components/audio-visualizer/index.tsx +0 -528
- package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
- package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
- package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
- package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
- package/src/components/audio-visualizer/sizes.ts +0 -54
- package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
- package/src/components/audio-visualizer/variant-bar.tsx +0 -175
- package/src/components/audio-visualizer/variant-custom.tsx +0 -344
- package/src/components/audio-visualizer/variant-grid.tsx +0 -175
- package/src/components/audio-visualizer/variant-radial.tsx +0 -163
- package/src/components/audio-visualizer/variant-wave.tsx +0 -130
- package/src/components/audio-visualizer/wave.glsl.ts +0 -130
- package/src/components/card-fallback.tsx +0 -60
- package/src/components/card-renderer.tsx +0 -220
- package/src/components/card.tsx +0 -212
- package/src/components/chain-of-thought.tsx +0 -348
- package/src/components/chat-container.tsx +0 -89
- package/src/components/chat-scope-picker.tsx +0 -57
- package/src/components/chat-thread.tsx +0 -1014
- package/src/components/checkpoint.tsx +0 -88
- package/src/components/choice-card.tsx +0 -743
- package/src/components/coachmark.tsx +0 -249
- package/src/components/code-block.tsx +0 -181
- package/src/components/composer-dom.ts +0 -159
- package/src/components/composer-highlight.ts +0 -242
- package/src/components/composer-history.ts +0 -82
- package/src/components/composer.tsx +0 -1075
- package/src/components/confirm-card.tsx +0 -402
- package/src/components/context.tsx +0 -377
- package/src/components/conversation-item.tsx +0 -241
- package/src/components/conversation-list.tsx +0 -429
- package/src/components/conversation-panel.tsx +0 -118
- package/src/components/dismissed-stub.tsx +0 -80
- package/src/components/embed.tsx +0 -200
- package/src/components/empty.tsx +0 -177
- package/src/components/feedback-bar.tsx +0 -166
- package/src/components/file-tree.tsx +0 -530
- package/src/components/file-upload.tsx +0 -167
- package/src/components/form-widgets.tsx +0 -525
- package/src/components/form.tsx +0 -1296
- package/src/components/home-panel.tsx +0 -159
- package/src/components/image.tsx +0 -65
- package/src/components/link-preview.tsx +0 -195
- package/src/components/loader.tsx +0 -367
- package/src/components/markdown.tsx +0 -162
- package/src/components/message-skills.tsx +0 -40
- package/src/components/message.tsx +0 -765
- package/src/components/model-switcher.tsx +0 -132
- package/src/components/prompt-input.tsx +0 -188
- package/src/components/prompt-suggestion.tsx +0 -150
- package/src/components/reasoning.tsx +0 -293
- package/src/components/response-compare-types.ts +0 -152
- package/src/components/response-compare.tsx +0 -455
- package/src/components/response-stream.tsx +0 -109
- package/src/components/screen.tsx +0 -181
- package/src/components/scroll-button.tsx +0 -93
- package/src/components/source.tsx +0 -192
- package/src/components/tasks-card.tsx +0 -801
- package/src/components/text-shimmer.tsx +0 -37
- package/src/components/thinking-bar.tsx +0 -50
- package/src/components/thread.tsx +0 -228
- package/src/components/toast.tsx +0 -503
- package/src/components/tool-classify.ts +0 -26
- package/src/components/tool-types.ts +0 -49
- package/src/components/tool.tsx +0 -190
- package/src/components/use-card-resolution.ts +0 -44
- package/src/components/voice-input.tsx +0 -222
- package/src/components/voice-output.tsx +0 -242
- package/src/components/widget-tab-bar.tsx +0 -73
- package/src/components/workspace-shell.tsx +0 -285
- package/src/diagnostics/hook.ts +0 -352
- package/src/diagnostics/index.ts +0 -82
- package/src/diagnostics/report-request.ts +0 -296
- package/src/elements/agent-card.tsx +0 -67
- package/src/elements/artifact.tsx +0 -194
- package/src/elements/attachments.tsx +0 -140
- package/src/elements/audio-visualizer.tsx +0 -227
- package/src/elements/autoloader.ts +0 -89
- package/src/elements/avatar.tsx +0 -33
- package/src/elements/badge.tsx +0 -32
- package/src/elements/button.tsx +0 -149
- package/src/elements/card.tsx +0 -133
- package/src/elements/cards.tsx +0 -266
- package/src/elements/chain-of-thought.tsx +0 -141
- package/src/elements/chat-scope-picker.tsx +0 -65
- package/src/elements/chat-types.ts +0 -102
- package/src/elements/chat-workspace.tsx +0 -188
- package/src/elements/chat.tsx +0 -304
- package/src/elements/checkbox-group.tsx +0 -164
- package/src/elements/checkbox.tsx +0 -134
- package/src/elements/checkpoint.tsx +0 -47
- package/src/elements/choice.tsx +0 -100
- package/src/elements/coachmark.tsx +0 -90
- package/src/elements/code-block.tsx +0 -60
- package/src/elements/command.tsx +0 -197
- package/src/elements/compare.tsx +0 -111
- package/src/elements/compiled.css +0 -2
- package/src/elements/composer.tsx +0 -162
- package/src/elements/confirm-card.tsx +0 -76
- package/src/elements/context-meter.tsx +0 -107
- package/src/elements/conversation-item.tsx +0 -120
- package/src/elements/conversation-list.tsx +0 -203
- package/src/elements/css.ts +0 -5
- package/src/elements/default-input.tsx +0 -392
- package/src/elements/define-entry.ts +0 -12
- package/src/elements/define.tsx +0 -563
- package/src/elements/diagnostic-events.ts +0 -114
- package/src/elements/dialog.tsx +0 -145
- package/src/elements/disclosure.ts +0 -95
- package/src/elements/dock.tsx +0 -215
- package/src/elements/dropdown.tsx +0 -123
- package/src/elements/editable-label.tsx +0 -140
- package/src/elements/element-data-types.ts +0 -152
- package/src/elements/element-diagnostics.ts +0 -392
- package/src/elements/element-manifest.json +0 -354
- package/src/elements/element-nonscalar.json +0 -187
- package/src/elements/element-types.d.ts +0 -4353
- package/src/elements/embed.tsx +0 -35
- package/src/elements/empty.tsx +0 -29
- package/src/elements/feedback-bar.tsx +0 -66
- package/src/elements/file-tree.tsx +0 -62
- package/src/elements/file-upload.tsx +0 -44
- package/src/elements/form.tsx +0 -94
- package/src/elements/hover-card.tsx +0 -80
- package/src/elements/icon.tsx +0 -41
- package/src/elements/image.tsx +0 -32
- package/src/elements/input.tsx +0 -492
- package/src/elements/kbd.tsx +0 -45
- package/src/elements/link-preview.tsx +0 -34
- package/src/elements/loader.tsx +0 -25
- package/src/elements/markdown.tsx +0 -38
- package/src/elements/menu.tsx +0 -241
- package/src/elements/message-skills.tsx +0 -83
- package/src/elements/message.tsx +0 -361
- package/src/elements/model-switcher.tsx +0 -121
- package/src/elements/nav.tsx +0 -87
- package/src/elements/notice.tsx +0 -53
- package/src/elements/pane-grid.tsx +0 -111
- package/src/elements/pane-group.tsx +0 -119
- package/src/elements/pane.tsx +0 -129
- package/src/elements/popover.tsx +0 -81
- package/src/elements/progress-bar.tsx +0 -39
- package/src/elements/prompt-dock.tsx +0 -82
- package/src/elements/prompt-input.tsx +0 -240
- package/src/elements/prompt-suggestions.tsx +0 -113
- package/src/elements/radio-group.tsx +0 -129
- package/src/elements/reasoning.tsx +0 -71
- package/src/elements/register-impl.ts +0 -148
- package/src/elements/register.ts +0 -76
- package/src/elements/remote.tsx +0 -225
- package/src/elements/resizable.globals.d.ts +0 -36
- package/src/elements/resizable.tsx +0 -704
- package/src/elements/response-stream.tsx +0 -40
- package/src/elements/screen.tsx +0 -90
- package/src/elements/scroll-area.tsx +0 -33
- package/src/elements/scroll-button.tsx +0 -179
- package/src/elements/search.tsx +0 -185
- package/src/elements/segmented.tsx +0 -129
- package/src/elements/select.tsx +0 -165
- package/src/elements/separator.tsx +0 -36
- package/src/elements/setting-item.tsx +0 -50
- package/src/elements/settings-group.tsx +0 -37
- package/src/elements/skeleton.tsx +0 -45
- package/src/elements/slider.tsx +0 -171
- package/src/elements/slot-text.ts +0 -73
- package/src/elements/slots.ts +0 -787
- package/src/elements/source.tsx +0 -126
- package/src/elements/status.tsx +0 -42
- package/src/elements/styles.css +0 -328
- package/src/elements/switch.tsx +0 -118
- package/src/elements/tabs.tsx +0 -95
- package/src/elements/tasks.tsx +0 -103
- package/src/elements/text-shimmer.tsx +0 -28
- package/src/elements/thinking-bar.tsx +0 -43
- package/src/elements/thread.tsx +0 -139
- package/src/elements/toast.tsx +0 -106
- package/src/elements/tool.tsx +0 -78
- package/src/elements/tooltip.tsx +0 -85
- package/src/elements/validate-messages.ts +0 -76
- package/src/elements/voice-input.tsx +0 -100
- package/src/elements/voice-output.tsx +0 -82
- package/src/index.ts +0 -430
- package/src/primitives/audio-bands.ts +0 -174
- package/src/primitives/card-component-types.ts +0 -63
- package/src/primitives/card-contract.ts +0 -80
- package/src/primitives/card-data-types.ts +0 -361
- package/src/primitives/card-host.tsx +0 -37
- package/src/primitives/card-recovery.ts +0 -160
- package/src/primitives/card-registry.tsx +0 -78
- package/src/primitives/card-resolution.ts +0 -36
- package/src/primitives/card-routing.ts +0 -85
- package/src/primitives/card-schemas/artifact.schema.json +0 -92
- package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
- package/src/primitives/card-schemas/card-event.schema.json +0 -12
- package/src/primitives/card-schemas/choice.schema.json +0 -75
- package/src/primitives/card-schemas/confirm.schema.json +0 -65
- package/src/primitives/card-schemas/embed.schema.json +0 -63
- package/src/primitives/card-schemas/form.result.schema.json +0 -7
- package/src/primitives/card-schemas/form.schema.json +0 -53
- package/src/primitives/card-schemas/link.schema.json +0 -54
- package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
- package/src/primitives/card-schemas/tasks.schema.json +0 -83
- package/src/primitives/card-tags.ts +0 -61
- package/src/primitives/card-validate-cards.ts +0 -255
- package/src/primitives/card-validate-schemas.ts +0 -58
- package/src/primitives/card-validate.ts +0 -131
- package/src/primitives/chat-config.tsx +0 -103
- package/src/primitives/composer-model.ts +0 -49
- package/src/primitives/composer-triggers.ts +0 -35
- package/src/primitives/controllable.ts +0 -21
- package/src/primitives/conversation-store.ts +0 -248
- package/src/primitives/create-kai-chat.ts +0 -66
- package/src/primitives/create-tween.ts +0 -266
- package/src/primitives/embed-providers.ts +0 -254
- package/src/primitives/field-mask.ts +0 -256
- package/src/primitives/field-semantics.ts +0 -115
- package/src/primitives/highlighter.ts +0 -157
- package/src/primitives/input-mask.ts +0 -853
- package/src/primitives/link-preview.ts +0 -87
- package/src/primitives/message-feedback.ts +0 -124
- package/src/primitives/pdf-preview.ts +0 -121
- package/src/primitives/toast-store.ts +0 -338
- package/src/primitives/url-scheme-policy.ts +0 -70
- package/src/primitives/use-audio-analysis.ts +0 -340
- package/src/primitives/use-auto-resize.ts +0 -31
- package/src/primitives/use-resize-observer.ts +0 -45
- package/src/primitives/use-sequencer.ts +0 -72
- package/src/primitives/use-speech-recognition.ts +0 -146
- package/src/primitives/use-stick-to-bottom.ts +0 -75
- package/src/primitives/use-text-stream.ts +0 -143
- package/src/primitives/use-voice-recorder.ts +0 -62
- package/src/primitives/visualizer-sequences.ts +0 -350
- package/src/remote/host-embed.ts +0 -345
- package/src/remote/index.ts +0 -2
- package/src/remote/origin.ts +0 -30
- package/src/remote/provider-runtime.ts +0 -262
- package/src/remote/provider.ts +0 -2
- package/src/remote/validate.ts +0 -22
- package/src/remote/version.ts +0 -12
- package/src/remote/wire.ts +0 -48
- package/src/schemas/from-tool-call.ts +0 -151
- package/src/schemas/index.ts +0 -341
- package/src/schemas/provider-subsets.ts +0 -538
- package/src/schemas/registry.ts +0 -355
- package/src/schemas/tool-defs.ts +0 -793
- package/src/solid.ts +0 -173
- package/src/state/index.ts +0 -44
- package/src/state/messages.ts +0 -53
- package/src/state/mock.ts +0 -297
- package/src/state/parts.ts +0 -295
- package/src/state/persistence.ts +0 -180
- package/src/state/stream.ts +0 -246
- package/src/state/suggestions.ts +0 -9
- package/src/state/threads.ts +0 -142
- package/src/test-utils/fake-clock.ts +0 -88
- package/src/types.ts +0 -95
- package/src/ui/action-icons.ts +0 -53
- package/src/ui/agent-card.tsx +0 -218
- package/src/ui/avatar.tsx +0 -23
- package/src/ui/badge.tsx +0 -21
- package/src/ui/button.tsx +0 -49
- package/src/ui/card.tsx +0 -245
- package/src/ui/checkbox-group.tsx +0 -153
- package/src/ui/checkbox.tsx +0 -63
- package/src/ui/collapsible.tsx +0 -163
- package/src/ui/command.tsx +0 -103
- package/src/ui/dialog.tsx +0 -223
- package/src/ui/dock.tsx +0 -630
- package/src/ui/dropdown.tsx +0 -636
- package/src/ui/editable-label.tsx +0 -112
- package/src/ui/hover-card.tsx +0 -345
- package/src/ui/icon.tsx +0 -167
- package/src/ui/input.tsx +0 -391
- package/src/ui/kbd.tsx +0 -118
- package/src/ui/nav.tsx +0 -282
- package/src/ui/notice.tsx +0 -84
- package/src/ui/overlay.tsx +0 -277
- package/src/ui/pane-grid.tsx +0 -117
- package/src/ui/pane-group.tsx +0 -291
- package/src/ui/pane.tsx +0 -193
- package/src/ui/popover.tsx +0 -106
- package/src/ui/progress-bar.tsx +0 -83
- package/src/ui/prompt-dock.tsx +0 -145
- package/src/ui/radio.tsx +0 -150
- package/src/ui/resizable.tsx +0 -693
- package/src/ui/scroll-area.tsx +0 -26
- package/src/ui/segmented.tsx +0 -109
- package/src/ui/select.tsx +0 -168
- package/src/ui/separator.tsx +0 -10
- package/src/ui/settings-group.tsx +0 -67
- package/src/ui/skeleton.tsx +0 -73
- package/src/ui/slider.tsx +0 -178
- package/src/ui/status.tsx +0 -48
- package/src/ui/switch.tsx +0 -119
- package/src/ui/tabs.tsx +0 -168
- package/src/ui/textarea.tsx +0 -21
- package/src/ui/tooltip.tsx +0 -118
- package/src/utils/cn.ts +0 -30
- package/src/wire/chunk.ts +0 -248
- package/src/wire/consume.ts +0 -599
- package/src/wire/diagnostics.ts +0 -727
- package/src/wire/encode-probe.ts +0 -214
- package/src/wire/encode.ts +0 -947
- package/src/wire/files.ts +0 -342
- package/src/wire/formats/anthropic.ts +0 -274
- package/src/wire/formats/openai.ts +0 -228
- package/src/wire/index.ts +0 -95
- package/src/wire/media-types.ts +0 -344
- package/src/wire/read.ts +0 -396
- package/src/wire/sink-helpers.ts +0 -50
- package/src/wire/sse.ts +0 -153
|
@@ -1,801 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type JSX,
|
|
3
|
-
For,
|
|
4
|
-
Show,
|
|
5
|
-
splitProps,
|
|
6
|
-
mergeProps,
|
|
7
|
-
createSignal,
|
|
8
|
-
createMemo,
|
|
9
|
-
createEffect,
|
|
10
|
-
on,
|
|
11
|
-
onMount,
|
|
12
|
-
ErrorBoundary,
|
|
13
|
-
createUniqueId,
|
|
14
|
-
} from 'solid-js';
|
|
15
|
-
import { cn } from '../utils/cn';
|
|
16
|
-
import { Button } from '../ui/button';
|
|
17
|
-
import { Checkbox } from '../ui/checkbox';
|
|
18
|
-
import { ProgressBar } from '../ui/progress-bar';
|
|
19
|
-
import { Card } from './card';
|
|
20
|
-
import { DismissedStub } from './dismissed-stub';
|
|
21
|
-
import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
|
|
22
|
-
import { useCardResolution } from './use-card-resolution';
|
|
23
|
-
import { emitCardEvent } from '../primitives/card-routing';
|
|
24
|
-
import { useCardHost } from '../primitives/card-host';
|
|
25
|
-
import { Check, Circle, CircleCheck } from 'lucide-solid';
|
|
26
|
-
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
// Types (tasks.schema.json) — AUTHORED IN ../primitives/card-data-types.ts.
|
|
29
|
-
// See the note in confirm-card.tsx, or that file's header, for why they left a
|
|
30
|
-
// `.tsx` and why they are `type` aliases. Re-exported here unchanged.
|
|
31
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
import type { TasksCardData, TasksCardResult, TasksTask } from '../primitives/card-data-types';
|
|
34
|
-
|
|
35
|
-
export type {
|
|
36
|
-
TasksCardData,
|
|
37
|
-
TasksCardEnvelope,
|
|
38
|
-
TasksCardResult,
|
|
39
|
-
TasksTask,
|
|
40
|
-
} from '../primitives/card-data-types';
|
|
41
|
-
|
|
42
|
-
export const TASKS_CARD_TYPE = 'tasks' as const;
|
|
43
|
-
|
|
44
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
-
// Pure helpers (unit-tested in isolation).
|
|
46
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
/** De-dupe tasks by id (first wins). Returns the usable list + an optional error. */
|
|
49
|
-
export function normalizeTasks(tasks: unknown): { tasks: TasksTask[]; error?: string } {
|
|
50
|
-
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
51
|
-
return { tasks: [], error: "This card couldn't be displayed." };
|
|
52
|
-
}
|
|
53
|
-
const seen = new Set<string>();
|
|
54
|
-
const out: TasksTask[] = [];
|
|
55
|
-
for (const t of tasks) {
|
|
56
|
-
if (!t || typeof t !== 'object') continue;
|
|
57
|
-
const task = t as Partial<TasksTask>;
|
|
58
|
-
if (typeof task.id !== 'string' || task.id.length === 0) continue;
|
|
59
|
-
if (typeof task.label !== 'string' || task.label.length === 0) continue;
|
|
60
|
-
if (seen.has(task.id)) {
|
|
61
|
-
// eslint-disable-next-line no-console
|
|
62
|
-
console.warn(`[kai-tasks] duplicate task id "${task.id}" ignored`);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
seen.add(task.id);
|
|
66
|
-
out.push({
|
|
67
|
-
id: task.id,
|
|
68
|
-
label: task.label,
|
|
69
|
-
description: typeof task.description === 'string' ? task.description : undefined,
|
|
70
|
-
checked: task.checked === true,
|
|
71
|
-
disabled: task.disabled === true,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (out.length === 0) return { tasks: [], error: "This card couldn't be displayed." };
|
|
75
|
-
return { tasks: out };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The ids of the initially-checked tasks (in input order). */
|
|
79
|
-
export function initialSelected(tasks: TasksTask[]): string[] {
|
|
80
|
-
return tasks.filter((t) => t.checked).map((t) => t.id);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The selected ids in input order (selection set ∩ tasks, preserving task order). */
|
|
84
|
-
export function selectedInOrder(tasks: TasksTask[], selected: Set<string>): string[] {
|
|
85
|
-
return tasks.filter((t) => selected.has(t.id)).map((t) => t.id);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The toggleable (non-disabled) task ids. */
|
|
89
|
-
export function toggleableIds(tasks: TasksTask[]): string[] {
|
|
90
|
-
return tasks.filter((t) => !t.disabled).map((t) => t.id);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export type SelectAllState = 'checked' | 'unchecked' | 'indeterminate';
|
|
94
|
-
|
|
95
|
-
/** Select-all tri-state over the toggleable rows. */
|
|
96
|
-
export function selectAllState(tasks: TasksTask[], selected: Set<string>): SelectAllState {
|
|
97
|
-
const ids = toggleableIds(tasks);
|
|
98
|
-
if (ids.length === 0) return 'unchecked';
|
|
99
|
-
const n = ids.filter((id) => selected.has(id)).length;
|
|
100
|
-
if (n === 0) return 'unchecked';
|
|
101
|
-
if (n === ids.length) return 'checked';
|
|
102
|
-
return 'indeterminate';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Whether select-all should be shown: requested AND not blocked by `max` (since
|
|
106
|
-
* "all" would violate max). */
|
|
107
|
-
export function showSelectAll(data: TasksCardData, tasks: TasksTask[]): boolean {
|
|
108
|
-
if (data.selectAll !== true) return false;
|
|
109
|
-
const count = toggleableIds(tasks).length;
|
|
110
|
-
if (data.max !== undefined && count > data.max) return false;
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Whether the card is in the onboarding-checklist `progress` look (no confirm). */
|
|
115
|
-
export function isProgressMode(data: TasksCardData): boolean {
|
|
116
|
-
return data.mode === 'progress';
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** The header progress count (`done` checked / `total` rows) for `progress` mode. */
|
|
120
|
-
export function progressCount(tasks: TasksTask[], selected: Set<string>): { done: number; total: number } {
|
|
121
|
-
return { done: tasks.filter((t) => selected.has(t.id)).length, total: tasks.length };
|
|
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
|
-
/** Imperative handle exposed via `controllerRef` — surfaces the tasks card's latent
|
|
151
|
-
* selection model (set the checked ids, toggle one, confirm, focus the group,
|
|
152
|
-
* dismiss/reopen) so the `<kai-tasks>` facade can forward them as instance methods. */
|
|
153
|
-
export interface TasksCardController {
|
|
154
|
-
/** Set the checked ids (local-only, no emit), respecting disabled/max. No arg = select all toggleable rows. */
|
|
155
|
-
select(taskIds?: string[]): void;
|
|
156
|
-
/** Toggle one task by id, honoring the max gate. */
|
|
157
|
-
toggle(taskId: string, checked?: boolean): void;
|
|
158
|
-
/** Confirm the current selection (only when allowed) — emits `submit` + resolves. */
|
|
159
|
-
send(): void;
|
|
160
|
-
/** Focus the task group (select-all checkbox if shown, else the first row). */
|
|
161
|
-
focus(options?: FocusOptions): void;
|
|
162
|
-
/** Trigger the dismiss path (emit `dismiss` + collapse to the re-openable stub). */
|
|
163
|
-
dismiss(): void;
|
|
164
|
-
/** Re-open a dismissed card from its stub (emit `reopen`). */
|
|
165
|
-
reopen(): void;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface TasksCardProps {
|
|
169
|
-
/** The tasks definition (CardEnvelope.data). */
|
|
170
|
-
data?: TasksCardData;
|
|
171
|
-
/** The card id used to correlate every emitted CardEvent. */
|
|
172
|
-
cardId?: string;
|
|
173
|
-
/** The envelope title rendered in the card chrome. */
|
|
174
|
-
heading?: string;
|
|
175
|
-
/** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
|
|
176
|
-
* bubbling `kai-card` CustomEvent off `hostElement`). */
|
|
177
|
-
host?: CardHost;
|
|
178
|
-
/** The custom-element host node, for the bubbling `kai-card` fallback emit. */
|
|
179
|
-
hostElement?: HTMLElement;
|
|
180
|
-
class?: string;
|
|
181
|
-
/** When set, render the chromed read-only summary instead of the interactive controls. */
|
|
182
|
-
resolution?: CardResolution;
|
|
183
|
-
/** Controlled selection (task ids) — when set, this wins over internal state. */
|
|
184
|
-
value?: string[];
|
|
185
|
-
/** Uncontrolled initial selection (task ids), overlaying per-task `checked`. */
|
|
186
|
-
defaultValue?: string[];
|
|
187
|
-
/** Freeze the whole list + Confirm. */
|
|
188
|
-
disabled?: boolean;
|
|
189
|
-
/** Display-only: rows can't be toggled and show the default cursor (no pointer,
|
|
190
|
-
* no hover background, no focus ring). Keeps the visual content as-is — this only
|
|
191
|
-
* drops the interactive affordances + a11y exposure. */
|
|
192
|
-
readonly?: boolean;
|
|
193
|
-
/** Fires on every selection change with the selected ids (distinct from submit). */
|
|
194
|
-
onValueChange?: (payload: { value: string[] }) => void;
|
|
195
|
-
/** Receive the imperative controller once mounted. */
|
|
196
|
-
controllerRef?: (controller: TasksCardController) => void;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const DEFAULT_DATA: TasksCardData = { tasks: [] };
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* `TasksCard` — a selectable task/plan list (checkbox rows + optional select-all
|
|
203
|
-
* + a confirm button) inside `Card` chrome. Row toggling and select-all are local
|
|
204
|
-
* UI state; only the final confirm emits the Card contract's `submit` verb
|
|
205
|
-
* (`{ kind:'submit', cardId, data:{ selected } }`) with the checked ids in
|
|
206
|
-
* input order. Emits `ready` on mount and `error` for an unusable definition.
|
|
207
|
-
*/
|
|
208
|
-
export function TasksCard(props: TasksCardProps): JSX.Element {
|
|
209
|
-
const merged = mergeProps({ cardId: 'kai-tasks' }, props);
|
|
210
|
-
const [local] = splitProps(merged, [
|
|
211
|
-
'data', 'cardId', 'heading', 'host', 'hostElement', 'class', 'resolution',
|
|
212
|
-
'value', 'defaultValue', 'disabled', 'readonly', 'onValueChange', 'controllerRef',
|
|
213
|
-
]);
|
|
214
|
-
|
|
215
|
-
const ctxHost = useCardHost();
|
|
216
|
-
const uid = createUniqueId();
|
|
217
|
-
|
|
218
|
-
const emit = (event: CardEvent): void => {
|
|
219
|
-
const h = local.host ?? ctxHost;
|
|
220
|
-
if (h) h.emit(event);
|
|
221
|
-
else if (local.hostElement) emitCardEvent(local.hostElement, event);
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const normalized = createMemo(() => normalizeTasks(local.data?.tasks));
|
|
225
|
-
const valid = createMemo(() => normalized().error === undefined);
|
|
226
|
-
const errorMessage = createMemo(() => normalized().error ?? '');
|
|
227
|
-
const tasks = createMemo(() => normalized().tasks);
|
|
228
|
-
const data = createMemo<TasksCardData>(() => local.data ?? DEFAULT_DATA);
|
|
229
|
-
|
|
230
|
-
const [selected, setSelected] = createSignal<Set<string>>(new Set());
|
|
231
|
-
|
|
232
|
-
const res = useCardResolution({ prop: () => local.resolution, data: () => local.data });
|
|
233
|
-
|
|
234
|
-
// Only ids that exist in the current task list (drop unknown ids defensively).
|
|
235
|
-
const validIds = (ids: string[]): string[] => {
|
|
236
|
-
const known = new Set(tasks().map((t) => t.id));
|
|
237
|
-
return ids.filter((id) => known.has(id));
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// Seed selection from `value` (controlled) > `defaultValue` > per-task `checked`,
|
|
241
|
-
// when a NEW definition arrives.
|
|
242
|
-
createEffect(
|
|
243
|
-
on(
|
|
244
|
-
() => local.data,
|
|
245
|
-
() => {
|
|
246
|
-
const seedIds = local.value ?? local.defaultValue ?? initialSelected(tasks());
|
|
247
|
-
setSelected(new Set(validIds(seedIds)));
|
|
248
|
-
},
|
|
249
|
-
),
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
// Controlled selection: when the consumer drives `value`, mirror it into the set
|
|
253
|
-
// (the controlled prop wins). Deferred so mount's seed runs first.
|
|
254
|
-
createEffect(on(() => local.value, (v) => {
|
|
255
|
-
if (!v) return;
|
|
256
|
-
setSelected(new Set(validIds(v)));
|
|
257
|
-
}, { defer: true }));
|
|
258
|
-
|
|
259
|
-
// Fire the live change signal whenever the selection set changes (distinct from
|
|
260
|
-
// the terminal submit). Deferred so the initial seed doesn't emit.
|
|
261
|
-
createEffect(on(selected, (s) => {
|
|
262
|
-
local.onValueChange?.({ value: selectedInOrder(tasks(), s) });
|
|
263
|
-
}, { defer: true }));
|
|
264
|
-
|
|
265
|
-
// ready / error lifecycle emits.
|
|
266
|
-
createEffect(
|
|
267
|
-
on(valid, (ok) => {
|
|
268
|
-
if (ok) emit({ kind: 'ready', cardId: local.cardId });
|
|
269
|
-
else emit({ kind: 'error', cardId: local.cardId, message: errorMessage() });
|
|
270
|
-
}),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const groupDisabled = (): boolean => local.disabled === true;
|
|
274
|
-
// Display-only: block all toggling/confirm and drop the interactive affordances,
|
|
275
|
-
// without the dimmed/`disabled` look.
|
|
276
|
-
const readOnly = (): boolean => local.readonly === true;
|
|
277
|
-
|
|
278
|
-
const count = createMemo(() => selected().size);
|
|
279
|
-
// `progress` mode = the onboarding-checklist look: a header `done / total`
|
|
280
|
-
// count, circular indicators, no confirm button. Same selection model.
|
|
281
|
-
const progress = createMemo(() => isProgressMode(data()));
|
|
282
|
-
const progressN = createMemo(() => progressCount(tasks(), selected()));
|
|
283
|
-
const confirmLabel = () => data().confirmLabel ?? 'Confirm';
|
|
284
|
-
const masterState = createMemo(() => selectAllState(tasks(), selected()));
|
|
285
|
-
const showMaster = createMemo(() => showSelectAll(data(), tasks()));
|
|
286
|
-
const reason = createMemo(() => confirmReason(data(), count()));
|
|
287
|
-
const confirmEnabled = createMemo(() => canConfirm(data(), count()) && !res.isResolved() && !groupDisabled() && !readOnly());
|
|
288
|
-
|
|
289
|
-
// Apply one id to a selection set, honoring the max gate (mutates+returns `next`).
|
|
290
|
-
const applyToggle = (next: Set<string>, id: string, on: boolean): Set<string> => {
|
|
291
|
-
if (on) {
|
|
292
|
-
// Respect max: block adding past max.
|
|
293
|
-
if (isMaxReached(data(), next.size) && !next.has(id)) return next;
|
|
294
|
-
next.add(id);
|
|
295
|
-
} else {
|
|
296
|
-
next.delete(id);
|
|
297
|
-
}
|
|
298
|
-
return next;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const toggle = (id: string, on: boolean): void => {
|
|
302
|
-
if (res.isResolved() || groupDisabled() || readOnly()) return;
|
|
303
|
-
setSelected(applyToggle(new Set(selected()), id, on));
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const toggleAll = (on: boolean): void => {
|
|
307
|
-
if (res.isResolved() || groupDisabled() || readOnly()) return;
|
|
308
|
-
const next = new Set(selected());
|
|
309
|
-
for (const id of toggleableIds(tasks())) {
|
|
310
|
-
if (on) next.add(id);
|
|
311
|
-
else next.delete(id);
|
|
312
|
-
}
|
|
313
|
-
setSelected(next);
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const onConfirm = (): void => {
|
|
317
|
-
if (!confirmEnabled()) return;
|
|
318
|
-
const result: TasksCardResult = { selected: selectedInOrder(tasks(), selected()) };
|
|
319
|
-
emit({ kind: 'submit', cardId: local.cardId, data: result });
|
|
320
|
-
res.setLocal({ kind: 'submit', data: result });
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
// Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
|
|
324
|
-
// the card collapses to its re-openable stub immediately.
|
|
325
|
-
const onDismiss = (): void => {
|
|
326
|
-
if (res.isResolved()) return;
|
|
327
|
-
emit({ kind: 'dismiss', cardId: local.cardId });
|
|
328
|
-
res.setLocal({ kind: 'dismissed' });
|
|
329
|
-
};
|
|
330
|
-
const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
|
|
331
|
-
|
|
332
|
-
// Imperative controller (Pattern C): hand the facade a handle over the tasks
|
|
333
|
-
// card's latent selection model. select sets the checked ids local-only (no
|
|
334
|
-
// submit); toggle drives one row; send confirms (same path as Confirm); focus
|
|
335
|
-
// targets the group; dismiss/reopen drive the stub.
|
|
336
|
-
onMount(() => {
|
|
337
|
-
local.controllerRef?.({
|
|
338
|
-
select: (taskIds) => {
|
|
339
|
-
if (res.isResolved() || groupDisabled() || readOnly()) return;
|
|
340
|
-
if (taskIds === undefined) { toggleAll(true); return; }
|
|
341
|
-
// Build the set fresh, honoring max + disabled per id (skip disabled rows).
|
|
342
|
-
const next = new Set<string>();
|
|
343
|
-
const toggleable = new Set(toggleableIds(tasks()));
|
|
344
|
-
for (const id of validIds(taskIds)) {
|
|
345
|
-
if (!toggleable.has(id)) continue; // skip per-task-disabled rows
|
|
346
|
-
applyToggle(next, id, true);
|
|
347
|
-
}
|
|
348
|
-
setSelected(next);
|
|
349
|
-
},
|
|
350
|
-
toggle: (taskId, checked) => {
|
|
351
|
-
// No explicit `checked` → flip the current state.
|
|
352
|
-
const on = checked ?? !selected().has(taskId);
|
|
353
|
-
toggle(taskId, on);
|
|
354
|
-
},
|
|
355
|
-
send: () => onConfirm(),
|
|
356
|
-
focus: (options) => {
|
|
357
|
-
const root = local.hostElement?.shadowRoot ?? document;
|
|
358
|
-
const group = root.querySelector<HTMLElement>('[role="group"]');
|
|
359
|
-
// Prefer the select-all checkbox (it's the first labelled input), else the
|
|
360
|
-
// first row's checkbox; fall back to the group container itself.
|
|
361
|
-
const firstInput = group?.querySelector<HTMLElement>('input[type="checkbox"]');
|
|
362
|
-
(firstInput ?? group)?.focus(options);
|
|
363
|
-
},
|
|
364
|
-
dismiss: () => onDismiss(),
|
|
365
|
-
reopen: () => onReopen(),
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Surface the resolved state for host styling. Only a `submit` resolution is the
|
|
370
|
-
// "submitted" state; a deferred `dismissed` (or `expired`) is not.
|
|
371
|
-
createEffect(() => {
|
|
372
|
-
const el = local.hostElement;
|
|
373
|
-
if (!el) return;
|
|
374
|
-
if (res.resolution()?.kind === 'submit') el.setAttribute('data-kai-resolved', 'submitted');
|
|
375
|
-
else el.removeAttribute('data-kai-resolved');
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const resolvedSummary = createMemo(() => {
|
|
379
|
-
const r = res.resolution();
|
|
380
|
-
if (!r || r.kind !== 'submit') return undefined;
|
|
381
|
-
const sel = (r.data as { selected?: unknown })?.selected;
|
|
382
|
-
const ids: string[] = Array.isArray(sel) ? (sel as string[]) : [];
|
|
383
|
-
const all = tasks();
|
|
384
|
-
const chosen = all.filter((t) => ids.includes(t.id)).map((t) => t.label);
|
|
385
|
-
return { count: chosen.length, total: all.length, labels: chosen };
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const reasonId = `kai-tl-reason-${uid}`;
|
|
389
|
-
const countId = `kai-tl-count-${uid}`;
|
|
390
|
-
const progressCountId = `kai-tl-progress-${uid}`;
|
|
391
|
-
const progressHeadingId = `kai-tl-heading-${uid}`;
|
|
392
|
-
const headingText = () => local.heading ?? local.data?.heading;
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
|
|
396
|
-
<ErrorBoundary
|
|
397
|
-
fallback={() => {
|
|
398
|
-
emit({ kind: 'error', cardId: local.cardId, message: 'The card failed to render.' });
|
|
399
|
-
return <Card heading={local.heading} errorMessage="The card failed to render." />;
|
|
400
|
-
}}
|
|
401
|
-
>
|
|
402
|
-
<Show
|
|
403
|
-
when={!res.isDeferred()}
|
|
404
|
-
fallback={
|
|
405
|
-
<DismissedStub type={TASKS_CARD_TYPE} title={local.heading ?? local.data?.heading} onReopen={onReopen} />
|
|
406
|
-
}
|
|
407
|
-
>
|
|
408
|
-
<Card
|
|
409
|
-
// In `progress` mode the heading + count render as a custom in-body row
|
|
410
|
-
// (so the count can sit right-aligned beside the heading), so the Card
|
|
411
|
-
// chrome's own heading is suppressed.
|
|
412
|
-
heading={progress() ? undefined : headingText()}
|
|
413
|
-
actions={
|
|
414
|
-
// `progress` mode is confirm-less: checking a row IS the action.
|
|
415
|
-
// `readonly` is display-only, so there's no confirm/dismiss row either.
|
|
416
|
-
progress() || res.isResolved() || readOnly() ? undefined : (
|
|
417
|
-
<div class="flex w-full flex-wrap items-center justify-between gap-2">
|
|
418
|
-
<Show when={local.data?.dismissible === true}>
|
|
419
|
-
{/* The contract `dismiss` verb — a footer ACTION (it emits
|
|
420
|
-
`{kind:'dismiss'}` and collapses the card to a re-openable
|
|
421
|
-
stub), not the chrome close in `Card`'s own `dismissible`
|
|
422
|
-
prop, which the contract cards deliberately never set.
|
|
423
|
-
Labelled rather than a bare ghost ✕: every other control in
|
|
424
|
-
this row says what it does, and an unlabelled 28px glyph
|
|
425
|
-
300px from the primary button reads as stray chrome. Same
|
|
426
|
-
shape in all four cards — `form` already looked like this.
|
|
427
|
-
`aria-label` is kept (redundant with the text) so the
|
|
428
|
-
element tests that select on it keep working. */}
|
|
429
|
-
<Button
|
|
430
|
-
type="button"
|
|
431
|
-
variant="ghost"
|
|
432
|
-
aria-label="Dismiss"
|
|
433
|
-
onClick={onDismiss}
|
|
434
|
-
>
|
|
435
|
-
Dismiss
|
|
436
|
-
</Button>
|
|
437
|
-
</Show>
|
|
438
|
-
<span id={countId} aria-live="polite" class="text-xs text-muted-foreground">
|
|
439
|
-
{count()} selected
|
|
440
|
-
</span>
|
|
441
|
-
<div class="ml-auto flex items-center gap-2">
|
|
442
|
-
<Show when={reason()}>
|
|
443
|
-
<span id={reasonId} class="sr-only">
|
|
444
|
-
{reason()}
|
|
445
|
-
</span>
|
|
446
|
-
</Show>
|
|
447
|
-
<Button
|
|
448
|
-
type="button"
|
|
449
|
-
disabled={!confirmEnabled()}
|
|
450
|
-
aria-describedby={reason() ? reasonId : undefined}
|
|
451
|
-
onClick={onConfirm}
|
|
452
|
-
>
|
|
453
|
-
{confirmLabel()}
|
|
454
|
-
</Button>
|
|
455
|
-
</div>
|
|
456
|
-
</div>
|
|
457
|
-
)
|
|
458
|
-
}
|
|
459
|
-
>
|
|
460
|
-
<Show
|
|
461
|
-
when={!res.isResolved()}
|
|
462
|
-
fallback={<TasksResolved summary={resolvedSummary()!} optimistic={res.isOptimistic()} />}
|
|
463
|
-
>
|
|
464
|
-
<Show when={progress()}>
|
|
465
|
-
<ProgressChecklist
|
|
466
|
-
heading={headingText()}
|
|
467
|
-
headingId={progressHeadingId}
|
|
468
|
-
countId={progressCountId}
|
|
469
|
-
done={progressN().done}
|
|
470
|
-
total={progressN().total}
|
|
471
|
-
tasks={tasks()}
|
|
472
|
-
uid={uid}
|
|
473
|
-
isChecked={(id) => selected().has(id)}
|
|
474
|
-
isBlocked={(task) =>
|
|
475
|
-
groupDisabled() || task.disabled === true ||
|
|
476
|
-
(!selected().has(task.id) && isMaxReached(data(), count()))
|
|
477
|
-
}
|
|
478
|
-
onToggle={(id, on) => toggle(id, on)}
|
|
479
|
-
groupClass={local.class}
|
|
480
|
-
maxNote={data().max !== undefined ? data().max : undefined}
|
|
481
|
-
readonly={readOnly()}
|
|
482
|
-
/>
|
|
483
|
-
</Show>
|
|
484
|
-
<Show when={!progress()}>
|
|
485
|
-
<div
|
|
486
|
-
role="group"
|
|
487
|
-
aria-label={local.heading ?? local.data?.heading ?? 'Tasks'}
|
|
488
|
-
aria-readonly={readOnly() ? 'true' : undefined}
|
|
489
|
-
class={cn('flex flex-col', local.class)}
|
|
490
|
-
onKeyDown={(e) => {
|
|
491
|
-
// Enter anywhere in the card (off a checkbox) confirms when enabled.
|
|
492
|
-
if (e.key !== 'Enter') return;
|
|
493
|
-
const target = e.target as HTMLElement;
|
|
494
|
-
if (target.tagName === 'INPUT') return;
|
|
495
|
-
if (confirmEnabled()) onConfirm();
|
|
496
|
-
}}
|
|
497
|
-
>
|
|
498
|
-
<div class="divide-y divide-border overflow-hidden rounded-lg border border-border">
|
|
499
|
-
<Show when={showMaster()}>
|
|
500
|
-
{(() => {
|
|
501
|
-
const indeterminate = () => masterState() === 'indeterminate';
|
|
502
|
-
return (
|
|
503
|
-
<label
|
|
504
|
-
class={cn(
|
|
505
|
-
'flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors',
|
|
506
|
-
readOnly() ? 'cursor-default' : 'cursor-pointer',
|
|
507
|
-
masterState() === 'checked'
|
|
508
|
-
? 'bg-accent text-accent-foreground'
|
|
509
|
-
: readOnly()
|
|
510
|
-
? 'text-foreground'
|
|
511
|
-
: 'text-foreground hover:bg-muted/50',
|
|
512
|
-
)}
|
|
513
|
-
>
|
|
514
|
-
{/* SQUARE `Checkbox` (the `.kai-checkbox` look), and the checklist below draws a
|
|
515
|
-
ROUND lucide Circle/CircleCheck instead. Deliberate, not drift: this
|
|
516
|
-
is `select` mode — a pick-list whose rows are a pending selection that
|
|
517
|
-
only commits when the confirm button fires, which is what a checkbox
|
|
518
|
-
means everywhere else. `progress` mode has no confirm and toggling a
|
|
519
|
-
row IS the action, so it uses the done/not-done affordance of a todo
|
|
520
|
-
list. See the D-2 row in
|
|
521
|
-
docs/superpowers/plans/2026-08-25-form-control-primitives.md; owner has
|
|
522
|
-
not ruled, so do not converge these on your own. */}
|
|
523
|
-
<Checkbox
|
|
524
|
-
checked={masterState() === 'checked'}
|
|
525
|
-
disabled={groupDisabled() || readOnly()}
|
|
526
|
-
indeterminate={indeterminate()}
|
|
527
|
-
aria-checked={indeterminate() ? 'mixed' : masterState() === 'checked'}
|
|
528
|
-
onChange={(e) => toggleAll(e.currentTarget.checked)}
|
|
529
|
-
/>
|
|
530
|
-
<span>Select all</span>
|
|
531
|
-
</label>
|
|
532
|
-
);
|
|
533
|
-
})()}
|
|
534
|
-
</Show>
|
|
535
|
-
|
|
536
|
-
<For each={tasks()}>
|
|
537
|
-
{(task) => {
|
|
538
|
-
const checked = () => selected().has(task.id);
|
|
539
|
-
const blocked = () =>
|
|
540
|
-
groupDisabled() || task.disabled || (!checked() && isMaxReached(data(), count()));
|
|
541
|
-
const descId = `kai-tl-desc-${uid}-${task.id}`;
|
|
542
|
-
return (
|
|
543
|
-
<label
|
|
544
|
-
class={cn(
|
|
545
|
-
'flex items-start gap-3 px-3 py-2.5 text-sm transition-colors',
|
|
546
|
-
readOnly()
|
|
547
|
-
? 'cursor-default'
|
|
548
|
-
: blocked()
|
|
549
|
-
? 'cursor-not-allowed opacity-60'
|
|
550
|
-
: 'cursor-pointer hover:bg-muted/50',
|
|
551
|
-
checked() && !blocked()
|
|
552
|
-
? 'bg-accent font-medium text-accent-foreground'
|
|
553
|
-
: 'text-foreground',
|
|
554
|
-
)}
|
|
555
|
-
data-task-id={task.id}
|
|
556
|
-
>
|
|
557
|
-
<Checkbox
|
|
558
|
-
class="mt-0.5"
|
|
559
|
-
checked={checked()}
|
|
560
|
-
disabled={blocked() || readOnly()}
|
|
561
|
-
aria-disabled={blocked() ? 'true' : undefined}
|
|
562
|
-
aria-describedby={task.description ? descId : undefined}
|
|
563
|
-
onChange={(e) => toggle(task.id, e.currentTarget.checked)}
|
|
564
|
-
/>
|
|
565
|
-
<span class="flex flex-col gap-0.5">
|
|
566
|
-
<span>{task.label}</span>
|
|
567
|
-
<Show when={task.description}>
|
|
568
|
-
<span id={descId} class="text-xs font-normal text-muted-foreground">
|
|
569
|
-
{task.description}
|
|
570
|
-
</span>
|
|
571
|
-
</Show>
|
|
572
|
-
</span>
|
|
573
|
-
</label>
|
|
574
|
-
);
|
|
575
|
-
}}
|
|
576
|
-
</For>
|
|
577
|
-
</div>
|
|
578
|
-
|
|
579
|
-
<Show when={data().max !== undefined}>
|
|
580
|
-
<p class="pt-1 text-xs text-muted-foreground">Up to {data().max} selected.</p>
|
|
581
|
-
</Show>
|
|
582
|
-
</div>
|
|
583
|
-
</Show>
|
|
584
|
-
</Show>
|
|
585
|
-
</Card>
|
|
586
|
-
</Show>
|
|
587
|
-
</ErrorBoundary>
|
|
588
|
-
</Show>
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
593
|
-
// Onboarding-checklist (`mode: 'progress'`) presenter: a header `done / total`
|
|
594
|
-
// count + circular indicators + per-item title/description, no confirm button.
|
|
595
|
-
// Drives the same selection model, where toggling a row IS the action.
|
|
596
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
597
|
-
|
|
598
|
-
function ProgressChecklist(props: {
|
|
599
|
-
heading?: string;
|
|
600
|
-
headingId: string;
|
|
601
|
-
countId: string;
|
|
602
|
-
done: number;
|
|
603
|
-
total: number;
|
|
604
|
-
tasks: TasksTask[];
|
|
605
|
-
uid: string;
|
|
606
|
-
isChecked: (id: string) => boolean;
|
|
607
|
-
isBlocked: (task: TasksTask) => boolean;
|
|
608
|
-
onToggle: (id: string, on: boolean) => void;
|
|
609
|
-
groupClass?: string;
|
|
610
|
-
maxNote?: number;
|
|
611
|
-
/** Display-only: rows aren't focusable/toggleable and show the default cursor. */
|
|
612
|
-
readonly?: boolean;
|
|
613
|
-
}): JSX.Element {
|
|
614
|
-
return (
|
|
615
|
-
<div class={cn('flex flex-col gap-3', props.groupClass)}>
|
|
616
|
-
<Show when={props.heading}>
|
|
617
|
-
<div class="flex items-center justify-between gap-3">
|
|
618
|
-
<span
|
|
619
|
-
id={props.headingId}
|
|
620
|
-
class="text-title font-semibold tracking-tight text-foreground"
|
|
621
|
-
>
|
|
622
|
-
{props.heading}
|
|
623
|
-
</span>
|
|
624
|
-
<span
|
|
625
|
-
id={props.countId}
|
|
626
|
-
aria-live="polite"
|
|
627
|
-
class="shrink-0 text-sm font-medium tabular-nums text-muted-foreground"
|
|
628
|
-
>
|
|
629
|
-
{props.done} / {props.total}
|
|
630
|
-
</span>
|
|
631
|
-
</div>
|
|
632
|
-
</Show>
|
|
633
|
-
|
|
634
|
-
{/* Thin progress bar under the heading: filled to done / total. Named off the
|
|
635
|
-
checklist heading (no duplicate caption), or a generic label when headless. */}
|
|
636
|
-
<ProgressBar
|
|
637
|
-
value={props.done}
|
|
638
|
-
max={props.total}
|
|
639
|
-
tone="success"
|
|
640
|
-
aria-labelledby={props.heading ? props.headingId : undefined}
|
|
641
|
-
aria-label={props.heading ? undefined : 'Task progress'}
|
|
642
|
-
/>
|
|
643
|
-
|
|
644
|
-
<ul
|
|
645
|
-
aria-labelledby={props.heading ? props.headingId : undefined}
|
|
646
|
-
aria-label={props.heading ? undefined : 'Checklist'}
|
|
647
|
-
class="flex flex-col gap-1"
|
|
648
|
-
>
|
|
649
|
-
<For each={props.tasks}>
|
|
650
|
-
{(task) => {
|
|
651
|
-
const checked = () => props.isChecked(task.id);
|
|
652
|
-
const blocked = () => props.isBlocked(task);
|
|
653
|
-
const descId = `kai-tl-pdesc-${props.uid}-${task.id}`;
|
|
654
|
-
return (
|
|
655
|
-
<li>
|
|
656
|
-
<label
|
|
657
|
-
class={cn(
|
|
658
|
-
'group flex items-start gap-3 rounded-lg px-2.5 py-2 text-sm transition-colors',
|
|
659
|
-
// Keyboard focus ring (kit standard, inset so it stays within the
|
|
660
|
-
// row). focus-visible only, so a mouse click that parks focus on the
|
|
661
|
-
// row doesn't leave a noisy persistent outline — and it makes a
|
|
662
|
-
// Tab-focused completed row's open description perceivable. Dropped
|
|
663
|
-
// entirely when readonly (the row isn't focusable then).
|
|
664
|
-
//
|
|
665
|
-
// TWO tab stops exist per row and the ring has to cover both.
|
|
666
|
-
// `focus-visible:` matches only when the LABEL itself is focused,
|
|
667
|
-
// which is completed rows only (the `tabindex` below). Every row's
|
|
668
|
-
// other — and for an incomplete row, only — tab stop is the sr-only
|
|
669
|
-
// checkbox inside, so without `has-[:focus-visible]:` tabbing to an
|
|
670
|
-
// unchecked row put focus on an off-screen input with no indicator
|
|
671
|
-
// anywhere on screen. Both halves stay on the focus-VISIBLE axis on
|
|
672
|
-
// purpose: `focus-within:` would also fire for a mouse click (which
|
|
673
|
-
// forwards focus from the label to the checkbox), which is exactly
|
|
674
|
-
// the persistent-outline noise the paragraph above rejects. The
|
|
675
|
-
// description reveal below uses `group-focus-within:` because a
|
|
676
|
-
// hover-or-focus reveal wants the looser condition.
|
|
677
|
-
!props.readonly &&
|
|
678
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
|
679
|
-
!props.readonly &&
|
|
680
|
-
'has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-inset has-[:focus-visible]:ring-ring',
|
|
681
|
-
props.readonly
|
|
682
|
-
? 'cursor-default'
|
|
683
|
-
: blocked()
|
|
684
|
-
? 'cursor-not-allowed opacity-60'
|
|
685
|
-
: 'cursor-pointer hover:bg-muted/50',
|
|
686
|
-
)}
|
|
687
|
-
data-task-id={task.id}
|
|
688
|
-
// Completed rows hide their description until hover/focus, so make the
|
|
689
|
-
// row itself focusable to drive the focus-reveal (group-focus-within).
|
|
690
|
-
// Incomplete rows — and every row when readonly — stay unfocusable.
|
|
691
|
-
// Expose the label + description to AT.
|
|
692
|
-
tabindex={!props.readonly && checked() ? 0 : undefined}
|
|
693
|
-
aria-label={checked() ? task.label : undefined}
|
|
694
|
-
aria-describedby={checked() && task.description ? descId : undefined}
|
|
695
|
-
>
|
|
696
|
-
<input
|
|
697
|
-
type="checkbox"
|
|
698
|
-
class="sr-only"
|
|
699
|
-
checked={checked()}
|
|
700
|
-
disabled={blocked() || props.readonly}
|
|
701
|
-
aria-disabled={blocked() ? 'true' : undefined}
|
|
702
|
-
aria-describedby={task.description ? descId : undefined}
|
|
703
|
-
onChange={(e) => props.onToggle(task.id, e.currentTarget.checked)}
|
|
704
|
-
/>
|
|
705
|
-
<span
|
|
706
|
-
class={cn(
|
|
707
|
-
'mt-0.5 shrink-0 text-muted-foreground',
|
|
708
|
-
// Completed rows: the check gently scales on hover.
|
|
709
|
-
checked() && 'motion-safe:transition-transform motion-safe:group-hover:scale-110',
|
|
710
|
-
)}
|
|
711
|
-
aria-hidden="true"
|
|
712
|
-
>
|
|
713
|
-
{/* ROUND, where `select` mode uses the square `Checkbox`. Deliberate:
|
|
714
|
-
this mode has no confirm button, so ticking a row IS the action and the
|
|
715
|
-
row is done-or-not rather than selected-or-not — the todo-list
|
|
716
|
-
affordance, which is how this mode has been described since it landed
|
|
717
|
-
(see the `mode` argType in tasks-card.stories.tsx and the JSDoc on
|
|
718
|
-
src/elements/tasks.tsx). D-2 in
|
|
719
|
-
docs/superpowers/plans/2026-08-25-form-control-primitives.md tracks
|
|
720
|
-
whether to keep the split; unruled, so leave both. NOTE the cost: the
|
|
721
|
-
icon replaces the visible native control, which is why the real input
|
|
722
|
-
above is `sr-only` and why the row's focus ring has to be driven by
|
|
723
|
-
`has-[:focus-visible]:`. Converging on `Checkbox` would take that
|
|
724
|
-
hazard away with it. */}
|
|
725
|
-
<Show
|
|
726
|
-
when={checked()}
|
|
727
|
-
fallback={<Circle size={18} />}
|
|
728
|
-
>
|
|
729
|
-
<CircleCheck size={18} class="text-foreground" />
|
|
730
|
-
</Show>
|
|
731
|
-
</span>
|
|
732
|
-
<span class="flex min-w-0 flex-col gap-0.5">
|
|
733
|
-
<span
|
|
734
|
-
class={cn(
|
|
735
|
-
'font-medium text-foreground',
|
|
736
|
-
// Completed: strike through + muted, brightening on hover.
|
|
737
|
-
checked() && 'line-through opacity-70 motion-safe:transition-opacity motion-safe:group-hover:opacity-100',
|
|
738
|
-
)}
|
|
739
|
-
>
|
|
740
|
-
{task.label}
|
|
741
|
-
</span>
|
|
742
|
-
<Show when={task.description}>
|
|
743
|
-
<Show
|
|
744
|
-
when={checked()}
|
|
745
|
-
fallback={
|
|
746
|
-
<span id={descId} class="text-xs font-normal text-muted-foreground">
|
|
747
|
-
{task.description}
|
|
748
|
-
</span>
|
|
749
|
-
}
|
|
750
|
-
>
|
|
751
|
-
{/* Completed: collapse the description and reveal it on hover or
|
|
752
|
-
keyboard focus via the grid-rows height trick. Height-collapse
|
|
753
|
-
keeps the text in the DOM + a11y tree (unlike display:none). */}
|
|
754
|
-
<div class="grid grid-rows-[0fr] group-hover:grid-rows-[1fr] group-focus-within:grid-rows-[1fr] motion-safe:transition-[grid-template-rows] motion-safe:duration-200">
|
|
755
|
-
<span
|
|
756
|
-
id={descId}
|
|
757
|
-
class="overflow-hidden text-xs font-normal text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 motion-safe:transition-opacity"
|
|
758
|
-
>
|
|
759
|
-
{task.description}
|
|
760
|
-
</span>
|
|
761
|
-
</div>
|
|
762
|
-
</Show>
|
|
763
|
-
</Show>
|
|
764
|
-
</span>
|
|
765
|
-
</label>
|
|
766
|
-
</li>
|
|
767
|
-
);
|
|
768
|
-
}}
|
|
769
|
-
</For>
|
|
770
|
-
</ul>
|
|
771
|
-
|
|
772
|
-
<Show when={props.maxNote !== undefined}>
|
|
773
|
-
<p class="text-xs text-muted-foreground">Up to {props.maxNote} selected.</p>
|
|
774
|
-
</Show>
|
|
775
|
-
</div>
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
780
|
-
// Read-only resolved summary presenter.
|
|
781
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
782
|
-
|
|
783
|
-
function TasksResolved(props: {
|
|
784
|
-
summary: { count: number; total: number; labels: string[] };
|
|
785
|
-
optimistic: boolean;
|
|
786
|
-
}): JSX.Element {
|
|
787
|
-
return (
|
|
788
|
-
<div class="flex flex-col gap-2" role={props.optimistic ? 'status' : undefined}>
|
|
789
|
-
<p class="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
790
|
-
<Check size={16} aria-hidden="true" />
|
|
791
|
-
<span>Selected {props.summary.count} of {props.summary.total}</span>
|
|
792
|
-
</p>
|
|
793
|
-
<Show when={props.summary.labels.length > 0}
|
|
794
|
-
fallback={<p class="text-sm text-muted-foreground">None selected</p>}>
|
|
795
|
-
<ul class="list-disc pl-5 text-sm text-foreground">
|
|
796
|
-
<For each={props.summary.labels}>{(l) => <li>{l}</li>}</For>
|
|
797
|
-
</ul>
|
|
798
|
-
</Show>
|
|
799
|
-
</div>
|
|
800
|
-
);
|
|
801
|
-
}
|