@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
package/src/ui/dock.tsx DELETED
@@ -1,630 +0,0 @@
1
- import {
2
- createEffect, createSignal, createUniqueId, Show, untrack, type Accessor, type JSX,
3
- } from 'solid-js';
4
- import { MessageCircle, X } from 'lucide-solid';
5
-
6
- /** Which corner the dock sits in. LOGICAL, so `-end` follows the writing direction
7
- * and an RTL page docks on the left with no extra work (both reference
8
- * implementations reached for `inset-inline-end` independently). */
9
- export type DockPosition = 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
10
-
11
- /** Where focus goes when the panel opens.
12
- * - `content` (default) — `focus()` the first element assigned to the panel, falling
13
- * back to the panel itself when that element cannot take focus. Content-AGNOSTIC:
14
- * the dock never learns what it is holding.
15
- * - `panel` — focus the `tabindex="-1"` panel container (what `kai-dialog` does).
16
- * - `none` — never move focus. */
17
- export type DockFocusOnOpen = 'content' | 'panel' | 'none';
18
-
19
- /** Imperative open controller, handed to a parent (the `kai-dock` facade) via
20
- * `controllerRef` so it can drive/observe open state with `wireDisclosure`. */
21
- export interface DockController { open: Accessor<boolean>; setOpen: (v: boolean) => void }
22
-
23
- export interface DockProps {
24
- /** The panel body. ANY content — the dock never reads or types it. */
25
- children?: JSX.Element;
26
- /** Content inside the built-in button while CLOSED. Defaults to a chat glyph. */
27
- launcher?: JSX.Element;
28
- /** Content inside the button while OPEN. Defaults to a ✕; when only `launcher` is
29
- * given, that content stays rather than morphing into a clashing built-in. */
30
- launcherOpen?: JSX.Element;
31
- /** The widget's NAME. Derives the panel's accessible name and both launcher names
32
- * (`Open ${label}` / `Close ${label}`) — one string instead of three. */
33
- label?: string;
34
- /** i18n override for the launcher's name while closed. */
35
- openLabel?: string;
36
- /** i18n override for the launcher's name while open. */
37
- closeLabel?: string;
38
- /** Which corner. Logical, RTL-correct. Defaults to `bottom-end`. */
39
- position?: DockPosition;
40
- /** Render the unread dot. CONSUMER-OWNED: shown only while closed, and never
41
- * written back by the dock (see the component doc). */
42
- unread?: boolean;
43
- /** Disable the launcher. */
44
- disabled?: boolean;
45
- /** Suppress the dock's own built-in mobile `[part="close"]` X. Off (rendered) by
46
- * default — the built-in X is the FALLBACK close route for panel content with no
47
- * header of its own, which is the general case the dock has to cover since it
48
- * never reads what it holds. Set `true` when the slotted/composed content
49
- * supplies its own close affordance in its own header row (e.g. `ChatThread`'s
50
- * `headerEndContent`) — with both present the two X's stack, one floating over
51
- * the other's row, which is exactly the "doesn't look intentional" feedback this
52
- * prop exists to let a caller avoid. TRADEOFF: the mobile panel reserves a
53
- * padding band above its content so the built-in X never paints over it; that
54
- * band stays reserved unless you set `hideClose` true, so only set it once your
55
- * own control is actually in place — otherwise you get the band with no X to
56
- * justify it. */
57
- hideClose?: boolean;
58
- /** Where focus lands on open. Defaults to `content`. */
59
- focusOnOpen?: DockFocusOnOpen;
60
- /** Controlled open state. When set, the component never changes it itself — drive it
61
- * from `onOpenChange`. Omit for uncontrolled (internal) state. */
62
- open?: boolean;
63
- /** Initial open state when uncontrolled. Does NOT move focus (see the focus contract). */
64
- defaultOpen?: boolean;
65
- /** Fires whenever the dock wants to open or close (launcher / Escape / a method). */
66
- onOpenChange?: (open: boolean) => void;
67
- /** Receive the open controller once mounted. */
68
- controllerRef?: (api: DockController) => void;
69
- /** Receive the focusable panel node so a facade's `focus()` can target it. */
70
- panelRef?: (el: HTMLElement) => void;
71
- /** Receive the launcher button so a facade's `focus()` can target it. */
72
- launcherRef?: (el: HTMLButtonElement) => void;
73
- /**
74
- * Resolve the element `focusOnOpen: 'content'` should focus.
75
- *
76
- * A PROP, because the answer is "the first element assigned to the panel slot" and
77
- * only the web-component facade can see slot assignments — a Solid caller passes
78
- * its own ref instead. Returning nothing falls back to focusing the panel.
79
- */
80
- contentTarget?: () => HTMLElement | null | undefined;
81
- }
82
-
83
- /** The built-in closed glyph. Exported so the facade can use it as its `launcher`
84
- * slot's FALLBACK content — one definition, two users, rather than a second copy
85
- * of the same icon choice living in the element layer. */
86
- export function DockLauncherGlyph(): JSX.Element {
87
- return <MessageCircle size={24} />;
88
- }
89
-
90
- /** The built-in open glyph. Exported for the same reason as {@link DockLauncherGlyph}. */
91
- export function DockCloseGlyph(): JSX.Element {
92
- return <X size={24} />;
93
- }
94
-
95
- export interface DockLauncherImageProps {
96
- /** The icon URL. */
97
- src: string;
98
- /** Alt text — usually left empty: the launcher BUTTON already carries the
99
- * accessible name (Dock derives it from `label`/`openLabel`/`closeLabel`),
100
- * so a second name on the icon inside it would be redundant, not missing. */
101
- alt?: string;
102
- }
103
-
104
- /**
105
- * A branded launcher icon with a built-in fallback to {@link DockLauncherGlyph}
106
- * on load failure — a dead link, or a placeholder URL nobody swapped for a
107
- * real asset yet (exactly what shipped in `kai dev`'s own `owner-widget`
108
- * fixture: `launcherIcon: "https://example.com/logo.png"`, which never
109
- * resolves, so the FAB rendered a permanently broken image). Decides loudly
110
- * with one `console.warn` naming the failing URL, then swaps — never a
111
- * silent broken-image icon sitting in the corner of someone's page.
112
- *
113
- * Lives here, not on {@link Dock} itself: `Dock`'s `launcher` prop is
114
- * deliberately ANY content (an emoji, a "Support" text pill, this image,
115
- * whatever a consumer slots in) and the dock never inspects what it's
116
- * holding — teaching it to specifically understand "this might be an
117
- * `<img>` that can 404" would be the one prop where that boundary breaks.
118
- * This is a plain sibling component a `launcher` value can be built FROM,
119
- * same relationship `DockLauncherGlyph`/`DockCloseGlyph` already have to
120
- * `Dock`. The construct engine's `emitDockLauncher` (codegen.ts) renders
121
- * `<DockLauncherImage src={...} />` for a construct-authored `launcherIcon`
122
- * rather than hand-rolling the same onError dance in emitted output.
123
- */
124
- export function DockLauncherImage(props: DockLauncherImageProps) {
125
- const [failed, setFailed] = createSignal(false);
126
- return (
127
- <Show when={!failed()} fallback={<DockLauncherGlyph />}>
128
- <img
129
- src={props.src}
130
- alt={props.alt ?? ''}
131
- style={{ width: '24px', height: '24px', 'border-radius': '9999px' }}
132
- onError={() => {
133
- console.warn(`[kai] Dock launcher icon failed to load (${props.src}); falling back to the default glyph.`);
134
- setFailed(true);
135
- }}
136
- />
137
- </Show>
138
- );
139
- }
140
-
141
- /**
142
- * The dock's own stylesheet, scoped to its shadow root.
143
- *
144
- * IN A `<style>` RATHER THAN INLINE, and that is a decision. `kai-prompt-dock` puts
145
- * its tokenized chrome in an inline `style={{}}`, which resolves `var()` fallbacks
146
- * and lets an outside override win — but an inline declaration also beats every rule
147
- * in the cascade, and this element ships TWO rules that have to be able to win: the
148
- * narrow-viewport full-bleed default and the reduced-motion rule. Both are media
149
- * queries, which cannot be expressed inline at all. So the geometry lives here and
150
- * only the open/closed state (visibility / opacity / pointer-events) is inline, where
151
- * it belongs: that one must beat any consumer rule, and it is also the half a jsdom
152
- * test can actually read.
153
- *
154
- * EVERY RULE IS SCOPED UNDER `[data-kai-dock]`, including the `[part=...]` ones. Inside
155
- * a shadow root that scoping is redundant; in a plain Solid page it is the difference
156
- * between styling this dock and restyling every `[part="panel"]` on the document.
157
- *
158
- * DOCUMENTED CONSTRAINT, which the element cannot fix from inside: put `<kai-dock>`
159
- * as a body-level child. A `transform` / `filter` / `contain` ancestor makes
160
- * `position: fixed` resolve against that ancestor instead of the viewport.
161
- */
162
- const DOCK_CSS = `
163
- :host { display: contents; }
164
-
165
- [data-kai-dock] {
166
- position: fixed;
167
- z-index: var(--kai-dock-z, 1000);
168
- display: flex;
169
- gap: var(--kai-dock-gap, 0.85rem);
170
- /* Only a positioning frame: it must not swallow clicks on the page it floats
171
- over. The launcher and the open panel opt back in. */
172
- pointer-events: none;
173
- }
174
-
175
- /* Logical insets throughout, so an RTL page docks on the other side for free.
176
- column-reverse for the bottom corners keeps the LAUNCHER FIRST IN DOM ORDER --
177
- Tab reaches the control before the panel content -- while painting it below. */
178
- [data-kai-dock][data-position^="bottom"] {
179
- flex-direction: column-reverse;
180
- inset-block-end: var(--kai-dock-inset, 1.5rem);
181
- }
182
- [data-kai-dock][data-position^="top"] {
183
- flex-direction: column;
184
- inset-block-start: var(--kai-dock-inset, 1.5rem);
185
- }
186
- [data-kai-dock][data-position$="end"] {
187
- inset-inline-end: var(--kai-dock-inset, 1.5rem);
188
- align-items: flex-end;
189
- }
190
- [data-kai-dock][data-position$="start"] {
191
- inset-inline-start: var(--kai-dock-inset, 1.5rem);
192
- align-items: flex-start;
193
- }
194
-
195
- [data-kai-dock] [part="launcher"] {
196
- pointer-events: auto;
197
- position: relative;
198
- /* ABOVE the panel, always. At <=480px the panel goes fixed/inset:0 and it comes
199
- after the launcher in DOM order, so at z-index:auto it painted OVER the launcher
200
- and a touch user lost every pointer route to closing. Both reference
201
- implementations stacked the launcher over the panel for the same reason. The
202
- launcher itself is now HIDDEN while the panel is open at that width (see the
203
- <=480px block below) in favour of the in-panel [part="close"] X, but this
204
- z-index still matters for the moment between "open" landing and the panel's
205
- own transition finishing, and for any consumer CSS that reintroduces the
206
- launcher there. */
207
- z-index: 1;
208
- flex: none;
209
- display: inline-flex;
210
- align-items: center;
211
- justify-content: center;
212
- gap: 0.5rem;
213
- /* The size token is the BLOCK size and the inline MINIMUM, not a fixed square. An
214
- icon-only launcher is still a perfect disc (the icon plus padding is narrower
215
- than the minimum), but a slotted TEXT label — the reference app's "Support"
216
- pill — widens the button instead of overflowing it. */
217
- inline-size: auto;
218
- min-inline-size: var(--kai-dock-launcher-size, 56px);
219
- block-size: var(--kai-dock-launcher-size, 56px);
220
- padding-block: 0;
221
- padding-inline: 0.9rem;
222
- white-space: nowrap;
223
- border: 0;
224
- border-radius: 9999px;
225
- background: var(--color-primary);
226
- color: var(--color-primary-foreground);
227
- box-shadow: 0 10px 25px -6px rgb(0 0 0 / 0.35);
228
- cursor: pointer;
229
- transition: transform 150ms ease, box-shadow 150ms ease;
230
- }
231
- [data-kai-dock] [part="launcher"]:hover:not(:disabled) { transform: translateY(-1px); }
232
- [data-kai-dock] [part="launcher"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
233
- [data-kai-dock] [part="launcher"]:disabled { opacity: 0.55; cursor: not-allowed; }
234
-
235
- /* The mobile-only close affordance rendered INSIDE the panel. Hidden by default and
236
- only switched on inside the <=480px media block below, alongside the rule that
237
- hides the launcher while the panel is open at that width — same query drives both,
238
- CSS-only, no JS viewport logic. Absolutely positioned against the panel, which is
239
- the panel's own containing block once the narrow-viewport rule below sets it to
240
- position: fixed.
241
-
242
- The inset is a CONSUMER-OVERRIDABLE TOKEN, same idiom as every other geometry
243
- value in this file (--kai-dock-width and friends). The dock is content-agnostic —
244
- it never reads what is slotted into the panel — so it cannot know whether that
245
- content renders its own trailing controls (a ChatThread header-end slot with
246
- share/settings icons, say) in the same top-right corner.
247
-
248
- The mobile block below reserves a BAND of the panel's own padding above whatever
249
- is slotted, sized to this button's own footprint, so nothing paints under this X
250
- by default -- verified fix (round 1) against a real ChatThread header-end slot
251
- colliding with it at 375px. Owner feedback on the real widget (round 2): for a
252
- construct with only a title in the header, that band read as a dead empty strip
253
- with the X floating alone in it -- "that doesn't look like the empty component
254
- either, lets do better." The actual fix composes instead of reserving space:
255
- ChatThread now has its own headerEndContent escape hatch (see its prop doc) so a
256
- caller puts the close control INSIDE the header row, sharing it with the title --
257
- zero collision by construction, no band needed. hideClose on this component (see
258
- its prop doc) then suppresses this built-in X for exactly that case, and the
259
- band above is scoped to :not([data-hide-close]) so it comes off ONLY there --
260
- every other consumer (anyone who hasn't opted into hideClose, including a
261
- hand-authored dock with real header-end content) keeps the band, same as round 1
262
- shipped, so the collision it fixed stays fixed for them. What remains here for
263
- that default case is a plain absolute overlay, top-right of the panel, with
264
- these consumer-overridable inset tokens for the rare case it should deliberately
265
- sit in front of something (e.g. a single small badge). */
266
- [data-kai-dock] [part="close"] {
267
- display: none;
268
- position: absolute;
269
- inset-block-start: var(--kai-dock-close-inset-block, 0.75rem);
270
- inset-inline-end: var(--kai-dock-close-inset-inline, 0.75rem);
271
- z-index: 1;
272
- align-items: center;
273
- justify-content: center;
274
- inline-size: 2.25rem;
275
- block-size: 2.25rem;
276
- padding: 0;
277
- border: 0;
278
- border-radius: 9999px;
279
- background: var(--color-background);
280
- color: var(--color-foreground);
281
- box-shadow: 0 2px 8px -2px rgb(0 0 0 / 0.3);
282
- cursor: pointer;
283
- }
284
- [data-kai-dock] [part="close"]:hover { background: var(--color-muted, var(--color-background)); }
285
- [data-kai-dock] [part="close"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
286
-
287
- [data-kai-dock] [part="badge"] {
288
- position: absolute;
289
- inset-block-start: 3px;
290
- inset-inline-end: 3px;
291
- inline-size: 0.75rem;
292
- block-size: 0.75rem;
293
- border-radius: 9999px;
294
- background: var(--color-unread);
295
- box-shadow: 0 0 0 2px var(--color-primary);
296
- }
297
-
298
- [data-kai-dock] [part="panel"] {
299
- pointer-events: auto;
300
- display: flex;
301
- flex-direction: column;
302
- overflow: hidden;
303
- inline-size: var(--kai-dock-width, 380px);
304
- block-size: var(--kai-dock-height, 600px);
305
- max-inline-size: calc(100vw - 2 * var(--kai-dock-inset, 1.5rem));
306
- max-block-size: calc(
307
- 100dvh - 2 * var(--kai-dock-inset, 1.5rem)
308
- - var(--kai-dock-launcher-size, 56px) - var(--kai-dock-gap, 0.85rem)
309
- );
310
- border: 1px solid var(--color-border);
311
- border-radius: var(--kai-dock-radius, 16px);
312
- background: var(--color-background);
313
- color: var(--color-foreground);
314
- box-shadow: 0 20px 45px -12px rgb(0 0 0 / 0.35);
315
- /* VISIBILITY IS SWITCHED, NEVER ANIMATED, and the zero duration is the whole point.
316
- visibility 180ms here meant the COMPUTED value stayed hidden for about two
317
- frames after opening while the inline style already said visible — and a
318
- focus() into a hidden subtree is dropped silently, so focusOnOpen never landed
319
- in a real browser (it worked under reduced motion, which is what made it look like
320
- a focus bug rather than a paint one).
321
- This rule is the CLOSING direction: hold visibility for the length of the fade so
322
- the panel does not vanish mid-transition, then switch. The opening direction sets
323
- the delay to 0s below, so opening is synchronous and focus has somewhere to go. */
324
- transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
325
- }
326
- [data-kai-dock] [part="panel"][data-expanded] {
327
- transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 0s;
328
- }
329
- /* The panel is a flex column and the slotted surface fills it -- the one layout
330
- fact both reference implementations wrote by hand (flex:1; min-height:0). Both
331
- spellings are here on purpose: ::slotted covers the shadow-DOM facade, > *
332
- covers a plain Solid caller, and each is inert in the other's world (a <slot>
333
- is display:contents, so flex properties do nothing to it). */
334
- [data-kai-dock] [part="panel"] ::slotted(*) { flex: 1 1 auto; min-block-size: 0; }
335
- [data-kai-dock] [part="panel"] > * { flex: 1 1 auto; min-block-size: 0; }
336
- [data-kai-dock] [part="panel"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
337
- [data-kai-dock][data-position^="bottom"] [part="panel"][data-closed] { transform: translateY(0.5rem); }
338
- [data-kai-dock][data-position^="top"] [part="panel"][data-closed] { transform: translateY(-0.5rem); }
339
-
340
- /* Narrow viewports take the panel full-bleed. A SHIPPED DEFAULT, not a prop: both
341
- references wrote this rule independently, which makes it a HOW. While the panel is
342
- OPEN at this width the launcher hides and the panel's own [part="close"] X becomes
343
- the pointer close route instead — a FAB floating over a full-bleed panel read as
344
- wrong (owner feedback) and Escape alone strands a touch user with no visible
345
- affordance at all. Closed, the launcher is unaffected: it is the panel's own
346
- [data-expanded] state that gates both rules below, not the media query alone. */
347
- @media (max-width: 480px) {
348
- [data-kai-dock] [part="panel"] {
349
- position: fixed;
350
- inset: 0;
351
- inline-size: auto;
352
- block-size: auto;
353
- max-inline-size: none;
354
- max-block-size: none;
355
- border: 0;
356
- border-radius: 0;
357
- }
358
-
359
- /* Reserve a band for the built-in [part="close"] X ABOVE whatever is slotted,
360
- rather than overlaying it on top of the panel's own content — the fix round 1
361
- collision protection (verified against a real ChatThread header-end slot at
362
- 375px: without this, the X painted directly over trailing icon buttons).
363
- DERIVED from the button's own inset + size tokens, not a second hand-typed
364
- number.
365
-
366
- SCOPED to :not([data-hide-close]) — round 2 (owner feedback: the band read as
367
- a dead strip when nothing but the X occupied it) removed this UNCONDITIONALLY
368
- and reintroduced the very collision round 1 fixed, for every consumer who
369
- never opted into hideClose. hideClose is only true for a caller that supplies
370
- its OWN close control in its own header row (ChatThread's headerEndContent is
371
- the one that does today) — the built-in X and this band both come off
372
- together for that case, since there is nothing left for the band to protect.
373
- Every other consumer -- including a hand-authored kai-dock with real
374
- slot="header-end" content and no opinion on hideClose -- keeps the band, the
375
- same as round 1 shipped. */
376
- [data-kai-dock]:not([data-hide-close]) [part="panel"] {
377
- padding-block-start: calc(2 * var(--kai-dock-close-inset-block, 0.75rem) + 2.25rem);
378
- }
379
-
380
- /* The full-bleed panel gets its own close affordance instead of relying on the
381
- launcher, which is hidden below while the panel is open at this width — a FAB
382
- floating over a full-bleed panel with no other visible close route is exactly
383
- what the owner flagged. Escape and the X both still work; the launcher comes
384
- back the instant the panel closes because data-expanded is gone by then.
385
- Absolutely positioned, so the padding-block-start above (a content-box
386
- concern) does not shift IT — only the in-flow slotted content. Content that
387
- supplies its own close control passes hideClose, which drops the button from
388
- the render tree entirely (see the component body) rather than being gated
389
- here — so this rule stays a plain, unconditional width-query switch; the band
390
- above is what actually varies with hideClose. */
391
- [data-kai-dock] [part="close"] { display: inline-flex; }
392
- [data-kai-dock]:has([part="panel"][data-expanded]) [part="launcher"] { display: none; }
393
- }
394
-
395
- @media (prefers-reduced-motion: reduce) {
396
- /* [data-expanded] is listed explicitly because the open-direction rule above is
397
- MORE SPECIFIC than a bare [part="panel"], so without it an opening panel kept
398
- animating opacity and transform for exactly the users who asked it not to. */
399
- [data-kai-dock] [part="launcher"],
400
- [data-kai-dock] [part="panel"],
401
- [data-kai-dock] [part="panel"][data-expanded] { transition: none; }
402
- [data-kai-dock] [part="panel"][data-closed] { transform: none; }
403
- }
404
- `;
405
-
406
- /**
407
- * The deepest active element, drilling through nested shadow roots.
408
- *
409
- * A local copy of the helper `ui/dialog.tsx` already carries. Deliberately not
410
- * refactored into a shared module by this change: dialog is not this task's file, and
411
- * a four-line tree walk duplicated once is cheaper than a cross-file move nobody
412
- * asked for. If a third caller appears, hoist it then.
413
- */
414
- function deepActiveElement(): HTMLElement | null {
415
- let el = document.activeElement as HTMLElement | null;
416
- while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement as HTMLElement;
417
- return el;
418
- }
419
-
420
- /**
421
- * Is `node` inside `root` in the FLATTENED tree?
422
- *
423
- * `root.contains(node)` is the wrong question here and would answer `false` for every
424
- * real case: the panel's content is light-DOM nodes ASSIGNED to a slot, so it lives in
425
- * the host's tree, not the panel's. This walks up through `assignedSlot` at each slot
426
- * and through `host` at each shadow boundary, which is the tree focus actually moves in.
427
- */
428
- function isWithin(root: Node, node: Node | null): boolean {
429
- let cur: Node | null = node;
430
- while (cur) {
431
- if (cur === root) return true;
432
- const slot = cur instanceof Element ? cur.assignedSlot : null;
433
- if (slot) { cur = slot; continue; }
434
- if (cur.parentNode) { cur = cur.parentNode; continue; }
435
- cur = cur instanceof ShadowRoot ? cur.host : null;
436
- }
437
- return false;
438
- }
439
-
440
- /**
441
- * Dock — a corner-docked launcher button with a panel that opens above it.
442
- *
443
- * The affordance behind a support widget: a circular button pinned to a corner of the
444
- * viewport, and a floating panel holding whatever you slot into it. The panel is
445
- * content-agnostic on purpose — a chat, a form, your own component — and the dock
446
- * never reads, types or reaches into it.
447
- *
448
- * WHAT IT OWNS (all of it non-configurable, because these are facts about the medium
449
- * rather than preferences):
450
- *
451
- * - **Hide semantics.** Closed means `visibility: hidden` + `opacity: 0` + `inert`,
452
- * and the panel is NEVER unmounted. `display: none` would leave the panel with no
453
- * layout box while closed, so anything inside that measures itself — a thread
454
- * scroller, a `ResizeObserver` — measures zero and re-measures on every open. The
455
- * cost of keeping the box is that a stream keeps rendering into a closed panel,
456
- * which is precisely what makes an unread dot honest.
457
- * - **Focus.** On open, focus moves per `focusOnOpen`; on close it returns to the
458
- * launcher, always. `inert` is cleared and visibility restored BEFORE `focus()` runs
459
- * — a `focus()` into an inert or hidden subtree is silently dropped, no error — via
460
- * a `queueMicrotask`, the same shape `ui/dialog.tsx` uses. Mount moves no focus.
461
- * - **Escape.** Closes only while the dock CONTAINS focus, and never stops the event.
462
- * A background widget that ate every Escape on the page would break the host page's
463
- * own menus; swallowing a key from a page you are a guest on is a decide-quietly move.
464
- * - **No focus trap**, deliberately, unlike `kai-dialog`. The page staying usable is
465
- * the whole point of "docked", which is also why the panel is `aria-modal="false"`.
466
- * - **Mobile close route.** At <=480px the panel goes full-bleed and the launcher
467
- * hides while it is open — a floating launcher over a full-bleed panel is not an
468
- * obvious close affordance. The panel gets its own `[part="close"]` X, top-right,
469
- * CSS-gated by the SAME media query and `[data-expanded]`, so it needs no viewport
470
- * JS. Desktop is unaffected: the launcher keeps toggling and no X ever renders.
471
- * This is the FALLBACK route, for panel content with no header of its own to hold
472
- * a close control — content that does (a `ChatThread` using `headerEndContent`,
473
- * say) passes `hideClose` to drop this X instead of stacking a second one over
474
- * its own header row.
475
- *
476
- * WHAT IT REFUSES: it never aborts a request, never persists its own open state, never
477
- * decides what "unread" means, and never clears `unread` on open — writing over a
478
- * consumer's prop is the trap `kai-chat.loading` fell into. Those are the app's calls;
479
- * `open` + `onOpenChange` is the seam.
480
- *
481
- * Geometry is CSS custom properties rather than props: `--kai-dock-width` ·
482
- * `--kai-dock-height` · `--kai-dock-inset` · `--kai-dock-gap` · `--kai-dock-radius` ·
483
- * `--kai-dock-z` · `--kai-dock-launcher-size`.
484
- *
485
- * @see `elements/dock.tsx` for the `<kai-dock>` web-component facade.
486
- */
487
- export function Dock(props: DockProps) {
488
- const panelId = `kai-dock-panel-${createUniqueId()}`;
489
- const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false);
490
-
491
- const isControlled = () => props.open !== undefined;
492
- const isOpen = () => (isControlled() ? !!props.open : internalOpen());
493
- const setOpen = (v: boolean) => {
494
- if (!isControlled()) setInternalOpen(v);
495
- props.onOpenChange?.(v);
496
- };
497
-
498
- props.controllerRef?.({ open: isOpen, setOpen });
499
-
500
- let panel: HTMLElement | undefined;
501
- let launcher: HTMLButtonElement | undefined;
502
-
503
- const label = () => props.label ?? 'Chat';
504
- const launcherName = () => (isOpen()
505
- ? props.closeLabel ?? `Close ${label()}`
506
- : props.openLabel ?? `Open ${label()}`);
507
-
508
- /** The focus half of an open, deferred so the state flip above has already landed. */
509
- const focusOnOpen = () => {
510
- const mode = props.focusOnOpen ?? 'content';
511
- if (mode === 'none') return;
512
- if (mode === 'content') {
513
- const target = props.contentTarget?.();
514
- target?.focus();
515
- // Content-agnostic fallback: a plain <div> is not focusable, so `focus()` was a
516
- // silent no-op and the panel takes it instead. Asking where focus ACTUALLY
517
- // landed is the only way to tell, since the dock is not allowed to inspect what
518
- // it is holding.
519
- if (target && isWithin(panel!, deepActiveElement())) return;
520
- }
521
- panel?.focus();
522
- };
523
-
524
- // The focus choreography, and the ONE ordering that is a hard invariant.
525
- //
526
- // SEEDED WITH THE INITIAL OPEN VALUE, not `false`. `kai-dialog` seeds `false` on
527
- // purpose so a `defaultOpen` modal DOES take focus at mount; a docked widget is the
528
- // opposite — it is a guest on someone's page and must not steal focus on load, from
529
- // `default-open` OR from `open`. Same mechanism, opposite default, and the
530
- // difference is exactly the modal/non-modal split.
531
- createEffect((wasOpen: boolean) => {
532
- const open = isOpen();
533
- if (open !== wasOpen) {
534
- // The attributes and inline styles below are written by render effects, which
535
- // Solid runs before this one; the microtask is belt-and-braces for a state
536
- // change driven from outside a Solid batch, and gives a freshly-slotted element
537
- // a turn to upgrade before we call focus() on it.
538
- if (open) queueMicrotask(focusOnOpen);
539
- else queueMicrotask(() => launcher?.focus());
540
- }
541
- return open;
542
- }, untrack(() => props.open ?? props.defaultOpen ?? false));
543
-
544
- const onKeyDown = (e: KeyboardEvent) => {
545
- if (e.key !== 'Escape' || !isOpen()) return;
546
- // D-C. This listener is ON the dock rather than on `document`, so the only
547
- // keydowns it can ever see are ones whose target is inside it — which IS the
548
- // condition "the dock contains focus", with no ambiguity and no second check. It
549
- // also composes: an overlay inside the panel that handles its own Escape and
550
- // stops the event correctly prevents the dock from closing underneath it.
551
- // No stopPropagation, no preventDefault: the host page keeps its key.
552
- setOpen(false);
553
- };
554
-
555
- return (
556
- <>
557
- <style>{DOCK_CSS}</style>
558
- <div
559
- data-kai-dock
560
- data-position={props.position ?? 'bottom-end'}
561
- // Drives the mobile panel's reserved-band rule below: present only when the
562
- // built-in [part="close"] X is ACTUALLY suppressed (hideClose true), so the
563
- // band stays on for every consumer who hasn't opted into their own close
564
- // control — see the [part="close"]/reserved-band comments for why removing
565
- // it unconditionally regressed the collision it existed to prevent.
566
- bool:data-hide-close={!!props.hideClose}
567
- onKeyDown={onKeyDown}
568
- >
569
- <button
570
- ref={(el) => { launcher = el; props.launcherRef?.(el); }}
571
- part="launcher"
572
- type="button"
573
- aria-expanded={isOpen() ? 'true' : 'false'}
574
- aria-controls={panelId}
575
- // The NAME is what changes for assistive tech; the glyph morph is decorative.
576
- aria-label={launcherName()}
577
- disabled={props.disabled || undefined}
578
- onClick={() => setOpen(!isOpen())}
579
- >
580
- <Show when={props.unread && !isOpen()}>
581
- <span part="badge" aria-hidden="true" />
582
- </Show>
583
- <span aria-hidden="true" style={{ display: 'inline-flex' }}>
584
- <Show when={isOpen()} fallback={props.launcher ?? <DockLauncherGlyph />}>
585
- {props.launcherOpen ?? props.launcher ?? <DockCloseGlyph />}
586
- </Show>
587
- </span>
588
- </button>
589
- <div
590
- ref={(el) => { panel = el; props.panelRef?.(el); }}
591
- part="panel"
592
- id={panelId}
593
- role="dialog"
594
- // Non-modal, and saying so is load-bearing given there is no focus trap:
595
- // claiming modality without trapping is worse than claiming neither.
596
- aria-modal="false"
597
- aria-label={label()}
598
- tabindex={-1}
599
- data-expanded={isOpen() ? '' : undefined}
600
- data-closed={isOpen() ? undefined : ''}
601
- // `inert` is the half that actually stops Tab: a visibility:hidden panel is
602
- // already skipped by most AT, but a focusable control inside it is not.
603
- bool:inert={!isOpen()}
604
- // INLINE, unlike the geometry: the closed state must beat any consumer rule,
605
- // and it is the half a test can read without a layout engine.
606
- style={{
607
- visibility: isOpen() ? 'visible' : 'hidden',
608
- opacity: isOpen() ? '1' : '0',
609
- 'pointer-events': isOpen() ? 'auto' : 'none',
610
- }}
611
- >
612
- {/* Mobile-only (see the <=480px rule in DOCK_CSS): the full-bleed panel's
613
- own close affordance, since the launcher is hidden by the same query
614
- while the panel is open. Reuses the launcher's derived close name
615
- rather than a new prop — same string, same i18n override. Dropped
616
- from the render tree entirely (not just hidden) when `hideClose` is
617
- set — the panel content supplies its own, in its own header, and a
618
- second X floating over that row is the exact "doesn't look
619
- intentional" feedback `hideClose` exists to avoid. */}
620
- <Show when={!props.hideClose}>
621
- <button type="button" part="close" aria-label={launcherName()} onClick={() => setOpen(false)}>
622
- <DockCloseGlyph />
623
- </button>
624
- </Show>
625
- {props.children}
626
- </div>
627
- </div>
628
- </>
629
- );
630
- }