@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,772 @@
1
+ /**
2
+ * Tests for RoleShell — issue happyvertical/smrt#1226 (Phase 4b).
3
+ *
4
+ * Verifies the thin RoleConfig-driven wrapper composes WorkspaceShell +
5
+ * NavTree + Breadcrumbs correctly, propagates the role color as a CSS
6
+ * custom property, and surfaces the bindable mobile-drawer state.
7
+ */
8
+ import { createRawSnippet, mount, tick, unmount } from 'svelte';
9
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
+ import RoleShell from '../RoleShell.svelte';
11
+ import BindHarness from './role-shell-bind-harness.svelte';
12
+ function textSnippet(text) {
13
+ return createRawSnippet(() => ({
14
+ render: () => `<span>${text}</span>`,
15
+ }));
16
+ }
17
+ const superRole = {
18
+ id: 'super',
19
+ label: 'Super Admin',
20
+ description: 'Cross-network operations',
21
+ icon: '⌘',
22
+ color: 'blue',
23
+ sections: [
24
+ {
25
+ href: '/super/tenants',
26
+ label: 'Tenants',
27
+ icon: '◌',
28
+ description: 'Tenant hierarchy',
29
+ },
30
+ {
31
+ href: '/super/users',
32
+ label: 'Users',
33
+ icon: '◎',
34
+ description: 'Global user directory',
35
+ },
36
+ ],
37
+ };
38
+ const advertiserRole = {
39
+ id: 'advertiser',
40
+ label: 'Advertiser',
41
+ description: 'Self-service ad management',
42
+ icon: '📢',
43
+ color: 'purple',
44
+ sections: [
45
+ {
46
+ href: '/advertiser',
47
+ label: 'Overview',
48
+ icon: '◉',
49
+ },
50
+ {
51
+ href: '/advertiser/campaigns',
52
+ label: 'Campaigns',
53
+ icon: '▣',
54
+ },
55
+ ],
56
+ };
57
+ const ROLES = [superRole, advertiserRole];
58
+ let container;
59
+ beforeEach(() => {
60
+ container = document.createElement('div');
61
+ document.body.appendChild(container);
62
+ });
63
+ afterEach(() => {
64
+ container.remove();
65
+ });
66
+ describe('RoleShell', () => {
67
+ it("renders the current role's label as the WorkspaceShell title", () => {
68
+ const component = mount(RoleShell, {
69
+ target: container,
70
+ props: {
71
+ roles: ROLES,
72
+ currentRole: 'super',
73
+ currentPath: '/super/tenants',
74
+ children: textSnippet('main content'),
75
+ },
76
+ });
77
+ try {
78
+ const brand = container.querySelector('.brand');
79
+ expect(brand?.textContent).toContain('Super Admin');
80
+ expect(brand?.textContent).toContain('Cross-network operations');
81
+ }
82
+ finally {
83
+ unmount(component);
84
+ }
85
+ });
86
+ it("renders the current role's sections in the nav tree", () => {
87
+ const component = mount(RoleShell, {
88
+ target: container,
89
+ props: {
90
+ roles: ROLES,
91
+ currentRole: 'super',
92
+ currentPath: '/super/tenants',
93
+ children: textSnippet('content'),
94
+ },
95
+ });
96
+ try {
97
+ const navItems = container.querySelectorAll('.smrt-nav-tree .nav-item');
98
+ const labels = Array.from(navItems).map((el) => el.textContent ?? '');
99
+ // The nav-item textContent includes the icon glyph + label, so match
100
+ // with substring contains rather than strict equality.
101
+ expect(labels.some((l) => l.includes('Tenants'))).toBe(true);
102
+ expect(labels.some((l) => l.includes('Users'))).toBe(true);
103
+ // Should NOT render the other role's sections.
104
+ expect(labels.some((l) => l.includes('Campaigns'))).toBe(false);
105
+ }
106
+ finally {
107
+ unmount(component);
108
+ }
109
+ });
110
+ it('throws a descriptive error in dev when currentRole does not match any role.id', async () => {
111
+ // vitest runs with NODE_ENV=test which `esm-env` treats as
112
+ // development (DEV === true), so the dev-throw branch fires here.
113
+ // In production builds (`NODE_ENV=production`), the shell falls back
114
+ // to `roles[0]` rather than crashing — see the DEV check inside
115
+ // RoleShell's `$derived.by`. The fallback path is statically
116
+ // wired against esm-env's resolution and can't be cleanly faked in
117
+ // vitest, so we only assert the loud-error contract here.
118
+ const { DEV } = await import('esm-env');
119
+ expect(DEV).toBe(true);
120
+ expect(() => mount(RoleShell, {
121
+ target: container,
122
+ props: {
123
+ roles: ROLES,
124
+ currentRole: 'nonexistent',
125
+ currentPath: '/',
126
+ children: textSnippet('content'),
127
+ },
128
+ })).toThrow(/No role found for id "nonexistent"/);
129
+ });
130
+ it('sets --smrt-role-color CSS custom property when role.color is provided', () => {
131
+ const component = mount(RoleShell, {
132
+ target: container,
133
+ props: {
134
+ roles: ROLES,
135
+ currentRole: 'super',
136
+ currentPath: '/super',
137
+ children: textSnippet('content'),
138
+ },
139
+ });
140
+ try {
141
+ const wrapper = container.querySelector('.smrt-role-shell');
142
+ expect(wrapper).not.toBeNull();
143
+ // The style attribute on the wrapper sets the custom property; we
144
+ // assert on the inline style string rather than computed style
145
+ // because jsdom's CSS engine doesn't always resolve custom props.
146
+ expect(wrapper?.getAttribute('style')).toContain('--smrt-role-color');
147
+ expect(wrapper?.getAttribute('style')).toContain('blue');
148
+ }
149
+ finally {
150
+ unmount(component);
151
+ }
152
+ });
153
+ it('omits the --smrt-role-color style when role.color is not set', () => {
154
+ const colorlessRoles = [
155
+ {
156
+ id: 'plain',
157
+ label: 'Plain',
158
+ sections: [{ href: '/plain', label: 'Home' }],
159
+ },
160
+ ];
161
+ const component = mount(RoleShell, {
162
+ target: container,
163
+ props: {
164
+ roles: colorlessRoles,
165
+ currentRole: 'plain',
166
+ currentPath: '/plain',
167
+ children: textSnippet('content'),
168
+ },
169
+ });
170
+ try {
171
+ const wrapper = container.querySelector('.smrt-role-shell');
172
+ expect(wrapper?.getAttribute('style')).toBeNull();
173
+ }
174
+ finally {
175
+ unmount(component);
176
+ }
177
+ });
178
+ it('uses the title override when provided', () => {
179
+ const component = mount(RoleShell, {
180
+ target: container,
181
+ props: {
182
+ roles: ROLES,
183
+ currentRole: 'super',
184
+ currentPath: '/super',
185
+ title: 'Custom Title',
186
+ subtitle: 'Custom Subtitle',
187
+ children: textSnippet('content'),
188
+ },
189
+ });
190
+ try {
191
+ const brand = container.querySelector('.brand');
192
+ expect(brand?.textContent).toContain('Custom Title');
193
+ expect(brand?.textContent).toContain('Custom Subtitle');
194
+ expect(brand?.textContent).not.toContain('Super Admin');
195
+ }
196
+ finally {
197
+ unmount(component);
198
+ }
199
+ });
200
+ it('renders breadcrumbs by default with the role label as root', () => {
201
+ const component = mount(RoleShell, {
202
+ target: container,
203
+ props: {
204
+ roles: ROLES,
205
+ currentRole: 'super',
206
+ currentPath: '/super/tenants',
207
+ children: textSnippet('content'),
208
+ },
209
+ });
210
+ try {
211
+ const crumbs = container.querySelector('.smrt-breadcrumbs');
212
+ expect(crumbs).not.toBeNull();
213
+ // The first crumb is the rootCrumb (role label).
214
+ const items = crumbs?.querySelectorAll('.crumb-item');
215
+ expect(items?.[0]?.textContent).toContain('Super Admin');
216
+ }
217
+ finally {
218
+ unmount(component);
219
+ }
220
+ });
221
+ it('hides breadcrumbs when showBreadcrumbs is false', () => {
222
+ const component = mount(RoleShell, {
223
+ target: container,
224
+ props: {
225
+ roles: ROLES,
226
+ currentRole: 'super',
227
+ currentPath: '/super/tenants',
228
+ showBreadcrumbs: false,
229
+ children: textSnippet('content'),
230
+ },
231
+ });
232
+ try {
233
+ expect(container.querySelector('.smrt-breadcrumbs')).toBeNull();
234
+ }
235
+ finally {
236
+ unmount(component);
237
+ }
238
+ });
239
+ it('passes through the brand snippet to WorkspaceShell', () => {
240
+ const component = mount(RoleShell, {
241
+ target: container,
242
+ props: {
243
+ roles: ROLES,
244
+ currentRole: 'super',
245
+ currentPath: '/super',
246
+ brand: textSnippet('MyBrand'),
247
+ children: textSnippet('content'),
248
+ },
249
+ });
250
+ try {
251
+ const brand = container.querySelector('.brand');
252
+ expect(brand?.textContent).toContain('MyBrand');
253
+ // The brand snippet should override the default title rendering.
254
+ expect(brand?.textContent).not.toContain('Super Admin');
255
+ }
256
+ finally {
257
+ unmount(component);
258
+ }
259
+ });
260
+ it('passes through the sidebarFooter and topbarActions snippets', () => {
261
+ const component = mount(RoleShell, {
262
+ target: container,
263
+ props: {
264
+ roles: ROLES,
265
+ currentRole: 'super',
266
+ currentPath: '/super',
267
+ sidebarFooter: textSnippet('account-menu'),
268
+ topbarActions: textSnippet('action-button'),
269
+ children: textSnippet('content'),
270
+ },
271
+ });
272
+ try {
273
+ const footer = container.querySelector('.sidebar-footer');
274
+ expect(footer?.textContent).toContain('account-menu');
275
+ const actions = container.querySelector('.topbar-actions');
276
+ expect(actions?.textContent).toContain('action-button');
277
+ }
278
+ finally {
279
+ unmount(component);
280
+ }
281
+ });
282
+ it('passes through the inspectorRail snippet', () => {
283
+ const component = mount(RoleShell, {
284
+ target: container,
285
+ props: {
286
+ roles: ROLES,
287
+ currentRole: 'super',
288
+ currentPath: '/super',
289
+ inspectorRail: textSnippet('rail-button'),
290
+ children: textSnippet('content'),
291
+ },
292
+ });
293
+ try {
294
+ const rail = container.querySelector('.smrt-workspace-inspector-rail');
295
+ expect(rail?.textContent).toContain('rail-button');
296
+ }
297
+ finally {
298
+ unmount(component);
299
+ }
300
+ });
301
+ it('renders the inspector snippet only when showInspector is true', () => {
302
+ // The inspector passthrough requires both the snippet AND
303
+ // `showInspector` because WorkspaceShell gates rendering on the latter.
304
+ // Regression for happyvertical/smrt#1238 — the original RoleShell
305
+ // forwarded only the snippet, making the inspector silently
306
+ // un-renderable through this wrapper.
307
+ const componentHidden = mount(RoleShell, {
308
+ target: container,
309
+ props: {
310
+ roles: ROLES,
311
+ currentRole: 'super',
312
+ currentPath: '/super',
313
+ inspector: textSnippet('inspector-body'),
314
+ children: textSnippet('content'),
315
+ },
316
+ });
317
+ try {
318
+ expect(container.querySelector('.smrt-workspace-inspector')).toBeNull();
319
+ }
320
+ finally {
321
+ unmount(componentHidden);
322
+ }
323
+ // Mount a second instance with showInspector: true into the same
324
+ // container — the inspector should now render.
325
+ const componentShown = mount(RoleShell, {
326
+ target: container,
327
+ props: {
328
+ roles: ROLES,
329
+ currentRole: 'super',
330
+ currentPath: '/super',
331
+ inspector: textSnippet('inspector-body'),
332
+ showInspector: true,
333
+ children: textSnippet('content'),
334
+ },
335
+ });
336
+ try {
337
+ const inspector = container.querySelector('.smrt-workspace-inspector .inspector-body');
338
+ expect(inspector?.textContent).toContain('inspector-body');
339
+ }
340
+ finally {
341
+ unmount(componentShown);
342
+ }
343
+ });
344
+ it('forwards collapsed + onToggleCollapsed to WorkspaceShell', () => {
345
+ let toggled = 0;
346
+ const component = mount(RoleShell, {
347
+ target: container,
348
+ props: {
349
+ roles: ROLES,
350
+ currentRole: 'super',
351
+ currentPath: '/super',
352
+ collapsed: true,
353
+ onToggleCollapsed: () => {
354
+ toggled += 1;
355
+ },
356
+ children: textSnippet('content'),
357
+ },
358
+ });
359
+ try {
360
+ // The collapse state surfaces as a class on the shell root.
361
+ const shell = container.querySelector('.smrt-workspace-shell');
362
+ expect(shell?.classList.contains('sidebar-collapsed')).toBe(true);
363
+ // The toggle button only renders when `onToggleCollapsed` is provided —
364
+ // clicking it should invoke our callback.
365
+ const toggle = container.querySelector('.shell-toggle');
366
+ expect(toggle).not.toBeNull();
367
+ toggle?.click();
368
+ expect(toggled).toBe(1);
369
+ }
370
+ finally {
371
+ unmount(component);
372
+ }
373
+ });
374
+ it('forwards mode/eyebrow props to the WorkspaceShell topbar', () => {
375
+ const component = mount(RoleShell, {
376
+ target: container,
377
+ props: {
378
+ roles: ROLES,
379
+ currentRole: 'super',
380
+ currentPath: '/super',
381
+ eyebrow: 'EYE',
382
+ modeLabel: 'Local-first mode',
383
+ modeStatus: 'local-only',
384
+ modeDetail: 'detail line',
385
+ children: textSnippet('content'),
386
+ },
387
+ });
388
+ try {
389
+ const topbar = container.querySelector('.smrt-workspace-topbar');
390
+ expect(topbar?.textContent).toContain('Local-first mode');
391
+ expect(topbar?.textContent).toContain('detail line');
392
+ const badge = topbar?.querySelector('.mode-badge');
393
+ expect(badge?.dataset.status).toBe('local-only');
394
+ // The eyebrow appears in the topbar's eyebrow slot.
395
+ expect(topbar?.querySelector('.topbar-eyebrow')?.textContent).toContain('EYE');
396
+ }
397
+ finally {
398
+ unmount(component);
399
+ }
400
+ });
401
+ it('defaults breadcrumbStartAfter to role.id so the role segment is not duplicated', () => {
402
+ // Regression for happyvertical/smrt#1238 round-3: without a default
403
+ // skip, the auto-walk on `/super/tenants` produced `Super Admin /
404
+ // Super / Tenants` (the rootCrumb already represents the role, then
405
+ // the `/super` segment becomes a duplicate fallback crumb).
406
+ const component = mount(RoleShell, {
407
+ target: container,
408
+ props: {
409
+ roles: ROLES,
410
+ currentRole: 'super',
411
+ currentPath: '/super/tenants',
412
+ children: textSnippet('content'),
413
+ },
414
+ });
415
+ try {
416
+ const crumbs = container.querySelector('.smrt-breadcrumbs');
417
+ const labels = Array.from(crumbs?.querySelectorAll('.crumb-item') ?? []).map((el) => (el.textContent ?? '').trim());
418
+ expect(labels[0]).toContain('Super Admin');
419
+ // The duplicate `Super` crumb must NOT appear.
420
+ expect(labels.some((l) => l === 'Super' || l === '/ Super')).toBe(false);
421
+ // The Tenants crumb still renders.
422
+ expect(labels.some((l) => l.includes('Tenants'))).toBe(true);
423
+ // And the total trail is exactly 2: rootCrumb + Tenants.
424
+ expect(labels.length).toBe(2);
425
+ }
426
+ finally {
427
+ unmount(component);
428
+ }
429
+ });
430
+ it('respects an explicit breadcrumbStartAfter override for deeper prefixes', () => {
431
+ const component = mount(RoleShell, {
432
+ target: container,
433
+ props: {
434
+ roles: ROLES,
435
+ currentRole: 'super',
436
+ currentPath: '/dashboard/super/tenants',
437
+ rootCrumb: { label: 'Dashboard', href: '/dashboard' },
438
+ breadcrumbStartAfter: 'dashboard/super',
439
+ children: textSnippet('content'),
440
+ },
441
+ });
442
+ try {
443
+ const crumbs = container.querySelector('.smrt-breadcrumbs');
444
+ const labels = Array.from(crumbs?.querySelectorAll('.crumb-item') ?? []).map((el) => (el.textContent ?? '').trim());
445
+ // Explicit override skips `dashboard/super`, leaving Dashboard (root)
446
+ // + Tenants — the role-id default is not used.
447
+ expect(labels[0]).toContain('Dashboard');
448
+ expect(labels.some((l) => l.includes('Tenants'))).toBe(true);
449
+ expect(labels.length).toBe(2);
450
+ }
451
+ finally {
452
+ unmount(component);
453
+ }
454
+ });
455
+ it('opts out of the default startAfter when an empty string is passed', () => {
456
+ // `??` preserves empty string (it is not nullish), so consumers can
457
+ // explicitly disable the default skip and render the full path walk.
458
+ const component = mount(RoleShell, {
459
+ target: container,
460
+ props: {
461
+ roles: ROLES,
462
+ currentRole: 'super',
463
+ currentPath: '/super/tenants',
464
+ breadcrumbStartAfter: '',
465
+ children: textSnippet('content'),
466
+ },
467
+ });
468
+ try {
469
+ const crumbs = container.querySelector('.smrt-breadcrumbs');
470
+ const labels = Array.from(crumbs?.querySelectorAll('.crumb-item') ?? []).map((el) => (el.textContent ?? '').trim());
471
+ // With the default skip disabled, the `super` segment renders as a
472
+ // fallback crumb: Super Admin (root) / Super / Tenants. We use
473
+ // anchor hrefs to disambiguate from the rootCrumb (`/super`) since
474
+ // the rendered text contents include leading separator glyphs.
475
+ expect(labels.length).toBe(3);
476
+ expect(labels[0]).toContain('Super Admin');
477
+ const links = Array.from(crumbs?.querySelectorAll('.crumb-link') ?? []);
478
+ const hrefs = links.map((a) => a.getAttribute('href'));
479
+ // Both `/super` (from rootCrumb) and `/super` (fallback walk) collapse
480
+ // to the same href — assert the middle text content shows `Super`.
481
+ expect(labels[1]).toContain('Super');
482
+ expect(hrefs).toContain('/super');
483
+ }
484
+ finally {
485
+ unmount(component);
486
+ }
487
+ });
488
+ it('propagates collapsed to the inner NavTree', () => {
489
+ // Regression for happyvertical/smrt#1238 round-3: WorkspaceShell
490
+ // switches to the 96px collapsed rail when `collapsed={true}` but
491
+ // RoleShell previously forgot to forward the flag to NavTree, so
492
+ // labels/badges rendered full-size and overflowed the rail.
493
+ const component = mount(RoleShell, {
494
+ target: container,
495
+ props: {
496
+ roles: ROLES,
497
+ currentRole: 'super',
498
+ currentPath: '/super',
499
+ collapsed: true,
500
+ children: textSnippet('content'),
501
+ },
502
+ });
503
+ try {
504
+ const navTree = container.querySelector('.smrt-nav-tree');
505
+ expect(navTree).not.toBeNull();
506
+ expect(navTree?.classList.contains('collapsed')).toBe(true);
507
+ }
508
+ finally {
509
+ unmount(component);
510
+ }
511
+ });
512
+ it('renders the NavTree in its expanded state when collapsed is omitted or false', () => {
513
+ const component = mount(RoleShell, {
514
+ target: container,
515
+ props: {
516
+ roles: ROLES,
517
+ currentRole: 'super',
518
+ currentPath: '/super',
519
+ children: textSnippet('content'),
520
+ },
521
+ });
522
+ try {
523
+ const navTree = container.querySelector('.smrt-nav-tree');
524
+ expect(navTree).not.toBeNull();
525
+ expect(navTree?.classList.contains('collapsed')).toBe(false);
526
+ }
527
+ finally {
528
+ unmount(component);
529
+ }
530
+ });
531
+ it('uses rootCrumb override when provided', () => {
532
+ const component = mount(RoleShell, {
533
+ target: container,
534
+ props: {
535
+ roles: ROLES,
536
+ currentRole: 'super',
537
+ currentPath: '/dashboard/super/tenants',
538
+ rootCrumb: { label: 'Dashboard', href: '/dashboard' },
539
+ breadcrumbStartAfter: 'dashboard',
540
+ children: textSnippet('content'),
541
+ },
542
+ });
543
+ try {
544
+ const crumbs = container.querySelector('.smrt-breadcrumbs');
545
+ expect(crumbs).not.toBeNull();
546
+ const first = crumbs?.querySelector('.crumb-item:first-child .crumb-link');
547
+ expect(first?.textContent).toBe('Dashboard');
548
+ expect(first?.getAttribute('href')).toBe('/dashboard');
549
+ // With startAfter=dashboard, the auto-walk skips the `dashboard`
550
+ // prefix and the resulting trail should NOT contain a duplicate
551
+ // `Dashboard` fallback crumb.
552
+ const labels = Array.from(crumbs?.querySelectorAll('.crumb-item') ?? []).map((el) => el.textContent ?? '');
553
+ const dashboardCrumbs = labels.filter((l) => l.includes('Dashboard'));
554
+ expect(dashboardCrumbs.length).toBe(1);
555
+ }
556
+ finally {
557
+ unmount(component);
558
+ }
559
+ });
560
+ it('renders role.icon in the default brand area when no brand snippet is provided', () => {
561
+ const component = mount(RoleShell, {
562
+ target: container,
563
+ props: {
564
+ roles: ROLES,
565
+ currentRole: 'super',
566
+ currentPath: '/super',
567
+ children: textSnippet('content'),
568
+ },
569
+ });
570
+ try {
571
+ const icon = container.querySelector('.smrt-role-shell-icon');
572
+ expect(icon).not.toBeNull();
573
+ // RoleConfig.icon is a string glyph — render as text content.
574
+ expect(icon?.textContent).toContain('⌘');
575
+ }
576
+ finally {
577
+ unmount(component);
578
+ }
579
+ });
580
+ it('omits role.icon when a consumer brand snippet is provided', () => {
581
+ const component = mount(RoleShell, {
582
+ target: container,
583
+ props: {
584
+ roles: ROLES,
585
+ currentRole: 'super',
586
+ currentPath: '/super',
587
+ brand: textSnippet('CustomBrand'),
588
+ children: textSnippet('content'),
589
+ },
590
+ });
591
+ try {
592
+ expect(container.querySelector('.smrt-role-shell-icon')).toBeNull();
593
+ const brand = container.querySelector('.brand');
594
+ expect(brand?.textContent).toContain('CustomBrand');
595
+ }
596
+ finally {
597
+ unmount(component);
598
+ }
599
+ });
600
+ it('forwards navIconComponent to the inner NavTree', async () => {
601
+ // Use a tiny stub component so we can assert the icon-rendering branch
602
+ // fired. NavTree renders `<IconComponent name=... size=... />` in the
603
+ // icon slot when this prop is provided.
604
+ const TestIcon = (await import('./test-icon.svelte')).default;
605
+ const component = mount(RoleShell, {
606
+ target: container,
607
+ props: {
608
+ roles: ROLES,
609
+ currentRole: 'super',
610
+ currentPath: '/super',
611
+ navIconComponent: TestIcon,
612
+ children: textSnippet('content'),
613
+ },
614
+ });
615
+ try {
616
+ const iconStub = container.querySelector('.smrt-nav-tree [data-testid="custom-icon"]');
617
+ expect(iconStub).not.toBeNull();
618
+ }
619
+ finally {
620
+ unmount(component);
621
+ }
622
+ });
623
+ it('invokes the composed onNavigate callback alongside closing the drawer', async () => {
624
+ let called = 0;
625
+ let controls;
626
+ const component = mount(BindHarness, {
627
+ target: container,
628
+ props: {
629
+ roles: ROLES,
630
+ currentRole: 'super',
631
+ currentPath: '/super',
632
+ initial: true,
633
+ onNavigate: () => {
634
+ called += 1;
635
+ },
636
+ onReady: (c) => {
637
+ controls = c;
638
+ },
639
+ },
640
+ });
641
+ try {
642
+ await tick();
643
+ expect(controls.getMobileNavOpen()).toBe(true);
644
+ const navLink = container.querySelector('.smrt-nav-tree .nav-item');
645
+ navLink?.addEventListener('click', (e) => e.preventDefault());
646
+ navLink?.click();
647
+ await tick();
648
+ // The internal close still fires AND the consumer callback ran.
649
+ expect(controls.getMobileNavOpen()).toBe(false);
650
+ expect(called).toBe(1);
651
+ }
652
+ finally {
653
+ unmount(component);
654
+ }
655
+ });
656
+ it('exposes data-role-id on the wrapper', () => {
657
+ const component = mount(RoleShell, {
658
+ target: container,
659
+ props: {
660
+ roles: ROLES,
661
+ currentRole: 'advertiser',
662
+ currentPath: '/advertiser',
663
+ children: textSnippet('content'),
664
+ },
665
+ });
666
+ try {
667
+ const wrapper = container.querySelector('.smrt-role-shell');
668
+ expect(wrapper?.getAttribute('data-role-id')).toBe('advertiser');
669
+ }
670
+ finally {
671
+ unmount(component);
672
+ }
673
+ });
674
+ describe('bindable mobileNavOpen', () => {
675
+ it('reflects external state changes into the WorkspaceShell drawer class', async () => {
676
+ let controls;
677
+ const component = mount(BindHarness, {
678
+ target: container,
679
+ props: {
680
+ roles: ROLES,
681
+ currentRole: 'super',
682
+ currentPath: '/super',
683
+ onReady: (c) => {
684
+ controls = c;
685
+ },
686
+ },
687
+ });
688
+ try {
689
+ await tick();
690
+ const shell = container.querySelector('.smrt-workspace-shell');
691
+ expect(shell?.classList.contains('nav-open')).toBe(false);
692
+ controls.setMobileNavOpen(true);
693
+ await tick();
694
+ expect(shell?.classList.contains('nav-open')).toBe(true);
695
+ expect(controls.getMobileNavOpen()).toBe(true);
696
+ controls.setMobileNavOpen(false);
697
+ await tick();
698
+ expect(shell?.classList.contains('nav-open')).toBe(false);
699
+ }
700
+ finally {
701
+ unmount(component);
702
+ }
703
+ });
704
+ it('closes the drawer when a nav link is clicked', async () => {
705
+ let controls;
706
+ const component = mount(BindHarness, {
707
+ target: container,
708
+ props: {
709
+ roles: ROLES,
710
+ currentRole: 'super',
711
+ currentPath: '/super',
712
+ initial: true,
713
+ onReady: (c) => {
714
+ controls = c;
715
+ },
716
+ },
717
+ });
718
+ try {
719
+ await tick();
720
+ expect(controls.getMobileNavOpen()).toBe(true);
721
+ const navLink = container.querySelector('.smrt-nav-tree .nav-item');
722
+ expect(navLink).not.toBeNull();
723
+ // Prevent jsdom from actually navigating away while clicking.
724
+ navLink?.addEventListener('click', (e) => e.preventDefault());
725
+ navLink?.click();
726
+ await tick();
727
+ expect(controls.getMobileNavOpen()).toBe(false);
728
+ }
729
+ finally {
730
+ unmount(component);
731
+ }
732
+ });
733
+ });
734
+ it('re-renders with the new role sections when currentRole changes', async () => {
735
+ let setCurrentRole;
736
+ // Build a small switcher harness inline using a wrapper component is
737
+ // overkill — instead, drive currentRole via component prop updates.
738
+ // Svelte 5's `mount` returns the component instance; reassignments
739
+ // happen via $state in a parent, so we use a tiny inline harness.
740
+ const Harness = (await import('./role-shell-switch-harness.svelte'))
741
+ .default;
742
+ const component = mount(Harness, {
743
+ target: container,
744
+ props: {
745
+ roles: ROLES,
746
+ initialRole: 'super',
747
+ currentPath: '/super',
748
+ onReady: (controls) => {
749
+ setCurrentRole = controls.setCurrentRole;
750
+ },
751
+ },
752
+ });
753
+ try {
754
+ await tick();
755
+ let labels = Array.from(container.querySelectorAll('.smrt-nav-tree .nav-item')).map((el) => el.textContent ?? '');
756
+ expect(labels.some((l) => l.includes('Tenants'))).toBe(true);
757
+ expect(labels.some((l) => l.includes('Campaigns'))).toBe(false);
758
+ setCurrentRole('advertiser');
759
+ await tick();
760
+ labels = Array.from(container.querySelectorAll('.smrt-nav-tree .nav-item')).map((el) => el.textContent ?? '');
761
+ expect(labels.some((l) => l.includes('Campaigns'))).toBe(true);
762
+ expect(labels.some((l) => l.includes('Tenants'))).toBe(false);
763
+ // role-color should have changed too.
764
+ const wrapper = container.querySelector('.smrt-role-shell');
765
+ expect(wrapper?.getAttribute('style')).toContain('purple');
766
+ expect(wrapper?.getAttribute('data-role-id')).toBe('advertiser');
767
+ }
768
+ finally {
769
+ unmount(component);
770
+ }
771
+ });
772
+ });