@happyvertical/smrt-svelte 0.30.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 (357) hide show
  1. package/AGENTS.md +317 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +185 -0
  5. package/dist/Provider.svelte +204 -0
  6. package/dist/Provider.svelte.d.ts +73 -0
  7. package/dist/Provider.svelte.d.ts.map +1 -0
  8. package/dist/__tests__/app-state.test.js +156 -0
  9. package/dist/__tests__/warm-clients.test.js +186 -0
  10. package/dist/browser-ai/adapters/llm/factory.d.ts +38 -0
  11. package/dist/browser-ai/adapters/llm/factory.d.ts.map +1 -0
  12. package/dist/browser-ai/adapters/llm/factory.js +91 -0
  13. package/dist/browser-ai/adapters/llm/index.d.ts +7 -0
  14. package/dist/browser-ai/adapters/llm/index.d.ts.map +1 -0
  15. package/dist/browser-ai/adapters/llm/index.js +6 -0
  16. package/dist/browser-ai/adapters/llm/types.d.ts +182 -0
  17. package/dist/browser-ai/adapters/llm/types.d.ts.map +1 -0
  18. package/dist/browser-ai/adapters/llm/types.js +43 -0
  19. package/dist/browser-ai/adapters/llm/webllm.d.ts +33 -0
  20. package/dist/browser-ai/adapters/llm/webllm.d.ts.map +1 -0
  21. package/dist/browser-ai/adapters/llm/webllm.js +225 -0
  22. package/dist/browser-ai/adapters/stt/browser-speech.d.ts +31 -0
  23. package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -0
  24. package/dist/browser-ai/adapters/stt/browser-speech.js +217 -0
  25. package/dist/browser-ai/adapters/stt/factory.d.ts +49 -0
  26. package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -0
  27. package/dist/browser-ai/adapters/stt/factory.js +110 -0
  28. package/dist/browser-ai/adapters/stt/index.d.ts +9 -0
  29. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -0
  30. package/dist/browser-ai/adapters/stt/index.js +8 -0
  31. package/dist/browser-ai/adapters/stt/types.d.ts +154 -0
  32. package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -0
  33. package/dist/browser-ai/adapters/stt/types.js +4 -0
  34. package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts +46 -0
  35. package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts.map +1 -0
  36. package/dist/browser-ai/adapters/stt/whisper-cpp.js +348 -0
  37. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +51 -0
  38. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +1 -0
  39. package/dist/browser-ai/adapters/stt/whisper-wasm.js +380 -0
  40. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +42 -0
  41. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -0
  42. package/dist/browser-ai/adapters/tts/browser-synthesis.js +235 -0
  43. package/dist/browser-ai/adapters/tts/factory.d.ts +53 -0
  44. package/dist/browser-ai/adapters/tts/factory.d.ts.map +1 -0
  45. package/dist/browser-ai/adapters/tts/factory.js +92 -0
  46. package/dist/browser-ai/adapters/tts/index.d.ts +7 -0
  47. package/dist/browser-ai/adapters/tts/index.d.ts.map +1 -0
  48. package/dist/browser-ai/adapters/tts/index.js +6 -0
  49. package/dist/browser-ai/adapters/tts/types.d.ts +140 -0
  50. package/dist/browser-ai/adapters/tts/types.d.ts.map +1 -0
  51. package/dist/browser-ai/adapters/tts/types.js +4 -0
  52. package/dist/browser-ai/capabilities/detector.d.ts +38 -0
  53. package/dist/browser-ai/capabilities/detector.d.ts.map +1 -0
  54. package/dist/browser-ai/capabilities/detector.js +211 -0
  55. package/dist/browser-ai/core/errors.d.ts +62 -0
  56. package/dist/browser-ai/core/errors.d.ts.map +1 -0
  57. package/dist/browser-ai/core/errors.js +92 -0
  58. package/dist/browser-ai/core/index.d.ts +6 -0
  59. package/dist/browser-ai/core/index.d.ts.map +1 -0
  60. package/dist/browser-ai/core/index.js +5 -0
  61. package/dist/browser-ai/core/types.d.ts +115 -0
  62. package/dist/browser-ai/core/types.d.ts.map +1 -0
  63. package/dist/browser-ai/core/types.js +34 -0
  64. package/dist/browser-ai/index.d.ts +12 -0
  65. package/dist/browser-ai/index.d.ts.map +1 -0
  66. package/dist/browser-ai/index.js +16 -0
  67. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte +77 -0
  68. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts +16 -0
  69. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts.map +1 -0
  70. package/dist/browser-ai/svelte/components/CapabilityGate.svelte +57 -0
  71. package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts +15 -0
  72. package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts.map +1 -0
  73. package/dist/browser-ai/svelte/components/DownloadProgress.svelte +141 -0
  74. package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts +15 -0
  75. package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts.map +1 -0
  76. package/dist/browser-ai/svelte/components/STTTest.svelte +379 -0
  77. package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts +9 -0
  78. package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts.map +1 -0
  79. package/dist/browser-ai/svelte/components/VoiceInput.svelte +200 -0
  80. package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts +16 -0
  81. package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts.map +1 -0
  82. package/dist/browser-ai/svelte/index.d.ts +15 -0
  83. package/dist/browser-ai/svelte/index.d.ts.map +1 -0
  84. package/dist/browser-ai/svelte/index.js +28 -0
  85. package/dist/browser-ai/ui.d.ts +16 -0
  86. package/dist/browser-ai/ui.d.ts.map +1 -0
  87. package/dist/browser-ai/ui.js +67 -0
  88. package/dist/components/admin/AgentAdminPanel.svelte +111 -0
  89. package/dist/components/admin/AgentAdminPanel.svelte.d.ts +25 -0
  90. package/dist/components/admin/AgentAdminPanel.svelte.d.ts.map +1 -0
  91. package/dist/components/admin/AgentAdminTabs.svelte +280 -0
  92. package/dist/components/admin/AgentAdminTabs.svelte.d.ts +23 -0
  93. package/dist/components/admin/AgentAdminTabs.svelte.d.ts.map +1 -0
  94. package/dist/components/admin/AgentSettingsShell.svelte +257 -0
  95. package/dist/components/admin/AgentSettingsShell.svelte.d.ts +33 -0
  96. package/dist/components/admin/AgentSettingsShell.svelte.d.ts.map +1 -0
  97. package/dist/components/admin/index.d.ts +5 -0
  98. package/dist/components/admin/index.d.ts.map +1 -0
  99. package/dist/components/admin/index.js +6 -0
  100. package/dist/components/forms/AddressInput.svelte +500 -0
  101. package/dist/components/forms/AddressInput.svelte.d.ts +36 -0
  102. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -0
  103. package/dist/components/forms/CheckboxInput.svelte +208 -0
  104. package/dist/components/forms/CheckboxInput.svelte.d.ts +20 -0
  105. package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -0
  106. package/dist/components/forms/DateRangeInput.svelte +628 -0
  107. package/dist/components/forms/DateRangeInput.svelte.d.ts +33 -0
  108. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -0
  109. package/dist/components/forms/DateTimeInput.svelte +521 -0
  110. package/dist/components/forms/DateTimeInput.svelte.d.ts +24 -0
  111. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -0
  112. package/dist/components/forms/FileUpload.svelte +358 -0
  113. package/dist/components/forms/FileUpload.svelte.d.ts +22 -0
  114. package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -0
  115. package/dist/components/forms/Form.svelte +771 -0
  116. package/dist/components/forms/Form.svelte.d.ts +26 -0
  117. package/dist/components/forms/Form.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/FormGroup.svelte +86 -0
  119. package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
  120. package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/FormMicButton.svelte +179 -0
  122. package/dist/components/forms/FormMicButton.svelte.d.ts +10 -0
  123. package/dist/components/forms/FormMicButton.svelte.d.ts.map +1 -0
  124. package/dist/components/forms/Input.svelte +83 -0
  125. package/dist/components/forms/Input.svelte.d.ts +9 -0
  126. package/dist/components/forms/Input.svelte.d.ts.map +1 -0
  127. package/dist/components/forms/MeasurementInput.svelte +505 -0
  128. package/dist/components/forms/MeasurementInput.svelte.d.ts +35 -0
  129. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -0
  130. package/dist/components/forms/MoneyInput.svelte +412 -0
  131. package/dist/components/forms/MoneyInput.svelte.d.ts +30 -0
  132. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -0
  133. package/dist/components/forms/NumberInput.svelte +310 -0
  134. package/dist/components/forms/NumberInput.svelte.d.ts +28 -0
  135. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -0
  136. package/dist/components/forms/PhoneInput.svelte +530 -0
  137. package/dist/components/forms/PhoneInput.svelte.d.ts +22 -0
  138. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -0
  139. package/dist/components/forms/SearchInput.svelte +358 -0
  140. package/dist/components/forms/SearchInput.svelte.d.ts +33 -0
  141. package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
  142. package/dist/components/forms/Select.svelte +83 -0
  143. package/dist/components/forms/Select.svelte.d.ts +11 -0
  144. package/dist/components/forms/Select.svelte.d.ts.map +1 -0
  145. package/dist/components/forms/SelectInput.svelte +254 -0
  146. package/dist/components/forms/SelectInput.svelte.d.ts +25 -0
  147. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -0
  148. package/dist/components/forms/TextInput.svelte +415 -0
  149. package/dist/components/forms/TextInput.svelte.d.ts +26 -0
  150. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -0
  151. package/dist/components/forms/Textarea.svelte +85 -0
  152. package/dist/components/forms/Textarea.svelte.d.ts +10 -0
  153. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
  154. package/dist/components/forms/TextareaInput.svelte +386 -0
  155. package/dist/components/forms/TextareaInput.svelte.d.ts +26 -0
  156. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -0
  157. package/dist/components/forms/Toggle.svelte +217 -0
  158. package/dist/components/forms/Toggle.svelte.d.ts +37 -0
  159. package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
  160. package/dist/components/forms/__tests__/AddressInput.behavior.test.js +122 -0
  161. package/dist/components/forms/__tests__/CheckboxInput.test.js +92 -0
  162. package/dist/components/forms/__tests__/DateRangeInput.behavior.test.js +135 -0
  163. package/dist/components/forms/__tests__/DateTimeInput.behavior.test.js +103 -0
  164. package/dist/components/forms/__tests__/FileUpload.test.js +90 -0
  165. package/dist/components/forms/__tests__/Form.behavior.test.js +137 -0
  166. package/dist/components/forms/__tests__/Form.test.js +58 -0
  167. package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
  168. package/dist/components/forms/__tests__/FormMicButton.test.js +86 -0
  169. package/dist/components/forms/__tests__/Input.test.js +49 -0
  170. package/dist/components/forms/__tests__/MeasurementInput.behavior.test.js +129 -0
  171. package/dist/components/forms/__tests__/MoneyInput.behavior.test.js +124 -0
  172. package/dist/components/forms/__tests__/NumberInput.behavior.test.js +141 -0
  173. package/dist/components/forms/__tests__/PhoneInput.behavior.test.js +96 -0
  174. package/dist/components/forms/__tests__/SearchInput.test.js +79 -0
  175. package/dist/components/forms/__tests__/Select.test.js +37 -0
  176. package/dist/components/forms/__tests__/SelectInput.behavior.test.js +132 -0
  177. package/dist/components/forms/__tests__/TextInput.behavior.test.js +131 -0
  178. package/dist/components/forms/__tests__/Textarea.test.js +39 -0
  179. package/dist/components/forms/__tests__/TextareaInput.behavior.test.js +96 -0
  180. package/dist/components/forms/__tests__/Toggle.test.js +87 -0
  181. package/dist/components/forms/__tests__/composite-inputs-a11y.test.js +69 -0
  182. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
  183. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
  184. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
  185. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +33 -0
  186. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +12 -0
  187. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -0
  188. package/dist/components/forms/__tests__/rich-inputs-a11y.test.js +87 -0
  189. package/dist/components/forms/index.d.ts +25 -0
  190. package/dist/components/forms/index.d.ts.map +1 -0
  191. package/dist/components/forms/index.js +25 -0
  192. package/dist/components/forms/types.d.ts +33 -0
  193. package/dist/components/forms/types.d.ts.map +1 -0
  194. package/dist/components/forms/types.js +4 -0
  195. package/dist/components/module/ModulePanel.svelte +134 -0
  196. package/dist/components/module/ModulePanel.svelte.d.ts +22 -0
  197. package/dist/components/module/ModulePanel.svelte.d.ts.map +1 -0
  198. package/dist/components/module/index.d.ts +5 -0
  199. package/dist/components/module/index.d.ts.map +1 -0
  200. package/dist/components/module/index.js +4 -0
  201. package/dist/components/workspace/Breadcrumbs.svelte +141 -0
  202. package/dist/components/workspace/Breadcrumbs.svelte.d.ts +21 -0
  203. package/dist/components/workspace/Breadcrumbs.svelte.d.ts.map +1 -0
  204. package/dist/components/workspace/NavTree.svelte +354 -0
  205. package/dist/components/workspace/NavTree.svelte.d.ts +45 -0
  206. package/dist/components/workspace/NavTree.svelte.d.ts.map +1 -0
  207. package/dist/components/workspace/README.md +34 -0
  208. package/dist/components/workspace/RoleShell.svelte +309 -0
  209. package/dist/components/workspace/RoleShell.svelte.d.ts +91 -0
  210. package/dist/components/workspace/RoleShell.svelte.d.ts.map +1 -0
  211. package/dist/components/workspace/WorkspaceShell.svelte +951 -0
  212. package/dist/components/workspace/WorkspaceShell.svelte.d.ts +112 -0
  213. package/dist/components/workspace/WorkspaceShell.svelte.d.ts.map +1 -0
  214. package/dist/components/workspace/__tests__/RoleShell.test.js +772 -0
  215. package/dist/components/workspace/__tests__/WorkspaceShell.test.js +630 -0
  216. package/dist/components/workspace/__tests__/breadcrumbs-helpers.test.js +141 -0
  217. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte +45 -0
  218. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts +21 -0
  219. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts.map +1 -0
  220. package/dist/components/workspace/__tests__/define-tools-dock.test.js +1010 -0
  221. package/dist/components/workspace/__tests__/harness.svelte +25 -0
  222. package/dist/components/workspace/__tests__/harness.svelte.d.ts +14 -0
  223. package/dist/components/workspace/__tests__/harness.svelte.d.ts.map +1 -0
  224. package/dist/components/workspace/__tests__/index.test.js +37 -0
  225. package/dist/components/workspace/__tests__/manifest-nav-helpers.test.js +24 -0
  226. package/dist/components/workspace/__tests__/manifest-nav.test.js +599 -0
  227. package/dist/components/workspace/__tests__/nav-helpers.test.js +95 -0
  228. package/dist/components/workspace/__tests__/render-harness.svelte +66 -0
  229. package/dist/components/workspace/__tests__/render-harness.svelte.d.ts +32 -0
  230. package/dist/components/workspace/__tests__/render-harness.svelte.d.ts.map +1 -0
  231. package/dist/components/workspace/__tests__/render-tools-dock.test.js +243 -0
  232. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte +58 -0
  233. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts +16 -0
  234. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts.map +1 -0
  235. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte +41 -0
  236. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts +13 -0
  237. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts.map +1 -0
  238. package/dist/components/workspace/__tests__/test-icon.svelte +17 -0
  239. package/dist/components/workspace/__tests__/test-icon.svelte.d.ts +19 -0
  240. package/dist/components/workspace/__tests__/test-icon.svelte.d.ts.map +1 -0
  241. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte +38 -0
  242. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts +22 -0
  243. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts.map +1 -0
  244. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts +65 -0
  245. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts.map +1 -0
  246. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.js +115 -0
  247. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts +15 -0
  248. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts.map +1 -0
  249. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.js +7 -0
  250. package/dist/components/workspace/__tests__/typed-tool-fixture.test.js +115 -0
  251. package/dist/components/workspace/__tests__/use-harness-orphan.svelte +9 -0
  252. package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts +19 -0
  253. package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts.map +1 -0
  254. package/dist/components/workspace/__tests__/use-harness.svelte +23 -0
  255. package/dist/components/workspace/__tests__/use-harness.svelte.d.ts +8 -0
  256. package/dist/components/workspace/__tests__/use-harness.svelte.d.ts.map +1 -0
  257. package/dist/components/workspace/__tests__/use-tools-dock.test.js +33 -0
  258. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte +43 -0
  259. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts +11 -0
  260. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts.map +1 -0
  261. package/dist/components/workspace/breadcrumbs-helpers.d.ts +44 -0
  262. package/dist/components/workspace/breadcrumbs-helpers.d.ts.map +1 -0
  263. package/dist/components/workspace/breadcrumbs-helpers.js +88 -0
  264. package/dist/components/workspace/index.d.ts +16 -0
  265. package/dist/components/workspace/index.d.ts.map +1 -0
  266. package/dist/components/workspace/index.js +14 -0
  267. package/dist/components/workspace/manifest-nav.d.ts +200 -0
  268. package/dist/components/workspace/manifest-nav.d.ts.map +1 -0
  269. package/dist/components/workspace/manifest-nav.js +408 -0
  270. package/dist/components/workspace/nav-helpers.d.ts +36 -0
  271. package/dist/components/workspace/nav-helpers.d.ts.map +1 -0
  272. package/dist/components/workspace/nav-helpers.js +60 -0
  273. package/dist/components/workspace/server/__tests__/compose-availability.test.js +383 -0
  274. package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts +78 -0
  275. package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts.map +1 -0
  276. package/dist/components/workspace/server/__tests__/typed-context-fixture.js +104 -0
  277. package/dist/components/workspace/server/compose-availability.d.ts +73 -0
  278. package/dist/components/workspace/server/compose-availability.d.ts.map +1 -0
  279. package/dist/components/workspace/server/compose-availability.js +114 -0
  280. package/dist/components/workspace/server/index.d.ts +13 -0
  281. package/dist/components/workspace/server/index.d.ts.map +1 -0
  282. package/dist/components/workspace/server/index.js +11 -0
  283. package/dist/components/workspace/server/types.d.ts +108 -0
  284. package/dist/components/workspace/server/types.d.ts.map +1 -0
  285. package/dist/components/workspace/server/types.js +11 -0
  286. package/dist/components/workspace/tools-dock/ToolsDock.svelte +565 -0
  287. package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts +14 -0
  288. package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts.map +1 -0
  289. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts +143 -0
  290. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts.map +1 -0
  291. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.js +487 -0
  292. package/dist/components/workspace/tools-dock/use-tools-dock.d.ts +41 -0
  293. package/dist/components/workspace/tools-dock/use-tools-dock.d.ts.map +1 -0
  294. package/dist/components/workspace/tools-dock/use-tools-dock.js +50 -0
  295. package/dist/components/workspace/types.d.ts +372 -0
  296. package/dist/components/workspace/types.d.ts.map +1 -0
  297. package/dist/components/workspace/types.js +6 -0
  298. package/dist/hooks/index.d.ts +11 -0
  299. package/dist/hooks/index.d.ts.map +1 -0
  300. package/dist/hooks/index.js +10 -0
  301. package/dist/hooks/useAppState.svelte.d.ts +46 -0
  302. package/dist/hooks/useAppState.svelte.d.ts.map +1 -0
  303. package/dist/hooks/useAppState.svelte.js +59 -0
  304. package/dist/hooks/useAuth.svelte.d.ts +41 -0
  305. package/dist/hooks/useAuth.svelte.d.ts.map +1 -0
  306. package/dist/hooks/useAuth.svelte.js +43 -0
  307. package/dist/hooks/useLLM.svelte.d.ts +69 -0
  308. package/dist/hooks/useLLM.svelte.d.ts.map +1 -0
  309. package/dist/hooks/useLLM.svelte.js +85 -0
  310. package/dist/hooks/useSTT.svelte.d.ts +68 -0
  311. package/dist/hooks/useSTT.svelte.d.ts.map +1 -0
  312. package/dist/hooks/useSTT.svelte.js +97 -0
  313. package/dist/hooks/useSocket.svelte.d.ts +45 -0
  314. package/dist/hooks/useSocket.svelte.d.ts.map +1 -0
  315. package/dist/hooks/useSocket.svelte.js +54 -0
  316. package/dist/hooks/useTTS.svelte.d.ts +65 -0
  317. package/dist/hooks/useTTS.svelte.d.ts.map +1 -0
  318. package/dist/hooks/useTTS.svelte.js +93 -0
  319. package/dist/hooks/useTheme.d.ts +13 -0
  320. package/dist/hooks/useTheme.d.ts.map +1 -0
  321. package/dist/hooks/useTheme.js +16 -0
  322. package/dist/i18n/__tests__/server.spec.js +50 -0
  323. package/dist/i18n/server.d.ts +47 -0
  324. package/dist/i18n/server.d.ts.map +1 -0
  325. package/dist/i18n/server.js +58 -0
  326. package/dist/i18n/strings.forms.d.ts +33 -0
  327. package/dist/i18n/strings.forms.d.ts.map +1 -0
  328. package/dist/i18n/strings.forms.js +54 -0
  329. package/dist/i18n/strings.workspace.d.ts +34 -0
  330. package/dist/i18n/strings.workspace.d.ts.map +1 -0
  331. package/dist/i18n/strings.workspace.js +40 -0
  332. package/dist/index.d.ts +18 -0
  333. package/dist/index.d.ts.map +1 -0
  334. package/dist/index.js +23 -0
  335. package/dist/state/__tests__/warm-clients.test.js +40 -0
  336. package/dist/state/app-state.d.ts +308 -0
  337. package/dist/state/app-state.d.ts.map +1 -0
  338. package/dist/state/app-state.js +64 -0
  339. package/dist/state/app-state.svelte.d.ts +196 -0
  340. package/dist/state/app-state.svelte.d.ts.map +1 -0
  341. package/dist/state/app-state.svelte.js +774 -0
  342. package/dist/state/context.d.ts +23 -0
  343. package/dist/state/context.d.ts.map +1 -0
  344. package/dist/state/context.js +32 -0
  345. package/dist/state/form-context.d.ts +59 -0
  346. package/dist/state/form-context.d.ts.map +1 -0
  347. package/dist/state/form-context.js +31 -0
  348. package/dist/state/form-group-context.d.ts +13 -0
  349. package/dist/state/form-group-context.d.ts.map +1 -0
  350. package/dist/state/form-group-context.js +28 -0
  351. package/dist/state/index.d.ts +9 -0
  352. package/dist/state/index.d.ts.map +1 -0
  353. package/dist/state/index.js +8 -0
  354. package/dist/state/warm-clients.d.ts +136 -0
  355. package/dist/state/warm-clients.d.ts.map +1 -0
  356. package/dist/state/warm-clients.js +231 -0
  357. package/package.json +137 -0
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Tests for WorkspaceShell — issue happyvertical/smrt#1227.
3
+ *
4
+ * Uses Svelte 5's `mount` / `unmount` APIs and `createRawSnippet` to exercise
5
+ * the component in jsdom without pulling in `@testing-library/svelte` (which
6
+ * the package doesn't depend on).
7
+ */
8
+ import { createRawSnippet, mount, tick, unmount } from 'svelte';
9
+ import { compile } from 'svelte/compiler';
10
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
+ import WorkspaceShell from '../WorkspaceShell.svelte';
12
+ import workspaceShellSource from '../WorkspaceShell.svelte?raw';
13
+ import BindHarness from './workspace-shell-bind-harness.svelte';
14
+ function textSnippet(text) {
15
+ return createRawSnippet(() => ({
16
+ render: () => `<span>${text}</span>`,
17
+ }));
18
+ }
19
+ function longNavSnippet(itemCount = 48) {
20
+ const links = Array.from({ length: itemCount }, (_, index) => `<a href="/item-${index}">Navigation item ${index}</a>`).join('');
21
+ return createRawSnippet(() => ({
22
+ render: () => `<div class="long-nav">${links}</div>`,
23
+ }));
24
+ }
25
+ function getWorkspaceShellStyle(selector) {
26
+ const { css } = compile(workspaceShellSource, {
27
+ filename: 'WorkspaceShell.svelte',
28
+ generate: 'client',
29
+ });
30
+ const style = document.createElement('style');
31
+ style.textContent = css?.code ?? '';
32
+ document.head.appendChild(style);
33
+ try {
34
+ const rules = Array.from(style.sheet?.cssRules ?? []);
35
+ const rule = rules.find((candidate) => 'selectorText' in candidate &&
36
+ candidate.selectorText.includes(selector));
37
+ return rule?.style ?? null;
38
+ }
39
+ finally {
40
+ style.remove();
41
+ }
42
+ }
43
+ let container;
44
+ beforeEach(() => {
45
+ container = document.createElement('div');
46
+ document.body.appendChild(container);
47
+ });
48
+ afterEach(() => {
49
+ container.remove();
50
+ });
51
+ describe('WorkspaceShell', () => {
52
+ it('renders with only required children prop', () => {
53
+ const component = mount(WorkspaceShell, {
54
+ target: container,
55
+ props: {
56
+ children: textSnippet('main content'),
57
+ },
58
+ });
59
+ try {
60
+ expect(container.querySelector('.smrt-workspace-shell')).not.toBeNull();
61
+ expect(container.querySelector('.smrt-workspace-content')?.textContent).toContain('main content');
62
+ }
63
+ finally {
64
+ unmount(component);
65
+ }
66
+ });
67
+ it('renders the brand snippet when provided', () => {
68
+ const component = mount(WorkspaceShell, {
69
+ target: container,
70
+ props: {
71
+ children: textSnippet('content'),
72
+ brand: textSnippet('MyBrand'),
73
+ },
74
+ });
75
+ try {
76
+ const brand = container.querySelector('.brand');
77
+ expect(brand?.textContent).toContain('MyBrand');
78
+ }
79
+ finally {
80
+ unmount(component);
81
+ }
82
+ });
83
+ it('falls back to title / subtitle / eyebrow when no brand snippet is given', () => {
84
+ const component = mount(WorkspaceShell, {
85
+ target: container,
86
+ props: {
87
+ children: textSnippet('content'),
88
+ title: 'Workspace',
89
+ subtitle: 'Engineering',
90
+ eyebrow: 'SMRT',
91
+ },
92
+ });
93
+ try {
94
+ const brand = container.querySelector('.brand');
95
+ expect(brand?.textContent).toContain('Workspace');
96
+ expect(brand?.textContent).toContain('Engineering');
97
+ expect(brand?.textContent).toContain('SMRT');
98
+ }
99
+ finally {
100
+ unmount(component);
101
+ }
102
+ });
103
+ it('renders the nav snippet inside an aria-labelled region', () => {
104
+ const component = mount(WorkspaceShell, {
105
+ target: container,
106
+ props: {
107
+ children: textSnippet('content'),
108
+ nav: textSnippet('nav-content'),
109
+ },
110
+ });
111
+ try {
112
+ const navRegion = container.querySelector('nav.nav-region');
113
+ expect(navRegion).not.toBeNull();
114
+ expect(navRegion?.getAttribute('aria-label')).toBe('Workspace navigation');
115
+ expect(navRegion?.textContent).toContain('nav-content');
116
+ }
117
+ finally {
118
+ unmount(component);
119
+ }
120
+ });
121
+ it('renders the sidebar footer snippet when provided', () => {
122
+ const component = mount(WorkspaceShell, {
123
+ target: container,
124
+ props: {
125
+ children: textSnippet('content'),
126
+ sidebarFooter: textSnippet('account-menu'),
127
+ },
128
+ });
129
+ try {
130
+ const footer = container.querySelector('.sidebar-footer');
131
+ expect(footer?.textContent).toContain('account-menu');
132
+ }
133
+ finally {
134
+ unmount(component);
135
+ }
136
+ });
137
+ it('keeps long sidebar navigation scrolling above a fixed footer', () => {
138
+ const component = mount(WorkspaceShell, {
139
+ target: container,
140
+ props: {
141
+ children: textSnippet('content'),
142
+ nav: longNavSnippet(),
143
+ sidebarFooter: textSnippet('account-menu'),
144
+ },
145
+ });
146
+ try {
147
+ const sidebar = container.querySelector('.smrt-workspace-sidebar');
148
+ const navRegion = container.querySelector('.nav-region');
149
+ const footer = container.querySelector('.sidebar-footer');
150
+ expect(sidebar).not.toBeNull();
151
+ expect(navRegion).not.toBeNull();
152
+ expect(footer).not.toBeNull();
153
+ const sidebarStyle = getWorkspaceShellStyle('.smrt-workspace-sidebar');
154
+ const navStyle = getWorkspaceShellStyle('.nav-region');
155
+ const footerStyle = getWorkspaceShellStyle('.sidebar-footer');
156
+ expect(sidebarStyle?.position).toBe('sticky');
157
+ expect(sidebarStyle?.overflow).toBe('hidden');
158
+ expect(navStyle?.flex).toBe('1 1 auto');
159
+ expect(navStyle?.overflowY).toBe('auto');
160
+ expect(footerStyle?.flex).toBe('0 0 auto');
161
+ }
162
+ finally {
163
+ unmount(component);
164
+ }
165
+ });
166
+ it('renders topbar actions on the right of the top bar', () => {
167
+ const component = mount(WorkspaceShell, {
168
+ target: container,
169
+ props: {
170
+ children: textSnippet('content'),
171
+ topbarActions: textSnippet('action-button'),
172
+ },
173
+ });
174
+ try {
175
+ const actions = container.querySelector('.topbar-actions');
176
+ expect(actions?.textContent).toContain('action-button');
177
+ }
178
+ finally {
179
+ unmount(component);
180
+ }
181
+ });
182
+ it('does NOT render inspector when showInspector is false', () => {
183
+ const component = mount(WorkspaceShell, {
184
+ target: container,
185
+ props: {
186
+ children: textSnippet('content'),
187
+ inspector: textSnippet('inspector-content'),
188
+ showInspector: false,
189
+ },
190
+ });
191
+ try {
192
+ expect(container.querySelector('.smrt-workspace-inspector')).toBeNull();
193
+ const shell = container.querySelector('.smrt-workspace-shell');
194
+ expect(shell?.classList.contains('has-inspector')).toBe(false);
195
+ }
196
+ finally {
197
+ unmount(component);
198
+ }
199
+ });
200
+ it('renders inspector and has-inspector class when showInspector is true', () => {
201
+ const component = mount(WorkspaceShell, {
202
+ target: container,
203
+ props: {
204
+ children: textSnippet('content'),
205
+ inspector: textSnippet('inspector-content'),
206
+ showInspector: true,
207
+ },
208
+ });
209
+ try {
210
+ const inspector = container.querySelector('.smrt-workspace-inspector');
211
+ expect(inspector).not.toBeNull();
212
+ expect(inspector?.textContent).toContain('inspector-content');
213
+ const shell = container.querySelector('.smrt-workspace-shell');
214
+ expect(shell?.classList.contains('has-inspector')).toBe(true);
215
+ }
216
+ finally {
217
+ unmount(component);
218
+ }
219
+ });
220
+ it('renders inspector close button when onCloseInspector is provided', () => {
221
+ let closed = false;
222
+ const component = mount(WorkspaceShell, {
223
+ target: container,
224
+ props: {
225
+ children: textSnippet('content'),
226
+ inspector: textSnippet('inspector-content'),
227
+ showInspector: true,
228
+ onCloseInspector: () => {
229
+ closed = true;
230
+ },
231
+ },
232
+ });
233
+ try {
234
+ const closeBtn = container.querySelector('.inspector-close');
235
+ expect(closeBtn).not.toBeNull();
236
+ closeBtn?.click();
237
+ expect(closed).toBe(true);
238
+ }
239
+ finally {
240
+ unmount(component);
241
+ }
242
+ });
243
+ it('omits the inspector close button when onCloseInspector is not provided', () => {
244
+ const component = mount(WorkspaceShell, {
245
+ target: container,
246
+ props: {
247
+ children: textSnippet('content'),
248
+ inspector: textSnippet('inspector-content'),
249
+ showInspector: true,
250
+ },
251
+ });
252
+ try {
253
+ expect(container.querySelector('.inspector-close')).toBeNull();
254
+ }
255
+ finally {
256
+ unmount(component);
257
+ }
258
+ });
259
+ it('renders the inspector rail snippet when provided', () => {
260
+ const component = mount(WorkspaceShell, {
261
+ target: container,
262
+ props: {
263
+ children: textSnippet('content'),
264
+ inspectorRail: textSnippet('rail-button'),
265
+ },
266
+ });
267
+ try {
268
+ const rail = container.querySelector('.smrt-workspace-inspector-rail');
269
+ expect(rail).not.toBeNull();
270
+ expect(rail?.textContent).toContain('rail-button');
271
+ }
272
+ finally {
273
+ unmount(component);
274
+ }
275
+ });
276
+ it('shows a collapse toggle by default and invokes onToggleCollapsed', () => {
277
+ let toggles = 0;
278
+ const component = mount(WorkspaceShell, {
279
+ target: container,
280
+ props: {
281
+ children: textSnippet('content'),
282
+ title: 'Brand',
283
+ collapsed: false,
284
+ onToggleCollapsed: () => {
285
+ toggles += 1;
286
+ },
287
+ },
288
+ });
289
+ try {
290
+ const toggle = container.querySelector('.shell-toggle');
291
+ expect(toggle).not.toBeNull();
292
+ expect(toggle?.getAttribute('aria-expanded')).toBe('true');
293
+ toggle?.click();
294
+ expect(toggles).toBe(1);
295
+ }
296
+ finally {
297
+ unmount(component);
298
+ }
299
+ });
300
+ it('hides the collapse toggle when collapsible is false', () => {
301
+ const component = mount(WorkspaceShell, {
302
+ target: container,
303
+ props: {
304
+ children: textSnippet('content'),
305
+ title: 'Brand',
306
+ collapsible: false,
307
+ },
308
+ });
309
+ try {
310
+ expect(container.querySelector('.shell-toggle')).toBeNull();
311
+ }
312
+ finally {
313
+ unmount(component);
314
+ }
315
+ });
316
+ it('applies sidebar-collapsed class when collapsed prop is true', () => {
317
+ const component = mount(WorkspaceShell, {
318
+ target: container,
319
+ props: {
320
+ children: textSnippet('content'),
321
+ collapsed: true,
322
+ },
323
+ });
324
+ try {
325
+ const shell = container.querySelector('.smrt-workspace-shell');
326
+ expect(shell?.classList.contains('sidebar-collapsed')).toBe(true);
327
+ }
328
+ finally {
329
+ unmount(component);
330
+ }
331
+ });
332
+ it('renders the mode badge when modeLabel is provided', () => {
333
+ const component = mount(WorkspaceShell, {
334
+ target: container,
335
+ props: {
336
+ children: textSnippet('content'),
337
+ modeLabel: 'Local-first mode',
338
+ modeStatus: 'local-only',
339
+ },
340
+ });
341
+ try {
342
+ const badge = container.querySelector('.mode-badge');
343
+ expect(badge).not.toBeNull();
344
+ expect(badge?.getAttribute('data-status')).toBe('local-only');
345
+ expect(badge?.textContent).toContain('Local-first mode');
346
+ }
347
+ finally {
348
+ unmount(component);
349
+ }
350
+ });
351
+ it('omits the mode badge when no modeLabel/modeStatus is provided', () => {
352
+ const component = mount(WorkspaceShell, {
353
+ target: container,
354
+ props: {
355
+ children: textSnippet('content'),
356
+ },
357
+ });
358
+ try {
359
+ expect(container.querySelector('.mode-badge')).toBeNull();
360
+ }
361
+ finally {
362
+ unmount(component);
363
+ }
364
+ });
365
+ it('invokes onCloseInspector when Escape is pressed while inspector is open', async () => {
366
+ let closes = 0;
367
+ const component = mount(WorkspaceShell, {
368
+ target: container,
369
+ props: {
370
+ children: textSnippet('content'),
371
+ inspector: textSnippet('inspector-content'),
372
+ showInspector: true,
373
+ onCloseInspector: () => {
374
+ closes += 1;
375
+ },
376
+ },
377
+ });
378
+ try {
379
+ await tick();
380
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
381
+ expect(closes).toBe(1);
382
+ }
383
+ finally {
384
+ unmount(component);
385
+ }
386
+ });
387
+ it('renders an interactive button backdrop only when onCloseInspector is set', () => {
388
+ const component = mount(WorkspaceShell, {
389
+ target: container,
390
+ props: {
391
+ children: textSnippet('content'),
392
+ inspector: textSnippet('inspector-content'),
393
+ showInspector: true,
394
+ onCloseInspector: () => { },
395
+ },
396
+ });
397
+ try {
398
+ const backdrop = container.querySelector('.inspector-backdrop');
399
+ expect(backdrop).not.toBeNull();
400
+ expect(backdrop?.tagName).toBe('BUTTON');
401
+ }
402
+ finally {
403
+ unmount(component);
404
+ }
405
+ });
406
+ it('renders a non-interactive backdrop when onCloseInspector is omitted', () => {
407
+ const component = mount(WorkspaceShell, {
408
+ target: container,
409
+ props: {
410
+ children: textSnippet('content'),
411
+ inspector: textSnippet('inspector-content'),
412
+ showInspector: true,
413
+ },
414
+ });
415
+ try {
416
+ const backdrop = container.querySelector('.inspector-backdrop');
417
+ expect(backdrop).not.toBeNull();
418
+ expect(backdrop?.tagName).toBe('DIV');
419
+ expect(backdrop?.getAttribute('aria-hidden')).toBe('true');
420
+ }
421
+ finally {
422
+ unmount(component);
423
+ }
424
+ });
425
+ it('renders the default "Inspector" header label when inspectorTitle is omitted', () => {
426
+ const component = mount(WorkspaceShell, {
427
+ target: container,
428
+ props: {
429
+ children: textSnippet('content'),
430
+ inspector: textSnippet('inspector-content'),
431
+ showInspector: true,
432
+ },
433
+ });
434
+ try {
435
+ const title = container.querySelector('.inspector-title');
436
+ expect(title?.textContent).toBe('Inspector');
437
+ }
438
+ finally {
439
+ unmount(component);
440
+ }
441
+ });
442
+ it('uses a custom inspectorTitle and exposes it via aria-label', () => {
443
+ // The inspector aside uses aria-label (not aria-labelledby with a hard-coded
444
+ // id) so multiple <WorkspaceShell> instances on the same page can't collide
445
+ // on duplicate ids — see Copilot review on PR #1232.
446
+ const component = mount(WorkspaceShell, {
447
+ target: container,
448
+ props: {
449
+ children: textSnippet('content'),
450
+ inspector: textSnippet('inspector-content'),
451
+ showInspector: true,
452
+ inspectorTitle: 'Properties',
453
+ },
454
+ });
455
+ try {
456
+ const title = container.querySelector('.inspector-title');
457
+ expect(title?.textContent).toBe('Properties');
458
+ const aside = container.querySelector('.smrt-workspace-inspector');
459
+ expect(aside?.getAttribute('aria-label')).toBe('Properties');
460
+ expect(aside?.getAttribute('aria-labelledby')).toBeNull();
461
+ }
462
+ finally {
463
+ unmount(component);
464
+ }
465
+ });
466
+ it('marks the sidebar inert on mobile when the drawer is closed', async () => {
467
+ const originalMatchMedia = window.matchMedia;
468
+ // Force the mobile breakpoint so `isMobileViewport` becomes true.
469
+ window.matchMedia = ((query) => ({
470
+ matches: /max-width:\s*960px/.test(query),
471
+ media: query,
472
+ onchange: null,
473
+ addListener: () => { },
474
+ removeListener: () => { },
475
+ addEventListener: () => { },
476
+ removeEventListener: () => { },
477
+ dispatchEvent: () => false,
478
+ }));
479
+ const component = mount(WorkspaceShell, {
480
+ target: container,
481
+ props: {
482
+ children: textSnippet('content'),
483
+ nav: textSnippet('nav-content'),
484
+ },
485
+ });
486
+ try {
487
+ await tick();
488
+ const sidebar = container.querySelector('.smrt-workspace-sidebar');
489
+ // jsdom reflects the `inert` attribute as a boolean property — both
490
+ // forms are acceptable, the contract is "keyboard tab order skips
491
+ // the sidebar when it's offscreen".
492
+ expect(sidebar?.hasAttribute('inert') ||
493
+ sidebar?.inert === true).toBe(true);
494
+ }
495
+ finally {
496
+ unmount(component);
497
+ window.matchMedia = originalMatchMedia;
498
+ }
499
+ });
500
+ describe('bindable mobileNavOpen', () => {
501
+ it('opens the drawer when the bound value flips to true externally', async () => {
502
+ let controls;
503
+ const component = mount(BindHarness, {
504
+ target: container,
505
+ props: {
506
+ onReady: (c) => {
507
+ controls = c;
508
+ },
509
+ },
510
+ });
511
+ try {
512
+ await tick();
513
+ const shell = container.querySelector('.smrt-workspace-shell');
514
+ expect(shell?.classList.contains('nav-open')).toBe(false);
515
+ controls.setMobileNavOpen(true);
516
+ await tick();
517
+ expect(shell?.classList.contains('nav-open')).toBe(true);
518
+ expect(controls.getMobileNavOpen()).toBe(true);
519
+ controls.setMobileNavOpen(false);
520
+ await tick();
521
+ expect(shell?.classList.contains('nav-open')).toBe(false);
522
+ expect(controls.getMobileNavOpen()).toBe(false);
523
+ }
524
+ finally {
525
+ unmount(component);
526
+ }
527
+ });
528
+ it('propagates internal hamburger clicks back through bind:', async () => {
529
+ let controls;
530
+ const component = mount(BindHarness, {
531
+ target: container,
532
+ props: {
533
+ onReady: (c) => {
534
+ controls = c;
535
+ },
536
+ },
537
+ });
538
+ try {
539
+ await tick();
540
+ expect(controls.getMobileNavOpen()).toBe(false);
541
+ const hamburger = container.querySelector('.mobile-menu');
542
+ expect(hamburger).not.toBeNull();
543
+ hamburger?.click();
544
+ await tick();
545
+ expect(controls.getMobileNavOpen()).toBe(true);
546
+ // Toggle a second time — closes the drawer.
547
+ hamburger?.click();
548
+ await tick();
549
+ expect(controls.getMobileNavOpen()).toBe(false);
550
+ }
551
+ finally {
552
+ unmount(component);
553
+ }
554
+ });
555
+ it('propagates internal backdrop clicks back through bind:', async () => {
556
+ let controls;
557
+ const component = mount(BindHarness, {
558
+ target: container,
559
+ props: {
560
+ initial: true,
561
+ onReady: (c) => {
562
+ controls = c;
563
+ },
564
+ },
565
+ });
566
+ try {
567
+ await tick();
568
+ expect(controls.getMobileNavOpen()).toBe(true);
569
+ const backdrop = container.querySelector('.mobile-backdrop');
570
+ expect(backdrop).not.toBeNull();
571
+ backdrop?.click();
572
+ await tick();
573
+ expect(controls.getMobileNavOpen()).toBe(false);
574
+ }
575
+ finally {
576
+ unmount(component);
577
+ }
578
+ });
579
+ it('honors a non-default initial value passed by the consumer', async () => {
580
+ let controls;
581
+ const component = mount(BindHarness, {
582
+ target: container,
583
+ props: {
584
+ initial: true,
585
+ onReady: (c) => {
586
+ controls = c;
587
+ },
588
+ },
589
+ });
590
+ try {
591
+ await tick();
592
+ const shell = container.querySelector('.smrt-workspace-shell');
593
+ expect(shell?.classList.contains('nav-open')).toBe(true);
594
+ expect(controls.getMobileNavOpen()).toBe(true);
595
+ }
596
+ finally {
597
+ unmount(component);
598
+ }
599
+ });
600
+ });
601
+ it('restores focus to the hamburger button when the mobile drawer closes via Escape', async () => {
602
+ const component = mount(WorkspaceShell, {
603
+ target: container,
604
+ props: {
605
+ children: textSnippet('content'),
606
+ nav: textSnippet('nav-content'),
607
+ },
608
+ });
609
+ try {
610
+ const hamburger = container.querySelector('.mobile-menu');
611
+ expect(hamburger).not.toBeNull();
612
+ // Simulate keyboard-driven opening of the drawer.
613
+ hamburger?.focus();
614
+ expect(document.activeElement).toBe(hamburger);
615
+ hamburger?.click();
616
+ await tick();
617
+ // Move focus elsewhere as a real drawer interaction would.
618
+ const elsewhere = document.createElement('button');
619
+ container.appendChild(elsewhere);
620
+ elsewhere.focus();
621
+ expect(document.activeElement).toBe(elsewhere);
622
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
623
+ await tick();
624
+ expect(document.activeElement).toBe(hamburger);
625
+ }
626
+ finally {
627
+ unmount(component);
628
+ }
629
+ });
630
+ });