@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,771 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import type { Snippet } from 'svelte';
4
+ import { onDestroy } from 'svelte';
5
+ import { useAppState } from '../../hooks/useAppState.svelte.js';
6
+ import { useSTT } from '../../hooks/useSTT.svelte.js';
7
+ import { M } from '../../i18n/strings.forms.js';
8
+ import {
9
+ type FieldDefinition,
10
+ type SMRTFormContext,
11
+ setFormContext,
12
+ } from '../../state/form-context.js';
13
+ import type { LLMModelId, STTAdapterType } from './types.js';
14
+
15
+ const { t } = useI18n();
16
+
17
+ export interface Props {
18
+ /** Form children */
19
+ children: Snippet;
20
+ /** Show mode toggle button */
21
+ showModeToggle?: boolean;
22
+ /** Show form-level listen button */
23
+ showFormListen?: boolean;
24
+ /** Silence timeout in seconds before stopping */
25
+ silenceTimeout?: number;
26
+ /** STT adapter type */
27
+ sttAdapter?: STTAdapterType;
28
+ /** LLM model for extraction (or 'none' for regex-only) */
29
+ llmModel?: LLMModelId;
30
+ /** Called when form is submitted (if provided, prevents native submission) */
31
+ onsubmit?: (data: Record<string, unknown>) => void;
32
+ /** HTTP method for native form submission (default: GET) */
33
+ method?: 'GET' | 'POST';
34
+ /** Form action URL for native form submission */
35
+ action?: string;
36
+ }
37
+
38
+ const {
39
+ children,
40
+ showModeToggle = false,
41
+ showFormListen = false,
42
+ silenceTimeout = 5,
43
+ sttAdapter = 'whisper-wasm',
44
+ llmModel = 'none',
45
+ onsubmit,
46
+ method,
47
+ action,
48
+ }: Props = $props();
49
+
50
+ const app = useAppState();
51
+ const stt = useSTT();
52
+
53
+ // Internal state
54
+ let fields = $state<Map<string, FieldDefinition>>(new Map());
55
+ let isFormListening = $state(false);
56
+ let isExtracting = $state(false);
57
+ let spokenText = $state('');
58
+ let extractError = $state<string | null>(null);
59
+ let lastSpeechTime = $state(0);
60
+ let silenceTimer: ReturnType<typeof setTimeout> | null = null;
61
+ let audioLevelInterval: ReturnType<typeof setInterval> | null = null;
62
+ let audioContext: AudioContext | null = null;
63
+ let analyser: AnalyserNode | null = null;
64
+ let levelMonitorStream: MediaStream | null = null;
65
+
66
+ const isSmrt = $derived(app.state.mode === 'smrt');
67
+
68
+ // Create form context
69
+ const formContext: SMRTFormContext = {
70
+ get mode() {
71
+ return app.state.mode === 'smrt' ? 'smrt' : 'default';
72
+ },
73
+ registerField(field: FieldDefinition) {
74
+ fields.set(field.name, field);
75
+ fields = new Map(fields); // Trigger reactivity
76
+ },
77
+ unregisterField(name: string) {
78
+ fields.delete(name);
79
+ fields = new Map(fields);
80
+ },
81
+ getFieldSchema() {
82
+ return Array.from(fields.values());
83
+ },
84
+ get isFormListening() {
85
+ return isFormListening;
86
+ },
87
+ get isExtracting() {
88
+ return isExtracting;
89
+ },
90
+ toggleListening: () => toggleFormListening(),
91
+ };
92
+
93
+ // Provide context to children
94
+ setFormContext(formContext);
95
+
96
+ // Clean up extracted values based on field type
97
+ function cleanValue(value: unknown, fieldType: string): unknown {
98
+ if (typeof value !== 'string') return value;
99
+
100
+ let cleaned = value.trim();
101
+
102
+ switch (fieldType) {
103
+ case 'text':
104
+ // Remove trailing periods from names
105
+ cleaned = cleaned.replace(/\.$/, '');
106
+ // Remove common speech artifacts
107
+ cleaned = cleaned.replace(/^(my |the |it's |is )/i, '');
108
+ break;
109
+ case 'email':
110
+ // Ensure email is properly formatted
111
+ cleaned = cleaned
112
+ .toLowerCase()
113
+ .replace(/\s+at\s+/gi, '@')
114
+ .replace(/\bat\b/gi, '@')
115
+ .replace(/\s+dot\s+/gi, '.')
116
+ .replace(/\bdot\b/gi, '.')
117
+ .replace(/\s+/g, '');
118
+ break;
119
+ }
120
+
121
+ return cleaned;
122
+ }
123
+
124
+ // Regex-based extraction from spoken text
125
+ function extractFieldsFromText(text: string): Record<string, unknown> {
126
+ const result: Record<string, unknown> = {};
127
+ const fieldDefs = Array.from(fields.values());
128
+
129
+ // Normalize text: remove commas, extra spaces
130
+ const normalized = text.replace(/,/g, ' ').replace(/\s+/g, ' ').trim();
131
+
132
+ // Build list of all field triggers for boundary detection
133
+ const allTriggers: string[] = [];
134
+ for (const f of fieldDefs) {
135
+ allTriggers.push(f.name.toLowerCase());
136
+ if (f.label) {
137
+ allTriggers.push(f.label.toLowerCase().replace(/\s+/g, '\\s+'));
138
+ }
139
+ }
140
+
141
+ for (const field of fieldDefs) {
142
+ const name = field.name.toLowerCase();
143
+ const label = field.label?.toLowerCase() || '';
144
+
145
+ // Build trigger patterns for this field
146
+ // For "email" field with label "Email Address", match both:
147
+ // - "email ..."
148
+ // - "email address ..."
149
+ const triggers: string[] = [];
150
+
151
+ // Add label first (more specific, e.g., "email address")
152
+ if (label) {
153
+ triggers.push(label.replace(/\s+/g, '\\s+'));
154
+ }
155
+ // Add field name
156
+ triggers.push(name);
157
+
158
+ // Build boundary pattern (other field triggers)
159
+ const otherTriggers = allTriggers.filter(
160
+ (t) => t !== name && t !== label.replace(/\s+/g, '\\s+'),
161
+ );
162
+ const boundaryPattern =
163
+ otherTriggers.length > 0
164
+ ? `(?=\\s+(?:${otherTriggers.join('|')})|$)`
165
+ : '$';
166
+
167
+ for (const trigger of triggers) {
168
+ // Match: trigger followed by optional "is", then capture value until boundary
169
+ const pattern = new RegExp(
170
+ `(?:^|\\s)${trigger}\\s+(?:is\\s+|and\\s+)?(.+?)${boundaryPattern}`,
171
+ 'i',
172
+ );
173
+ const match = normalized.match(pattern);
174
+
175
+ if (match?.[1]) {
176
+ let value = match[1].trim();
177
+
178
+ // Remove trailing punctuation
179
+ value = value.replace(/[.,!?]+$/, '').trim();
180
+
181
+ // Clean up based on field type
182
+ if (field.type === 'email') {
183
+ value = value
184
+ .toLowerCase()
185
+ .replace(/\s+at\s+/gi, '@')
186
+ .replace(/\bat\b/gi, '@')
187
+ .replace(/\s+dot\s+/gi, '.')
188
+ .replace(/\bdot\b/gi, '.')
189
+ .replace(/\s+/g, '');
190
+ }
191
+
192
+ if (value) {
193
+ result[field.name] = value;
194
+ break; // Found value for this field, move to next
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ // Extract fields from spoken text
204
+ // Currently uses regex-only since small local LLMs produce unreliable output
205
+ async function extractFields(text: string): Promise<Record<string, unknown>> {
206
+ // Use regex extraction directly - fast and reliable
207
+ // TODO: Add LLM enhancement when larger models are available
208
+ const result = extractFieldsFromText(text);
209
+ return result;
210
+ }
211
+
212
+ // Apply extracted values to fields with cleanup
213
+ function applyExtractedValues(values: Record<string, unknown>) {
214
+ for (const [name, value] of Object.entries(values)) {
215
+ const field = fields.get(name);
216
+ if (field && value !== undefined && value !== null) {
217
+ const cleanedValue = cleanValue(value, field.type);
218
+ field.setValue(cleanedValue);
219
+ }
220
+ }
221
+ }
222
+
223
+ // Reset silence timer
224
+ function resetSilenceTimer() {
225
+ if (silenceTimer) {
226
+ clearTimeout(silenceTimer);
227
+ }
228
+ lastSpeechTime = Date.now();
229
+ silenceTimer = setTimeout(() => {
230
+ if (isFormListening) {
231
+ stopFormListening();
232
+ }
233
+ }, silenceTimeout * 1000);
234
+ }
235
+
236
+ // Start monitoring audio levels to detect speech (for Whisper which has no interim results)
237
+ async function startAudioLevelMonitoring(stream: MediaStream) {
238
+ try {
239
+ levelMonitorStream = stream;
240
+ audioContext = new AudioContext();
241
+
242
+ // Ensure AudioContext is running (may be suspended until user interaction)
243
+ if (audioContext.state === 'suspended') {
244
+ await audioContext.resume();
245
+ }
246
+
247
+ analyser = audioContext.createAnalyser();
248
+ analyser.fftSize = 256;
249
+ analyser.smoothingTimeConstant = 0.3;
250
+
251
+ const source = audioContext.createMediaStreamSource(stream);
252
+ source.connect(analyser);
253
+
254
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
255
+ const SPEECH_THRESHOLD = 20; // Lowered from 30 for better sensitivity
256
+ let checksWithSpeech = 0;
257
+
258
+ audioLevelInterval = setInterval(() => {
259
+ if (!analyser || !isFormListening) return;
260
+
261
+ analyser.getByteFrequencyData(dataArray);
262
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
263
+
264
+ // Log periodically to debug
265
+ checksWithSpeech++;
266
+ if (checksWithSpeech % 10 === 0) {
267
+ }
268
+
269
+ if (average > SPEECH_THRESHOLD) {
270
+ // Speech detected - reset silence timer
271
+ resetSilenceTimer();
272
+ }
273
+ }, 200); // Check every 200ms
274
+ } catch (err) {}
275
+ }
276
+
277
+ // Stop audio level monitoring
278
+ function stopAudioLevelMonitoring() {
279
+ if (audioLevelInterval) {
280
+ clearInterval(audioLevelInterval);
281
+ audioLevelInterval = null;
282
+ }
283
+ if (levelMonitorStream) {
284
+ for (const track of levelMonitorStream.getTracks()) {
285
+ track.stop();
286
+ }
287
+ levelMonitorStream = null;
288
+ }
289
+ if (audioContext) {
290
+ audioContext.close().catch(() => {});
291
+ audioContext = null;
292
+ analyser = null;
293
+ }
294
+ }
295
+
296
+ // Check for "done" keyword
297
+ function checkForDoneKeyword(text: string): boolean {
298
+ const lowerText = text.toLowerCase();
299
+ // Check if the last word is "done" or ends with "done"
300
+ return lowerText.endsWith('done') || lowerText.endsWith('done.');
301
+ }
302
+
303
+ // Guard to prevent multiple simultaneous stop calls
304
+ let isStopping = false;
305
+ // Guard to prevent premature stop during startup
306
+ let isStarting = false;
307
+ // Timestamp of last stop to prevent immediate restart
308
+ let lastStopTime = 0;
309
+ const RESTART_COOLDOWN_MS = 1000; // 1 second cooldown after stopping
310
+ // Track the last processed STT result to avoid re-processing stale results
311
+ let lastProcessedResult = '';
312
+
313
+ // Watch STT results while form is listening
314
+ // Only track speech and check for "done" - extraction happens at the end
315
+ $effect(() => {
316
+ if (isFormListening && stt.lastResult && !isStopping) {
317
+ const newText = stt.lastResult;
318
+
319
+ // Skip if this is the same result we already processed (stale from previous session)
320
+ if (newText === lastProcessedResult) {
321
+ return;
322
+ }
323
+ lastProcessedResult = newText;
324
+ spokenText = newText;
325
+
326
+ // Reset silence timer on new speech (for browser STT with interim results)
327
+ if (sttAdapter === 'browser-speech') {
328
+ resetSilenceTimer();
329
+ }
330
+
331
+ // Check for "done" keyword
332
+ if (checkForDoneKeyword(newText)) {
333
+ // Don't set isStopping here - let stopFormListening() do it after passing the guard
334
+ stopFormListening();
335
+ }
336
+ }
337
+ });
338
+
339
+ // Watch for STT stopping unexpectedly (e.g., browser STT auto-stops after silence)
340
+ $effect(() => {
341
+ // If form thinks we're listening but STT has stopped, handle it
342
+ // Don't trigger during startup phase (isStarting) or shutdown phase (isStopping)
343
+ if (isFormListening && !stt.isListening && !isStopping && !isStarting) {
344
+ // Use setTimeout to avoid state update during render
345
+ setTimeout(() => {
346
+ if (isFormListening && !isStopping && !isStarting) {
347
+ stopFormListening();
348
+ }
349
+ }, 0);
350
+ }
351
+ });
352
+
353
+ async function toggleFormListening() {
354
+ if (isFormListening) {
355
+ await stopFormListening();
356
+ } else {
357
+ // Prevent immediate restart after stopping (UI might lag behind state)
358
+ const timeSinceStop = Date.now() - lastStopTime;
359
+ if (lastStopTime > 0 && timeSinceStop < RESTART_COOLDOWN_MS) {
360
+ return;
361
+ }
362
+ await startFormListening();
363
+ }
364
+ }
365
+
366
+ async function startFormListening() {
367
+ if (!isSmrt) return;
368
+
369
+ // Set starting flag to prevent premature stop detection
370
+ isStarting = true;
371
+
372
+ // Initialize STT with selected adapter
373
+ if (!stt.isReady || stt.adapterType !== sttAdapter) {
374
+ await stt.initialize({ type: sttAdapter });
375
+ }
376
+
377
+ extractError = null;
378
+ spokenText = '';
379
+ // Set to current stale result so the effect skips it, but new results will be processed
380
+ lastProcessedResult = stt.lastResult || '';
381
+ isFormListening = true;
382
+ isStopping = false;
383
+ lastSpeechTime = Date.now();
384
+
385
+ // Start silence timer
386
+ resetSilenceTimer();
387
+
388
+ // Start audio level monitoring for Whisper (no interim results)
389
+ // Browser STT has interim results so doesn't need this
390
+ if (sttAdapter === 'whisper-wasm') {
391
+ try {
392
+ const levelStream = await navigator.mediaDevices.getUserMedia({
393
+ audio: true,
394
+ });
395
+ startAudioLevelMonitoring(levelStream);
396
+ } catch (err) {}
397
+ }
398
+
399
+ await stt.start({ continuous: true, interimResults: true });
400
+
401
+ // Clear starting flag now that STT is actually listening
402
+ isStarting = false;
403
+ }
404
+
405
+ async function stopFormListening() {
406
+ if (!isFormListening || isStopping) {
407
+ return;
408
+ }
409
+ isStopping = true;
410
+ isStarting = false; // Ensure starting flag is cleared
411
+
412
+ // Clear silence timer
413
+ if (silenceTimer) {
414
+ clearTimeout(silenceTimer);
415
+ silenceTimer = null;
416
+ }
417
+
418
+ // Stop audio level monitoring
419
+ stopAudioLevelMonitoring();
420
+
421
+ // IMPORTANT: Keep isFormListening = true until after stt.stop() completes
422
+ // This allows the $effect to capture any final results
423
+ await stt.stop();
424
+
425
+ // Now capture the final result AFTER stop completes
426
+ // stt.stop() waits for transcription to finish in whisper-wasm
427
+ const finalResult = stt.lastResult || '';
428
+
429
+ // NOW we can set isFormListening to false
430
+ isFormListening = false;
431
+
432
+ // Final extraction on the captured result
433
+ const textToExtract = (finalResult || spokenText || '')
434
+ .replace(/\s*done\.?$/i, '')
435
+ .trim();
436
+
437
+ if (textToExtract) {
438
+ isExtracting = true;
439
+ extractError = null;
440
+
441
+ try {
442
+ const values = await extractFields(textToExtract);
443
+ applyExtractedValues(values);
444
+ } catch (err) {
445
+ extractError =
446
+ err instanceof Error ? err.message : 'Failed to extract fields';
447
+ } finally {
448
+ isExtracting = false;
449
+ isStopping = false;
450
+ }
451
+ } else {
452
+ isStopping = false;
453
+ }
454
+
455
+ // Record stop time to prevent immediate restart
456
+ lastStopTime = Date.now();
457
+ }
458
+
459
+ // Cleanup on destroy
460
+ onDestroy(() => {
461
+ if (silenceTimer) {
462
+ clearTimeout(silenceTimer);
463
+ }
464
+ stopAudioLevelMonitoring();
465
+ });
466
+
467
+ function handleModeToggle() {
468
+ const newMode = app.state.mode === 'smrt' ? 'default' : 'smrt';
469
+ app.setMode(newMode);
470
+ }
471
+
472
+ function handleSubmit(e: Event) {
473
+ // Only prevent default if we have an onsubmit handler
474
+ // This allows native form submission for SvelteKit form actions
475
+ if (onsubmit) {
476
+ e.preventDefault();
477
+ const data: Record<string, unknown> = {};
478
+ for (const [name, field] of fields) {
479
+ data[name] = field.getValue();
480
+ }
481
+ onsubmit(data);
482
+ }
483
+ // Otherwise, let native form submission happen (e.g., for SvelteKit use:enhance)
484
+ }
485
+
486
+ function getFormData(): Record<string, unknown> {
487
+ const data: Record<string, unknown> = {};
488
+ for (const [name, field] of fields) {
489
+ data[name] = field.getValue();
490
+ }
491
+ return data;
492
+ }
493
+ </script>
494
+
495
+ <form class="smrt-form" onsubmit={handleSubmit} {method} {action}>
496
+ <!--
497
+ Screen-reader status region (L1 #1420): announces async STT / field-
498
+ extraction state politely, since the visible cues live on a button whose
499
+ label change isn't reliably announced. Visually hidden — the button still
500
+ shows the same state to sighted users.
501
+ -->
502
+ <div class="smrt-form__status" role="status" aria-live="polite">
503
+ {#if isExtracting}
504
+ {t(M['ui.form.extracting'])}
505
+ {:else if isFormListening}
506
+ {t(M['ui.form.listening_status'])}
507
+ {/if}
508
+ </div>
509
+
510
+ {#if showModeToggle || showFormListen}
511
+ <div class="form-controls">
512
+ {#if showModeToggle}
513
+ <div class="mode-toggle">
514
+ <button
515
+ type="button"
516
+ class="mode-btn"
517
+ class:active={!isSmrt}
518
+ onclick={handleModeToggle}
519
+ >
520
+ Dumb
521
+ </button>
522
+ <button
523
+ type="button"
524
+ class="mode-btn"
525
+ class:active={isSmrt}
526
+ onclick={handleModeToggle}
527
+ >
528
+ s-m-r-t
529
+ </button>
530
+ </div>
531
+ {/if}
532
+
533
+ {#if showFormListen && isSmrt}
534
+ <button
535
+ type="button"
536
+ class="form-listen-btn"
537
+ class:listening={isFormListening}
538
+ class:extracting={isExtracting}
539
+ onclick={toggleFormListening}
540
+ >
541
+ {#if isExtracting}
542
+ <svg
543
+ width="18"
544
+ height="18"
545
+ viewBox="0 0 24 24"
546
+ fill="none"
547
+ stroke="currentColor"
548
+ stroke-width="2"
549
+ class="spinner"
550
+ >
551
+ <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/>
552
+ </svg>
553
+ Extracting...
554
+ {:else if isFormListening}
555
+ <svg
556
+ width="18"
557
+ height="18"
558
+ viewBox="0 0 24 24"
559
+ fill="none"
560
+ stroke="currentColor"
561
+ stroke-width="2"
562
+ stroke-linecap="round"
563
+ stroke-linejoin="round"
564
+ >
565
+ <rect x="6" y="4" width="4" height="16"/>
566
+ <rect x="14" y="4" width="4" height="16"/>
567
+ </svg>
568
+ {t(M['ui.form.listening_button'])}
569
+ {:else}
570
+ <svg
571
+ width="18"
572
+ height="18"
573
+ viewBox="0 0 24 24"
574
+ fill="none"
575
+ stroke="currentColor"
576
+ stroke-width="2"
577
+ stroke-linecap="round"
578
+ stroke-linejoin="round"
579
+ >
580
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
581
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
582
+ <line x1="12" x2="12" y1="19" y2="22"/>
583
+ </svg>
584
+ {t(M['ui.form.speak_all_fields'])}
585
+ {/if}
586
+ </button>
587
+ {/if}
588
+ </div>
589
+ {/if}
590
+
591
+ {#if extractError}
592
+ <div class="extract-error" role="alert">{extractError}</div>
593
+ {/if}
594
+
595
+ <div class="form-fields">
596
+ {@render children()}
597
+ </div>
598
+ </form>
599
+
600
+ {#if isFormListening && spokenText}
601
+ <div class="spoken-toaster">
602
+ <strong>{t(M['ui.form.you_said'])}</strong> {spokenText}
603
+ </div>
604
+ {/if}
605
+
606
+ <style>
607
+ .smrt-form {
608
+ display: flex;
609
+ flex-direction: column;
610
+ gap: var(--smrt-spacing-4, 16px);
611
+ }
612
+
613
+ /* Visually-hidden live region (L1 #1420) — announced to screen readers only. */
614
+ .smrt-form__status {
615
+ position: absolute;
616
+ width: 1px;
617
+ height: 1px;
618
+ padding: 0;
619
+ margin: -1px;
620
+ overflow: hidden;
621
+ clip: rect(0, 0, 0, 0);
622
+ white-space: nowrap;
623
+ border: 0;
624
+ }
625
+
626
+ .form-controls {
627
+ display: flex;
628
+ align-items: center;
629
+ gap: var(--smrt-spacing-4, 16px);
630
+ flex-wrap: wrap;
631
+ }
632
+
633
+ .mode-toggle {
634
+ display: flex;
635
+ background: var(--smrt-color-surface-container-high, #f3f4f6);
636
+ padding: var(--smrt-spacing-1, 4px);
637
+ border-radius: var(--smrt-radius-md, 8px);
638
+ }
639
+
640
+ .mode-btn {
641
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-4, 16px);
642
+ border: none;
643
+ background: transparent;
644
+ color: var(--smrt-color-on-surface-variant, #6b7280);
645
+ border-radius: var(--smrt-radius-md, 8px);
646
+ cursor: pointer;
647
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
648
+ font-weight: var(--smrt-typography-weight-medium, 500);
649
+ transition: all var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
650
+ }
651
+
652
+ .mode-btn:hover {
653
+ color: var(--smrt-color-on-surface, #374151);
654
+ }
655
+
656
+ .mode-btn.active {
657
+ background: var(--smrt-color-surface, #fff);
658
+ color: var(--smrt-color-primary, #3b82f6);
659
+ box-shadow: var(--smrt-elevation-1, 0 1px 3px color-mix(in srgb, var(--smrt-color-shadow) 10%, transparent));
660
+ }
661
+
662
+ .form-listen-btn {
663
+ display: flex;
664
+ align-items: center;
665
+ gap: var(--smrt-spacing-2, 8px);
666
+ padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-5, 20px);
667
+ border: 2px solid var(--smrt-color-primary, #3b82f6);
668
+ background: var(--smrt-color-surface);
669
+ color: var(--smrt-color-primary);
670
+ border-radius: var(--smrt-radius-md, 8px);
671
+ cursor: pointer;
672
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
673
+ font-weight: var(--smrt-typography-weight-medium, 500);
674
+ transition: all var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
675
+ }
676
+
677
+ .form-listen-btn:hover {
678
+ background: var(--smrt-color-primary-container, #eff6ff);
679
+ }
680
+
681
+ .form-listen-btn.listening {
682
+ background: var(--smrt-color-primary, #22c55e);
683
+ border-color: var(--smrt-color-primary, #22c55e);
684
+ color: var(--smrt-color-on-primary, #fff);
685
+ animation: pulse-btn 1.5s var(--smrt-easing-standard, ease-in-out) infinite;
686
+ }
687
+
688
+ .form-listen-btn.extracting {
689
+ background: var(--smrt-color-secondary, #f59e0b);
690
+ border-color: var(--smrt-color-secondary, #f59e0b);
691
+ color: var(--smrt-color-on-secondary, #fff);
692
+ }
693
+
694
+ .form-listen-btn:disabled {
695
+ cursor: not-allowed;
696
+ }
697
+
698
+ @keyframes pulse-btn {
699
+ 0%, 100% {
700
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--smrt-color-success) 40%, transparent);
701
+ }
702
+ 50% {
703
+ box-shadow: 0 0 0 8px transparent;
704
+ }
705
+ }
706
+
707
+ @media (prefers-reduced-motion: reduce) {
708
+ .form-listen-btn.listening {
709
+ animation: none;
710
+ }
711
+ }
712
+
713
+ .spinner {
714
+ animation: spin 1s linear infinite;
715
+ }
716
+
717
+ @keyframes spin {
718
+ to {
719
+ transform: rotate(360deg);
720
+ }
721
+ }
722
+
723
+ .spoken-toaster {
724
+ position: fixed;
725
+ bottom: 0;
726
+ left: 0;
727
+ right: 0;
728
+ padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-6, 24px);
729
+ /* 90%-opacity primary. The old `--smrt-color-primary-rgb` channel token
730
+ was never emitted (issue #1431); color-mix derives the alpha from the
731
+ emitted `--smrt-color-primary` token instead. */
732
+ background: color-mix(in srgb, var(--smrt-color-primary, #166534) 90%, transparent);
733
+ color: white;
734
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
735
+ box-shadow: 0 -2px 12px color-mix(in srgb, var(--smrt-color-shadow) 15%, transparent);
736
+ z-index: var(--smrt-z-index-toast, 1500);
737
+ text-align: center;
738
+ backdrop-filter: blur(8px);
739
+ animation: slideUp 0.2s ease-out;
740
+ }
741
+
742
+ .spoken-toaster strong {
743
+ color: var(--smrt-color-on-primary-container, #bbf7d0);
744
+ }
745
+
746
+ @keyframes slideUp {
747
+ from {
748
+ opacity: 0;
749
+ transform: translateY(100%);
750
+ }
751
+ to {
752
+ opacity: 1;
753
+ transform: translateY(0);
754
+ }
755
+ }
756
+
757
+ .extract-error {
758
+ padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-4, 16px);
759
+ background: var(--smrt-color-error-container);
760
+ border: 1px solid var(--smrt-color-error);
761
+ border-radius: var(--smrt-radius-md, 8px);
762
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
763
+ color: var(--smrt-color-error);
764
+ }
765
+
766
+ .form-fields {
767
+ display: flex;
768
+ flex-direction: column;
769
+ gap: var(--smrt-spacing-4, 16px);
770
+ }
771
+ </style>