@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,2086 @@
1
+ // Theme system — preset themes + custom color editing, stored in localStorage
2
+ // ES6 module
3
+
4
+ import Storage from './storage.js';
5
+ import uiModule from './ui.js';
6
+ import { initColorPickers, attachColorPicker } from './colorPicker.js';
7
+ import { hexToRgb } from './color/hex.js';
8
+ import { makeWindowDraggable } from './windowDrag.js';
9
+ import { snapModalToZone } from './tileManager.js';
10
+
11
+ export const THEMES = {
12
+ dark: { bg:'#282c34', fg:'#9cdef2', panel:'#111111', border:'#355a66', red:'#e06c75' },
13
+ light: { bg:'#f0ebe3', fg:'#5a5248', panel:'#faf6f0', border:'#d4cdc2', red:'#c47d5a' },
14
+ midnight: { bg:'#0d1117', fg:'#c9d1d9', panel:'#161b22', border:'#30363d', red:'#f85149' },
15
+ paper: { bg:'#faf8f5', fg:'#3b3836', panel:'#ffffff', border:'#d5d0c8', red:'#c5ac4a' },
16
+ // Spicy / fun themes
17
+ cyberpunk: { bg:'#0a0a0f', fg:'#0ff0fc', panel:'#12101a', border:'#9b30ff', red:'#e040fb' },
18
+ retrowave: { bg:'#1a1a2e', fg:'#e94560', panel:'#16213e', border:'#533483', red:'#e94560' },
19
+ forest: { bg:'#1b2a1b', fg:'#a8d5a2', panel:'#142414', border:'#3d6b3d', red:'#7cb871' },
20
+ ocean: { bg:'#0b1a2c', fg:'#64d2ff', panel:'#091422', border:'#1e5074', red:'#4facfe' },
21
+ ume: { bg:'#2b1b2e', fg:'#f5c2e7', panel:'#1e1420', border:'#6c4675', red:'#f5a0c0' },
22
+ copper: { bg:'#1c1410', fg:'#e8c39e', panel:'#140f0a', border:'#7a5533', red:'#d4764e' },
23
+ terminal: { bg:'#000000', fg:'#00ff41', panel:'#0a0a0a', border:'#003b00', red:'#00ff41' },
24
+ organs: { bg:'#0a0406', fg:'#efe1c8', panel:'#15080a', border:'#3a1519', red:'#c83240' },
25
+ lavender: { bg:'#f3eef8', fg:'#3d3551', panel:'#faf7ff', border:'#cec3de', red:'#9b6dcc' },
26
+ gpt: { bg:'#212121', fg:'#ececec', panel:'#171717', border:'#424242', red:'#949494',
27
+ advanced: { sendBtnBg: '#949494', sendBtnHover: '#7f7f7f',
28
+ userBubbleBg: '#2f2f2f', aiBubbleBg: '#171717',
29
+ inputBg: '#2f2f2f' } },
30
+ claude: { bg:'#262624', fg:'#f5f4f0', panel:'#30302e', border:'#4a4a47', red:'#c6613f' },
31
+ cute: { bg:'#fff0f5', fg:'#d4608a', panel:'#fff8fa', border:'#f0c0d0', red:'#ff6b9d' },
32
+ };
33
+
34
+ const DEFAULT_THEME = 'dark';
35
+ const LS_KEY = 'zero-theme';
36
+ const CUSTOM_THEMES_KEY = 'zero-custom-themes';
37
+
38
+ const FONT_MAP = {
39
+ mono: "'Fira Code', monospace",
40
+ sans: "system-ui, -apple-system, 'Segoe UI', sans-serif",
41
+ serif: "Georgia, 'Times New Roman', serif",
42
+ };
43
+ const DEFAULT_FONT = 'mono';
44
+ const DEFAULT_DENSITY = 'comfortable';
45
+ const MAX_CUSTOM_THEMES = 8;
46
+
47
+ // Default background patterns for built-in themes
48
+ const THEME_DEFAULT_PATTERN = {
49
+ dark: 'none',
50
+ light: 'dots',
51
+ midnight: 'rain',
52
+ paper: 'dots',
53
+ cyberpunk: 'synapse',
54
+ retrowave: 'embers',
55
+ forest: 'petals',
56
+ ocean: 'constellations',
57
+ terminal: 'perlin-flow',
58
+ organs: 'rain',
59
+ ume: 'petals',
60
+ cute: 'sparkles',
61
+ };
62
+
63
+ // Default effect colors for specific themes (overrides --fg)
64
+ const THEME_DEFAULT_EFFECT_COLOR = {
65
+ midnight: '#ffffff',
66
+ organs: '#451616',
67
+ cute: '#ff8cb8',
68
+ ume: '#f5a0c0',
69
+ };
70
+
71
+ // Default effect intensity (0..1) per theme. Any theme not listed defaults to 1.
72
+ const THEME_DEFAULT_INTENSITY = {
73
+ midnight: 0.5,
74
+ terminal: 0.8,
75
+ organs: 0.65,
76
+ };
77
+
78
+ // Default frosted-glass state per theme. Themes not listed default to false.
79
+ const THEME_DEFAULT_FROSTED = {
80
+ lavender: true,
81
+ };
82
+
83
+ // ── Custom theme persistence ──
84
+ function _loadCustomThemes() {
85
+ return Storage.getJSON(CUSTOM_THEMES_KEY, {});
86
+ }
87
+ function _saveCustomThemes(obj) {
88
+ Storage.setJSON(CUSTOM_THEMES_KEY, obj);
89
+ }
90
+ export function saveCustomTheme(name, colors, opts) {
91
+ const ct = _loadCustomThemes();
92
+ // Enforce limit — allow overwriting existing, block new past max
93
+ if (!ct[name] && Object.keys(ct).length >= MAX_CUSTOM_THEMES) {
94
+ return 'limit';
95
+ }
96
+ const entry = { ...colors };
97
+ if (opts) {
98
+ if (opts.font) entry.font = opts.font;
99
+ if (opts.density) entry.density = opts.density;
100
+ if (opts.bgPattern) entry.bgPattern = opts.bgPattern;
101
+ if (opts.bgEffectColor) entry.bgEffectColor = opts.bgEffectColor;
102
+ if (opts.bgEffectIntensity !== undefined) entry.bgEffectIntensity = opts.bgEffectIntensity;
103
+ if (opts.bgEffectSize !== undefined) entry.bgEffectSize = opts.bgEffectSize;
104
+ if (opts.frosted !== undefined) entry.frosted = !!opts.frosted;
105
+ }
106
+ ct[name] = entry;
107
+ _saveCustomThemes(ct);
108
+ _syncCustomThemesToServer(ct);
109
+ initThemeUI();
110
+ return 'ok';
111
+ }
112
+ export function deleteCustomTheme(name) {
113
+ const ct = _loadCustomThemes();
114
+ delete ct[name];
115
+ _saveCustomThemes(ct);
116
+ _syncCustomThemesToServer(ct);
117
+ initThemeUI();
118
+ }
119
+ function _syncCustomThemesToServer(ct) {
120
+ try {
121
+ fetch('/api/prefs/custom-themes', {
122
+ method: 'PUT',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ credentials: 'same-origin',
125
+ body: JSON.stringify({ value: ct }),
126
+ }).catch(e => console.warn('Theme sync (custom) failed:', e));
127
+ } catch (e) { console.warn('Theme sync (custom) error:', e); }
128
+ }
129
+
130
+ // --- Syntax color derivation from theme base colors ---
131
+ function hexToHSL(hex) {
132
+ const rgb = hexToRgb(hex) || { r: 0, g: 0, b: 0 };
133
+ const r = rgb.r / 255;
134
+ const g = rgb.g / 255;
135
+ const b = rgb.b / 255;
136
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
137
+ let h, s, l = (max + min) / 2;
138
+ if (max === min) { h = s = 0; }
139
+ else {
140
+ const d = max - min;
141
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
142
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
143
+ else if (max === g) h = ((b - r) / d + 2) / 6;
144
+ else h = ((r - g) / d + 4) / 6;
145
+ }
146
+ return [h * 360, s * 100, l * 100];
147
+ }
148
+
149
+ function hslToHex(h, s, l) {
150
+ h = ((h % 360) + 360) % 360;
151
+ s = Math.max(0, Math.min(100, s)) / 100;
152
+ l = Math.max(0, Math.min(100, l)) / 100;
153
+ const a = s * Math.min(l, 1 - l);
154
+ const f = n => { const k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); };
155
+ const toHex = v => Math.round(v * 255).toString(16).padStart(2, '0');
156
+ return '#' + toHex(f(0)) + toHex(f(8)) + toHex(f(4));
157
+ }
158
+
159
+ function deriveSyntaxColors(colors) {
160
+ const [fgH, fgS, fgL] = hexToHSL(colors.fg);
161
+ const [bgH, bgS, bgL] = hexToHSL(colors.bg);
162
+ const [redH, redS, redL] = hexToHSL(colors.red || '#e06c75');
163
+ const isDark = bgL < 50;
164
+ const codeBgL = isDark ? Math.max(bgL - 4, 0) : Math.min(bgL + 4, 100);
165
+ return {
166
+ bg: hslToHex(bgH, bgS, codeBgL),
167
+ fg: colors.fg,
168
+ keyword: hslToHex((redH + 280) % 360, Math.min(redS + 10, 80), isDark ? 70 : 45),
169
+ string: hslToHex(40, Math.min(fgS + 20, 70), isDark ? 72 : 42),
170
+ comment: hslToHex(fgH, Math.max(fgS - 20, 5), isDark ? (fgL * 0.5 + bgL * 0.5) : (fgL * 0.5 + bgL * 0.5)),
171
+ function: hslToHex(210, Math.min(fgS + 20, 75), isDark ? 70 : 45),
172
+ // Extra token colors for richer highlighting
173
+ number: hslToHex(20, Math.min(fgS + 15, 65), isDark ? 68 : 48),
174
+ builtin: hslToHex(180, Math.min(fgS + 15, 60), isDark ? 65 : 40),
175
+ variable: hslToHex((fgH + 30) % 360, Math.min(fgS + 5, 60), isDark ? fgL : fgL),
176
+ params: hslToHex(fgH, Math.max(fgS - 5, 10), isDark ? Math.min(fgL + 8, 85) : Math.max(fgL - 8, 25)),
177
+ };
178
+ }
179
+
180
+ // Advanced picker key → CSS variable mapping
181
+ const ADV_KEYS = [
182
+ { key: 'userBubbleBg', css: '--user-bubble-bg', label: 'User Chat Bubble', group: 'Chat Bubbles' },
183
+ { key: 'aiBubbleBg', css: '--ai-bubble-bg', label: 'AI Chat Bubble', group: 'Chat Bubbles' },
184
+ { key: 'bubbleBorder', css: '--bubble-border', label: 'Border Chat Bubble', group: 'Chat Bubbles' },
185
+ { key: 'sidebarBg', css: '--sidebar-bg', label: 'Sidebar Bg', group: 'Sidebar' },
186
+ { key: 'brandColor', css: '--brand-color', label: 'Zero Logo', group: 'Sidebar' },
187
+ { key: 'hamburgerColor', css: '--hamburger-color', label: 'Hamburger Menu', group: 'Sidebar' },
188
+ { key: 'inputBg', css: '--input-bg', label: 'Input Bg', group: 'Chat Input / Prompt Area' },
189
+ { key: 'inputBorder', css: '--input-border', label: 'Input Border', group: 'Chat Input / Prompt Area' },
190
+ { key: 'sendBtnBg', css: '--send-btn-bg', label: 'Send Btn', group: 'Chat Input / Prompt Area' },
191
+ { key: 'sendBtnHover', css: '--send-btn-hover', label: 'Send Hover', group: 'Chat Input / Prompt Area' },
192
+ { key: 'codeBg', css: '--code-bg', label: 'Code Bg', group: 'Code Blocks' },
193
+ { key: 'codeFg', css: '--code-fg', label: 'Code Text', group: 'Code Blocks' },
194
+ { key: 'toggleActive', css: '--toggle-active', label: 'Toggle On', group: 'Controls' },
195
+ ];
196
+
197
+ function computeAdvancedDefaults(colors) {
198
+ const syn = deriveSyntaxColors(colors);
199
+ const red = colors.red || '#e06c75';
200
+ return {
201
+ userBubbleBg: colors.bg,
202
+ aiBubbleBg: colors.panel,
203
+ bubbleBorder: colors.border,
204
+ sidebarBg: colors.panel,
205
+ brandColor: red,
206
+ hamburgerColor: colors.fg,
207
+ inputBg: colors.panel,
208
+ inputBorder: colors.border,
209
+ sendBtnBg: red,
210
+ sendBtnHover: red,
211
+ codeBg: syn.bg,
212
+ codeFg: syn.fg,
213
+ toggleActive: red,
214
+ };
215
+ }
216
+
217
+ function generateHarmonyColors(accentHex, harmonyType, mode) {
218
+ const [h, s] = hexToHSL(accentHex);
219
+ const isDark = mode === 'dark';
220
+
221
+ let bgH, bgS, bgL, fgS, fgL, panelL, borderH, borderS, borderL;
222
+
223
+ if (harmonyType === 'complementary') {
224
+ bgH = h; bgS = Math.max(s * 0.15, 3);
225
+ bgL = isDark ? 13 : 95; fgL = isDark ? 85 : 15; fgS = Math.max(s * 0.2, 5);
226
+ panelL = isDark ? 8 : 98;
227
+ borderH = h; borderS = Math.max(s * 0.25, 8); borderL = isDark ? 28 : 75;
228
+ } else if (harmonyType === 'analogous') {
229
+ bgH = (h - 30 + 360) % 360; bgS = Math.max(s * 0.12, 3);
230
+ bgL = isDark ? 14 : 95; fgL = isDark ? 84 : 18; fgS = Math.max(s * 0.15, 5);
231
+ panelL = isDark ? 9 : 97;
232
+ borderH = (h + 30) % 360; borderS = Math.max(s * 0.3, 10); borderL = isDark ? 30 : 72;
233
+ } else if (harmonyType === 'triadic') {
234
+ bgH = (h + 240) % 360; bgS = Math.max(s * 0.1, 2);
235
+ bgL = isDark ? 13 : 96; fgL = isDark ? 86 : 14; fgS = Math.max(s * 0.18, 5);
236
+ panelL = isDark ? 8 : 99;
237
+ borderH = (h + 120) % 360; borderS = Math.max(s * 0.2, 8); borderL = isDark ? 28 : 74;
238
+ } else { // monochromatic
239
+ bgH = h; bgS = Math.max(s * 0.08, 2);
240
+ bgL = isDark ? 12 : 96; fgL = isDark ? 87 : 13; fgS = Math.max(s * 0.15, 5);
241
+ panelL = isDark ? 7 : 99;
242
+ borderH = h; borderS = Math.max(s * 0.2, 6); borderL = isDark ? 26 : 76;
243
+ }
244
+
245
+ return {
246
+ bg: hslToHex(bgH, bgS, bgL),
247
+ fg: hslToHex(h, fgS, fgL),
248
+ panel: hslToHex(bgH, bgS * 0.6, panelL),
249
+ border: hslToHex(borderH, borderS, borderL),
250
+ red: accentHex,
251
+ };
252
+ }
253
+
254
+ export function applyColors(colors) {
255
+ const s = document.documentElement.style;
256
+ s.setProperty('--bg', colors.bg);
257
+ s.setProperty('--fg', colors.fg);
258
+ s.setProperty('--panel', colors.panel);
259
+ s.setProperty('--border', colors.border);
260
+ if (colors.red) s.setProperty('--red', colors.red);
261
+
262
+ // Keep the mobile browser toolbar / status bar matched to the theme bg
263
+ // (same as the early head-script does on first paint).
264
+ const _mtc = document.querySelector('meta[name="theme-color"]');
265
+ if (_mtc && colors.bg) _mtc.setAttribute('content', colors.bg);
266
+
267
+ // Derive and apply syntax highlighting colors
268
+ const syn = deriveSyntaxColors(colors);
269
+ s.setProperty('--hl-bg', syn.bg);
270
+ s.setProperty('--hl-fg', syn.fg);
271
+ s.setProperty('--hl-keyword', syn.keyword);
272
+ s.setProperty('--hl-string', syn.string);
273
+ s.setProperty('--hl-comment', syn.comment);
274
+ s.setProperty('--hl-function', syn.function);
275
+ s.setProperty('--hl-number', syn.number);
276
+ s.setProperty('--hl-builtin', syn.builtin);
277
+ s.setProperty('--hl-variable', syn.variable);
278
+ s.setProperty('--hl-params', syn.params);
279
+
280
+ // Apply advanced overrides (or defaults)
281
+ const adv = colors.advanced || {};
282
+ const defaults = computeAdvancedDefaults(colors);
283
+ for (const { key, css } of ADV_KEYS) {
284
+ s.setProperty(css, adv[key] || defaults[key]);
285
+ }
286
+
287
+ // Update favicon to match theme accent color
288
+ _updateFavicon(colors.red || '#e06c75');
289
+ }
290
+
291
+ // Per-route SVG shape registry — kept in sync with the inline favicon
292
+ // script in index.html so a theme change keeps the route icon, not the
293
+ // default boat. Returns the inner SVG markup colored with `fg`.
294
+ const _ROUTE_FAVICON_SHAPES = {
295
+ '/calendar':
296
+ "<rect x='4' y='6' width='24' height='22' rx='2' fill='none' stroke='__C__' stroke-width='2.5'/>" +
297
+ "<line x1='4' y1='12' x2='28' y2='12' stroke='__C__' stroke-width='2.5'/>" +
298
+ "<line x1='10' y1='3' x2='10' y2='9' stroke='__C__' stroke-width='2.5' stroke-linecap='round'/>" +
299
+ "<line x1='22' y1='3' x2='22' y2='9' stroke='__C__' stroke-width='2.5' stroke-linecap='round'/>",
300
+ '/notes':
301
+ "<rect x='6' y='4' width='20' height='24' rx='2' fill='none' stroke='__C__' stroke-width='2.5'/>" +
302
+ "<line x1='10' y1='10' x2='22' y2='10' stroke='__C__' stroke-width='2'/>" +
303
+ "<line x1='10' y1='15' x2='22' y2='15' stroke='__C__' stroke-width='2'/>" +
304
+ "<line x1='10' y1='20' x2='18' y2='20' stroke='__C__' stroke-width='2'/>",
305
+ '/cookbook':
306
+ "<path d='M5 8 L5 26 A2 2 0 0 0 7 28 L25 28 A2 2 0 0 0 27 26 L27 8' fill='none' stroke='__C__' stroke-width='2.5' stroke-linejoin='round'/>" +
307
+ "<path d='M9 4 L23 4 L23 8 L9 8 Z' fill='none' stroke='__C__' stroke-width='2.5' stroke-linejoin='round'/>" +
308
+ "<line x1='11' y1='14' x2='21' y2='14' stroke='__C__' stroke-width='2'/>" +
309
+ "<line x1='11' y1='19' x2='17' y2='19' stroke='__C__' stroke-width='2'/>",
310
+ '/email':
311
+ "<rect x='4' y='7' width='24' height='18' rx='2' fill='none' stroke='__C__' stroke-width='2.5'/>" +
312
+ "<path d='M5 9 L16 17 L27 9' fill='none' stroke='__C__' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/>",
313
+ '/memory':
314
+ "<path d='M16 5 C10 5 6 9 6 14 C6 19 10 21 11 22 L11 26 L21 26 L21 22 C22 21 26 19 26 14 C26 9 22 5 16 5 Z' fill='none' stroke='__C__' stroke-width='2.5' stroke-linejoin='round'/>" +
315
+ "<line x1='12' y1='28' x2='20' y2='28' stroke='__C__' stroke-width='2'/>",
316
+ '/gallery':
317
+ "<rect x='4' y='4' width='24' height='24' rx='2' fill='none' stroke='__C__' stroke-width='2.5'/>" +
318
+ "<circle cx='12' cy='12' r='2.5' fill='__C__'/>" +
319
+ "<path d='M4 22 L11 16 L18 21 L23 17 L28 22' fill='none' stroke='__C__' stroke-width='2.5' stroke-linejoin='round'/>",
320
+ '/tasks':
321
+ "<rect x='4' y='4' width='24' height='24' rx='3' fill='none' stroke='__C__' stroke-width='2.5'/>" +
322
+ "<path d='M9 16 L14 21 L23 11' fill='none' stroke='__C__' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/>",
323
+ '/library':
324
+ "<rect x='5' y='5' width='5' height='22' rx='1' fill='none' stroke='__C__' stroke-width='2.5'/>" +
325
+ "<rect x='13' y='5' width='5' height='22' rx='1' fill='none' stroke='__C__' stroke-width='2.5'/>" +
326
+ "<rect x='21' y='8' width='6' height='19' rx='1' fill='none' stroke='__C__' stroke-width='2.5' transform='rotate(8 24 17)'/>",
327
+ };
328
+
329
+ function _updateFavicon(fg) {
330
+ const path = (window.location.pathname || '').toLowerCase();
331
+ const routeShape = _ROUTE_FAVICON_SHAPES[path];
332
+ let svg;
333
+ if (routeShape) {
334
+ svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>${routeShape.split('__C__').join(fg)}</svg>`;
335
+ } else {
336
+ svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 4L16 22L6 22Z' fill='${fg}'/><path d='M16 8L16 22L24 22Z' fill='${fg}' opacity='0.6'/><path d='M4 24Q10 20 16 24Q22 28 28 24' stroke='${fg}' stroke-width='2.5' fill='none' stroke-linecap='round'/></svg>`;
337
+ }
338
+ const href = 'data:image/svg+xml,' + encodeURIComponent(svg);
339
+ let link = document.querySelector("link[rel='icon']");
340
+ if (!link) {
341
+ link = document.createElement('link');
342
+ link.rel = 'icon';
343
+ link.type = 'image/svg+xml';
344
+ document.head.appendChild(link);
345
+ }
346
+ link.href = href;
347
+ let apple = document.querySelector("link[rel='apple-touch-icon']");
348
+ if (!apple) {
349
+ apple = document.createElement('link');
350
+ apple.rel = 'apple-touch-icon';
351
+ document.head.appendChild(apple);
352
+ }
353
+ apple.href = href;
354
+ }
355
+
356
+ // Cache of discovered custom fonts: { "Family Name": [ {file, url, format} ] }
357
+ let _customFonts = {};
358
+ // Track which custom font families already have @font-face injected
359
+ const _injectedFonts = new Set();
360
+
361
+ function _injectFontFace(familyName, variants) {
362
+ if (_injectedFonts.has(familyName)) return;
363
+ const style = document.createElement('style');
364
+ style.dataset.customFont = familyName;
365
+ const fmtMap = { woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype' };
366
+ for (const v of variants) {
367
+ style.textContent += `@font-face { font-family: '${familyName}'; src: url('${v.url}') format('${fmtMap[v.format] || v.format}'); font-display: swap; }\n`;
368
+ }
369
+ document.head.appendChild(style);
370
+ _injectedFonts.add(familyName);
371
+ }
372
+
373
+ export function applyFontDensity(font, density) {
374
+ const f = font || DEFAULT_FONT;
375
+ const d = density || DEFAULT_DENSITY;
376
+ let family = FONT_MAP[f];
377
+ if (!family && _customFonts[f]) {
378
+ // It's a custom font from the local folder
379
+ _injectFontFace(f, _customFonts[f]);
380
+ family = "'" + f + "', sans-serif";
381
+ }
382
+ if (!family) family = FONT_MAP[DEFAULT_FONT];
383
+ document.documentElement.style.setProperty('--font-family', family);
384
+ document.documentElement.classList.remove('density-compact', 'density-spacious');
385
+ if (d !== 'comfortable') document.documentElement.classList.add('density-' + d);
386
+ }
387
+
388
+ const _BG_CLASSES = ['bg-pattern-dots',
389
+ 'bg-pattern-synapse', 'bg-pattern-rain', 'bg-pattern-constellations',
390
+ 'bg-pattern-perlin-flow',
391
+ 'bg-pattern-petals', 'bg-pattern-sparkles', 'bg-pattern-embers'];
392
+ const _CANVAS_PATTERNS = { synapse: _initSynapse, rain: _initRain, constellations: _initConstellations,
393
+ 'perlin-flow': _initPerlinFlow,
394
+ petals: _initPetals, sparkles: _initSparkles, embers: _initEmbers };
395
+
396
+ export function applyBgEffectColor(color) {
397
+ document.documentElement.style.setProperty('--bg-effect-color', color || '');
398
+ }
399
+
400
+ export function applyBgEffectIntensity(v) {
401
+ // v is 0..1. Default 1 (full intensity) when missing.
402
+ const n = (v === undefined || v === null || isNaN(v)) ? 1 : Math.max(0, Math.min(1, Number(v)));
403
+ document.documentElement.style.setProperty('--bg-effect-intensity', String(n));
404
+ }
405
+
406
+ export function applyBgEffectSize(v) {
407
+ // v is a multiplier 0.3..2.5. Default 1 when missing.
408
+ const n = (v === undefined || v === null || isNaN(v)) ? 1 : Math.max(0.2, Math.min(3, Number(v)));
409
+ document.documentElement.style.setProperty('--bg-effect-size', String(n));
410
+ }
411
+
412
+ /** Toggle the global "frosted glass" look — applies a translucent + blurred
413
+ * treatment to every panel, sidebar, modal, dropdown, and popover via CSS
414
+ * rules scoped to `body.theme-frosted`. */
415
+ export function applyFrostedGlass(on) {
416
+ document.body.classList.toggle('theme-frosted', !!on);
417
+ }
418
+
419
+ // Read current size multiplier for JS effects (canvas-based).
420
+ function _getEffectSize() {
421
+ const v = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--bg-effect-size'));
422
+ return isNaN(v) ? 1 : v;
423
+ }
424
+
425
+ // Patterns where the intensity/size sliders have no visible effect.
426
+ const _STATIC_PATTERNS = new Set(['none', 'dots']);
427
+
428
+ export function applyBgPattern(pattern) {
429
+ const p = pattern || 'none';
430
+ document.body.classList.remove(..._BG_CLASSES);
431
+ // Clean up any canvas backgrounds
432
+ document.querySelectorAll('#synapse-canvas, #rain-canvas, #constellations-canvas, #perlin-flow-canvas, #petals-canvas, #sparkles-canvas, #embers-canvas').forEach(c => c.remove());
433
+ if (p !== 'none') document.body.classList.add('bg-pattern-' + p);
434
+ if (_CANVAS_PATTERNS[p]) _CANVAS_PATTERNS[p]();
435
+ // Hide sliders that do nothing on static patterns.
436
+ const hide = _STATIC_PATTERNS.has(p);
437
+ const ig = document.getElementById('theme-bg-intensity-group');
438
+ const sg = document.getElementById('theme-bg-size-group');
439
+ if (ig) ig.style.display = hide ? 'none' : '';
440
+ if (sg) sg.style.display = hide ? 'none' : '';
441
+ }
442
+
443
+ export function getSaved() {
444
+ const obj = Storage.getJSON(LS_KEY, null);
445
+ // Migration: 'chatgpt' preset was renamed to 'gpt'
446
+ if (obj && obj.name === 'chatgpt') obj.name = 'gpt';
447
+ // Migration: 'sakura' preset was renamed to 'ume'
448
+ if (obj && obj.name === 'sakura') obj.name = 'ume';
449
+ return obj;
450
+ }
451
+
452
+ export function save(name, colors, opts) {
453
+ const obj = { name, colors };
454
+ if (opts) {
455
+ if (opts.font && opts.font !== DEFAULT_FONT) obj.font = opts.font;
456
+ if (opts.density && opts.density !== DEFAULT_DENSITY) obj.density = opts.density;
457
+ if (opts.bgPattern && opts.bgPattern !== 'none') obj.bgPattern = opts.bgPattern;
458
+ if (opts.bgEffectColor) obj.bgEffectColor = opts.bgEffectColor;
459
+ if (opts.bgEffectIntensity !== undefined && opts.bgEffectIntensity !== 1) obj.bgEffectIntensity = opts.bgEffectIntensity;
460
+ if (opts.bgEffectSize !== undefined && opts.bgEffectSize !== 1) obj.bgEffectSize = opts.bgEffectSize;
461
+ if (opts.frosted) obj.frosted = true;
462
+ }
463
+ Storage.setJSON(LS_KEY, obj);
464
+ _syncToServer(obj);
465
+ }
466
+
467
+ function _syncToServer(obj) {
468
+ try {
469
+ fetch('/api/prefs/theme', {
470
+ method: 'PUT',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ credentials: 'same-origin',
473
+ body: JSON.stringify({ value: obj }),
474
+ }).catch(e => console.warn('Theme sync failed:', e));
475
+ } catch (e) { console.warn('Theme sync error:', e); }
476
+ }
477
+
478
+ async function _loadFromServer() {
479
+ try {
480
+ const res = await fetch('/api/prefs/theme', { credentials: 'same-origin' });
481
+ const data = await res.json();
482
+ return data.value || null;
483
+ } catch { return null; }
484
+ }
485
+
486
+
487
+ function syncPickers(colors) {
488
+ document.getElementById('clr-bg').value = colors.bg;
489
+ document.getElementById('clr-fg').value = colors.fg;
490
+ document.getElementById('clr-panel').value = colors.panel;
491
+ document.getElementById('clr-border').value = colors.border;
492
+ document.getElementById('clr-red').value = colors.red;
493
+ syncAdvancedPickers(colors);
494
+ }
495
+
496
+
497
+ function syncAdvancedPickers(colors) {
498
+ const adv = colors.advanced || {};
499
+ const defaults = computeAdvancedDefaults(colors);
500
+ for (const { key } of ADV_KEYS) {
501
+ const el = document.getElementById('adv-' + key);
502
+ if (el) el.value = adv[key] || defaults[key];
503
+ }
504
+ }
505
+
506
+ export function initThemeUI() {
507
+ const themePopup = document.getElementById('theme-popup');
508
+ const themeHeader = document.getElementById('theme-popup-header');
509
+ if (themePopup && themeHeader && !themePopup.dataset.dragWired) {
510
+ themePopup.dataset.dragWired = '1';
511
+ makeDraggable(themePopup, themeHeader);
512
+ }
513
+
514
+ // Attach the in-house color picker to every color input in the theme panel.
515
+ // Safe to call repeatedly — the picker marks inputs it's already wrapped.
516
+ try { initColorPickers(document); } catch (e) { console.warn('Color picker init failed', e); }
517
+
518
+ // Populate the advanced color inputs with their computed defaults right now.
519
+ // BUG FIX: without this, untouched inputs sat at the browser-default `#000000`
520
+ // until the user clicked a swatch; the first edit of ANY advanced input then
521
+ // tripped readAdvanced() into storing every other `#000000` as an override —
522
+ // e.g. editing Chat Bubble Border turned Sidebar Bg pure black.
523
+ try {
524
+ const saved = getSaved();
525
+ if (saved && saved.colors) {
526
+ syncAdvancedPickers(saved.colors);
527
+ }
528
+ } catch (e) { console.warn('syncAdvancedPickers on init failed', e); }
529
+ // Wire up theme tabs (Themes / Customize)
530
+ const themeTabs = document.getElementById('theme-tabs');
531
+ if (themeTabs) {
532
+ themeTabs.addEventListener('click', (e) => {
533
+ const tab = e.target.closest('.admin-tab');
534
+ if (!tab) return;
535
+ const targetId = tab.dataset.tab;
536
+ themeTabs.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
537
+ tab.classList.add('active');
538
+ document.querySelectorAll('.theme-tab-panel').forEach(p => p.style.display = 'none');
539
+ const panel = document.getElementById(targetId);
540
+ if (panel) panel.style.display = '';
541
+ // Show the opacity slider only on the Customize tab.
542
+ const opWrap = document.getElementById('theme-opacity-wrap');
543
+ if (opWrap) opWrap.classList.toggle('hidden', targetId !== 'theme-tab-customize');
544
+ // Restore full opacity / blur on every other tab. The slider's effect
545
+ // is meant to be Customize-only — peeking at the page while tweaking
546
+ // colors — so swapping back to Themes (or Schedule) should look
547
+ // exactly like the rest of the app's modals again.
548
+ const popup = document.getElementById('theme-popup');
549
+ if (popup) {
550
+ if (targetId === 'theme-tab-customize') {
551
+ // Reapply the Peek toggle's current state.
552
+ if (opWrap && opWrap._apply) opWrap._apply();
553
+ } else {
554
+ popup.style.removeProperty('opacity');
555
+ popup.style.removeProperty('background');
556
+ popup.style.removeProperty('backdrop-filter');
557
+ popup.style.removeProperty('-webkit-backdrop-filter');
558
+ popup.querySelectorAll('.admin-card').forEach(c => {
559
+ c.style.removeProperty('background');
560
+ c.style.removeProperty('backdrop-filter');
561
+ c.style.removeProperty('-webkit-backdrop-filter');
562
+ });
563
+ }
564
+ }
565
+ });
566
+ }
567
+
568
+
569
+ // Wire the "Peek" opacity toggle — fades the theme modal so the user can
570
+ // see the page behind it while tweaking colors on the Customize tab.
571
+ // On/off only (no slider); starts off, lives in the title bar, and is
572
+ // cleared when the user swaps to Themes / Schedule.
573
+ (function _wireOpacityToggle() {
574
+ const toggle = document.getElementById('theme-opacity-wrap');
575
+ const popup = document.getElementById('theme-popup');
576
+ if (!toggle || !popup || toggle.dataset.bound === '1') return;
577
+ toggle.dataset.bound = '1';
578
+ const PEEK = 55; // % opacity when peeking
579
+ const apply = (on) => {
580
+ const cards = popup.querySelectorAll('.admin-card');
581
+ if (on) {
582
+ // Fade the modal + each inner card via color-mix — never element
583
+ // opacity, so text, controls and swatches stay sharp.
584
+ const bgMix = `color-mix(in srgb, var(--bg) ${PEEK}%, transparent)`;
585
+ const panelMix = `color-mix(in srgb, var(--panel) ${PEEK}%, transparent)`;
586
+ popup.style.setProperty('background', bgMix, 'important');
587
+ popup.style.setProperty('backdrop-filter', 'none', 'important');
588
+ popup.style.setProperty('-webkit-backdrop-filter', 'none', 'important');
589
+ popup.style.removeProperty('opacity');
590
+ cards.forEach(c => {
591
+ c.style.setProperty('background', panelMix, 'important');
592
+ c.style.setProperty('backdrop-filter', 'none', 'important');
593
+ c.style.setProperty('-webkit-backdrop-filter', 'none', 'important');
594
+ });
595
+ } else {
596
+ popup.style.removeProperty('opacity');
597
+ popup.style.removeProperty('background');
598
+ popup.style.removeProperty('backdrop-filter');
599
+ popup.style.removeProperty('-webkit-backdrop-filter');
600
+ cards.forEach(c => {
601
+ c.style.removeProperty('background');
602
+ c.style.removeProperty('backdrop-filter');
603
+ c.style.removeProperty('-webkit-backdrop-filter');
604
+ });
605
+ }
606
+ };
607
+ // Expose so the tab-switch handler can reapply when returning to Customize.
608
+ toggle._apply = () => apply(toggle.classList.contains('active'));
609
+ toggle.addEventListener('click', () => {
610
+ const on = !toggle.classList.contains('active');
611
+ toggle.classList.toggle('active', on);
612
+ toggle.setAttribute('aria-pressed', on ? 'true' : 'false');
613
+ apply(on);
614
+ });
615
+ })();
616
+
617
+ const grid = document.getElementById('themeGrid');
618
+ if (!grid) return;
619
+
620
+ const saved = getSaved();
621
+ const activeName = saved ? saved.name : DEFAULT_THEME;
622
+ const customThemes = _loadCustomThemes();
623
+
624
+ // Render preset swatches
625
+ grid.innerHTML = Object.entries(THEMES).map(([name, c]) => `
626
+ <div class="theme-swatch${name === activeName ? ' active' : ''}" data-theme="${name}">
627
+ <div class="theme-swatch-colors">
628
+ <span style="background:${c.bg}"></span>
629
+ <span style="background:${c.panel}"></span>
630
+ <span style="background:${c.fg}"></span>
631
+ <span style="background:${c.red}"></span>
632
+ </div>
633
+ ${name === 'dark' ? 'original' : (name === 'gpt' ? 'GPT' : name)}
634
+ </div>
635
+ `).join('');
636
+
637
+ // Render custom theme swatches into separate card
638
+ const userGrid = document.getElementById('themeUserGrid');
639
+ const userCard = document.getElementById('themeUserCard');
640
+ const customEntries = Object.entries(customThemes);
641
+ if (customEntries.length > 0 && userGrid && userCard) {
642
+ userCard.style.display = '';
643
+ userGrid.innerHTML = customEntries.map(([name, c]) => `
644
+ <div class="theme-swatch${name === activeName ? ' active' : ''}" data-theme="${name}" data-custom="1">
645
+ <div class="theme-swatch-colors">
646
+ <span style="background:${c.bg}"></span>
647
+ <span style="background:${c.panel}"></span>
648
+ <span style="background:${c.fg}"></span>
649
+ <span style="background:${c.red}"></span>
650
+ </div>
651
+ <span class="theme-swatch-name">${name}</span>
652
+ <button type="button" class="theme-delete-btn" data-delete="${name}" title="Delete theme"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
653
+ </div>
654
+ `).join('');
655
+ } else if (userCard) {
656
+ userCard.style.display = 'none';
657
+ }
658
+
659
+ // Helper: save with current font/density/bgPattern from UI selects
660
+ function _getOpts() {
661
+ const opts = {};
662
+ const fs = document.getElementById('theme-font-select');
663
+ const ds = document.getElementById('theme-density-select');
664
+ const ps = document.getElementById('theme-bg-pattern-select');
665
+ const ec = document.getElementById('theme-bg-effect-color');
666
+ const es = document.getElementById('theme-bg-intensity');
667
+ const sz = document.getElementById('theme-bg-size');
668
+ if (fs) opts.font = fs.value;
669
+ if (ds) opts.density = ds.value;
670
+ if (ps) opts.bgPattern = ps.value;
671
+ if (ec) opts.bgEffectColor = ec.value;
672
+ if (es) opts.bgEffectIntensity = parseFloat(es.value) / 100;
673
+ if (sz) opts.bgEffectSize = parseFloat(sz.value) / 100;
674
+ const fr = document.getElementById('theme-frosted-toggle');
675
+ if (fr) opts.frosted = !!fr.checked;
676
+ return opts;
677
+ }
678
+ function _saveFull(name, colors) { save(name, colors, _getOpts()); }
679
+
680
+ // Click handlers for all swatches (preset + custom) across both grids
681
+ const allGrids = [grid, userGrid].filter(Boolean);
682
+ function clearAllActive() { allGrids.forEach(g => g.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('active'))); }
683
+ allGrids.forEach(g => {
684
+ g.querySelectorAll('.theme-swatch').forEach(sw => {
685
+ sw.addEventListener('click', (e) => {
686
+ if (e.target.closest('.theme-delete-btn')) return;
687
+ const name = sw.dataset.theme;
688
+ const colors = sw.dataset.custom ? customThemes[name] : THEMES[name];
689
+ if (!colors) return;
690
+ applyColors(colors);
691
+ clearAllActive();
692
+ sw.classList.add('active');
693
+ syncPickers(colors);
694
+ const ct = sw.dataset.custom ? customThemes[name] : null;
695
+ const f = ct && ct.font ? ct.font : DEFAULT_FONT;
696
+ const d = ct && ct.density ? ct.density : DEFAULT_DENSITY;
697
+ const p = ct && ct.bgPattern ? ct.bgPattern : (THEME_DEFAULT_PATTERN[name] || 'none');
698
+ const ec = ct && ct.bgEffectColor ? ct.bgEffectColor : (THEME_DEFAULT_EFFECT_COLOR[name] || '');
699
+ const ei = (ct && ct.bgEffectIntensity !== undefined) ? ct.bgEffectIntensity : (THEME_DEFAULT_INTENSITY[name] !== undefined ? THEME_DEFAULT_INTENSITY[name] : 1);
700
+ const sz = (ct && ct.bgEffectSize !== undefined) ? ct.bgEffectSize : 1;
701
+ const fr = (ct && ct.frosted !== undefined)
702
+ ? !!ct.frosted
703
+ : (THEME_DEFAULT_FROSTED[name] === true);
704
+ applyFontDensity(f, d);
705
+ applyBgEffectColor(ec);
706
+ applyBgEffectIntensity(ei);
707
+ applyBgEffectSize(sz);
708
+ applyFrostedGlass(fr);
709
+ applyBgPattern(p);
710
+ const fs = document.getElementById('theme-font-select');
711
+ const ds = document.getElementById('theme-density-select');
712
+ const ps = document.getElementById('theme-bg-pattern-select');
713
+ const ecs = document.getElementById('theme-bg-effect-color');
714
+ const eis = document.getElementById('theme-bg-intensity');
715
+ const szs = document.getElementById('theme-bg-size');
716
+ const frs = document.getElementById('theme-frosted-toggle');
717
+ if (fs) fs.value = f;
718
+ if (ds) ds.value = d;
719
+ if (ps) ps.value = p;
720
+ if (ecs) ecs.value = ec || colors.fg || '#9cdef2';
721
+ if (eis) eis.value = String(Math.round(ei * 100));
722
+ if (szs) szs.value = String(Math.round(sz * 100));
723
+ if (frs) frs.checked = fr;
724
+ save(name, colors, { font: f, density: d, bgPattern: p, bgEffectColor: ec, bgEffectIntensity: ei, bgEffectSize: sz, frosted: fr });
725
+ });
726
+ });
727
+ g.querySelectorAll('.theme-delete-btn').forEach(btn => {
728
+ btn.addEventListener('click', async (e) => {
729
+ e.stopPropagation();
730
+ const name = btn.dataset.delete;
731
+ if (uiModule && uiModule.styledConfirm) {
732
+ if (!await uiModule.styledConfirm(`Delete theme "${name}"?`, { confirmText: 'Delete', danger: true })) return;
733
+ }
734
+ deleteCustomTheme(name);
735
+ });
736
+ });
737
+ });
738
+
739
+ // Init color pickers from current theme and apply syntax colors
740
+ const currentColors = saved ? saved.colors : THEMES[DEFAULT_THEME];
741
+ applyColors(currentColors);
742
+ syncPickers(currentColors);
743
+
744
+ // Reference colors for per-picker reset (the theme you started from)
745
+ const refName = saved ? saved.name : DEFAULT_THEME;
746
+ const refColors = THEMES[refName] || customThemes[refName] || currentColors;
747
+ const refDefaults = computeAdvancedDefaults(refColors);
748
+
749
+ // Sync reset button visibility based on whether color differs from reference
750
+ function syncResetButtons() {
751
+ document.querySelectorAll('.color-reset-btn[data-reset]').forEach(btn => {
752
+ const key = btn.dataset.reset;
753
+ const picker = document.getElementById(pickerIds[key]);
754
+ if (picker && refColors[key]) {
755
+ btn.classList.toggle('changed', picker.value.toLowerCase() !== refColors[key].toLowerCase());
756
+ }
757
+ });
758
+ document.querySelectorAll('.color-reset-btn[data-reset-adv]').forEach(btn => {
759
+ const key = btn.dataset.resetAdv;
760
+ const picker = document.getElementById('adv-' + key);
761
+ const ref = refDefaults[key] || '';
762
+ if (picker && ref) {
763
+ btn.classList.toggle('changed', picker.value.toLowerCase() !== ref.toLowerCase());
764
+ }
765
+ });
766
+ }
767
+
768
+ // Color picker live updates.
769
+ // NOTE: do NOT clone the input. attachColorPicker installed a value-getter
770
+ // override + a mousedown handler on this exact element; cloning would orphan
771
+ // both. Use a one-time bind flag instead.
772
+ const pickerIds = { bg: 'clr-bg', fg: 'clr-fg', panel: 'clr-panel', border: 'clr-border', red: 'clr-red' };
773
+ Object.entries(pickerIds).forEach(([key, id]) => {
774
+ const el = document.getElementById(id);
775
+ if (!el || el.dataset.themeBound === '1') return;
776
+ el.dataset.themeBound = '1';
777
+ el.addEventListener('input', () => {
778
+ // Capture the OLD basic palette before we read the new picker values.
779
+ // Used below to decide which advanced pickers carry a real user-set
780
+ // override (value differs from the OLD computed default) vs. ones
781
+ // that are just stale-default and should auto-refresh.
782
+ const _oldColors = {};
783
+ Object.entries(pickerIds).forEach(([k, pid]) => {
784
+ // Picker value HAS already changed (input fired) for the one the
785
+ // user touched. For that one, reading the current value gives the
786
+ // NEW color, which is fine — _oldDefaults uses the rest. We use
787
+ // computeAdvancedDefaults({...new}) once for the new defaults, and
788
+ // the CSS variables for the OLD defaults.
789
+ });
790
+ const _rs = getComputedStyle(document.documentElement);
791
+ _oldColors.bg = (_rs.getPropertyValue('--bg') || '').trim();
792
+ _oldColors.fg = (_rs.getPropertyValue('--fg') || '').trim();
793
+ _oldColors.panel = (_rs.getPropertyValue('--panel') || '').trim();
794
+ _oldColors.border = (_rs.getPropertyValue('--border')|| '').trim();
795
+ _oldColors.red = (_rs.getPropertyValue('--red') || '').trim();
796
+ const _oldDefaults = computeAdvancedDefaults(_oldColors);
797
+
798
+ const colors = {};
799
+ Object.entries(pickerIds).forEach(([k, pid]) => {
800
+ colors[k] = document.getElementById(pid).value;
801
+ });
802
+
803
+ // Build the advanced override map: only pickers whose value differs
804
+ // from the OLD default count as user-set. Untouched pickers (still
805
+ // matching the old default) get auto-updated to the NEW default so
806
+ // they keep tracking the basic palette (e.g. Send Btn follows Accent).
807
+ const _newDefaults = computeAdvancedDefaults(colors);
808
+ const _adv = {};
809
+ let _hasAdv = false;
810
+ // Normalize color strings to lowercase 6-char hex so getComputedStyle
811
+ // values (which keep whatever was set — could be #abc, #ABCDEF, or
812
+ // rgb()) compare correctly against color-input pickers (always
813
+ // #rrggbb lowercase). Without this, every advanced picker reads as
814
+ // "user-set" and we'd revert to the v161 bug.
815
+ const _norm = (raw) => {
816
+ let h = String(raw || '').trim().toLowerCase();
817
+ if (!h) return '';
818
+ // rgb(r,g,b) or rgba(r,g,b,a)
819
+ const rgb = h.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
820
+ if (rgb) {
821
+ const hx = n => Math.max(0, Math.min(255, parseInt(n, 10))).toString(16).padStart(2, '0');
822
+ return '#' + hx(rgb[1]) + hx(rgb[2]) + hx(rgb[3]);
823
+ }
824
+ if (h[0] !== '#') h = '#' + h;
825
+ // Expand #rgb → #rrggbb
826
+ if (/^#[0-9a-f]{3}$/.test(h)) {
827
+ return '#' + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
828
+ }
829
+ return h;
830
+ };
831
+ for (const { key } of ADV_KEYS) {
832
+ const pEl = document.getElementById('adv-' + key);
833
+ if (!pEl) continue;
834
+ if (_norm(pEl.value) !== _norm(_oldDefaults[key])) {
835
+ _adv[key] = pEl.value;
836
+ _hasAdv = true;
837
+ } else {
838
+ // Untouched — slide to the new default so it tracks the new palette.
839
+ pEl.value = _newDefaults[key];
840
+ }
841
+ }
842
+ if (_hasAdv) colors.advanced = _adv;
843
+ applyColors(colors);
844
+ // Auto-save: if the active theme is one of the user's custom themes,
845
+ // route changes back into it so renaming/reloading keeps the edits.
846
+ // Otherwise fall back to the transient 'custom' slot (existing behavior).
847
+ const _activeSaved = getSaved();
848
+ const _activeName = _activeSaved && _activeSaved.name;
849
+ const _customMap = _loadCustomThemes();
850
+ if (_activeName && _customMap && _customMap[_activeName]) {
851
+ // Preserve advanced/opts keys that aren't part of basic colors.
852
+ saveCustomTheme(_activeName, colors, {
853
+ font: _activeSaved.font, density: _activeSaved.density,
854
+ bgPattern: _activeSaved.bgPattern, bgEffectColor: _activeSaved.bgEffectColor,
855
+ bgEffectIntensity: _activeSaved.bgEffectIntensity,
856
+ bgEffectSize: _activeSaved.bgEffectSize,
857
+ });
858
+ _saveFull(_activeName, colors);
859
+ } else {
860
+ _saveFull('custom', colors);
861
+ }
862
+ _flashAutosaved();
863
+ grid.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('active'));
864
+ syncResetButtons();
865
+ });
866
+ });
867
+
868
+ // Save custom theme — inline input
869
+ const saveNameInputOld = document.getElementById('theme-save-name');
870
+ const saveGoBtnOld = document.getElementById('theme-save-go');
871
+ const saveError = document.getElementById('theme-save-error');
872
+ if (saveGoBtnOld && saveNameInputOld) {
873
+ const newGoBtn = saveGoBtnOld.cloneNode(true);
874
+ saveGoBtnOld.parentNode.replaceChild(newGoBtn, saveGoBtnOld);
875
+ const newNameInput = saveNameInputOld.cloneNode(true);
876
+ saveNameInputOld.parentNode.replaceChild(newNameInput, saveNameInputOld);
877
+ const doSave = () => {
878
+ saveError.style.display = 'none';
879
+ const name = newNameInput.value.trim();
880
+ if (!name) { saveError.textContent = 'Enter a name.'; saveError.style.display = 'block'; return; }
881
+ const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
882
+ if (!slug) { saveError.textContent = 'Invalid name.'; saveError.style.display = 'block'; return; }
883
+ if (THEMES[slug]) { saveError.textContent = 'Cannot overwrite a built-in theme.'; saveError.style.display = 'block'; return; }
884
+ const colors = {};
885
+ const pickerIds2 = { bg: 'clr-bg', fg: 'clr-fg', panel: 'clr-panel', border: 'clr-border', red: 'clr-red' };
886
+ Object.entries(pickerIds2).forEach(([k, pid]) => { colors[k] = document.getElementById(pid).value; });
887
+ const adv = {};
888
+ const defaults = computeAdvancedDefaults(colors);
889
+ let hasAdv = false;
890
+ for (const { key } of ADV_KEYS) {
891
+ const el = document.getElementById('adv-' + key);
892
+ if (el && el.value !== defaults[key]) { adv[key] = el.value; hasAdv = true; }
893
+ }
894
+ if (hasAdv) colors.advanced = adv;
895
+ const opts = _getOpts();
896
+ const result = saveCustomTheme(slug, colors, opts);
897
+ if (result === 'limit') { saveError.textContent = 'Max ' + MAX_CUSTOM_THEMES + ' custom themes. Delete one first.'; saveError.style.display = 'block'; return; }
898
+ save(slug, colors, opts);
899
+ newNameInput.value = '';
900
+ _flashAutosaved('Theme saved');
901
+ uiModule.showToast?.('Theme saved');
902
+ const prevHtml = newGoBtn.innerHTML;
903
+ newGoBtn.disabled = true;
904
+ newGoBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg><span>Saved</span>';
905
+ setTimeout(() => {
906
+ newGoBtn.disabled = false;
907
+ newGoBtn.innerHTML = prevHtml;
908
+ }, 1200);
909
+ };
910
+ newGoBtn.addEventListener('click', doSave);
911
+ newNameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSave(); });
912
+ }
913
+
914
+ // Reset button
915
+ const resetBtn = document.getElementById('theme-reset-btn');
916
+ if (resetBtn) {
917
+ const newReset = resetBtn.cloneNode(true);
918
+ resetBtn.parentNode.replaceChild(newReset, resetBtn);
919
+ newReset.addEventListener('click', () => {
920
+ Storage.remove(LS_KEY);
921
+ const colors = THEMES[DEFAULT_THEME];
922
+ applyColors(colors);
923
+ syncPickers(colors);
924
+ applyFontDensity(DEFAULT_FONT, DEFAULT_DENSITY);
925
+ applyBgPattern('none');
926
+ const fs = document.getElementById('theme-font-select');
927
+ const ds = document.getElementById('theme-density-select');
928
+ const ps = document.getElementById('theme-bg-pattern-select');
929
+ if (fs) fs.value = DEFAULT_FONT;
930
+ if (ds) ds.value = DEFAULT_DENSITY;
931
+ if (ps) ps.value = 'none';
932
+ grid.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('active'));
933
+ const darkSwatch = grid.querySelector('[data-theme="dark"]');
934
+ if (darkSwatch) darkSwatch.classList.add('active');
935
+ });
936
+ }
937
+
938
+ // Advanced section toggle
939
+ const advToggle = document.getElementById('theme-adv-toggle');
940
+ const advSection = document.getElementById('themeAdvanced');
941
+ if (advToggle && advSection) {
942
+ const newToggle = advToggle.cloneNode(true);
943
+ advToggle.parentNode.replaceChild(newToggle, advToggle);
944
+ newToggle.addEventListener('click', () => {
945
+ advSection.classList.toggle('hidden');
946
+ newToggle.classList.toggle('open');
947
+ // Re-scan rows so advanced color inputs get the hover-highlight too.
948
+ const root = document.getElementById('theme-tab-customize');
949
+ if (root) root.dataset.zoneBound = '';
950
+ initThemeZoneHighlight();
951
+ });
952
+ }
953
+ // Wire hover-highlights on color rows so the user sees which UI zone
954
+ // each input edits.
955
+ initThemeZoneHighlight();
956
+
957
+ // Advanced color picker live updates
958
+ function readCurrentColors() {
959
+ const pickerIds2 = { bg: 'clr-bg', fg: 'clr-fg', panel: 'clr-panel', border: 'clr-border', red: 'clr-red' };
960
+ const c = {};
961
+ Object.entries(pickerIds2).forEach(([k, pid]) => { c[k] = document.getElementById(pid).value; });
962
+ return c;
963
+ }
964
+
965
+ function readAdvanced() {
966
+ const adv = {};
967
+ const base = readCurrentColors();
968
+ const defaults = computeAdvancedDefaults(base);
969
+ let hasOverrides = false;
970
+ for (const { key } of ADV_KEYS) {
971
+ const el = document.getElementById('adv-' + key);
972
+ if (!el) continue;
973
+ const v = (el.value || '').toLowerCase();
974
+ // Skip empty or never-populated inputs so we don't accidentally store
975
+ // them as overrides (and then write '#000000' to the CSS var).
976
+ if (!v || !/^#[0-9a-f]{6}$/.test(v)) continue;
977
+ if (v !== (defaults[key] || '').toLowerCase()) {
978
+ adv[key] = el.value;
979
+ hasOverrides = true;
980
+ }
981
+ }
982
+ return hasOverrides ? adv : undefined;
983
+ }
984
+
985
+ for (const { key } of ADV_KEYS) {
986
+ const el = document.getElementById('adv-' + key);
987
+ if (!el || el.dataset.themeBound === '1') continue;
988
+ el.dataset.themeBound = '1';
989
+ el.addEventListener('input', () => {
990
+ const base = readCurrentColors();
991
+ base.advanced = readAdvanced();
992
+ applyColors(base);
993
+ // Same auto-save routing as the basic color inputs above — write
994
+ // to the active custom theme if there is one, else fall back to
995
+ // the transient 'custom' slot.
996
+ const _activeSaved = getSaved();
997
+ const _activeName = _activeSaved && _activeSaved.name;
998
+ const _customMap = _loadCustomThemes();
999
+ if (_activeName && _customMap && _customMap[_activeName]) {
1000
+ saveCustomTheme(_activeName, base, {
1001
+ font: _activeSaved.font, density: _activeSaved.density,
1002
+ bgPattern: _activeSaved.bgPattern, bgEffectColor: _activeSaved.bgEffectColor,
1003
+ bgEffectIntensity: _activeSaved.bgEffectIntensity,
1004
+ bgEffectSize: _activeSaved.bgEffectSize,
1005
+ });
1006
+ _saveFull(_activeName, base);
1007
+ } else {
1008
+ _saveFull('custom', base);
1009
+ }
1010
+ _flashAutosaved();
1011
+ grid.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('active'));
1012
+ syncResetButtons();
1013
+ });
1014
+ }
1015
+
1016
+ // Clear advanced overrides button
1017
+ const advClearBtn = document.getElementById('theme-adv-clear');
1018
+ if (advClearBtn) {
1019
+ const newClear = advClearBtn.cloneNode(true);
1020
+ advClearBtn.parentNode.replaceChild(newClear, advClearBtn);
1021
+ newClear.addEventListener('click', () => {
1022
+ const base = readCurrentColors();
1023
+ delete base.advanced;
1024
+ applyColors(base);
1025
+ _saveFull('custom', base);
1026
+ syncAdvancedPickers(base);
1027
+ syncResetButtons();
1028
+ });
1029
+ }
1030
+
1031
+ // Per-picker reset buttons (base colors)
1032
+ document.querySelectorAll('.color-reset-btn[data-reset]').forEach(btn => {
1033
+ const newBtn = btn.cloneNode(true);
1034
+ btn.parentNode.replaceChild(newBtn, btn);
1035
+ newBtn.addEventListener('click', () => {
1036
+ const key = newBtn.dataset.reset;
1037
+ const picker = document.getElementById(pickerIds[key]);
1038
+ if (picker && refColors[key]) {
1039
+ picker.value = refColors[key];
1040
+ picker.dispatchEvent(new Event('input'));
1041
+ }
1042
+ });
1043
+ });
1044
+
1045
+ // Effect color reset button
1046
+ document.querySelectorAll('.color-reset-btn[data-reset-effect]').forEach(btn => {
1047
+ const newBtn = btn.cloneNode(true);
1048
+ btn.parentNode.replaceChild(newBtn, btn);
1049
+ newBtn.addEventListener('click', () => {
1050
+ const ec = document.getElementById('theme-bg-effect-color');
1051
+ if (ec) {
1052
+ const fg = currentColors.fg || '#9cdef2';
1053
+ ec.value = fg;
1054
+ applyBgEffectColor('');
1055
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1056
+ }
1057
+ });
1058
+ });
1059
+
1060
+ // Per-picker reset buttons (advanced colors)
1061
+ document.querySelectorAll('.color-reset-btn[data-reset-adv]').forEach(btn => {
1062
+ const newBtn = btn.cloneNode(true);
1063
+ btn.parentNode.replaceChild(newBtn, btn);
1064
+ newBtn.addEventListener('click', () => {
1065
+ const key = newBtn.dataset.resetAdv;
1066
+ const picker = document.getElementById('adv-' + key);
1067
+ if (picker) {
1068
+ picker.value = refDefaults[key] || computeAdvancedDefaults(refColors)[key];
1069
+ picker.dispatchEvent(new Event('input'));
1070
+ }
1071
+ });
1072
+ });
1073
+
1074
+ // Initial sync of reset button visibility
1075
+ syncResetButtons();
1076
+
1077
+ // Font, density, background pattern controls
1078
+ const _initFont = (saved && saved.font) || DEFAULT_FONT;
1079
+ const _initDensity = (saved && saved.density) || DEFAULT_DENSITY;
1080
+ const _initPattern = (saved && saved.bgPattern) || (saved && THEME_DEFAULT_PATTERN[saved.name]) || 'none';
1081
+ const _initEffectColor = (saved && saved.bgEffectColor) || (saved && THEME_DEFAULT_EFFECT_COLOR[saved.name]) || '';
1082
+ const _initEffectIntensity = (saved && saved.bgEffectIntensity !== undefined)
1083
+ ? saved.bgEffectIntensity
1084
+ : (saved && THEME_DEFAULT_INTENSITY[saved.name] !== undefined ? THEME_DEFAULT_INTENSITY[saved.name] : 1);
1085
+ const _initEffectSize = (saved && saved.bgEffectSize !== undefined) ? saved.bgEffectSize : 1;
1086
+ const _initFrosted = (saved && saved.frosted !== undefined)
1087
+ ? !!saved.frosted
1088
+ : (saved && THEME_DEFAULT_FROSTED[saved.name] === true);
1089
+ applyFontDensity(_initFont, _initDensity);
1090
+ applyBgEffectColor(_initEffectColor);
1091
+ applyBgEffectIntensity(_initEffectIntensity);
1092
+ applyBgEffectSize(_initEffectSize);
1093
+ applyFrostedGlass(_initFrosted);
1094
+ applyBgPattern(_initPattern);
1095
+
1096
+ const fontSelect = document.getElementById('theme-font-select');
1097
+ const densitySelect = document.getElementById('theme-density-select');
1098
+ const patternSelect = document.getElementById('theme-bg-pattern-select');
1099
+
1100
+ if (fontSelect) {
1101
+ const nf = fontSelect.cloneNode(true); fontSelect.parentNode.replaceChild(nf, fontSelect);
1102
+ nf.value = _initFont;
1103
+ nf.addEventListener('change', () => {
1104
+ applyFontDensity(nf.value, document.getElementById('theme-density-select').value);
1105
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1106
+ });
1107
+ // Fetch custom fonts from local folder and populate dropdown
1108
+ fetch('/api/fonts/custom', { credentials: 'same-origin' })
1109
+ .then(r => r.json())
1110
+ .then(data => {
1111
+ _customFonts = data.fonts || {};
1112
+ const families = Object.keys(_customFonts);
1113
+ nf.querySelectorAll('option[data-custom-font]').forEach(o => o.remove());
1114
+ for (const fam of families) {
1115
+ const opt = document.createElement('option');
1116
+ opt.value = fam;
1117
+ opt.textContent = fam;
1118
+ opt.dataset.customFont = '1';
1119
+ nf.appendChild(opt);
1120
+ }
1121
+ // Restore saved value after options are populated
1122
+ nf.value = _initFont;
1123
+ })
1124
+ .catch(e => console.warn('Custom fonts fetch failed:', e));
1125
+ }
1126
+ if (densitySelect) {
1127
+ const nd = densitySelect.cloneNode(true); densitySelect.parentNode.replaceChild(nd, densitySelect);
1128
+ nd.value = _initDensity;
1129
+ nd.addEventListener('change', () => {
1130
+ applyFontDensity(document.getElementById('theme-font-select').value, nd.value);
1131
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1132
+ });
1133
+ }
1134
+ if (patternSelect) {
1135
+ const np = patternSelect.cloneNode(true); patternSelect.parentNode.replaceChild(np, patternSelect);
1136
+ np.value = _initPattern;
1137
+ np.addEventListener('change', () => {
1138
+ applyBgPattern(np.value);
1139
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1140
+ });
1141
+ }
1142
+
1143
+ const effectColorPicker = document.getElementById('theme-bg-effect-color');
1144
+ if (effectColorPicker) {
1145
+ effectColorPicker.value = _initEffectColor || currentColors.fg || '#9cdef2';
1146
+ effectColorPicker.addEventListener('input', () => {
1147
+ applyBgEffectColor(effectColorPicker.value);
1148
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1149
+ });
1150
+ }
1151
+
1152
+ const intensitySlider = document.getElementById('theme-bg-intensity');
1153
+ if (intensitySlider) {
1154
+ intensitySlider.value = String(Math.round(_initEffectIntensity * 100));
1155
+ intensitySlider.addEventListener('input', () => {
1156
+ applyBgEffectIntensity(parseFloat(intensitySlider.value) / 100);
1157
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1158
+ });
1159
+ }
1160
+
1161
+ const sizeSlider = document.getElementById('theme-bg-size');
1162
+ if (sizeSlider) {
1163
+ sizeSlider.value = String(Math.round(_initEffectSize * 100));
1164
+ sizeSlider.addEventListener('input', () => {
1165
+ applyBgEffectSize(parseFloat(sizeSlider.value) / 100);
1166
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1167
+ });
1168
+ }
1169
+
1170
+ const frostedToggle = document.getElementById('theme-frosted-toggle');
1171
+ if (frostedToggle) {
1172
+ frostedToggle.checked = _initFrosted;
1173
+ frostedToggle.addEventListener('change', () => {
1174
+ applyFrostedGlass(frostedToggle.checked);
1175
+ const s = getSaved(); if (s) _saveFull(s.name, s.colors);
1176
+ });
1177
+ }
1178
+
1179
+ // --- Color Harmony Generator (inside Advanced section) ---
1180
+ const harmonyGenBtnEl = document.getElementById('harmony-generate-btn');
1181
+ const harmonyAccentEl = document.getElementById('harmony-accent');
1182
+ // Make sure the in-house color picker really attached to this one. The
1183
+ // global initColorPickers() call earlier in initThemeUI should have grabbed
1184
+ // it, but in older sessions / partial loads it sometimes wasn't wrapped —
1185
+ // call attachColorPicker idempotently so the popover, suggestions, recents
1186
+ // and hex syncing all match every other color row.
1187
+ if (harmonyAccentEl) {
1188
+ try { attachColorPicker(harmonyAccentEl); } catch (_) {}
1189
+ }
1190
+ // Keep the hex display chip in sync with whatever the picker reports.
1191
+ const _harmonyHex = document.getElementById('harmony-accent-hex');
1192
+ if (harmonyAccentEl && _harmonyHex) {
1193
+ _harmonyHex.textContent = harmonyAccentEl.value || '#e06c75';
1194
+ harmonyAccentEl.addEventListener('input', () => {
1195
+ _harmonyHex.textContent = harmonyAccentEl.value;
1196
+ });
1197
+ }
1198
+ if (harmonyGenBtnEl) {
1199
+ const newGen = harmonyGenBtnEl.cloneNode(true);
1200
+ harmonyGenBtnEl.parentNode.replaceChild(newGen, harmonyGenBtnEl);
1201
+ newGen.addEventListener('click', () => {
1202
+ const accent = document.getElementById('harmony-accent').value;
1203
+ const type = document.getElementById('harmony-type').value;
1204
+ const mode = document.getElementById('harmony-mode').value;
1205
+ const colors = generateHarmonyColors(accent, type, mode);
1206
+ applyColors(colors);
1207
+ syncPickers(colors);
1208
+ _saveFull('custom', colors);
1209
+ grid.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('active'));
1210
+ const prev = document.getElementById('harmony-preview');
1211
+ if (prev) prev.innerHTML = [colors.bg, colors.panel, colors.fg, colors.border, colors.red].map(c => `<span style="background:${c}"></span>`).join('');
1212
+ });
1213
+ }
1214
+ if (harmonyAccentEl) {
1215
+ const newAcc = harmonyAccentEl.cloneNode(true);
1216
+ harmonyAccentEl.parentNode.replaceChild(newAcc, harmonyAccentEl);
1217
+ // Re-attach the in-house color picker to the fresh clone. cloneNode
1218
+ // copies the data-cp-attached="1" flag but NOT the listeners, so we
1219
+ // have to clear the flag first or attachColorPicker bails as a no-op.
1220
+ delete newAcc.dataset.cpAttached;
1221
+ newAcc.type = 'color'; // clone may have been type=text from prior attach
1222
+ try { attachColorPicker(newAcc); } catch (_) {}
1223
+ newAcc.addEventListener('input', () => {
1224
+ const type = document.getElementById('harmony-type').value;
1225
+ const mode = document.getElementById('harmony-mode').value;
1226
+ const colors = generateHarmonyColors(newAcc.value, type, mode);
1227
+ const prev = document.getElementById('harmony-preview');
1228
+ if (prev) prev.innerHTML = [colors.bg, colors.panel, colors.fg, colors.border, colors.red].map(c => `<span style="background:${c}"></span>`).join('');
1229
+ // Sync the hex chip beside the picker.
1230
+ const hex = document.getElementById('harmony-accent-hex');
1231
+ if (hex) hex.textContent = newAcc.value;
1232
+ });
1233
+ }
1234
+
1235
+ // --- Import / Export ---
1236
+ const exportBtnEl = document.getElementById('theme-export-btn');
1237
+ const importBtnEl = document.getElementById('theme-import-btn');
1238
+ const importAreaEl = document.getElementById('theme-import-area');
1239
+ const importActionsEl = document.getElementById('theme-import-actions');
1240
+ const importGoEl = document.getElementById('theme-import-go');
1241
+ const importCancelEl = document.getElementById('theme-import-cancel');
1242
+
1243
+ if (exportBtnEl) {
1244
+ const newExp = exportBtnEl.cloneNode(true);
1245
+ exportBtnEl.parentNode.replaceChild(newExp, exportBtnEl);
1246
+ newExp.addEventListener('click', () => {
1247
+ const colors = readCurrentColors();
1248
+ const adv = readAdvanced();
1249
+ if (adv) colors.advanced = adv;
1250
+ const cur = getSaved();
1251
+ const obj = { name: cur ? cur.name : 'custom', colors };
1252
+ if (cur && cur.font) obj.font = cur.font;
1253
+ if (cur && cur.density) obj.density = cur.density;
1254
+ if (cur && cur.bgPattern) obj.bgPattern = cur.bgPattern;
1255
+ if (cur && cur.bgEffectColor) obj.bgEffectColor = cur.bgEffectColor;
1256
+ const json = JSON.stringify(obj, null, 2);
1257
+ const blob = new Blob([json], { type: 'application/json' });
1258
+ const url = URL.createObjectURL(blob);
1259
+ const a = document.createElement('a');
1260
+ a.href = url;
1261
+ a.download = 'zero_' + (obj.name || 'theme') + '.json';
1262
+ a.click();
1263
+ URL.revokeObjectURL(url);
1264
+ newExp.innerHTML = '&#x2713; Downloaded!';
1265
+ setTimeout(() => { newExp.innerHTML = '&#x2913; Export'; }, 1500);
1266
+ });
1267
+ }
1268
+
1269
+ if (importBtnEl && importAreaEl && importActionsEl) {
1270
+ const newImp = importBtnEl.cloneNode(true);
1271
+ importBtnEl.parentNode.replaceChild(newImp, importBtnEl);
1272
+ newImp.addEventListener('click', () => {
1273
+ importAreaEl.classList.toggle('hidden');
1274
+ importActionsEl.classList.toggle('hidden');
1275
+ importAreaEl.value = '';
1276
+ saveError.style.display = 'none';
1277
+ });
1278
+ }
1279
+
1280
+ if (importGoEl && importAreaEl) {
1281
+ const newGo = importGoEl.cloneNode(true);
1282
+ importGoEl.parentNode.replaceChild(newGo, importGoEl);
1283
+ newGo.addEventListener('click', () => {
1284
+ saveError.style.display = 'none';
1285
+ let parsed;
1286
+ try { parsed = JSON.parse(importAreaEl.value.trim()); }
1287
+ catch { saveError.textContent = 'Invalid JSON.'; saveError.style.display = 'block'; return; }
1288
+ let colors = parsed.colors || parsed;
1289
+ const name = parsed.name || 'imported';
1290
+ const required = ['bg', 'fg', 'panel', 'border', 'red'];
1291
+ const missing = required.filter(k => !colors[k]);
1292
+ if (missing.length) { saveError.textContent = 'Missing: ' + missing.join(', '); saveError.style.display = 'block'; return; }
1293
+ const hexRe = /^#[0-9a-fA-F]{6}$/;
1294
+ for (const k of required) {
1295
+ if (!hexRe.test(colors[k])) { saveError.textContent = 'Bad hex for ' + k; saveError.style.display = 'block'; return; }
1296
+ }
1297
+ const colorData = { bg: colors.bg, fg: colors.fg, panel: colors.panel, border: colors.border, red: colors.red };
1298
+ if (colors.advanced && typeof colors.advanced === 'object') colorData.advanced = colors.advanced;
1299
+ const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') || 'imported';
1300
+ const opts = {};
1301
+ if (parsed.font) opts.font = parsed.font;
1302
+ if (parsed.density) opts.density = parsed.density;
1303
+ if (parsed.bgPattern) opts.bgPattern = parsed.bgPattern;
1304
+ if (parsed.bgEffectColor) opts.bgEffectColor = parsed.bgEffectColor;
1305
+ const result = saveCustomTheme(slug, colorData, opts);
1306
+ if (result === 'limit') { saveError.textContent = 'Max ' + MAX_CUSTOM_THEMES + ' custom themes. Delete one first.'; saveError.style.display = 'block'; return; }
1307
+ save(slug, colorData, opts);
1308
+ applyColors(colorData);
1309
+ applyFontDensity(opts.font || DEFAULT_FONT, opts.density || DEFAULT_DENSITY);
1310
+ applyBgEffectColor(opts.bgEffectColor || '');
1311
+ applyBgPattern(opts.bgPattern || 'none');
1312
+ importAreaEl.classList.add('hidden');
1313
+ importActionsEl.classList.add('hidden');
1314
+ });
1315
+ }
1316
+
1317
+ if (importCancelEl && importAreaEl && importActionsEl) {
1318
+ const newCancel = importCancelEl.cloneNode(true);
1319
+ importCancelEl.parentNode.replaceChild(newCancel, importCancelEl);
1320
+ newCancel.addEventListener('click', () => {
1321
+ importAreaEl.classList.add('hidden');
1322
+ importActionsEl.classList.add('hidden');
1323
+ importAreaEl.value = '';
1324
+ saveError.style.display = 'none';
1325
+ });
1326
+ }
1327
+
1328
+ // Theme popup now uses standard modal frame (not draggable)
1329
+ }
1330
+
1331
+ // ── Zone highlighter ───────────────────────────────────────────────────
1332
+ // Maps each color input id to a selector for the part of the UI it affects.
1333
+ // When the user hovers the color row, we overlay a translucent box on the
1334
+ // matching elements so it's obvious what's being edited.
1335
+ const _THEME_ZONE_MAP = {
1336
+ 'clr-bg': 'body',
1337
+ 'clr-fg': '.msg .body, .chat-input-bar',
1338
+ 'clr-panel': '.sidebar',
1339
+ 'clr-border': '.chat-input-bar, .sidebar, .msg .body',
1340
+ 'clr-red': '.send-btn, .icon-rail-btn.active',
1341
+ 'theme-bg-effect-color': 'body',
1342
+ 'adv-userBubbleBg': '.msg.msg-user .body',
1343
+ 'adv-aiBubbleBg': '.msg.msg-ai .body',
1344
+ 'adv-bubbleBorder': '.msg .body',
1345
+ 'adv-sidebarBg': '.sidebar',
1346
+ 'adv-sectionAccent': '.sidebar h4',
1347
+ 'adv-brandColor': '#sidebar-brand-btn',
1348
+ 'adv-inputBg': '#message',
1349
+ 'adv-inputBorder': '.chat-input-bar',
1350
+ 'adv-sendBtnBg': '.send-btn',
1351
+ 'adv-sendBtnHover': '.send-btn',
1352
+ 'adv-codeBg': 'pre, code',
1353
+ 'adv-codeFg': 'pre code, p code',
1354
+ 'adv-toggleBg': '.mode-toggle, .admin-switch',
1355
+ 'adv-toggleActive': '.mode-toggle-btn.active, .admin-switch input:checked + .admin-slider',
1356
+ 'adv-accentPrimary': '.send-btn, .icon-rail-btn.active',
1357
+ 'adv-accentError': '.toast.error',
1358
+ };
1359
+
1360
+ function _showThemeZoneHighlight(selector) {
1361
+ _clearThemeZoneHighlight();
1362
+ if (!selector) return;
1363
+ let els;
1364
+ try { els = document.querySelectorAll(selector); }
1365
+ catch { return; }
1366
+ els.forEach(el => {
1367
+ // Skip elements inside the theme modal — highlighting itself is noise.
1368
+ if (el.closest && el.closest('#theme-modal')) return;
1369
+ const r = el.getBoundingClientRect();
1370
+ if (r.width < 2 || r.height < 2) return;
1371
+ const overlay = document.createElement('div');
1372
+ overlay.className = 'theme-zone-highlight';
1373
+ overlay.style.top = (r.top - 2) + 'px';
1374
+ overlay.style.left = (r.left - 2) + 'px';
1375
+ overlay.style.width = (r.width + 4) + 'px';
1376
+ overlay.style.height = (r.height + 4) + 'px';
1377
+ document.body.appendChild(overlay);
1378
+ });
1379
+ }
1380
+
1381
+ function _clearThemeZoneHighlight() {
1382
+ document.querySelectorAll('.theme-zone-highlight').forEach(el => el.remove());
1383
+ }
1384
+
1385
+ let _flashTimer = null;
1386
+ function _flashAutosaved(label = 'Auto-saved') {
1387
+ let pill = document.getElementById('theme-autosaved-pill');
1388
+ if (!pill) {
1389
+ pill = document.createElement('div');
1390
+ pill.id = 'theme-autosaved-pill';
1391
+ pill.className = 'theme-autosaved-pill';
1392
+ pill.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg><span></span>';
1393
+ // Anchor inside the customize tab so it floats with the form.
1394
+ const customizeTab = document.getElementById('theme-tab-customize');
1395
+ (customizeTab || document.body).appendChild(pill);
1396
+ }
1397
+ const labelEl = pill.querySelector('span');
1398
+ if (labelEl) labelEl.textContent = label;
1399
+ pill.classList.add('visible');
1400
+ clearTimeout(_flashTimer);
1401
+ _flashTimer = setTimeout(() => pill.classList.remove('visible'), 1100);
1402
+ }
1403
+
1404
+ // Wire hover-to-highlight on every color row inside the theme modal. Call
1405
+ // once after the modal markup is in the DOM. Idempotent.
1406
+ export function initThemeZoneHighlight() {
1407
+ const root = document.getElementById('theme-tab-customize');
1408
+ if (!root || root.dataset.zoneBound === '1') return;
1409
+ root.dataset.zoneBound = '1';
1410
+ root.querySelectorAll('.color-row').forEach(row => {
1411
+ const input = row.querySelector('input[type="color"]');
1412
+ if (!input) return;
1413
+ const sel = _THEME_ZONE_MAP[input.id];
1414
+ if (!sel) return;
1415
+ row.addEventListener('mouseenter', () => _showThemeZoneHighlight(sel));
1416
+ row.addEventListener('mouseleave', _clearThemeZoneHighlight);
1417
+ // Also trigger when the picker actually opens (input focus)
1418
+ input.addEventListener('focus', () => _showThemeZoneHighlight(sel));
1419
+ input.addEventListener('blur', _clearThemeZoneHighlight);
1420
+ });
1421
+ // Clear highlight when the modal closes.
1422
+ const modal = document.getElementById('theme-modal');
1423
+ if (modal) {
1424
+ new MutationObserver(() => {
1425
+ if (modal.classList.contains('hidden')) _clearThemeZoneHighlight();
1426
+ }).observe(modal, { attributes: true, attributeFilter: ['class'] });
1427
+ }
1428
+ }
1429
+
1430
+ // Generic draggable helper for fixed-position elements
1431
+ // Thin wrapper around the shared makeWindowDraggable helper. Existing
1432
+ // callers pass (el, handle) — `el` is what gets moved, `handle` is the
1433
+ // drag handle. No fullscreen support (none of these consumers wanted it).
1434
+ export function makeDraggable(el, handle) {
1435
+ if (!el || !handle) return;
1436
+ const dockTarget = (el.closest && el.closest('.modal')) || el;
1437
+ const dragOptions = {
1438
+ content: el,
1439
+ header: handle,
1440
+ // Don't start a window-drag when the user grabs an interactive control
1441
+ // in the header — e.g. the theme opacity slider now lives next to the
1442
+ // title, and dragging its thumb must move the slider, not the window.
1443
+ skipSelector: 'button, input, select, .theme-opacity-wrap',
1444
+ };
1445
+ if (dockTarget && dockTarget.id === 'theme-modal') {
1446
+ dragOptions.onEnterFullscreen = () => {
1447
+ snapModalToZone(dockTarget, {
1448
+ name: 'fullscreen',
1449
+ rect: {
1450
+ left: 0,
1451
+ top: 0,
1452
+ width: window.innerWidth || document.documentElement.clientWidth || 0,
1453
+ height: window.innerHeight || document.documentElement.clientHeight || 0,
1454
+ },
1455
+ });
1456
+ };
1457
+ }
1458
+ makeWindowDraggable(dockTarget, dragOptions);
1459
+ }
1460
+
1461
+ // Toggle the popup
1462
+ export function togglePopup() {
1463
+ const modal = document.getElementById('theme-modal');
1464
+ if (!modal) return;
1465
+ const visible = !modal.classList.contains('hidden');
1466
+ if (visible) {
1467
+ modal.classList.add('hidden');
1468
+ } else {
1469
+ modal.classList.remove('hidden');
1470
+ }
1471
+ }
1472
+
1473
+ export function closePopup() {
1474
+ const modal = document.getElementById('theme-modal');
1475
+ if (!modal) return;
1476
+ const content = modal.querySelector('.modal-content');
1477
+ if (content && !content.classList.contains('modal-closing')) {
1478
+ content.classList.add('modal-closing');
1479
+ content.addEventListener('animationend', () => {
1480
+ modal.classList.add('hidden');
1481
+ content.classList.remove('modal-closing');
1482
+ }, { once: true });
1483
+ setTimeout(() => { if (!modal.classList.contains('hidden')) { modal.classList.add('hidden'); content.classList.remove('modal-closing'); } }, 250);
1484
+ } else {
1485
+ modal.classList.add('hidden');
1486
+ }
1487
+ }
1488
+
1489
+ // Expose for app.js wiring + AI ui_control
1490
+ export function getCustomThemes() { return _loadCustomThemes(); }
1491
+
1492
+ // ── Synapse background effect ──
1493
+ // Uses the CSS grid pattern as base, overlays fast-moving small light pulses on grid lines
1494
+ function _initSynapse() {
1495
+ if (document.getElementById('synapse-canvas')) return;
1496
+ const canvas = document.createElement('canvas');
1497
+ canvas.id = 'synapse-canvas';
1498
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1499
+ // Decorative background effect — hide from assistive tech so screen readers
1500
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1501
+ canvas.setAttribute('aria-hidden', 'true');
1502
+ document.body.prepend(canvas);
1503
+ const ctx = canvas.getContext('2d');
1504
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1505
+ const GRID = 24; // matches CSS grid size
1506
+ const MAX_PULSES = 20;
1507
+ const SPEED_MIN = 2;
1508
+ const SPEED_MAX = 22;
1509
+ const TRAIL_LEN = 12; // pixels of trailing glow
1510
+
1511
+ let W, H, cols, rows, pulses = [];
1512
+
1513
+ function resize() {
1514
+ W = window.innerWidth; H = window.innerHeight;
1515
+ canvas.width = W * dpr; canvas.height = H * dpr;
1516
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1517
+ cols = Math.ceil(W / GRID); rows = Math.ceil(H / GRID);
1518
+ }
1519
+ resize();
1520
+ const _onResize = () => resize();
1521
+ window.addEventListener('resize', _onResize);
1522
+
1523
+ function getColor() {
1524
+ const s = getComputedStyle(document.documentElement);
1525
+ return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2';
1526
+ }
1527
+
1528
+ function spawnPulse() {
1529
+ const speed = SPEED_MIN + Math.random() * (SPEED_MAX - SPEED_MIN);
1530
+ if (Math.random() > 0.5) {
1531
+ // Horizontal — pick a grid row
1532
+ const row = Math.floor(Math.random() * (rows + 1));
1533
+ pulses.push({ x: -TRAIL_LEN, y: row * GRID, dx: speed, dy: 0 });
1534
+ } else {
1535
+ // Vertical — pick a grid column
1536
+ const col = Math.floor(Math.random() * (cols + 1));
1537
+ pulses.push({ x: col * GRID, y: -TRAIL_LEN, dx: 0, dy: speed });
1538
+ }
1539
+ }
1540
+
1541
+ function draw() {
1542
+ if (!document.body.classList.contains('bg-pattern-synapse')) {
1543
+ window.removeEventListener('resize', _onResize);
1544
+ canvas.remove();
1545
+ return;
1546
+ }
1547
+ requestAnimationFrame(draw);
1548
+ ctx.clearRect(0, 0, W, H);
1549
+ const c = getColor();
1550
+
1551
+ // Spawn
1552
+ if (pulses.length < MAX_PULSES && Math.random() < 0.12) spawnPulse();
1553
+
1554
+ // Draw pulses as small bright dots with a short trail
1555
+ for (let i = pulses.length - 1; i >= 0; i--) {
1556
+ const p = pulses[i];
1557
+ p.x += p.dx; p.y += p.dy;
1558
+
1559
+ // Off screen — remove
1560
+ if (p.x > W + TRAIL_LEN || p.y > H + TRAIL_LEN) { pulses.splice(i, 1); continue; }
1561
+
1562
+ // Trail (line gradient fading behind the dot)
1563
+ const tx = p.x - (p.dx > 0 ? TRAIL_LEN : 0);
1564
+ const ty = p.y - (p.dy > 0 ? TRAIL_LEN : 0);
1565
+ const grad = ctx.createLinearGradient(tx, ty, p.x, p.y);
1566
+ grad.addColorStop(0, 'transparent');
1567
+ grad.addColorStop(1, c);
1568
+ ctx.strokeStyle = grad;
1569
+ ctx.globalAlpha = 0.35;
1570
+ ctx.lineWidth = 1;
1571
+ ctx.beginPath();
1572
+ ctx.moveTo(tx, ty);
1573
+ ctx.lineTo(p.x, p.y);
1574
+ ctx.stroke();
1575
+
1576
+ // Bright dot at head
1577
+ ctx.globalAlpha = 0.55;
1578
+ ctx.fillStyle = c;
1579
+ ctx.beginPath();
1580
+ ctx.arc(p.x, p.y, 1.2, 0, Math.PI * 2);
1581
+ ctx.fill();
1582
+ }
1583
+
1584
+ ctx.globalAlpha = 1;
1585
+ }
1586
+ draw();
1587
+ }
1588
+
1589
+ // ── Rain — thin vertical streaks falling ──
1590
+ function _initRain() {
1591
+ if (document.getElementById('rain-canvas')) return;
1592
+ const canvas = document.createElement('canvas');
1593
+ canvas.id = 'rain-canvas';
1594
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1595
+ // Decorative background effect — hide from assistive tech so screen readers
1596
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1597
+ canvas.setAttribute('aria-hidden', 'true');
1598
+ document.body.prepend(canvas);
1599
+ const ctx = canvas.getContext('2d');
1600
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1601
+ let W, H;
1602
+ const drops = [];
1603
+ const MAX_DROPS = 130;
1604
+
1605
+ function resize() {
1606
+ W = window.innerWidth; H = window.innerHeight;
1607
+ canvas.width = W * dpr; canvas.height = H * dpr;
1608
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1609
+ }
1610
+ resize();
1611
+ const _onResize = () => resize();
1612
+ window.addEventListener('resize', _onResize);
1613
+
1614
+ function getColor() {
1615
+ const s = getComputedStyle(document.documentElement);
1616
+ return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2';
1617
+ }
1618
+
1619
+ function spawn() {
1620
+ const len = 20 + Math.random() * 40;
1621
+ const speed = 4 + Math.random() * 8;
1622
+ drops.push({ x: Math.random() * W, y: -len, len, speed, alpha: 0.32 + Math.random() * 0.28 });
1623
+ }
1624
+
1625
+ function draw() {
1626
+ if (!document.body.classList.contains('bg-pattern-rain')) {
1627
+ window.removeEventListener('resize', _onResize);
1628
+ canvas.remove();
1629
+ return;
1630
+ }
1631
+ requestAnimationFrame(draw);
1632
+ ctx.clearRect(0, 0, W, H);
1633
+ const c = getColor();
1634
+ // Intensity also controls rain speed + spawn rate (feels slower/lighter when dim)
1635
+ const intenCss = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--bg-effect-intensity'));
1636
+ const inten = isNaN(intenCss) ? 1 : intenCss;
1637
+ const speedMult = 0.35 + inten * 0.65;
1638
+ const sizeMult = _getEffectSize();
1639
+
1640
+ if (drops.length < MAX_DROPS * inten && Math.random() < 0.6 * inten) spawn();
1641
+
1642
+ for (let i = drops.length - 1; i >= 0; i--) {
1643
+ const d = drops[i];
1644
+ d.y += d.speed * speedMult;
1645
+ if (d.y > H + d.len * sizeMult) { drops.splice(i, 1); continue; }
1646
+
1647
+ const effLen = d.len * sizeMult;
1648
+ const grad = ctx.createLinearGradient(d.x, d.y - effLen, d.x, d.y);
1649
+ grad.addColorStop(0, 'transparent');
1650
+ grad.addColorStop(1, c);
1651
+ ctx.strokeStyle = grad;
1652
+ ctx.globalAlpha = d.alpha;
1653
+ ctx.lineWidth = 1.3 * Math.min(2, Math.max(0.6, sizeMult));
1654
+ ctx.beginPath();
1655
+ ctx.moveTo(d.x, d.y - effLen);
1656
+ ctx.lineTo(d.x, d.y);
1657
+ ctx.stroke();
1658
+ }
1659
+ ctx.globalAlpha = 1;
1660
+ }
1661
+ draw();
1662
+ }
1663
+
1664
+ // ── Constellations — static dots that slowly form/dissolve connecting lines ──
1665
+ function _initConstellations() {
1666
+ if (document.getElementById('constellations-canvas')) return;
1667
+ const canvas = document.createElement('canvas');
1668
+ canvas.id = 'constellations-canvas';
1669
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1670
+ // Decorative background effect — hide from assistive tech so screen readers
1671
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1672
+ canvas.setAttribute('aria-hidden', 'true');
1673
+ document.body.prepend(canvas);
1674
+ const ctx = canvas.getContext('2d');
1675
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1676
+ let W, H;
1677
+ const STAR_COUNT = 50;
1678
+ const CONNECT_DIST = 120;
1679
+ let stars = [];
1680
+
1681
+ function resize() {
1682
+ W = window.innerWidth; H = window.innerHeight;
1683
+ canvas.width = W * dpr; canvas.height = H * dpr;
1684
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1685
+ if (stars.length === 0) initStars();
1686
+ }
1687
+
1688
+ function initStars() {
1689
+ stars = [];
1690
+ for (let i = 0; i < STAR_COUNT; i++) {
1691
+ stars.push({
1692
+ x: Math.random() * W, y: Math.random() * H,
1693
+ vx: (Math.random() - 0.5) * 0.15,
1694
+ vy: (Math.random() - 0.5) * 0.15,
1695
+ r: 0.8 + Math.random() * 0.8,
1696
+ phase: Math.random() * Math.PI * 2,
1697
+ });
1698
+ }
1699
+ }
1700
+
1701
+ resize();
1702
+ const _onResize = () => { resize(); initStars(); };
1703
+ window.addEventListener('resize', _onResize);
1704
+
1705
+ function getColor() {
1706
+ const s = getComputedStyle(document.documentElement);
1707
+ return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2';
1708
+ }
1709
+
1710
+ let t = 0;
1711
+ function draw() {
1712
+ if (!document.body.classList.contains('bg-pattern-constellations')) {
1713
+ window.removeEventListener('resize', _onResize);
1714
+ canvas.remove();
1715
+ return;
1716
+ }
1717
+ requestAnimationFrame(draw);
1718
+ t += 0.01;
1719
+ ctx.clearRect(0, 0, W, H);
1720
+ const c = getColor();
1721
+
1722
+ // Move stars gently
1723
+ for (const s of stars) {
1724
+ s.x += s.vx; s.y += s.vy;
1725
+ if (s.x < 0) s.x = W; if (s.x > W) s.x = 0;
1726
+ if (s.y < 0) s.y = H; if (s.y > H) s.y = 0;
1727
+ }
1728
+
1729
+ // Draw connections
1730
+ ctx.strokeStyle = c;
1731
+ ctx.lineWidth = 0.5;
1732
+ for (let i = 0; i < stars.length; i++) {
1733
+ for (let j = i + 1; j < stars.length; j++) {
1734
+ const dx = stars[i].x - stars[j].x;
1735
+ const dy = stars[i].y - stars[j].y;
1736
+ const dist = Math.sqrt(dx * dx + dy * dy);
1737
+ if (dist < CONNECT_DIST) {
1738
+ ctx.globalAlpha = (1 - dist / CONNECT_DIST) * 0.15;
1739
+ ctx.beginPath();
1740
+ ctx.moveTo(stars[i].x, stars[i].y);
1741
+ ctx.lineTo(stars[j].x, stars[j].y);
1742
+ ctx.stroke();
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ // Draw stars with subtle twinkle
1748
+ ctx.fillStyle = c;
1749
+ for (const s of stars) {
1750
+ const twinkle = 0.5 + 0.5 * Math.sin(t * 2 + s.phase);
1751
+ ctx.globalAlpha = 0.15 + twinkle * 0.25;
1752
+ ctx.beginPath();
1753
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
1754
+ ctx.fill();
1755
+ }
1756
+ ctx.globalAlpha = 1;
1757
+ }
1758
+ draw();
1759
+ }
1760
+
1761
+ // ── Noise helper for Perlin effects ──
1762
+ function _bgNoise2d(x, y) { const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; return n - Math.floor(n); }
1763
+ function _bgSmoothNoise(x, y) {
1764
+ const ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
1765
+ const a = _bgNoise2d(ix, iy), b = _bgNoise2d(ix + 1, iy), cc = _bgNoise2d(ix, iy + 1), d = _bgNoise2d(ix + 1, iy + 1);
1766
+ const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
1767
+ return a + (b - a) * ux + (cc - a) * uy + (a - b - cc + d) * ux * uy;
1768
+ }
1769
+
1770
+ // ── Perlin Flow — colored particle streams ──
1771
+ function _initPerlinFlow() {
1772
+ if (document.getElementById('perlin-flow-canvas')) return;
1773
+ const canvas = document.createElement('canvas');
1774
+ canvas.id = 'perlin-flow-canvas';
1775
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1776
+ // Decorative background effect — hide from assistive tech so screen readers
1777
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1778
+ canvas.setAttribute('aria-hidden', 'true');
1779
+ document.body.prepend(canvas);
1780
+ const ctx = canvas.getContext('2d');
1781
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1782
+ let W, H, t = 0;
1783
+ const particles = [];
1784
+ function resize() {
1785
+ W = window.innerWidth; H = window.innerHeight;
1786
+ canvas.width = W * dpr; canvas.height = H * dpr;
1787
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1788
+ if (particles.length === 0) for (let i = 0; i < 200; i++) particles.push({ x: Math.random() * W, y: Math.random() * H, life: Math.random() });
1789
+ }
1790
+ resize();
1791
+ const _onResize = () => resize();
1792
+ window.addEventListener('resize', _onResize);
1793
+ function getColor() { const s = getComputedStyle(document.documentElement); return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2'; }
1794
+ function getBg() { return getComputedStyle(document.documentElement).getPropertyValue('--bg').trim() || '#282c34'; }
1795
+ let _cachedBg = '', _fadeStyle = '';
1796
+ function getFade() {
1797
+ const bg = getBg();
1798
+ if (bg !== _cachedBg) {
1799
+ _cachedBg = bg;
1800
+ // Parse hex to rgb for rgba fade
1801
+ const { r, g, b } = hexToRgb(bg) || { r: 0, g: 0, b: 0 };
1802
+ _fadeStyle = `rgba(${r},${g},${b},0.02)`;
1803
+ }
1804
+ return _fadeStyle;
1805
+ }
1806
+ function draw() {
1807
+ if (!document.body.classList.contains('bg-pattern-perlin-flow')) { window.removeEventListener('resize', _onResize); canvas.remove(); return; }
1808
+ requestAnimationFrame(draw);
1809
+ ctx.fillStyle = getFade();
1810
+ ctx.fillRect(0, 0, W, H);
1811
+ const c = getColor();
1812
+ particles.forEach(p => {
1813
+ const n = _bgSmoothNoise(p.x * 0.004 + t * 0.0008, p.y * 0.004 + 100);
1814
+ const angle = n * Math.PI * 6;
1815
+ const speed = 1 + _bgSmoothNoise(p.x * 0.003, p.y * 0.003 + 50) * 1.5;
1816
+ p.x += Math.cos(angle) * speed; p.y += Math.sin(angle) * speed; p.life -= 0.001;
1817
+ if (p.life <= 0 || p.x < 0 || p.x > W || p.y < 0 || p.y > H) { p.x = Math.random() * W; p.y = Math.random() * H; p.life = 1; }
1818
+ ctx.beginPath(); ctx.arc(p.x, p.y, 1, 0, Math.PI * 2);
1819
+ ctx.fillStyle = c; ctx.globalAlpha = p.life * 0.15; ctx.fill();
1820
+ });
1821
+ ctx.globalAlpha = 1;
1822
+ t++;
1823
+ }
1824
+ draw();
1825
+ }
1826
+
1827
+ // ── Petals — gentle falling flower petals ──
1828
+ function _initPetals() {
1829
+ if (document.getElementById('petals-canvas')) return;
1830
+ const canvas = document.createElement('canvas');
1831
+ canvas.id = 'petals-canvas';
1832
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1833
+ // Decorative background effect — hide from assistive tech so screen readers
1834
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1835
+ canvas.setAttribute('aria-hidden', 'true');
1836
+ document.body.prepend(canvas);
1837
+ const ctx = canvas.getContext('2d');
1838
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1839
+ let W, H;
1840
+ const petals = [];
1841
+ function makePetal() {
1842
+ return {
1843
+ x: Math.random() * W, y: -10 - Math.random() * 40,
1844
+ size: 3 + Math.random() * 5, rot: Math.random() * Math.PI * 2,
1845
+ vr: (Math.random() - 0.5) * 0.03, vy: 0.3 + Math.random() * 0.6,
1846
+ drift: Math.random() * Math.PI * 2, driftSpeed: 0.008 + Math.random() * 0.012,
1847
+ wobble: 0.3 + Math.random() * 0.8
1848
+ };
1849
+ }
1850
+ function resize() {
1851
+ W = window.innerWidth; H = window.innerHeight;
1852
+ canvas.width = W * dpr; canvas.height = H * dpr;
1853
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1854
+ if (petals.length === 0) for (let i = 0; i < 30; i++) { const p = makePetal(); p.y = Math.random() * H; petals.push(p); }
1855
+ }
1856
+ resize();
1857
+ const _onResize = () => resize();
1858
+ window.addEventListener('resize', _onResize);
1859
+ function getColor() { const s = getComputedStyle(document.documentElement); return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2'; }
1860
+ function draw() {
1861
+ if (!document.body.classList.contains('bg-pattern-petals')) { window.removeEventListener('resize', _onResize); canvas.remove(); return; }
1862
+ requestAnimationFrame(draw);
1863
+ ctx.clearRect(0, 0, W, H);
1864
+ const c = getColor();
1865
+ const sz = _getEffectSize();
1866
+ petals.forEach(p => {
1867
+ p.y += p.vy; p.rot += p.vr; p.drift += p.driftSpeed;
1868
+ p.x += Math.sin(p.drift) * p.wobble;
1869
+ if (p.y > H + 15) Object.assign(p, makePetal());
1870
+ ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
1871
+ ctx.globalAlpha = 0.2;
1872
+ // petal shape — two overlapping ellipses
1873
+ ctx.fillStyle = c;
1874
+ ctx.beginPath(); ctx.ellipse(-p.size * 0.2 * sz, 0, p.size * 0.6 * sz, p.size * 0.3 * sz, 0.3, 0, Math.PI * 2); ctx.fill();
1875
+ ctx.globalAlpha = 0.15;
1876
+ ctx.beginPath(); ctx.ellipse(p.size * 0.2 * sz, 0, p.size * 0.6 * sz, p.size * 0.3 * sz, -0.3, 0, Math.PI * 2); ctx.fill();
1877
+ ctx.restore();
1878
+ });
1879
+ ctx.globalAlpha = 1;
1880
+ }
1881
+ draw();
1882
+ }
1883
+
1884
+ // ── Sparkles — twinkling star-shaped sparkles ──
1885
+ function _initSparkles() {
1886
+ if (document.getElementById('sparkles-canvas')) return;
1887
+ const canvas = document.createElement('canvas');
1888
+ canvas.id = 'sparkles-canvas';
1889
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1890
+ // Decorative background effect — hide from assistive tech so screen readers
1891
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1892
+ canvas.setAttribute('aria-hidden', 'true');
1893
+ document.body.prepend(canvas);
1894
+ const ctx = canvas.getContext('2d');
1895
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1896
+ let W, H;
1897
+ const sparkles = [];
1898
+ function makeSpark() {
1899
+ return { x: Math.random() * W, y: Math.random() * H, size: 2 + Math.random() * 5, phase: Math.random() * Math.PI * 2, speed: 0.015 + Math.random() * 0.03, life: 0.5 + Math.random() * 0.5 };
1900
+ }
1901
+ function resize() {
1902
+ W = window.innerWidth; H = window.innerHeight;
1903
+ canvas.width = W * dpr; canvas.height = H * dpr;
1904
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1905
+ if (sparkles.length === 0) for (let i = 0; i < 35; i++) sparkles.push(makeSpark());
1906
+ }
1907
+ resize();
1908
+ const _onResize = () => resize();
1909
+ window.addEventListener('resize', _onResize);
1910
+ function getColor() { const s = getComputedStyle(document.documentElement); return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#9cdef2'; }
1911
+ function drawStar(x, y, r, c, alpha) {
1912
+ ctx.save(); ctx.translate(x, y); ctx.fillStyle = c; ctx.globalAlpha = alpha;
1913
+ // 4-point star
1914
+ ctx.beginPath();
1915
+ ctx.moveTo(0, -r); ctx.quadraticCurveTo(r * 0.15, -r * 0.15, r, 0);
1916
+ ctx.quadraticCurveTo(r * 0.15, r * 0.15, 0, r);
1917
+ ctx.quadraticCurveTo(-r * 0.15, r * 0.15, -r, 0);
1918
+ ctx.quadraticCurveTo(-r * 0.15, -r * 0.15, 0, -r);
1919
+ ctx.fill();
1920
+ ctx.restore();
1921
+ }
1922
+ function draw() {
1923
+ if (!document.body.classList.contains('bg-pattern-sparkles')) { window.removeEventListener('resize', _onResize); canvas.remove(); return; }
1924
+ requestAnimationFrame(draw);
1925
+ ctx.clearRect(0, 0, W, H);
1926
+ const c = getColor();
1927
+ const sizeMult = _getEffectSize();
1928
+ sparkles.forEach(s => {
1929
+ s.phase += s.speed;
1930
+ const twinkle = Math.sin(s.phase);
1931
+ const alpha = Math.max(0, twinkle) * 0.25 * s.life;
1932
+ const scale = 0.5 + Math.max(0, twinkle) * 0.5;
1933
+ if (alpha > 0.01) drawStar(s.x, s.y, s.size * scale * sizeMult, c, alpha);
1934
+ // respawn when cycle completes
1935
+ if (s.phase > Math.PI * 6) Object.assign(s, makeSpark());
1936
+ });
1937
+ ctx.globalAlpha = 1;
1938
+ }
1939
+ draw();
1940
+ }
1941
+
1942
+ // ── Embers — warm particles rising with glow and occasional spark bursts ──
1943
+ function _initEmbers() {
1944
+ if (document.getElementById('embers-canvas')) return;
1945
+ const canvas = document.createElement('canvas');
1946
+ canvas.id = 'embers-canvas';
1947
+ canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
1948
+ // Decorative background effect — hide from assistive tech so screen readers
1949
+ // don't announce an empty canvas and axe's "region" rule doesn't flag it.
1950
+ canvas.setAttribute('aria-hidden', 'true');
1951
+ document.body.prepend(canvas);
1952
+ const ctx = canvas.getContext('2d');
1953
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1954
+ let W, H;
1955
+ const embers = [];
1956
+ function makeEmber() {
1957
+ return {
1958
+ x: Math.random() * W,
1959
+ y: H + Math.random() * 40,
1960
+ vx: (Math.random() - 0.5) * 0.3,
1961
+ vy: -0.3 - Math.random() * 0.8,
1962
+ r: 0.3 + Math.random() * 0.6,
1963
+ life: 0,
1964
+ maxLife: 220 + Math.random() * 220,
1965
+ wobble: Math.random() * Math.PI * 2,
1966
+ spark: false,
1967
+ };
1968
+ }
1969
+ function resize() {
1970
+ W = window.innerWidth; H = window.innerHeight;
1971
+ canvas.width = W * dpr; canvas.height = H * dpr;
1972
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1973
+ if (embers.length === 0) {
1974
+ for (let i = 0; i < 60; i++) { const e = makeEmber(); e.y = Math.random() * H; e.life = Math.random() * e.maxLife; embers.push(e); }
1975
+ }
1976
+ }
1977
+ resize();
1978
+ const _onResize = () => resize();
1979
+ window.addEventListener('resize', _onResize);
1980
+ function getColor() {
1981
+ const s = getComputedStyle(document.documentElement);
1982
+ return s.getPropertyValue('--bg-effect-color').trim() || s.getPropertyValue('--fg').trim() || '#c9a95a';
1983
+ }
1984
+ function rgba(hex, a) {
1985
+ const { r, g, b } = hexToRgb(hex) || { r: 0, g: 0, b: 0 };
1986
+ return `rgba(${r},${g},${b},${a})`;
1987
+ }
1988
+ function draw() {
1989
+ if (!document.body.classList.contains('bg-pattern-embers')) {
1990
+ window.removeEventListener('resize', _onResize);
1991
+ canvas.remove();
1992
+ return;
1993
+ }
1994
+ requestAnimationFrame(draw);
1995
+ // Fade previous frame (destination-out keeps canvas transparent where no embers)
1996
+ ctx.globalCompositeOperation = 'destination-out';
1997
+ ctx.fillStyle = 'rgba(0,0,0,0.18)';
1998
+ ctx.fillRect(0, 0, W, H);
1999
+ ctx.globalCompositeOperation = 'lighter';
2000
+ const color = getColor();
2001
+ for (let i = embers.length - 1; i >= 0; i--) {
2002
+ const e = embers[i];
2003
+ e.wobble += 0.03;
2004
+ e.x += e.vx + Math.sin(e.wobble) * 0.5;
2005
+ e.y += e.vy;
2006
+ e.life++;
2007
+ if (e.life > e.maxLife || e.y < -20) {
2008
+ embers.splice(i, 1);
2009
+ if (embers.length < 70) embers.push(makeEmber());
2010
+ continue;
2011
+ }
2012
+ if (!e.spark && Math.random() < 0.003) e.spark = true;
2013
+ const lifeRatio = e.life / e.maxLife;
2014
+ const fade = Math.min(1, Math.min(lifeRatio * 4, (1 - lifeRatio) * 3));
2015
+ const sz = _getEffectSize();
2016
+ const r = e.r * (e.spark ? 2.4 : 1) * sz;
2017
+ const a = (e.spark ? 0.9 : 0.55) * fade;
2018
+ const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, r * 4);
2019
+ g.addColorStop(0, rgba(color, a));
2020
+ g.addColorStop(0.4, rgba(color, a * 0.3));
2021
+ g.addColorStop(1, rgba(color, 0));
2022
+ ctx.fillStyle = g;
2023
+ ctx.fillRect(e.x - r * 4, e.y - r * 4, r * 8, r * 8);
2024
+ ctx.fillStyle = rgba('#ffffff', a * 0.6);
2025
+ ctx.beginPath();
2026
+ ctx.arc(e.x, e.y, r * 0.5, 0, Math.PI * 2);
2027
+ ctx.fill();
2028
+ e.spark = false;
2029
+ }
2030
+ if (Math.random() < 0.015) {
2031
+ const bx = Math.random() * W;
2032
+ for (let i = 0; i < 5; i++) {
2033
+ const e = makeEmber();
2034
+ e.x = bx + (Math.random() - 0.5) * 40;
2035
+ e.y = H - 10;
2036
+ e.vy *= 1.5;
2037
+ embers.push(e);
2038
+ }
2039
+ }
2040
+ ctx.globalCompositeOperation = 'source-over';
2041
+ }
2042
+ draw();
2043
+ }
2044
+
2045
+ const themeModule = { initThemeUI, togglePopup, closePopup, makeDraggable,
2046
+ THEMES, applyColors, applyFontDensity, applyBgPattern,
2047
+ applyBgEffectColor, applyBgEffectIntensity, applyBgEffectSize,
2048
+ applyFrostedGlass,
2049
+ save, getSaved, saveCustomTheme, deleteCustomTheme,
2050
+ getCustomThemes };
2051
+
2052
+ export default themeModule;
2053
+
2054
+ // Init on DOM ready, with server-side sync fallback
2055
+ async function _initWithSync() {
2056
+ // If no local theme, try loading from server (cross-device sync)
2057
+ if (!getSaved()) {
2058
+ const serverTheme = await _loadFromServer();
2059
+ if (serverTheme && serverTheme.colors) {
2060
+ if (serverTheme.name === 'sakura') serverTheme.name = 'ume';
2061
+ Storage.setJSON(LS_KEY, serverTheme);
2062
+ applyColors(serverTheme.colors);
2063
+ }
2064
+ }
2065
+ // Also sync custom themes from server
2066
+ try {
2067
+ const res = await fetch('/api/prefs/custom-themes', { credentials: 'same-origin' });
2068
+ const data = await res.json();
2069
+ if (data.value && typeof data.value === 'object') {
2070
+ const local = _loadCustomThemes();
2071
+ // Merge: server themes fill in missing local ones
2072
+ let changed = false;
2073
+ for (const [name, colors] of Object.entries(data.value)) {
2074
+ if (!local[name]) { local[name] = colors; changed = true; }
2075
+ }
2076
+ if (changed) _saveCustomThemes(local);
2077
+ }
2078
+ } catch (e) { console.warn('Custom theme server sync failed:', e); }
2079
+ initThemeUI();
2080
+ }
2081
+
2082
+ if (document.readyState === 'loading') {
2083
+ document.addEventListener('DOMContentLoaded', () => _initWithSync());
2084
+ } else {
2085
+ _initWithSync();
2086
+ }