@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.
Files changed (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. 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
- }