@fraziym/axoniz 0.1.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 (512) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/agents/definition.d.ts +15 -0
  4. package/dist/agents/definition.d.ts.map +1 -0
  5. package/dist/agents/definition.js +41 -0
  6. package/dist/agents/definition.js.map +1 -0
  7. package/dist/agents/index.d.ts +12 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +11 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/registry.d.ts +17 -0
  12. package/dist/agents/registry.d.ts.map +1 -0
  13. package/dist/agents/registry.js +23 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/specialized.d.ts +31 -0
  16. package/dist/agents/specialized.d.ts.map +1 -0
  17. package/dist/agents/specialized.js +75 -0
  18. package/dist/agents/specialized.js.map +1 -0
  19. package/dist/awareness/index.d.ts +14 -0
  20. package/dist/awareness/index.d.ts.map +1 -0
  21. package/dist/awareness/index.js +22 -0
  22. package/dist/awareness/index.js.map +1 -0
  23. package/dist/awareness/service.d.ts +180 -0
  24. package/dist/awareness/service.d.ts.map +1 -0
  25. package/dist/awareness/service.js +789 -0
  26. package/dist/awareness/service.js.map +1 -0
  27. package/dist/cli/entry.d.ts +13 -0
  28. package/dist/cli/entry.d.ts.map +1 -0
  29. package/dist/cli/entry.js +35 -0
  30. package/dist/cli/entry.js.map +1 -0
  31. package/dist/comms/telegram.d.ts +41 -0
  32. package/dist/comms/telegram.d.ts.map +1 -0
  33. package/dist/comms/telegram.js +332 -0
  34. package/dist/comms/telegram.js.map +1 -0
  35. package/dist/core/agent.d.ts +252 -0
  36. package/dist/core/agent.d.ts.map +1 -0
  37. package/dist/core/agent.js +1399 -0
  38. package/dist/core/agent.js.map +1 -0
  39. package/dist/core/auth.d.ts +23 -0
  40. package/dist/core/auth.d.ts.map +1 -0
  41. package/dist/core/auth.js +113 -0
  42. package/dist/core/auth.js.map +1 -0
  43. package/dist/core/authority.d.ts +118 -0
  44. package/dist/core/authority.d.ts.map +1 -0
  45. package/dist/core/authority.js +402 -0
  46. package/dist/core/authority.js.map +1 -0
  47. package/dist/core/backend/async_queue.d.ts +17 -0
  48. package/dist/core/backend/async_queue.d.ts.map +1 -0
  49. package/dist/core/backend/async_queue.js +66 -0
  50. package/dist/core/backend/async_queue.js.map +1 -0
  51. package/dist/core/backend/index.d.ts +24 -0
  52. package/dist/core/backend/index.d.ts.map +1 -0
  53. package/dist/core/backend/index.js +128 -0
  54. package/dist/core/backend/index.js.map +1 -0
  55. package/dist/core/backend/llama_server.d.ts +39 -0
  56. package/dist/core/backend/llama_server.d.ts.map +1 -0
  57. package/dist/core/backend/llama_server.js +182 -0
  58. package/dist/core/backend/llama_server.js.map +1 -0
  59. package/dist/core/backend/llamacpp.d.ts +40 -0
  60. package/dist/core/backend/llamacpp.d.ts.map +1 -0
  61. package/dist/core/backend/llamacpp.js +200 -0
  62. package/dist/core/backend/llamacpp.js.map +1 -0
  63. package/dist/core/backend/lmstudio.d.ts +53 -0
  64. package/dist/core/backend/lmstudio.d.ts.map +1 -0
  65. package/dist/core/backend/lmstudio.js +274 -0
  66. package/dist/core/backend/lmstudio.js.map +1 -0
  67. package/dist/core/backend/ollama.d.ts +25 -0
  68. package/dist/core/backend/ollama.d.ts.map +1 -0
  69. package/dist/core/backend/ollama.js +86 -0
  70. package/dist/core/backend/ollama.js.map +1 -0
  71. package/dist/core/backend/openai.d.ts +27 -0
  72. package/dist/core/backend/openai.d.ts.map +1 -0
  73. package/dist/core/backend/openai.js +84 -0
  74. package/dist/core/backend/openai.js.map +1 -0
  75. package/dist/core/backend/types.d.ts +73 -0
  76. package/dist/core/backend/types.d.ts.map +1 -0
  77. package/dist/core/backend/types.js +137 -0
  78. package/dist/core/backend/types.js.map +1 -0
  79. package/dist/core/chat_store.d.ts +77 -0
  80. package/dist/core/chat_store.d.ts.map +1 -0
  81. package/dist/core/chat_store.js +332 -0
  82. package/dist/core/chat_store.js.map +1 -0
  83. package/dist/core/cli.d.ts +82 -0
  84. package/dist/core/cli.d.ts.map +1 -0
  85. package/dist/core/cli.js +602 -0
  86. package/dist/core/cli.js.map +1 -0
  87. package/dist/core/config.d.ts +127 -0
  88. package/dist/core/config.d.ts.map +1 -0
  89. package/dist/core/config.js +295 -0
  90. package/dist/core/config.js.map +1 -0
  91. package/dist/core/debug.d.ts +8 -0
  92. package/dist/core/debug.d.ts.map +1 -0
  93. package/dist/core/debug.js +8 -0
  94. package/dist/core/debug.js.map +1 -0
  95. package/dist/core/downloader.d.ts +31 -0
  96. package/dist/core/downloader.d.ts.map +1 -0
  97. package/dist/core/downloader.js +180 -0
  98. package/dist/core/downloader.js.map +1 -0
  99. package/dist/core/errors.d.ts +80 -0
  100. package/dist/core/errors.d.ts.map +1 -0
  101. package/dist/core/errors.js +134 -0
  102. package/dist/core/errors.js.map +1 -0
  103. package/dist/core/extras.d.ts +160 -0
  104. package/dist/core/extras.d.ts.map +1 -0
  105. package/dist/core/extras.js +553 -0
  106. package/dist/core/extras.js.map +1 -0
  107. package/dist/core/health.d.ts +27 -0
  108. package/dist/core/health.d.ts.map +1 -0
  109. package/dist/core/health.js +96 -0
  110. package/dist/core/health.js.map +1 -0
  111. package/dist/core/history.d.ts +50 -0
  112. package/dist/core/history.d.ts.map +1 -0
  113. package/dist/core/history.js +276 -0
  114. package/dist/core/history.js.map +1 -0
  115. package/dist/core/index.d.ts +6 -0
  116. package/dist/core/index.d.ts.map +1 -0
  117. package/dist/core/index.js +6 -0
  118. package/dist/core/index.js.map +1 -0
  119. package/dist/core/intelligence/ast_index.d.ts +255 -0
  120. package/dist/core/intelligence/ast_index.d.ts.map +1 -0
  121. package/dist/core/intelligence/ast_index.js +1369 -0
  122. package/dist/core/intelligence/ast_index.js.map +1 -0
  123. package/dist/core/intelligence/confidence.d.ts +113 -0
  124. package/dist/core/intelligence/confidence.d.ts.map +1 -0
  125. package/dist/core/intelligence/confidence.js +298 -0
  126. package/dist/core/intelligence/confidence.js.map +1 -0
  127. package/dist/core/intelligence/context_compressor.d.ts +80 -0
  128. package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
  129. package/dist/core/intelligence/context_compressor.js +245 -0
  130. package/dist/core/intelligence/context_compressor.js.map +1 -0
  131. package/dist/core/intelligence/daemon.d.ts +247 -0
  132. package/dist/core/intelligence/daemon.d.ts.map +1 -0
  133. package/dist/core/intelligence/daemon.js +842 -0
  134. package/dist/core/intelligence/daemon.js.map +1 -0
  135. package/dist/core/intelligence/index.d.ts +60 -0
  136. package/dist/core/intelligence/index.d.ts.map +1 -0
  137. package/dist/core/intelligence/index.js +101 -0
  138. package/dist/core/intelligence/index.js.map +1 -0
  139. package/dist/core/intelligence/predictor.d.ts +246 -0
  140. package/dist/core/intelligence/predictor.d.ts.map +1 -0
  141. package/dist/core/intelligence/predictor.js +590 -0
  142. package/dist/core/intelligence/predictor.js.map +1 -0
  143. package/dist/core/intelligence/reflex.d.ts +84 -0
  144. package/dist/core/intelligence/reflex.d.ts.map +1 -0
  145. package/dist/core/intelligence/reflex.js +344 -0
  146. package/dist/core/intelligence/reflex.js.map +1 -0
  147. package/dist/core/intelligence/self_correction.d.ts +121 -0
  148. package/dist/core/intelligence/self_correction.d.ts.map +1 -0
  149. package/dist/core/intelligence/self_correction.js +327 -0
  150. package/dist/core/intelligence/self_correction.js.map +1 -0
  151. package/dist/core/intelligence/skill_distiller.d.ts +100 -0
  152. package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
  153. package/dist/core/intelligence/skill_distiller.js +246 -0
  154. package/dist/core/intelligence/skill_distiller.js.map +1 -0
  155. package/dist/core/intelligence/swarm.d.ts +330 -0
  156. package/dist/core/intelligence/swarm.d.ts.map +1 -0
  157. package/dist/core/intelligence/swarm.js +1064 -0
  158. package/dist/core/intelligence/swarm.js.map +1 -0
  159. package/dist/core/intelligence/trajectory.d.ts +107 -0
  160. package/dist/core/intelligence/trajectory.d.ts.map +1 -0
  161. package/dist/core/intelligence/trajectory.js +330 -0
  162. package/dist/core/intelligence/trajectory.js.map +1 -0
  163. package/dist/core/logger.d.ts +111 -0
  164. package/dist/core/logger.d.ts.map +1 -0
  165. package/dist/core/logger.js +640 -0
  166. package/dist/core/logger.js.map +1 -0
  167. package/dist/core/loop.d.ts +123 -0
  168. package/dist/core/loop.d.ts.map +1 -0
  169. package/dist/core/loop.js +423 -0
  170. package/dist/core/loop.js.map +1 -0
  171. package/dist/core/memory.d.ts +81 -0
  172. package/dist/core/memory.d.ts.map +1 -0
  173. package/dist/core/memory.js +328 -0
  174. package/dist/core/memory.js.map +1 -0
  175. package/dist/core/metrics.d.ts +29 -0
  176. package/dist/core/metrics.d.ts.map +1 -0
  177. package/dist/core/metrics.js +109 -0
  178. package/dist/core/metrics.js.map +1 -0
  179. package/dist/core/models.d.ts +51 -0
  180. package/dist/core/models.d.ts.map +1 -0
  181. package/dist/core/models.js +146 -0
  182. package/dist/core/models.js.map +1 -0
  183. package/dist/core/optimizer.d.ts +34 -0
  184. package/dist/core/optimizer.d.ts.map +1 -0
  185. package/dist/core/optimizer.js +131 -0
  186. package/dist/core/optimizer.js.map +1 -0
  187. package/dist/core/persona.d.ts +35 -0
  188. package/dist/core/persona.d.ts.map +1 -0
  189. package/dist/core/persona.js +126 -0
  190. package/dist/core/persona.js.map +1 -0
  191. package/dist/core/rate_limit.d.ts +23 -0
  192. package/dist/core/rate_limit.d.ts.map +1 -0
  193. package/dist/core/rate_limit.js +62 -0
  194. package/dist/core/rate_limit.js.map +1 -0
  195. package/dist/core/runner.d.ts +63 -0
  196. package/dist/core/runner.d.ts.map +1 -0
  197. package/dist/core/runner.js +671 -0
  198. package/dist/core/runner.js.map +1 -0
  199. package/dist/core/stream_parser.d.ts +43 -0
  200. package/dist/core/stream_parser.d.ts.map +1 -0
  201. package/dist/core/stream_parser.js +140 -0
  202. package/dist/core/stream_parser.js.map +1 -0
  203. package/dist/core/tool_schemas.d.ts +26 -0
  204. package/dist/core/tool_schemas.d.ts.map +1 -0
  205. package/dist/core/tool_schemas.js +124 -0
  206. package/dist/core/tool_schemas.js.map +1 -0
  207. package/dist/core/which.d.ts +2 -0
  208. package/dist/core/which.d.ts.map +1 -0
  209. package/dist/core/which.js +35 -0
  210. package/dist/core/which.js.map +1 -0
  211. package/dist/goals/index.d.ts +9 -0
  212. package/dist/goals/index.d.ts.map +1 -0
  213. package/dist/goals/index.js +17 -0
  214. package/dist/goals/index.js.map +1 -0
  215. package/dist/goals/service.d.ts +115 -0
  216. package/dist/goals/service.d.ts.map +1 -0
  217. package/dist/goals/service.js +415 -0
  218. package/dist/goals/service.js.map +1 -0
  219. package/dist/goals/types.d.ts +47 -0
  220. package/dist/goals/types.d.ts.map +1 -0
  221. package/dist/goals/types.js +66 -0
  222. package/dist/goals/types.js.map +1 -0
  223. package/dist/index.d.ts +22 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +22 -0
  226. package/dist/index.js.map +1 -0
  227. package/dist/integrations/autonomous_startup.d.ts +51 -0
  228. package/dist/integrations/autonomous_startup.d.ts.map +1 -0
  229. package/dist/integrations/autonomous_startup.js +419 -0
  230. package/dist/integrations/autonomous_startup.js.map +1 -0
  231. package/dist/integrations/goose.d.ts +105 -0
  232. package/dist/integrations/goose.d.ts.map +1 -0
  233. package/dist/integrations/goose.js +408 -0
  234. package/dist/integrations/goose.js.map +1 -0
  235. package/dist/integrations/index.d.ts +21 -0
  236. package/dist/integrations/index.d.ts.map +1 -0
  237. package/dist/integrations/index.js +17 -0
  238. package/dist/integrations/index.js.map +1 -0
  239. package/dist/integrations/mcp.d.ts +73 -0
  240. package/dist/integrations/mcp.d.ts.map +1 -0
  241. package/dist/integrations/mcp.js +307 -0
  242. package/dist/integrations/mcp.js.map +1 -0
  243. package/dist/integrations/mempalace.d.ts +18 -0
  244. package/dist/integrations/mempalace.d.ts.map +1 -0
  245. package/dist/integrations/mempalace.js +25 -0
  246. package/dist/integrations/mempalace.js.map +1 -0
  247. package/dist/integrations/multi_agent.d.ts +82 -0
  248. package/dist/integrations/multi_agent.d.ts.map +1 -0
  249. package/dist/integrations/multi_agent.js +280 -0
  250. package/dist/integrations/multi_agent.js.map +1 -0
  251. package/dist/integrations/unified_memory.d.ts +415 -0
  252. package/dist/integrations/unified_memory.d.ts.map +1 -0
  253. package/dist/integrations/unified_memory.js +1489 -0
  254. package/dist/integrations/unified_memory.js.map +1 -0
  255. package/dist/roles/beru.yaml +48 -0
  256. package/dist/sidecar/client.d.ts +63 -0
  257. package/dist/sidecar/client.d.ts.map +1 -0
  258. package/dist/sidecar/client.js +222 -0
  259. package/dist/sidecar/client.js.map +1 -0
  260. package/dist/sidecar/index.d.ts +7 -0
  261. package/dist/sidecar/index.d.ts.map +1 -0
  262. package/dist/sidecar/index.js +6 -0
  263. package/dist/sidecar/index.js.map +1 -0
  264. package/dist/startup.d.ts +22 -0
  265. package/dist/startup.d.ts.map +1 -0
  266. package/dist/startup.js +194 -0
  267. package/dist/startup.js.map +1 -0
  268. package/dist/tools/_internal.d.ts +90 -0
  269. package/dist/tools/_internal.d.ts.map +1 -0
  270. package/dist/tools/_internal.js +375 -0
  271. package/dist/tools/_internal.js.map +1 -0
  272. package/dist/tools/_smoke.d.ts +2 -0
  273. package/dist/tools/_smoke.d.ts.map +1 -0
  274. package/dist/tools/_smoke.js +203 -0
  275. package/dist/tools/_smoke.js.map +1 -0
  276. package/dist/tools/axodex_tools.d.ts +51 -0
  277. package/dist/tools/axodex_tools.d.ts.map +1 -0
  278. package/dist/tools/axodex_tools.js +181 -0
  279. package/dist/tools/axodex_tools.js.map +1 -0
  280. package/dist/tools/code_tools.d.ts +33 -0
  281. package/dist/tools/code_tools.d.ts.map +1 -0
  282. package/dist/tools/code_tools.js +191 -0
  283. package/dist/tools/code_tools.js.map +1 -0
  284. package/dist/tools/computer_tools.d.ts +59 -0
  285. package/dist/tools/computer_tools.d.ts.map +1 -0
  286. package/dist/tools/computer_tools.js +348 -0
  287. package/dist/tools/computer_tools.js.map +1 -0
  288. package/dist/tools/file_tools.d.ts +51 -0
  289. package/dist/tools/file_tools.d.ts.map +1 -0
  290. package/dist/tools/file_tools.js +424 -0
  291. package/dist/tools/file_tools.js.map +1 -0
  292. package/dist/tools/index.d.ts +13 -0
  293. package/dist/tools/index.d.ts.map +1 -0
  294. package/dist/tools/index.js +13 -0
  295. package/dist/tools/index.js.map +1 -0
  296. package/dist/tools/shell_tools.d.ts +39 -0
  297. package/dist/tools/shell_tools.d.ts.map +1 -0
  298. package/dist/tools/shell_tools.js +163 -0
  299. package/dist/tools/shell_tools.js.map +1 -0
  300. package/dist/tools/web_tools.d.ts +31 -0
  301. package/dist/tools/web_tools.d.ts.map +1 -0
  302. package/dist/tools/web_tools.js +117 -0
  303. package/dist/tools/web_tools.js.map +1 -0
  304. package/dist/voice/index.d.ts +41 -0
  305. package/dist/voice/index.d.ts.map +1 -0
  306. package/dist/voice/index.js +58 -0
  307. package/dist/voice/index.js.map +1 -0
  308. package/dist/voice/light_daemon.d.ts +197 -0
  309. package/dist/voice/light_daemon.d.ts.map +1 -0
  310. package/dist/voice/light_daemon.js +746 -0
  311. package/dist/voice/light_daemon.js.map +1 -0
  312. package/dist/voice/setup.d.ts +63 -0
  313. package/dist/voice/setup.d.ts.map +1 -0
  314. package/dist/voice/setup.js +443 -0
  315. package/dist/voice/setup.js.map +1 -0
  316. package/dist/voice/stt.d.ts +148 -0
  317. package/dist/voice/stt.d.ts.map +1 -0
  318. package/dist/voice/stt.js +361 -0
  319. package/dist/voice/stt.js.map +1 -0
  320. package/dist/voice/tts.d.ts +168 -0
  321. package/dist/voice/tts.d.ts.map +1 -0
  322. package/dist/voice/tts.js +698 -0
  323. package/dist/voice/tts.js.map +1 -0
  324. package/dist/voice/voice_loop.d.ts +101 -0
  325. package/dist/voice/voice_loop.d.ts.map +1 -0
  326. package/dist/voice/voice_loop.js +463 -0
  327. package/dist/voice/voice_loop.js.map +1 -0
  328. package/dist/voice/wake_word.d.ts +153 -0
  329. package/dist/voice/wake_word.d.ts.map +1 -0
  330. package/dist/voice/wake_word.js +458 -0
  331. package/dist/voice/wake_word.js.map +1 -0
  332. package/dist/voice/wav.d.ts +99 -0
  333. package/dist/voice/wav.d.ts.map +1 -0
  334. package/dist/voice/wav.js +306 -0
  335. package/dist/voice/wav.js.map +1 -0
  336. package/dist/web/broker.d.ts +33 -0
  337. package/dist/web/broker.d.ts.map +1 -0
  338. package/dist/web/broker.js +58 -0
  339. package/dist/web/broker.js.map +1 -0
  340. package/dist/web/open_browser.d.ts +2 -0
  341. package/dist/web/open_browser.d.ts.map +1 -0
  342. package/dist/web/open_browser.js +42 -0
  343. package/dist/web/open_browser.js.map +1 -0
  344. package/dist/web/server.d.ts +54 -0
  345. package/dist/web/server.d.ts.map +1 -0
  346. package/dist/web/server.js +831 -0
  347. package/dist/web/server.js.map +1 -0
  348. package/dist/web/static/app.js +4121 -0
  349. package/dist/web/static/index.html +2281 -0
  350. package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
  351. package/dist/web/static/js/a11y.js +165 -0
  352. package/dist/web/static/js/admin.js +2145 -0
  353. package/dist/web/static/js/assistant.js +475 -0
  354. package/dist/web/static/js/calendar/reminders.js +114 -0
  355. package/dist/web/static/js/calendar/utils.js +166 -0
  356. package/dist/web/static/js/calendar.js +3348 -0
  357. package/dist/web/static/js/censor.js +356 -0
  358. package/dist/web/static/js/chat.js +4546 -0
  359. package/dist/web/static/js/chatRenderer.js +2294 -0
  360. package/dist/web/static/js/chatStream.js +276 -0
  361. package/dist/web/static/js/codeRunner.js +398 -0
  362. package/dist/web/static/js/color/hex.js +14 -0
  363. package/dist/web/static/js/colorPicker.js +453 -0
  364. package/dist/web/static/js/compare/icons.js +77 -0
  365. package/dist/web/static/js/compare/index.js +1482 -0
  366. package/dist/web/static/js/compare/models.js +104 -0
  367. package/dist/web/static/js/compare/panes.js +817 -0
  368. package/dist/web/static/js/compare/probe.js +78 -0
  369. package/dist/web/static/js/compare/scoreboard.js +223 -0
  370. package/dist/web/static/js/compare/selector.js +1335 -0
  371. package/dist/web/static/js/compare/state.js +58 -0
  372. package/dist/web/static/js/compare/stream.js +695 -0
  373. package/dist/web/static/js/compare/vote.js +254 -0
  374. package/dist/web/static/js/cookbook-diagnosis.js +694 -0
  375. package/dist/web/static/js/cookbook-hwfit.js +1888 -0
  376. package/dist/web/static/js/cookbook.js +2093 -0
  377. package/dist/web/static/js/cookbookDownload.js +633 -0
  378. package/dist/web/static/js/cookbookProgressSignal.js +29 -0
  379. package/dist/web/static/js/cookbookRunning.js +3506 -0
  380. package/dist/web/static/js/cookbookServe.js +2128 -0
  381. package/dist/web/static/js/document.js +9731 -0
  382. package/dist/web/static/js/documentLibrary.js +3376 -0
  383. package/dist/web/static/js/dragSort.js +265 -0
  384. package/dist/web/static/js/editor/ai-inpaint.js +374 -0
  385. package/dist/web/static/js/editor/ai-models.js +273 -0
  386. package/dist/web/static/js/editor/ai-rembg.js +230 -0
  387. package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
  388. package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
  389. package/dist/web/static/js/editor/build/controls.js +366 -0
  390. package/dist/web/static/js/editor/build/popups.js +112 -0
  391. package/dist/web/static/js/editor/build/right-panel.js +200 -0
  392. package/dist/web/static/js/editor/build/toolbar.js +73 -0
  393. package/dist/web/static/js/editor/build/topbar.js +131 -0
  394. package/dist/web/static/js/editor/build/transform-popup.js +109 -0
  395. package/dist/web/static/js/editor/canvas-coords.js +21 -0
  396. package/dist/web/static/js/editor/canvas-events.js +197 -0
  397. package/dist/web/static/js/editor/canvas-transforms.js +132 -0
  398. package/dist/web/static/js/editor/checkerboard.js +24 -0
  399. package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
  400. package/dist/web/static/js/editor/composite-helpers.js +83 -0
  401. package/dist/web/static/js/editor/filters/blur.js +118 -0
  402. package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
  403. package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
  404. package/dist/web/static/js/editor/fx/filter-string.js +38 -0
  405. package/dist/web/static/js/editor/fx/histogram.js +67 -0
  406. package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
  407. package/dist/web/static/js/editor/harmonize-masks.js +116 -0
  408. package/dist/web/static/js/editor/history-panel.js +176 -0
  409. package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
  410. package/dist/web/static/js/editor/layer-helpers.js +131 -0
  411. package/dist/web/static/js/editor/layer-panel.js +600 -0
  412. package/dist/web/static/js/editor/mask-utils.js +91 -0
  413. package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
  414. package/dist/web/static/js/editor/slider-ux.js +191 -0
  415. package/dist/web/static/js/editor/snap.js +107 -0
  416. package/dist/web/static/js/editor/state.js +257 -0
  417. package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
  418. package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
  419. package/dist/web/static/js/editor/tools/clone.js +81 -0
  420. package/dist/web/static/js/editor/tools/crop.js +137 -0
  421. package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
  422. package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
  423. package/dist/web/static/js/editor/tools/lasso.js +65 -0
  424. package/dist/web/static/js/editor/tools/move.js +79 -0
  425. package/dist/web/static/js/editor/tools/stroke.js +123 -0
  426. package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
  427. package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
  428. package/dist/web/static/js/editor/tools/transform-session.js +381 -0
  429. package/dist/web/static/js/editor/tools/wand.js +46 -0
  430. package/dist/web/static/js/editor/wire-import.js +148 -0
  431. package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
  432. package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
  433. package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
  434. package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
  435. package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
  436. package/dist/web/static/js/editor/wire-topbar.js +188 -0
  437. package/dist/web/static/js/emailInbox.js +1245 -0
  438. package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
  439. package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
  440. package/dist/web/static/js/emailLibrary/state.js +34 -0
  441. package/dist/web/static/js/emailLibrary/utils.js +202 -0
  442. package/dist/web/static/js/emailLibrary.js +5217 -0
  443. package/dist/web/static/js/emojiPicker.js +311 -0
  444. package/dist/web/static/js/escMenuStack.js +102 -0
  445. package/dist/web/static/js/fileHandler.js +295 -0
  446. package/dist/web/static/js/gallery.js +2835 -0
  447. package/dist/web/static/js/galleryEditor.js +3798 -0
  448. package/dist/web/static/js/group.js +955 -0
  449. package/dist/web/static/js/init.js +405 -0
  450. package/dist/web/static/js/keyboard-shortcuts.js +292 -0
  451. package/dist/web/static/js/langIcons.js +187 -0
  452. package/dist/web/static/js/markdown/tableRow.js +19 -0
  453. package/dist/web/static/js/markdown.js +806 -0
  454. package/dist/web/static/js/memory.js +1418 -0
  455. package/dist/web/static/js/modalManager.js +1552 -0
  456. package/dist/web/static/js/modalSnap.js +782 -0
  457. package/dist/web/static/js/model/matchKey.js +19 -0
  458. package/dist/web/static/js/modelPicker.js +695 -0
  459. package/dist/web/static/js/modelSort.js +33 -0
  460. package/dist/web/static/js/models.js +642 -0
  461. package/dist/web/static/js/notes.js +5054 -0
  462. package/dist/web/static/js/package.json +1 -0
  463. package/dist/web/static/js/platform.js +47 -0
  464. package/dist/web/static/js/presets.js +1110 -0
  465. package/dist/web/static/js/providers.js +93 -0
  466. package/dist/web/static/js/rag.js +177 -0
  467. package/dist/web/static/js/research/jobs.js +335 -0
  468. package/dist/web/static/js/research/panel.js +1233 -0
  469. package/dist/web/static/js/researchSynapse.js +225 -0
  470. package/dist/web/static/js/search-chat.js +201 -0
  471. package/dist/web/static/js/search.js +52 -0
  472. package/dist/web/static/js/section-management.js +260 -0
  473. package/dist/web/static/js/sessions.js +3108 -0
  474. package/dist/web/static/js/settings.js +4991 -0
  475. package/dist/web/static/js/sidebar-layout.js +573 -0
  476. package/dist/web/static/js/signature.js +504 -0
  477. package/dist/web/static/js/skills.js +1908 -0
  478. package/dist/web/static/js/slashAutocomplete.js +267 -0
  479. package/dist/web/static/js/slashCommands.js +6099 -0
  480. package/dist/web/static/js/spinner.js +406 -0
  481. package/dist/web/static/js/storage.js +123 -0
  482. package/dist/web/static/js/tasks.js +2699 -0
  483. package/dist/web/static/js/theme.js +2086 -0
  484. package/dist/web/static/js/tileManager.js +386 -0
  485. package/dist/web/static/js/tourAutoplay.js +133 -0
  486. package/dist/web/static/js/tourHints.js +179 -0
  487. package/dist/web/static/js/tts-ai.js +521 -0
  488. package/dist/web/static/js/ui.js +1288 -0
  489. package/dist/web/static/js/util/ordinal.js +13 -0
  490. package/dist/web/static/js/voiceRecorder.js +283 -0
  491. package/dist/web/static/js/windowDrag.js +326 -0
  492. package/dist/web/static/js/windowResize.js +233 -0
  493. package/dist/web/static/landing.html +824 -0
  494. package/dist/web/static/lib/docx.umd.min.js +20116 -0
  495. package/dist/web/static/lib/highlight.min.js +1213 -0
  496. package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
  497. package/dist/web/static/lib/mammoth.browser.min.js +21 -0
  498. package/dist/web/static/lib/qrcode.min.js +8 -0
  499. package/dist/web/static/lib/xlsx.full.min.js +24 -0
  500. package/dist/web/static/login.html +582 -0
  501. package/dist/web/static/manifest.json +15 -0
  502. package/dist/web/static/style.css +35733 -0
  503. package/dist/web/static/sw.js +144 -0
  504. package/dist/workflows/engine.d.ts +162 -0
  505. package/dist/workflows/engine.d.ts.map +1 -0
  506. package/dist/workflows/engine.js +625 -0
  507. package/dist/workflows/engine.js.map +1 -0
  508. package/dist/workflows/index.d.ts +8 -0
  509. package/dist/workflows/index.d.ts.map +1 -0
  510. package/dist/workflows/index.js +14 -0
  511. package/dist/workflows/index.js.map +1 -0
  512. package/package.json +112 -0
@@ -0,0 +1,1288 @@
1
+ // static/js/ui.js
2
+
3
+ /**
4
+ * UI utilities for toasts, modals, scrolling, and user feedback
5
+ */
6
+
7
+ import themeModule from './theme.js';
8
+ import * as Modals from './modalManager.js';
9
+ import spinnerModule from './spinner.js';
10
+ import { registerMenuDismiss, dismissTopMenu, dismissOrRemove } from './escMenuStack.js';
11
+
12
+ let toastEl = null;
13
+ let autoScrollEnabled = true;
14
+ let hoveredToggleCard = null;
15
+ let hoveredToggleWindow = null;
16
+ let hoveredDockChip = null;
17
+ let _lastPointerClientX = null;
18
+ let _lastPointerClientY = null;
19
+
20
+ // Smooth scroll state
21
+ let _scrollRafId = null;
22
+ let _scrollBox = null;
23
+
24
+ function _isTextEditingTarget(target) {
25
+ const el = target && target.nodeType === 1 ? target : target?.parentElement;
26
+ return !!(el && el.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
27
+ }
28
+
29
+ function _targetEl(target) {
30
+ return target && target.nodeType === 1 ? target : target?.parentElement || null;
31
+ }
32
+
33
+ const SPACE_CARD_SELECTOR = [
34
+ '#email-lib-modal .doclib-card',
35
+ '#doclib-modal .doclib-card',
36
+ '#doclib-modal .doclib-chat-row',
37
+ '#memory-modal .doclib-card',
38
+ '#tasks-modal .task-card',
39
+ '#tasks-modal .task-log-row',
40
+ '#research-overlay [data-job-id]',
41
+ '#cookbook-modal .doclib-card',
42
+ '.email-reader-tab-modal .doclib-card',
43
+ '.email-window-modal .doclib-card',
44
+ ].join(', ');
45
+
46
+ const SPACE_BLOCKED_SELECTOR = [
47
+ 'button',
48
+ 'a',
49
+ 'input',
50
+ 'textarea',
51
+ 'select',
52
+ '[contenteditable="true"]',
53
+ '[contenteditable=""]',
54
+ '.recipient-chip',
55
+ '.doclib-card-dropdown',
56
+ '.email-card-dropdown',
57
+ '.task-log-row-actions',
58
+ '.modal-header',
59
+ ].join(', ');
60
+
61
+ function _visibleModalForSpace(win) {
62
+ const modal = win?.closest?.('.modal[id]');
63
+ if (!modal || modal.classList.contains('hidden') || modal.classList.contains('modal-minimized')) return null;
64
+ return modal;
65
+ }
66
+
67
+ function _isSpaceVisible(el) {
68
+ if (!el || !document.contains(el)) return false;
69
+ if (el.closest?.('.modal.hidden, .modal.modal-minimized, [hidden]')) return false;
70
+ return true;
71
+ }
72
+
73
+ function _spaceWindowId(win) {
74
+ if (!win || !document.contains(win)) return null;
75
+ const modal = _visibleModalForSpace(win);
76
+ if (modal && Modals.isRegistered(modal.id)) return modal.id;
77
+ if (win.closest?.('.doc-editor-pane') && Modals.isRegistered('doc-panel') && !Modals.isMinimized('doc-panel')) return 'doc-panel';
78
+ return null;
79
+ }
80
+
81
+ function _windowAtPointer() {
82
+ if (_lastPointerClientX == null || _lastPointerClientY == null) return null;
83
+ const x = _lastPointerClientX;
84
+ const y = _lastPointerClientY;
85
+ const candidates = [
86
+ ...document.querySelectorAll('.modal:not(.hidden):not(.modal-minimized) .modal-content'),
87
+ ...document.querySelectorAll('.doc-editor-pane'),
88
+ ].filter(el => {
89
+ if (!document.contains(el)) return false;
90
+ const r = el.getBoundingClientRect();
91
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
92
+ });
93
+ if (!candidates.length) return null;
94
+ return candidates.reduce((top, el) => {
95
+ const mz = parseInt(getComputedStyle(el.closest('.modal') || el).zIndex, 10) || 0;
96
+ const tz = parseInt(getComputedStyle(top.closest('.modal') || top).zIndex, 10) || 0;
97
+ return mz >= tz ? el : top;
98
+ });
99
+ }
100
+
101
+ function _containsPointer(el) {
102
+ if (!el || _lastPointerClientX == null || _lastPointerClientY == null) return false;
103
+ const r = el.getBoundingClientRect();
104
+ return _lastPointerClientX >= r.left && _lastPointerClientX <= r.right
105
+ && _lastPointerClientY >= r.top && _lastPointerClientY <= r.bottom;
106
+ }
107
+
108
+ function _closeHoveredWindow() {
109
+ let win = _windowAtPointer();
110
+ if (!win) {
111
+ try {
112
+ const underPointer = document.elementFromPoint(_lastPointerClientX, _lastPointerClientY);
113
+ win = underPointer?.closest?.('.modal:not(.hidden):not(.modal-minimized) .modal-content, .doc-editor-pane') || null;
114
+ } catch {}
115
+ }
116
+ if (!win) win = hoveredToggleWindow;
117
+ if (!win || !document.contains(win)) return false;
118
+ const modalForWin = win.closest?.('.modal[id]');
119
+ if (modalForWin?.id === 'email-lib-modal') {
120
+ const closeBtn = document.getElementById('email-lib-close') || modalForWin.querySelector('.close-btn');
121
+ if (closeBtn) {
122
+ try { closeBtn.click(); return true; } catch {}
123
+ }
124
+ try { modalForWin.remove(); return true; } catch {}
125
+ }
126
+ const id = _spaceWindowId(win);
127
+ if (id && Modals.isRegistered(id)) {
128
+ Modals.close(id);
129
+ return true;
130
+ }
131
+ const modal = _visibleModalForSpace(win);
132
+ if (!modal) return false;
133
+ const closeBtn = modal.querySelector('.close-btn, .modal-close, .modal-close-btn, [data-action="close"]');
134
+ if (closeBtn) {
135
+ try { closeBtn.click(); return true; } catch {}
136
+ }
137
+ try { modal.classList.add('hidden'); return true; } catch {}
138
+ return false;
139
+ }
140
+
141
+ function _spaceIsBlocked(e, surface) {
142
+ const target = _targetEl(e.target);
143
+ if (!target) return false;
144
+ if (_isTextEditingTarget(target)) return !surface || surface.contains(target);
145
+ const blocked = target.closest?.(SPACE_BLOCKED_SELECTOR);
146
+ return !!(blocked && (!surface || surface.contains(blocked)));
147
+ }
148
+
149
+ function _activateSpaceCard(card) {
150
+ if (!card || !document.contains(card)) return false;
151
+ if (card.matches('#tasks-modal .task-card')) {
152
+ const titleRow = card.querySelector('.memory-item-title')?.closest('div');
153
+ if (titleRow) {
154
+ titleRow.click();
155
+ return true;
156
+ }
157
+ }
158
+ card.dataset.spaceToggle = '1';
159
+ card.click();
160
+ setTimeout(() => {
161
+ try { delete card.dataset.spaceToggle; } catch {}
162
+ }, 0);
163
+ return true;
164
+ }
165
+
166
+ function _initHoverCardSpaceToggle() {
167
+ if (document._zeroHoverCardSpaceToggle) return;
168
+ document._zeroHoverCardSpaceToggle = true;
169
+ document.addEventListener('pointerover', (e) => {
170
+ _lastPointerClientX = e.clientX;
171
+ _lastPointerClientY = e.clientY;
172
+ const chip = e.target?.closest?.('.minimized-dock-chip[data-modal-id]');
173
+ if (chip) hoveredDockChip = chip;
174
+ const card = e.target?.closest?.(SPACE_CARD_SELECTOR);
175
+ if (card) hoveredToggleCard = card;
176
+ const win = e.target?.closest?.('.modal:not(.hidden):not(.modal-minimized) .modal-content, .doc-editor-pane');
177
+ if (win) hoveredToggleWindow = win;
178
+ }, true);
179
+ document.addEventListener('pointermove', (e) => {
180
+ _lastPointerClientX = e.clientX;
181
+ _lastPointerClientY = e.clientY;
182
+ }, true);
183
+ document.addEventListener('pointerout', (e) => {
184
+ const next = e.relatedTarget;
185
+ if (hoveredDockChip && (!next || !hoveredDockChip.contains(next))) hoveredDockChip = null;
186
+ if (hoveredToggleCard && (!next || !hoveredToggleCard.contains(next))) hoveredToggleCard = null;
187
+ if (hoveredToggleWindow && (!next || !hoveredToggleWindow.contains(next))) hoveredToggleWindow = null;
188
+ }, true);
189
+ document.addEventListener('keydown', (e) => {
190
+ if (e.code !== 'Space' || e.repeat) return;
191
+ if (hoveredToggleCard && _isSpaceVisible(hoveredToggleCard)) {
192
+ if (_spaceIsBlocked(e, hoveredToggleCard)) return;
193
+ e.preventDefault();
194
+ _activateSpaceCard(hoveredToggleCard);
195
+ return;
196
+ }
197
+ if (hoveredDockChip && document.contains(hoveredDockChip)) {
198
+ if (_spaceIsBlocked(e, hoveredDockChip)) return;
199
+ const id = hoveredDockChip.dataset.modalId;
200
+ if (id && Modals.isRegistered(id)) {
201
+ e.preventDefault();
202
+ Modals.restore(id);
203
+ }
204
+ return;
205
+ }
206
+ const id = _spaceWindowId(hoveredToggleWindow);
207
+ if (!id) return;
208
+ if (_spaceIsBlocked(e, hoveredToggleWindow)) return;
209
+ e.preventDefault();
210
+ Modals.minimize(id);
211
+ }, true);
212
+ }
213
+
214
+ _initHoverCardSpaceToggle();
215
+
216
+ /**
217
+ * Copy text to clipboard
218
+ */
219
+ export async function copyToClipboard(text) {
220
+ try {
221
+ await navigator.clipboard.writeText(text);
222
+ showToast('Copied');
223
+ }
224
+ catch {
225
+ const ta = document.createElement('textarea');
226
+ ta.value = text;
227
+ ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0';
228
+ document.body.appendChild(ta);
229
+ ta.select();
230
+ document.execCommand('copy');
231
+ document.body.removeChild(ta);
232
+ showToast('Copied');
233
+ }
234
+ }
235
+
236
+ // Wire swipe-to-dismiss on the shared toast element. Runs once, the first
237
+ // time a toast is shown. Tracks horizontal touch drag; if the user drags
238
+ // more than DISMISS_PX, the toast slides off in the drag direction and
239
+ // hides early. Anything less snaps back. Desktop unaffected (touch
240
+ // listeners only fire from a touchscreen — mouse is handled by the
241
+ // existing × button and auto-hide timer).
242
+ function _wireToastSwipe(el) {
243
+ if (!el || el._swipeWired) return;
244
+ el._swipeWired = true;
245
+ const DISMISS_PX = 70;
246
+ let startX = 0, currentX = 0, swiping = false;
247
+ el.addEventListener('touchstart', (e) => {
248
+ if (!el.classList.contains('show')) return;
249
+ const t = e.touches[0];
250
+ if (!t) return;
251
+ startX = t.clientX;
252
+ currentX = t.clientX;
253
+ swiping = true;
254
+ // Kill the slide-in transition mid-flight so the touch tracks the
255
+ // finger 1:1 instead of fighting a still-running animation.
256
+ el.style.transition = 'none';
257
+ }, { passive: true });
258
+ el.addEventListener('touchmove', (e) => {
259
+ if (!swiping) return;
260
+ const t = e.touches[0];
261
+ if (!t) return;
262
+ currentX = t.clientX;
263
+ const dx = currentX - startX;
264
+ el.style.transform = `translateX(${dx}px)`;
265
+ // Fade as the toast leaves the rest position — visual cue for
266
+ // approaching the dismiss threshold.
267
+ el.style.opacity = String(Math.max(0.2, 1 - Math.abs(dx) / 200));
268
+ }, { passive: true });
269
+ const endSwipe = () => {
270
+ if (!swiping) return;
271
+ swiping = false;
272
+ const dx = currentX - startX;
273
+ // Restore the transition so the next mutation animates.
274
+ el.style.transition = '';
275
+ if (Math.abs(dx) > DISMISS_PX) {
276
+ // Fling off in the drag direction, then hide.
277
+ el.style.transform = `translateX(${dx > 0 ? '120%' : '-120%'})`;
278
+ el.style.opacity = '0';
279
+ clearTimeout(el._hideTimer);
280
+ setTimeout(() => {
281
+ el.classList.remove('show');
282
+ el.classList.add('exiting');
283
+ el.style.transform = '';
284
+ el.style.opacity = '';
285
+ }, 180);
286
+ } else {
287
+ // Snap back to rest.
288
+ el.style.transform = '';
289
+ el.style.opacity = '';
290
+ }
291
+ };
292
+ el.addEventListener('touchend', endSwipe);
293
+ el.addEventListener('touchcancel', endSwipe);
294
+ }
295
+
296
+ /**
297
+ * Show success toast message
298
+ */
299
+ export function showToast(msg, durationOrOpts) {
300
+ if (!toastEl) {
301
+ toastEl = document.getElementById('toast');
302
+ }
303
+ _wireToastSwipe(toastEl);
304
+ toastEl.textContent = '';
305
+ toastEl.classList.remove('error');
306
+
307
+ let duration = 1200, actionLabel = null, onAction = null, actionHint = null, actionIcon = null, leadingIcon = null;
308
+ if (typeof durationOrOpts === 'object' && durationOrOpts) {
309
+ duration = durationOrOpts.duration || 5000;
310
+ actionLabel = durationOrOpts.action;
311
+ onAction = durationOrOpts.onAction;
312
+ actionHint = durationOrOpts.actionHint || null;
313
+ actionIcon = durationOrOpts.actionIcon || null;
314
+ leadingIcon = durationOrOpts.leadingIcon || null;
315
+ } else if (typeof durationOrOpts === 'number') {
316
+ duration = durationOrOpts;
317
+ }
318
+
319
+ const textSpan = document.createElement('span');
320
+ if (leadingIcon === 'check') {
321
+ const icon = document.createElement('span');
322
+ icon.className = 'toast-checkmark';
323
+ icon.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>';
324
+ toastEl.appendChild(icon);
325
+ } else if (leadingIcon === 'spinner') {
326
+ const wp = spinnerModule.createWhirlpool(14);
327
+ const icon = wp.element;
328
+ icon.classList.add('toast-whirlpool');
329
+ icon.style.cssText = 'width:14px;height:14px;margin:0 8px 0 0;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;';
330
+ toastEl.appendChild(icon);
331
+ }
332
+ textSpan.textContent = msg;
333
+ toastEl.appendChild(textSpan);
334
+
335
+ if (actionLabel && onAction) {
336
+ // Wrap the action in a small column so we can stack a Ctrl-Z-style hint
337
+ // directly under the button.
338
+ const stack = document.createElement('span');
339
+ stack.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:1px;margin-left:10px;line-height:1;';
340
+
341
+ const btn = document.createElement('button');
342
+ // If the caller supplied an SVG icon, prepend it. We trust the icon string
343
+ // (only set internally) — never accept caller-controlled HTML otherwise.
344
+ if (actionIcon) {
345
+ btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:5px;">${actionIcon}<span></span></span>`;
346
+ btn.querySelector('span span').textContent = actionLabel;
347
+ } else {
348
+ btn.textContent = actionLabel;
349
+ }
350
+ // The toast itself is `pointer-events: none` so it doesn't block clicks
351
+ // beneath it. With an action button we need to flip both the toast AND
352
+ // the button so the user can actually click Undo. The flag is reset on
353
+ // the next plain showToast / showError call (those overwrite textContent
354
+ // which strips the button + we clear inline style at the top below).
355
+ btn.style.cssText = 'padding:2px 10px;border:1px solid var(--fg);border-radius:4px;background:none;color:var(--fg);cursor:pointer;font-size:12px;pointer-events:auto;display:inline-flex;align-items:center;';
356
+ btn.addEventListener('click', (e) => {
357
+ e.stopPropagation();
358
+ e.preventDefault();
359
+ toastEl.classList.remove('show');
360
+ onAction();
361
+ });
362
+ stack.appendChild(btn);
363
+
364
+ // Keyboard-shortcut hints (Ctrl+Z / ⌘Z) are meaningless on touch devices —
365
+ // skip them on mobile so the toast just shows the Undo button.
366
+ if (actionHint && window.innerWidth > 768) {
367
+ const hint = document.createElement('span');
368
+ hint.textContent = actionHint;
369
+ hint.style.cssText = 'font-size:9px;opacity:0.55;letter-spacing:0.4px;text-transform:uppercase;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin-top:1px;pointer-events:none;';
370
+ stack.appendChild(hint);
371
+ }
372
+
373
+ toastEl.appendChild(stack);
374
+
375
+ // Small × to dismiss the toast without taking the action. Useful when
376
+ // the user already acted (or just doesn't want the banner sitting there).
377
+ const closeBtn = document.createElement('button');
378
+ closeBtn.type = 'button';
379
+ closeBtn.setAttribute('aria-label', 'Dismiss');
380
+ closeBtn.title = 'Dismiss';
381
+ closeBtn.textContent = '×';
382
+ closeBtn.style.cssText = 'margin-left:8px;padding:0;width:20px;height:20px;line-height:1;border:none;background:none;color:var(--fg);opacity:0.55;cursor:pointer;font-size:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;pointer-events:auto;';
383
+ closeBtn.addEventListener('mouseenter', () => { closeBtn.style.opacity = '1'; });
384
+ closeBtn.addEventListener('mouseleave', () => { closeBtn.style.opacity = '0.55'; });
385
+ closeBtn.addEventListener('click', (e) => {
386
+ e.stopPropagation();
387
+ e.preventDefault();
388
+ clearTimeout(toastEl._hideTimer);
389
+ toastEl.classList.add('exiting');
390
+ toastEl.classList.remove('show');
391
+ });
392
+ toastEl.appendChild(closeBtn);
393
+
394
+ toastEl.style.pointerEvents = 'auto';
395
+ } else {
396
+ // No action — restore the default non-blocking behavior.
397
+ toastEl.style.pointerEvents = '';
398
+ }
399
+
400
+ // Pin to top-right via CSS — clear any legacy inline overrides so the
401
+ // slide-in-from-right / slide-out-to-left transition can run cleanly.
402
+ toastEl.style.left = '';
403
+ toastEl.style.transform = '';
404
+ toastEl.classList.remove('exiting');
405
+ toastEl.classList.add('show');
406
+ clearTimeout(toastEl._hideTimer);
407
+ toastEl._hideTimer = setTimeout(() => {
408
+ // Add `exiting` so the CSS rule slides it off to the LEFT instead of
409
+ // back to the right (where it came from). We piggyback on the same
410
+ // .toast base; .exiting overrides the resting transform.
411
+ toastEl.classList.add('exiting');
412
+ toastEl.classList.remove('show');
413
+ // Reset pointer-events so an action-toast (which sets it to 'auto'
414
+ // for its clickable button) doesn't leave the toast intercepting
415
+ // clicks after it's slid away. Was previously only cleared on the
416
+ // NEXT plain toast, so a lingering action-toast could appear to
417
+ // "lock" interaction near the top-right.
418
+ toastEl.style.pointerEvents = '';
419
+ }, duration);
420
+ }
421
+
422
+ /**
423
+ * Show error toast message
424
+ */
425
+ export function showError(msg) {
426
+ if (!toastEl) {
427
+ toastEl = document.getElementById('toast');
428
+ }
429
+ _wireToastSwipe(toastEl);
430
+ toastEl.textContent = msg;
431
+ toastEl.classList.add('error');
432
+ toastEl.style.left = '';
433
+ toastEl.style.transform = '';
434
+ toastEl.classList.remove('exiting');
435
+ toastEl.classList.add('show');
436
+ clearTimeout(toastEl._hideTimer);
437
+ toastEl._hideTimer = setTimeout(() => {
438
+ toastEl.classList.add('exiting');
439
+ toastEl.classList.remove('show');
440
+ }, 3000);
441
+ }
442
+
443
+ /**
444
+ * Smooth-scroll chat history to bottom using rAF lerp.
445
+ * Throttled during streaming so it doesn't fight user scrolling.
446
+ */
447
+ let _scrollThrottleTimer = null;
448
+ export function scrollHistory() {
449
+ if (!autoScrollEnabled) return;
450
+ if (!_scrollBox) {
451
+ _scrollBox = document.getElementById('chat-history');
452
+ }
453
+ // Throttle: only start a new scroll animation every 500ms
454
+ if (_scrollThrottleTimer) return;
455
+ _scrollThrottleTimer = setTimeout(() => { _scrollThrottleTimer = null; }, 500);
456
+ if (!_scrollRafId) {
457
+ _scrollRafId = requestAnimationFrame(_smoothScrollStep);
458
+ }
459
+ }
460
+
461
+ function _smoothScrollStep() {
462
+ const box = _scrollBox;
463
+ if (!box || !autoScrollEnabled) {
464
+ _scrollRafId = null;
465
+ return;
466
+ }
467
+ const target = box.scrollHeight - box.clientHeight;
468
+ const current = box.scrollTop;
469
+ const diff = target - current;
470
+
471
+ // If user scrolled up significantly, don't force them down
472
+ if (diff > 300) {
473
+ _scrollRafId = null;
474
+ return;
475
+ }
476
+
477
+ if (diff <= 1) {
478
+ box.scrollTop = target;
479
+ _scrollRafId = null;
480
+ return;
481
+ }
482
+
483
+ // Lerp: gentle catch-up
484
+ const factor = window.innerWidth <= 768 ? 0.4 : 0.2;
485
+ box.scrollTop = current + diff * factor;
486
+ _scrollRafId = requestAnimationFrame(_smoothScrollStep);
487
+ }
488
+
489
+ /**
490
+ * Instant scroll to bottom — use for non-streaming contexts
491
+ * like loading history or switching sessions.
492
+ */
493
+ export function scrollHistoryInstant() {
494
+ if (!_scrollBox) {
495
+ _scrollBox = document.getElementById('chat-history');
496
+ }
497
+ if (_scrollBox) {
498
+ _scrollBox.scrollTop = _scrollBox.scrollHeight;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Enable/disable auto-scroll
504
+ */
505
+ export function setAutoScroll(enabled) {
506
+ autoScrollEnabled = enabled;
507
+ }
508
+
509
+ /**
510
+ * Get auto-scroll state
511
+ */
512
+ export function getAutoScroll() {
513
+ return autoScrollEnabled;
514
+ }
515
+
516
+ /**
517
+ * Auto-resize textarea based on content
518
+ */
519
+ export function autoResize(textarea) {
520
+ const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);
521
+ const isMobile = window.innerWidth <= 768;
522
+ const maxHeight = isMobile ? 150 : lineHeight * 8;
523
+
524
+ // Use a hidden clone to measure without disrupting the real textarea
525
+ let clone = textarea._resizeClone;
526
+ if (!clone) {
527
+ clone = textarea.cloneNode(false);
528
+ clone.style.cssText = getComputedStyle(textarea).cssText;
529
+ clone.style.position = 'absolute';
530
+ clone.style.visibility = 'hidden';
531
+ clone.style.height = '0';
532
+ clone.style.transition = 'none';
533
+ clone.style.overflow = 'hidden';
534
+ clone.style.pointerEvents = 'none';
535
+ clone.style.zIndex = '-1';
536
+ textarea.parentNode.appendChild(clone);
537
+ textarea._resizeClone = clone;
538
+ }
539
+ clone.style.width = textarea.offsetWidth + 'px';
540
+ clone.value = textarea.value;
541
+ clone.style.height = '0';
542
+ const newHeight = Math.min(Math.max(clone.scrollHeight, lineHeight), maxHeight);
543
+ textarea.style.height = newHeight + 'px';
544
+ textarea.style.overflow = newHeight >= maxHeight ? 'auto' : 'hidden';
545
+ }
546
+
547
+ /**
548
+ * Debounce function for performance
549
+ */
550
+ export function debounce(func, wait) {
551
+ let timeout;
552
+ return function(...args) {
553
+ const later = () => {
554
+ timeout = null;
555
+ func.apply(this, args);
556
+ };
557
+ clearTimeout(timeout);
558
+ timeout = setTimeout(later, wait);
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Get element by ID (utility helper)
564
+ */
565
+ export function el(id) {
566
+ return document.getElementById(id);
567
+ }
568
+
569
+ /**
570
+ * Styled confirm dialog — replaces native browser confirm().
571
+ * Returns a Promise<boolean>.
572
+ */
573
+ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = 'Cancel', danger = false } = {}) {
574
+ return new Promise(resolve => {
575
+ // Reuse or create the modal
576
+ let overlay = document.getElementById('styled-confirm-overlay');
577
+ if (!overlay) {
578
+ overlay = document.createElement('div');
579
+ overlay.id = 'styled-confirm-overlay';
580
+ overlay.className = 'modal';
581
+ overlay.innerHTML =
582
+ '<div class="modal-content styled-confirm-box" role="dialog" aria-modal="true" aria-labelledby="styled-confirm-title" aria-describedby="styled-confirm-msg">' +
583
+ '<div class="modal-header"><h4 id="styled-confirm-title">Confirm</h4></div>' +
584
+ '<div class="modal-body"><p id="styled-confirm-msg"></p></div>' +
585
+ '<div class="modal-footer">' +
586
+ '<button id="styled-confirm-cancel"></button>' +
587
+ '<button id="styled-confirm-ok"></button>' +
588
+ '</div>' +
589
+ '</div>';
590
+ document.body.appendChild(overlay);
591
+ }
592
+
593
+ const msgEl = document.getElementById('styled-confirm-msg');
594
+ const okBtn = document.getElementById('styled-confirm-ok');
595
+ const cancelBtn = document.getElementById('styled-confirm-cancel');
596
+
597
+ msgEl.textContent = message;
598
+ okBtn.textContent = confirmText;
599
+ cancelBtn.textContent = cancelText;
600
+ okBtn.className = danger ? 'confirm-btn confirm-btn-danger' : 'confirm-btn confirm-btn-primary';
601
+ cancelBtn.className = 'confirm-btn confirm-btn-secondary';
602
+
603
+ // Remember what had focus so we can restore it when the dialog closes.
604
+ const _prevFocus = document.activeElement;
605
+ overlay.classList.remove('hidden');
606
+ overlay.style.display = '';
607
+
608
+ function cleanup(result) {
609
+ overlay.classList.add('hidden');
610
+ overlay.style.display = 'none';
611
+ okBtn.removeEventListener('click', onOk);
612
+ cancelBtn.removeEventListener('click', onCancel);
613
+ overlay.removeEventListener('click', onBackdrop);
614
+ document.removeEventListener('keydown', onKey);
615
+ try { _prevFocus && _prevFocus.focus && _prevFocus.focus(); } catch {}
616
+ resolve(result);
617
+ }
618
+ function onOk() { cleanup(true); }
619
+ function onCancel() { cleanup(false); }
620
+ function onBackdrop(e) { if (e.target === overlay) cleanup(false); }
621
+ function onKey(e) {
622
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
623
+ e.preventDefault();
624
+ const active = document.activeElement;
625
+ if (active === okBtn) cancelBtn.focus();
626
+ else okBtn.focus();
627
+ } else if (e.key === 'Escape') {
628
+ e.preventDefault();
629
+ e.stopPropagation();
630
+ e.stopImmediatePropagation();
631
+ cleanup(false);
632
+ } else if (e.key === 'Tab') {
633
+ // Trap focus inside the dialog so Tab can't wander to the page behind.
634
+ e.preventDefault();
635
+ const f = [cancelBtn, okBtn];
636
+ const i = f.indexOf(document.activeElement);
637
+ const n = e.shiftKey ? (i <= 0 ? f.length - 1 : i - 1) : (i >= f.length - 1 ? 0 : i + 1);
638
+ f[n].focus();
639
+ }
640
+ }
641
+
642
+ okBtn.addEventListener('click', onOk);
643
+ cancelBtn.addEventListener('click', onCancel);
644
+ overlay.addEventListener('click', onBackdrop);
645
+ document.addEventListener('keydown', onKey);
646
+ okBtn.focus();
647
+ });
648
+ }
649
+
650
+ /**
651
+ * Styled text-input prompt — drop-in replacement for window.prompt().
652
+ * Resolves to the trimmed string the user typed, or null on Cancel / Escape / backdrop.
653
+ */
654
+ export function styledPrompt(message, {
655
+ title = 'Name',
656
+ defaultValue = '',
657
+ placeholder = '',
658
+ confirmText = 'Save',
659
+ cancelText = 'Cancel',
660
+ maxLength = 80,
661
+ } = {}) {
662
+ return new Promise(resolve => {
663
+ let overlay = document.getElementById('styled-prompt-overlay');
664
+ if (!overlay) {
665
+ overlay = document.createElement('div');
666
+ overlay.id = 'styled-prompt-overlay';
667
+ overlay.className = 'modal';
668
+ overlay.innerHTML =
669
+ '<div class="modal-content styled-confirm-box styled-prompt-box" role="dialog" aria-modal="true" aria-labelledby="styled-prompt-title" aria-describedby="styled-prompt-msg">' +
670
+ '<div class="modal-header"><h4 id="styled-prompt-title"></h4></div>' +
671
+ '<div class="modal-body">' +
672
+ '<p id="styled-prompt-msg"></p>' +
673
+ '<input type="text" id="styled-prompt-input" class="styled-prompt-input" />' +
674
+ '</div>' +
675
+ '<div class="modal-footer">' +
676
+ '<button id="styled-prompt-cancel" class="confirm-btn confirm-btn-secondary"></button>' +
677
+ '<button id="styled-prompt-ok" class="confirm-btn confirm-btn-primary"></button>' +
678
+ '</div>' +
679
+ '</div>';
680
+ document.body.appendChild(overlay);
681
+ }
682
+
683
+ const titleEl = document.getElementById('styled-prompt-title');
684
+ const msgEl = document.getElementById('styled-prompt-msg');
685
+ const input = document.getElementById('styled-prompt-input');
686
+ const okBtn = document.getElementById('styled-prompt-ok');
687
+ const cancelBtn = document.getElementById('styled-prompt-cancel');
688
+
689
+ titleEl.textContent = title;
690
+ msgEl.textContent = message || '';
691
+ msgEl.style.display = message ? '' : 'none';
692
+ input.value = defaultValue || '';
693
+ input.placeholder = placeholder || '';
694
+ input.maxLength = maxLength;
695
+ okBtn.textContent = confirmText;
696
+ cancelBtn.textContent = cancelText;
697
+
698
+ // Remember what had focus so we can restore it when the dialog closes.
699
+ const _prevFocus = document.activeElement;
700
+ overlay.classList.remove('hidden');
701
+ overlay.style.display = '';
702
+
703
+ function cleanup(result) {
704
+ overlay.classList.add('hidden');
705
+ overlay.style.display = 'none';
706
+ okBtn.removeEventListener('click', onOk);
707
+ cancelBtn.removeEventListener('click', onCancel);
708
+ overlay.removeEventListener('click', onBackdrop);
709
+ document.removeEventListener('keydown', onKey);
710
+ input.removeEventListener('keydown', onInputKey);
711
+ try { _prevFocus && _prevFocus.focus && _prevFocus.focus(); } catch {}
712
+ resolve(result);
713
+ }
714
+ function onOk() { cleanup((input.value || '').trim()); }
715
+ function onCancel() { cleanup(null); }
716
+ function onBackdrop(e) { if (e.target === overlay) cleanup(null); }
717
+ function onKey(e) {
718
+ if (e.key === 'Escape') {
719
+ e.preventDefault();
720
+ e.stopPropagation();
721
+ e.stopImmediatePropagation();
722
+ cleanup(null);
723
+ } else if (e.key === 'Tab') {
724
+ // Trap focus inside the dialog (input → Cancel → OK → input …).
725
+ e.preventDefault();
726
+ const f = [input, cancelBtn, okBtn];
727
+ const i = f.indexOf(document.activeElement);
728
+ const n = e.shiftKey ? (i <= 0 ? f.length - 1 : i - 1) : (i >= f.length - 1 ? 0 : i + 1);
729
+ f[n].focus();
730
+ }
731
+ }
732
+ function onInputKey(e) {
733
+ if (e.key === 'Enter') {
734
+ e.preventDefault();
735
+ onOk();
736
+ }
737
+ }
738
+
739
+ okBtn.addEventListener('click', onOk);
740
+ cancelBtn.addEventListener('click', onCancel);
741
+ overlay.addEventListener('click', onBackdrop);
742
+ document.addEventListener('keydown', onKey);
743
+ input.addEventListener('keydown', onInputKey);
744
+
745
+ requestAnimationFrame(() => {
746
+ input.focus();
747
+ input.select();
748
+ });
749
+ });
750
+ }
751
+
752
+ // Lookup table for esc(); hoisted out of the replace callback so it is
753
+ // allocated once rather than per matched character.
754
+ const _ESC_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
755
+ /**
756
+ * HTML-escape a string to prevent XSS.
757
+ * Canonical implementation — other modules should use uiModule.esc() instead of local copies.
758
+ */
759
+ export function esc(s) {
760
+ return (s || '').replace(/[&<>"']/g, (m) => _ESC_MAP[m]);
761
+ }
762
+
763
+ // ── Mobile: suppress synthetic click/mousedown on backdrop ──
764
+ // When a touch starts inside .modal-content, set a flag so that
765
+ // synthetic mouse events on the backdrop are ignored.
766
+ let _touchInsideModal = false;
767
+ if ('ontouchstart' in window) {
768
+ document.addEventListener('touchstart', (e) => {
769
+ if (e.target.closest('.modal-content')) {
770
+ _touchInsideModal = true;
771
+ }
772
+ }, { passive: true });
773
+ document.addEventListener('touchend', () => {
774
+ // Clear after a short delay — synthetic click fires ~300ms after touchend
775
+ setTimeout(() => { _touchInsideModal = false; }, 400);
776
+ }, { passive: true });
777
+ }
778
+
779
+ /**
780
+ * Check if a backdrop dismiss should be suppressed on mobile.
781
+ * Other modules can call this to guard their own backdrop handlers.
782
+ */
783
+ export function isTouchInsideModal() {
784
+ return _touchInsideModal;
785
+ }
786
+
787
+ // Close floating dropdowns/popups on scroll to prevent them drifting
788
+ function _initScrollDismiss() {
789
+ const chatHistory = document.getElementById('chat-history');
790
+ if (chatHistory) {
791
+ chatHistory.addEventListener('scroll', () => {
792
+ chatHistory.querySelectorAll('.dropdown.show').forEach(d => d.classList.remove('show'));
793
+ document.querySelectorAll('.ctx-popup').forEach(dismissOrRemove);
794
+ }, { passive: true });
795
+ } else {
796
+ // Retry once if element doesn't exist yet
797
+ setTimeout(_initScrollDismiss, 500);
798
+ }
799
+ }
800
+ if (document.readyState === 'loading') {
801
+ document.addEventListener('DOMContentLoaded', _initScrollDismiss);
802
+ } else {
803
+ _initScrollDismiss();
804
+ }
805
+
806
+ /**
807
+ * Returns the SVG string for an empty-state icon. `kind` is one of
808
+ * 'smiley' | 'sad' | 'neutral'. The returned <svg> has NO inline style —
809
+ * callers wrap with `<span style="vertical-align:-3px;margin-left:6px;">…</span>`
810
+ * (or similar) to keep the per-site visual nudge they need.
811
+ */
812
+ export function emptyStateIcon(kind) {
813
+ const SVG_OPEN = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
814
+ const SVG_CLOSE = '</svg>';
815
+ let inner;
816
+ switch (kind) {
817
+ case 'sad':
818
+ inner = '<circle cx="12" cy="12" r="10"/><path d="M16 16s-1.5-2-4-2-4 2-4 2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>';
819
+ break;
820
+ case 'neutral':
821
+ inner = '<circle cx="12" cy="12" r="10"/><line x1="8" y1="15" x2="16" y2="15"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>';
822
+ break;
823
+ case 'smiley':
824
+ default:
825
+ inner = '<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>';
826
+ break;
827
+ }
828
+ return SVG_OPEN + inner + SVG_CLOSE;
829
+ }
830
+
831
+ const uiModule = {
832
+ copyToClipboard,
833
+ showToast,
834
+ showError,
835
+ styledConfirm,
836
+ styledPrompt,
837
+ scrollHistory,
838
+ scrollHistoryInstant,
839
+ setAutoScroll,
840
+ getAutoScroll,
841
+ autoResize,
842
+ debounce,
843
+ el,
844
+ esc,
845
+ isTouchInsideModal,
846
+ emptyStateIcon,
847
+ registerMenuDismiss
848
+ };
849
+
850
+ export default uiModule;
851
+
852
+ // Expose the styled confirm globally so any module can replace the native
853
+ // browser confirm() with the themed dialog — even files that don't import
854
+ // uiModule. Usage: `if (!await window.styledConfirm(msg, { danger:true })) return;`
855
+ if (typeof window !== 'undefined') {
856
+ window.styledConfirm = styledConfirm;
857
+ }
858
+
859
+ // ── Mobile: clear enter animation so inline transform works for dragging ──
860
+ // The CSS `animation: sheet-enter ... forwards` holds the final transform,
861
+ // blocking any inline style changes. We clear it once the animation completes.
862
+ if ('ontouchstart' in window || window.innerWidth <= 768) {
863
+ document.addEventListener('animationend', (e) => {
864
+ if (e.animationName === 'sheet-enter' &&
865
+ (e.target.classList.contains('modal-content') || e.target.id === 'theme-popup')) {
866
+ e.target.classList.add('sheet-ready');
867
+ }
868
+ });
869
+ // When a modal is re-shown, remove sheet-ready so the enter animation plays again
870
+ new MutationObserver((mutations) => {
871
+ for (const m of mutations) {
872
+ if (m.type === 'attributes' && m.attributeName === 'class') {
873
+ const modal = m.target;
874
+ if (modal.classList.contains('modal') && !modal.classList.contains('hidden')) {
875
+ const content = modal.querySelector('.modal-content') || modal.querySelector('#theme-popup');
876
+ if (content) {
877
+ content.classList.remove('sheet-ready', 'modal-closing');
878
+ }
879
+ }
880
+ }
881
+ }
882
+ }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ['class'] });
883
+ }
884
+
885
+ // ── Mobile swipe-down-to-dismiss for bottom sheet modals ──
886
+ // Finger-following drag with velocity-based dismiss.
887
+ // Works from grab handle, header, OR anywhere on the sheet when content is scrolled to top.
888
+ if ('ontouchstart' in window) {
889
+ const DISMISS_THRESHOLD = 50; // px — dismiss if dragged past this
890
+ const VELOCITY_THRESHOLD = 0.3; // px/ms — fast flick dismisses even below threshold
891
+ const RUBBER_RESISTANCE = 0.35; // drag resistance when pulling up past origin
892
+
893
+ let _swipeTarget = null;
894
+ let _startY = 0, _startX = 0;
895
+ let _lastY = 0, _lastT = 0;
896
+ let _velocity = 0;
897
+ let _dragging = false; // true once we've committed to a vertical drag
898
+ let _cancelled = false; // true if horizontal movement detected
899
+
900
+ // Close any floating dropdowns/menus that hang off body via position:fixed.
901
+ // Called when a swipe-dismiss gesture starts so the menu doesn't orphan over
902
+ // the page after the sheet slides away.
903
+ function _closeFloatingDropdownsForSwipe() {
904
+ document.querySelectorAll(
905
+ '.email-card-dropdown, .hwfit-cached-dropdown, .cookbook-saved-menu, .cookbook-dep-menu'
906
+ ).forEach(d => {
907
+ if (d._anchor) d._anchor.classList.remove('cookbook-menu-active', 'reader-more-active');
908
+ // Registered menus tear down through their own dismiss (releasing the
909
+ // Escape-stack entry); unregistered ones (email/dep) just get removed.
910
+ dismissOrRemove(d);
911
+ });
912
+ }
913
+
914
+ document.addEventListener('touchstart', (e) => {
915
+ // Match .modal-content or #theme-popup (which acts as modal-content but uses its own ID)
916
+ const content = e.target.closest('.modal-content') || e.target.closest('#theme-popup');
917
+ if (!content) return;
918
+
919
+ // The image editor owns all touches inside its container so the user
920
+ // can paint / move layers / draw selections without the modal trying
921
+ // to interpret it as a swipe-to-dismiss gesture. Skip the swipe init
922
+ // entirely when the touch starts inside the editor area.
923
+ if (e.target.closest('.gallery-editor, .gallery-editor-container')) return;
924
+ // Internal vertical drag handles (e.g. the calendar's cal-splitter that
925
+ // resizes the day-detail pane) consume vertical touches themselves. If
926
+ // we don't bail here, the swipe-dismiss path also tracks the touch and
927
+ // slides the whole modal down as the user drags the handle. The
928
+ // [data-no-swipe-dismiss] hook lets other components opt out the same
929
+ // way without having to hard-code their selector here.
930
+ if (e.target.closest('.cal-splitter, [data-no-swipe-dismiss]')) return;
931
+
932
+ // Only allow swipe-dismiss from header or grab handle (top 48px)
933
+ const isHeader = !!e.target.closest('.modal-header');
934
+ const isButton = !!e.target.closest('button, input, select, label');
935
+ if (isHeader && isButton) return; // let button clicks through
936
+ const touch = e.touches[0];
937
+ const contentRect = content.getBoundingClientRect();
938
+ const isGrabZone = (touch.clientY - contentRect.top) < 48;
939
+ // Also allow swipe-dismiss from anywhere on the sheet when it's already
940
+ // scrolled to the top — feels natural and matches iOS bottom-sheet UX.
941
+ const isAtScrollTop = content.scrollTop <= 0;
942
+
943
+ if (!isHeader && !isGrabZone && !isAtScrollTop) return; // body touches → let native scroll handle it
944
+
945
+ _swipeTarget = content;
946
+ // Ensure CSS animation is cleared so inline transform works
947
+ content.classList.add('sheet-ready');
948
+ content.style.animation = 'none';
949
+ _startY = touch.clientY;
950
+ _startX = touch.clientX;
951
+ _lastY = _startY;
952
+ _lastT = e.timeStamp;
953
+ _velocity = 0;
954
+ _dragging = false;
955
+ _cancelled = false;
956
+ }, { passive: true });
957
+
958
+ document.addEventListener('touchmove', (e) => {
959
+ if (!_swipeTarget || _cancelled) return;
960
+ const touch = e.touches[0];
961
+ const dx = Math.abs(touch.clientX - _startX);
962
+ const dy = touch.clientY - _startY;
963
+
964
+ // First few pixels: decide if this is horizontal scroll or content scroll
965
+ if (!_dragging) {
966
+ if (dx > 40 && dx > Math.abs(dy) * 2) {
967
+ _swipeTarget.style.transform = '';
968
+ _swipeTarget = null;
969
+ _cancelled = true;
970
+ return;
971
+ }
972
+ if (Math.abs(dy) > 8) {
973
+ // Find the nearest scrollable ancestor of the touch point
974
+ let scrollEl = e.target;
975
+ while (scrollEl && scrollEl !== _swipeTarget) {
976
+ if (scrollEl.scrollHeight > scrollEl.clientHeight + 1) {
977
+ const ov = getComputedStyle(scrollEl).overflowY;
978
+ if (ov === 'auto' || ov === 'scroll') break;
979
+ }
980
+ scrollEl = scrollEl.parentElement;
981
+ }
982
+ const hasScroller = scrollEl && scrollEl !== _swipeTarget;
983
+ // If touch is inside a scrollable child, let native scroll handle it
984
+ if (hasScroller) {
985
+ _swipeTarget.style.transform = '';
986
+ _swipeTarget = null;
987
+ _cancelled = true;
988
+ return;
989
+ }
990
+ // If swiping up and modal-content itself is scrollable, let native handle it
991
+ if (dy < 0 && _swipeTarget.scrollHeight > _swipeTarget.clientHeight + 1) {
992
+ _swipeTarget.style.transform = '';
993
+ _swipeTarget = null;
994
+ _cancelled = true;
995
+ return;
996
+ }
997
+ // If swiping down but content isn't at the top, let native scroll
998
+ if (dy > 0 && _swipeTarget.scrollTop > 0) {
999
+ _swipeTarget.style.transform = '';
1000
+ _swipeTarget = null;
1001
+ _cancelled = true;
1002
+ return;
1003
+ }
1004
+ _dragging = true;
1005
+ _swipeTarget.style.transition = 'none';
1006
+ _swipeTarget.style.willChange = 'transform';
1007
+ // A swipe is starting — close any floating menus/dropdowns so they
1008
+ // don't orphan over the page once the sheet slides away. Covers the
1009
+ // email reader More menu, cookbook serve kebab + saved-configs, and
1010
+ // anything else hanging off body via _anchor.
1011
+ _closeFloatingDropdownsForSwipe();
1012
+ } else {
1013
+ return;
1014
+ }
1015
+ }
1016
+
1017
+ // Track velocity (exponential moving average)
1018
+ const dt = e.timeStamp - _lastT;
1019
+ if (dt > 0) {
1020
+ const instantV = (touch.clientY - _lastY) / dt;
1021
+ _velocity = _velocity * 0.6 + instantV * 0.4;
1022
+ }
1023
+ _lastY = touch.clientY;
1024
+ _lastT = e.timeStamp;
1025
+
1026
+ e.preventDefault();
1027
+ if (dy > 0) {
1028
+ _swipeTarget.style.transform = `translateY(${dy}px)`;
1029
+ } else {
1030
+ const rubberDy = dy * RUBBER_RESISTANCE;
1031
+ _swipeTarget.style.transform = `translateY(${rubberDy}px)`;
1032
+ }
1033
+ }, { passive: false });
1034
+
1035
+ document.addEventListener('touchend', (e) => {
1036
+ if (!_swipeTarget || !_dragging) {
1037
+ _swipeTarget = null;
1038
+ return;
1039
+ }
1040
+ const el = _swipeTarget;
1041
+ _swipeTarget = null;
1042
+
1043
+ const dy = _lastY - _startY;
1044
+ const shouldDismiss = dy > DISMISS_THRESHOLD || (dy > 20 && _velocity > VELOCITY_THRESHOLD);
1045
+
1046
+ el.style.willChange = '';
1047
+
1048
+ if (shouldDismiss) {
1049
+ // Animate out — use remaining distance to calculate duration
1050
+ const remaining = el.offsetHeight - dy;
1051
+ const speed = Math.max(Math.abs(_velocity), 0.8); // min speed
1052
+ const duration = Math.min(Math.max(remaining / speed, 120), 300);
1053
+ el.style.transition = `transform ${duration}ms cubic-bezier(0.2, 0, 0.4, 1)`;
1054
+ el.style.transform = 'translateY(100%)';
1055
+ setTimeout(() => {
1056
+ const modal = el.closest('.modal');
1057
+ if (modal) {
1058
+ modal.classList.add('hidden');
1059
+ // Some modals (calendar, email library) toggle visibility via
1060
+ // inline display style which would override .hidden — clear it
1061
+ // so the modal is actually dismissed.
1062
+ modal.style.display = '';
1063
+ document.querySelectorAll('#settings-menu-list .list-item.active').forEach(i => i.classList.remove('active'));
1064
+ // Notify modules so they can sync internal open-state flags
1065
+ window.dispatchEvent(new CustomEvent('modal-dismissed', { detail: { id: modal.id } }));
1066
+ // Swiping a tool away to reveal a new/empty chat replays the welcome
1067
+ // "splash" reveal — the same nice effect notes gives on dismiss.
1068
+ // Only when the welcome screen is already the active state (new chat),
1069
+ // so we never cover a chat that has messages.
1070
+ const ws = document.getElementById('welcome-screen');
1071
+ if (ws && !ws.classList.contains('hidden')) {
1072
+ window.chatModule?.showWelcomeScreen?.();
1073
+ }
1074
+ }
1075
+ el.classList.remove('sheet-ready');
1076
+ el.style.transform = '';
1077
+ el.style.transition = '';
1078
+ el.style.animation = '';
1079
+ }, duration + 10);
1080
+ } else {
1081
+ // Snap back with spring-like easing
1082
+ el.style.transition = 'transform 0.25s cubic-bezier(0.2, 0.9, 0.3, 1.05)';
1083
+ el.style.transform = '';
1084
+ setTimeout(() => { el.style.transition = ''; el.style.animation = ''; }, 260);
1085
+ }
1086
+ }, { passive: true });
1087
+ }
1088
+
1089
+ // ---- Bring modal to front on click ----
1090
+ {
1091
+ let topModalZ = 250;
1092
+ document.addEventListener('mousedown', (e) => {
1093
+ const modalContent = e.target.closest('.modal-content');
1094
+ if (!modalContent) return;
1095
+ const modal = modalContent.closest('.modal');
1096
+ if (!modal) return;
1097
+ topModalZ += 1;
1098
+ modal.style.zIndex = topModalZ;
1099
+ });
1100
+
1101
+ // Backdrop tap to close — delegated for all modals
1102
+ document.addEventListener('mousedown', (e) => {
1103
+ if (_touchInsideModal) return; // suppress synthetic events from content scrolling
1104
+ if (!e.target.classList.contains('modal')) return;
1105
+ const modal = e.target;
1106
+ if (modal.classList.contains('hidden')) return;
1107
+ const content = modal.querySelector('.modal-content');
1108
+ if (content) {
1109
+ content.classList.add('modal-closing');
1110
+ content.addEventListener('animationend', () => {
1111
+ modal.classList.add('hidden');
1112
+ content.classList.remove('modal-closing');
1113
+ }, { once: true });
1114
+ setTimeout(() => {
1115
+ if (!modal.classList.contains('hidden')) {
1116
+ modal.classList.add('hidden');
1117
+ content.classList.remove('modal-closing');
1118
+ }
1119
+ }, 300);
1120
+ } else {
1121
+ modal.classList.add('hidden');
1122
+ }
1123
+ });
1124
+ }
1125
+
1126
+ // ── Mobile: keep focused inputs visible above the keyboard ──
1127
+ // When an input inside a modal gets focus on mobile, the OS keyboard
1128
+ // covers the bottom half of the screen. The browser is supposed to
1129
+ // scroll the input into view, but in bottom-sheet modals with their
1130
+ // own scrolling container that often fails — the user types blind.
1131
+ // Scroll the input into the middle of the still-visible viewport
1132
+ // after the keyboard has had a moment to animate in.
1133
+ if ('ontouchstart' in window || window.innerWidth <= 768) {
1134
+ let _kbScrollTimer = null;
1135
+ document.addEventListener('focusin', (e) => {
1136
+ const el = e.target;
1137
+ if (!el || el.nodeType !== 1) return;
1138
+ const tag = el.tagName;
1139
+ const isText = tag === 'INPUT' || tag === 'TEXTAREA' ||
1140
+ (tag === 'DIV' && el.isContentEditable);
1141
+ if (!isText) return;
1142
+ // Inputs of type button/checkbox/radio/range/etc. don't summon a keyboard
1143
+ if (tag === 'INPUT') {
1144
+ const t = (el.type || 'text').toLowerCase();
1145
+ if (['button','submit','reset','checkbox','radio','range','color','file','image'].includes(t)) return;
1146
+ }
1147
+ if (_kbScrollTimer) clearTimeout(_kbScrollTimer);
1148
+ // The keyboard typically takes 200–300ms to slide up; do the scroll
1149
+ // after that so we know the final visible viewport height.
1150
+ _kbScrollTimer = setTimeout(() => {
1151
+ _kbScrollTimer = null;
1152
+ // Skip the scroll if the input is already visible inside the
1153
+ // current viewport (with a small comfort margin). Otherwise every
1154
+ // re-focus — including the programmatic refocus that happens when
1155
+ // a typeahead input rebuilds the DOM on every keystroke — would
1156
+ // re-scroll the modal and yank the page up and down as the user
1157
+ // types.
1158
+ try {
1159
+ const r = el.getBoundingClientRect();
1160
+ const vh = (window.visualViewport?.height) || window.innerHeight;
1161
+ const margin = 24;
1162
+ const fullyVisible = r.top >= margin && r.bottom <= vh - margin;
1163
+ if (fullyVisible) return;
1164
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
1165
+ } catch {
1166
+ try { el.scrollIntoView(); } catch {}
1167
+ }
1168
+ }, 300);
1169
+ });
1170
+ }
1171
+
1172
+ // ── Global Escape arbiter: close exactly one thing per press ──
1173
+ // Priority: expanded library card → open chat thinking block → topmost modal.
1174
+ // Runs capture-phase + stopImmediatePropagation so per-modal ESC listeners
1175
+ // never also fire (which would otherwise close several modals at once).
1176
+ if (!window._odyEscExpandGuard) {
1177
+ window._odyEscExpandGuard = true;
1178
+
1179
+ // Auto-promote any modal that becomes visible to the top of the z-stack.
1180
+ // Every modal shares `z-index: 250` from the base `.modal` rule, so visual
1181
+ // stacking falls back to DOM order — which is unpredictable (cookbook is
1182
+ // a static HTML node, calendar gets appended once and stays, compare and
1183
+ // research get re-appended on each open). Result: opening compare AFTER
1184
+ // cookbook can render compare UNDER it. Bumping the z-index on every
1185
+ // open guarantees most-recently-opened wins both visually AND for ESC.
1186
+ let _zCounter = 1000;
1187
+ const _isVisible = (m) => !m.classList.contains('hidden') && getComputedStyle(m).display !== 'none';
1188
+ const _promote = (m) => {
1189
+ if (!m?.classList?.contains('modal') || !_isVisible(m)) return;
1190
+ // Re-entry guard: setting style.zIndex itself fires the observer that
1191
+ // calls us back. Skip if this element is already pinned to the top
1192
+ // (matches the current counter) so we don't spin into an infinite loop.
1193
+ const cur = parseInt(m.style.zIndex, 10) || 0;
1194
+ if (cur === _zCounter) return;
1195
+ m.style.zIndex = String(++_zCounter);
1196
+ };
1197
+ new MutationObserver((muts) => {
1198
+ for (const m of muts) {
1199
+ if (m.type === 'childList') m.addedNodes.forEach(n => n.nodeType === 1 && _promote(n));
1200
+ else if (m.type === 'attributes' && m.target?.classList?.contains('modal')) _promote(m.target);
1201
+ }
1202
+ }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
1203
+ document.querySelectorAll('.modal').forEach(_promote);
1204
+
1205
+ const pickTopModal = () => {
1206
+ const modals = [...document.querySelectorAll('.modal')].filter(_isVisible);
1207
+ if (!modals.length) return null;
1208
+ return modals.reduce((top, m) =>
1209
+ (parseInt(getComputedStyle(m).zIndex, 10) || 0) >= (parseInt(getComputedStyle(top).zIndex, 10) || 0)
1210
+ ? m : top
1211
+ );
1212
+ };
1213
+
1214
+ document.addEventListener('keydown', (e) => {
1215
+ if (e.key !== 'Escape' || e.defaultPrevented) return;
1216
+
1217
+ // Find the single thing to close, in priority order. The first hit wins.
1218
+ // Important: if a thinking block is open we MUST handle it ourselves and
1219
+ // not fall through to closing a modal — even if its header is missing
1220
+ // (the live-stream chat rebuilds thinking DOM mid-stream so the header
1221
+ // can briefly be absent). Toggling the `expanded` class directly is the
1222
+ // fallback so ESC never bypasses the thinking block to hit a modal.
1223
+ if (_closeHoveredWindow()) {
1224
+ e.stopImmediatePropagation(); e.preventDefault();
1225
+ return;
1226
+ }
1227
+ // Transient ad-hoc menus (dropdowns / context popups) live outside the
1228
+ // .modal system and register a dismiss callback in escMenuStack. Close the
1229
+ // most-recently-opened one first — so a menu opened over a modal dismisses
1230
+ // before the modal — and do it BEFORE the text-input guard below, since a
1231
+ // menu may own the focused input (e.g. a search dropdown).
1232
+ if (dismissTopMenu()) {
1233
+ e.stopImmediatePropagation(); e.preventDefault();
1234
+ return;
1235
+ }
1236
+ const t = e.target;
1237
+ if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
1238
+ const expanded = document.querySelector('.doclib-card-expanded');
1239
+ const think = document.querySelector('.thinking-content.expanded');
1240
+ if (expanded) {
1241
+ e.stopImmediatePropagation(); e.preventDefault();
1242
+ try { expanded.click(); } catch {}
1243
+ return;
1244
+ }
1245
+ if (think) {
1246
+ e.stopImmediatePropagation(); e.preventDefault();
1247
+ const thinkHeader = think.closest('.thinking-section')?.querySelector('.thinking-header[data-thinking-id]');
1248
+ if (thinkHeader) { try { thinkHeader.click(); } catch {} }
1249
+ else {
1250
+ // No header found — collapse the content directly.
1251
+ try { think.classList.remove('expanded'); } catch {}
1252
+ }
1253
+ return;
1254
+ }
1255
+ const galleryEditor = document.getElementById('gallery-editor-container');
1256
+ const galleryModal = galleryEditor?.closest('.modal');
1257
+ const galleryEditing = !!(
1258
+ galleryEditor &&
1259
+ galleryModal &&
1260
+ !galleryModal.classList.contains('hidden') &&
1261
+ getComputedStyle(galleryEditor).display !== 'none' &&
1262
+ galleryEditor.querySelector('.gallery-editor')
1263
+ );
1264
+ if (galleryEditing) {
1265
+ e.preventDefault();
1266
+ e.stopImmediatePropagation();
1267
+ return;
1268
+ }
1269
+ const settingsModal = document.getElementById('settings-modal');
1270
+ if (settingsModal && _isVisible(settingsModal)) {
1271
+ const innerForm = settingsModal.querySelector('#unified-intg-form, #set-email-accounts-form');
1272
+ if (innerForm && innerForm.style.display !== 'none' && innerForm.children.length > 0) {
1273
+ e.preventDefault();
1274
+ e.stopImmediatePropagation();
1275
+ innerForm.style.display = 'none';
1276
+ innerForm.innerHTML = '';
1277
+ return;
1278
+ }
1279
+ }
1280
+ const topModal = pickTopModal();
1281
+ if (!topModal) return;
1282
+ const closeBtn = topModal.querySelector('.close-btn, .modal-close-btn, [data-action="close"]');
1283
+ e.stopImmediatePropagation();
1284
+ e.preventDefault();
1285
+ if (closeBtn) { try { closeBtn.click(); } catch {} }
1286
+ else { try { topModal.classList.add('hidden'); } catch {} }
1287
+ }, true);
1288
+ }