@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/agents/definition.d.ts +15 -0
- package/dist/agents/definition.d.ts.map +1 -0
- package/dist/agents/definition.js +41 -0
- package/dist/agents/definition.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +11 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +23 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/specialized.d.ts +31 -0
- package/dist/agents/specialized.d.ts.map +1 -0
- package/dist/agents/specialized.js +75 -0
- package/dist/agents/specialized.js.map +1 -0
- package/dist/awareness/index.d.ts +14 -0
- package/dist/awareness/index.d.ts.map +1 -0
- package/dist/awareness/index.js +22 -0
- package/dist/awareness/index.js.map +1 -0
- package/dist/awareness/service.d.ts +180 -0
- package/dist/awareness/service.d.ts.map +1 -0
- package/dist/awareness/service.js +789 -0
- package/dist/awareness/service.js.map +1 -0
- package/dist/cli/entry.d.ts +13 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/entry.js +35 -0
- package/dist/cli/entry.js.map +1 -0
- package/dist/comms/telegram.d.ts +41 -0
- package/dist/comms/telegram.d.ts.map +1 -0
- package/dist/comms/telegram.js +332 -0
- package/dist/comms/telegram.js.map +1 -0
- package/dist/core/agent.d.ts +252 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1399 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/auth.d.ts +23 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +113 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/authority.d.ts +118 -0
- package/dist/core/authority.d.ts.map +1 -0
- package/dist/core/authority.js +402 -0
- package/dist/core/authority.js.map +1 -0
- package/dist/core/backend/async_queue.d.ts +17 -0
- package/dist/core/backend/async_queue.d.ts.map +1 -0
- package/dist/core/backend/async_queue.js +66 -0
- package/dist/core/backend/async_queue.js.map +1 -0
- package/dist/core/backend/index.d.ts +24 -0
- package/dist/core/backend/index.d.ts.map +1 -0
- package/dist/core/backend/index.js +128 -0
- package/dist/core/backend/index.js.map +1 -0
- package/dist/core/backend/llama_server.d.ts +39 -0
- package/dist/core/backend/llama_server.d.ts.map +1 -0
- package/dist/core/backend/llama_server.js +182 -0
- package/dist/core/backend/llama_server.js.map +1 -0
- package/dist/core/backend/llamacpp.d.ts +40 -0
- package/dist/core/backend/llamacpp.d.ts.map +1 -0
- package/dist/core/backend/llamacpp.js +200 -0
- package/dist/core/backend/llamacpp.js.map +1 -0
- package/dist/core/backend/lmstudio.d.ts +53 -0
- package/dist/core/backend/lmstudio.d.ts.map +1 -0
- package/dist/core/backend/lmstudio.js +274 -0
- package/dist/core/backend/lmstudio.js.map +1 -0
- package/dist/core/backend/ollama.d.ts +25 -0
- package/dist/core/backend/ollama.d.ts.map +1 -0
- package/dist/core/backend/ollama.js +86 -0
- package/dist/core/backend/ollama.js.map +1 -0
- package/dist/core/backend/openai.d.ts +27 -0
- package/dist/core/backend/openai.d.ts.map +1 -0
- package/dist/core/backend/openai.js +84 -0
- package/dist/core/backend/openai.js.map +1 -0
- package/dist/core/backend/types.d.ts +73 -0
- package/dist/core/backend/types.d.ts.map +1 -0
- package/dist/core/backend/types.js +137 -0
- package/dist/core/backend/types.js.map +1 -0
- package/dist/core/chat_store.d.ts +77 -0
- package/dist/core/chat_store.d.ts.map +1 -0
- package/dist/core/chat_store.js +332 -0
- package/dist/core/chat_store.js.map +1 -0
- package/dist/core/cli.d.ts +82 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +602 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +127 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +295 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/debug.d.ts +8 -0
- package/dist/core/debug.d.ts.map +1 -0
- package/dist/core/debug.js +8 -0
- package/dist/core/debug.js.map +1 -0
- package/dist/core/downloader.d.ts +31 -0
- package/dist/core/downloader.d.ts.map +1 -0
- package/dist/core/downloader.js +180 -0
- package/dist/core/downloader.js.map +1 -0
- package/dist/core/errors.d.ts +80 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +134 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/extras.d.ts +160 -0
- package/dist/core/extras.d.ts.map +1 -0
- package/dist/core/extras.js +553 -0
- package/dist/core/extras.js.map +1 -0
- package/dist/core/health.d.ts +27 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +96 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/history.d.ts +50 -0
- package/dist/core/history.d.ts.map +1 -0
- package/dist/core/history.js +276 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/intelligence/ast_index.d.ts +255 -0
- package/dist/core/intelligence/ast_index.d.ts.map +1 -0
- package/dist/core/intelligence/ast_index.js +1369 -0
- package/dist/core/intelligence/ast_index.js.map +1 -0
- package/dist/core/intelligence/confidence.d.ts +113 -0
- package/dist/core/intelligence/confidence.d.ts.map +1 -0
- package/dist/core/intelligence/confidence.js +298 -0
- package/dist/core/intelligence/confidence.js.map +1 -0
- package/dist/core/intelligence/context_compressor.d.ts +80 -0
- package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
- package/dist/core/intelligence/context_compressor.js +245 -0
- package/dist/core/intelligence/context_compressor.js.map +1 -0
- package/dist/core/intelligence/daemon.d.ts +247 -0
- package/dist/core/intelligence/daemon.d.ts.map +1 -0
- package/dist/core/intelligence/daemon.js +842 -0
- package/dist/core/intelligence/daemon.js.map +1 -0
- package/dist/core/intelligence/index.d.ts +60 -0
- package/dist/core/intelligence/index.d.ts.map +1 -0
- package/dist/core/intelligence/index.js +101 -0
- package/dist/core/intelligence/index.js.map +1 -0
- package/dist/core/intelligence/predictor.d.ts +246 -0
- package/dist/core/intelligence/predictor.d.ts.map +1 -0
- package/dist/core/intelligence/predictor.js +590 -0
- package/dist/core/intelligence/predictor.js.map +1 -0
- package/dist/core/intelligence/reflex.d.ts +84 -0
- package/dist/core/intelligence/reflex.d.ts.map +1 -0
- package/dist/core/intelligence/reflex.js +344 -0
- package/dist/core/intelligence/reflex.js.map +1 -0
- package/dist/core/intelligence/self_correction.d.ts +121 -0
- package/dist/core/intelligence/self_correction.d.ts.map +1 -0
- package/dist/core/intelligence/self_correction.js +327 -0
- package/dist/core/intelligence/self_correction.js.map +1 -0
- package/dist/core/intelligence/skill_distiller.d.ts +100 -0
- package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
- package/dist/core/intelligence/skill_distiller.js +246 -0
- package/dist/core/intelligence/skill_distiller.js.map +1 -0
- package/dist/core/intelligence/swarm.d.ts +330 -0
- package/dist/core/intelligence/swarm.d.ts.map +1 -0
- package/dist/core/intelligence/swarm.js +1064 -0
- package/dist/core/intelligence/swarm.js.map +1 -0
- package/dist/core/intelligence/trajectory.d.ts +107 -0
- package/dist/core/intelligence/trajectory.d.ts.map +1 -0
- package/dist/core/intelligence/trajectory.js +330 -0
- package/dist/core/intelligence/trajectory.js.map +1 -0
- package/dist/core/logger.d.ts +111 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +640 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/loop.d.ts +123 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +423 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/memory.d.ts +81 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +328 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/metrics.d.ts +29 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +109 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/models.d.ts +51 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +146 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/optimizer.d.ts +34 -0
- package/dist/core/optimizer.d.ts.map +1 -0
- package/dist/core/optimizer.js +131 -0
- package/dist/core/optimizer.js.map +1 -0
- package/dist/core/persona.d.ts +35 -0
- package/dist/core/persona.d.ts.map +1 -0
- package/dist/core/persona.js +126 -0
- package/dist/core/persona.js.map +1 -0
- package/dist/core/rate_limit.d.ts +23 -0
- package/dist/core/rate_limit.d.ts.map +1 -0
- package/dist/core/rate_limit.js +62 -0
- package/dist/core/rate_limit.js.map +1 -0
- package/dist/core/runner.d.ts +63 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +671 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/stream_parser.d.ts +43 -0
- package/dist/core/stream_parser.d.ts.map +1 -0
- package/dist/core/stream_parser.js +140 -0
- package/dist/core/stream_parser.js.map +1 -0
- package/dist/core/tool_schemas.d.ts +26 -0
- package/dist/core/tool_schemas.d.ts.map +1 -0
- package/dist/core/tool_schemas.js +124 -0
- package/dist/core/tool_schemas.js.map +1 -0
- package/dist/core/which.d.ts +2 -0
- package/dist/core/which.d.ts.map +1 -0
- package/dist/core/which.js +35 -0
- package/dist/core/which.js.map +1 -0
- package/dist/goals/index.d.ts +9 -0
- package/dist/goals/index.d.ts.map +1 -0
- package/dist/goals/index.js +17 -0
- package/dist/goals/index.js.map +1 -0
- package/dist/goals/service.d.ts +115 -0
- package/dist/goals/service.d.ts.map +1 -0
- package/dist/goals/service.js +415 -0
- package/dist/goals/service.js.map +1 -0
- package/dist/goals/types.d.ts +47 -0
- package/dist/goals/types.d.ts.map +1 -0
- package/dist/goals/types.js +66 -0
- package/dist/goals/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/autonomous_startup.d.ts +51 -0
- package/dist/integrations/autonomous_startup.d.ts.map +1 -0
- package/dist/integrations/autonomous_startup.js +419 -0
- package/dist/integrations/autonomous_startup.js.map +1 -0
- package/dist/integrations/goose.d.ts +105 -0
- package/dist/integrations/goose.d.ts.map +1 -0
- package/dist/integrations/goose.js +408 -0
- package/dist/integrations/goose.js.map +1 -0
- package/dist/integrations/index.d.ts +21 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +17 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/mcp.d.ts +73 -0
- package/dist/integrations/mcp.d.ts.map +1 -0
- package/dist/integrations/mcp.js +307 -0
- package/dist/integrations/mcp.js.map +1 -0
- package/dist/integrations/mempalace.d.ts +18 -0
- package/dist/integrations/mempalace.d.ts.map +1 -0
- package/dist/integrations/mempalace.js +25 -0
- package/dist/integrations/mempalace.js.map +1 -0
- package/dist/integrations/multi_agent.d.ts +82 -0
- package/dist/integrations/multi_agent.d.ts.map +1 -0
- package/dist/integrations/multi_agent.js +280 -0
- package/dist/integrations/multi_agent.js.map +1 -0
- package/dist/integrations/unified_memory.d.ts +415 -0
- package/dist/integrations/unified_memory.d.ts.map +1 -0
- package/dist/integrations/unified_memory.js +1489 -0
- package/dist/integrations/unified_memory.js.map +1 -0
- package/dist/roles/beru.yaml +48 -0
- package/dist/sidecar/client.d.ts +63 -0
- package/dist/sidecar/client.d.ts.map +1 -0
- package/dist/sidecar/client.js +222 -0
- package/dist/sidecar/client.js.map +1 -0
- package/dist/sidecar/index.d.ts +7 -0
- package/dist/sidecar/index.d.ts.map +1 -0
- package/dist/sidecar/index.js +6 -0
- package/dist/sidecar/index.js.map +1 -0
- package/dist/startup.d.ts +22 -0
- package/dist/startup.d.ts.map +1 -0
- package/dist/startup.js +194 -0
- package/dist/startup.js.map +1 -0
- package/dist/tools/_internal.d.ts +90 -0
- package/dist/tools/_internal.d.ts.map +1 -0
- package/dist/tools/_internal.js +375 -0
- package/dist/tools/_internal.js.map +1 -0
- package/dist/tools/_smoke.d.ts +2 -0
- package/dist/tools/_smoke.d.ts.map +1 -0
- package/dist/tools/_smoke.js +203 -0
- package/dist/tools/_smoke.js.map +1 -0
- package/dist/tools/axodex_tools.d.ts +51 -0
- package/dist/tools/axodex_tools.d.ts.map +1 -0
- package/dist/tools/axodex_tools.js +181 -0
- package/dist/tools/axodex_tools.js.map +1 -0
- package/dist/tools/code_tools.d.ts +33 -0
- package/dist/tools/code_tools.d.ts.map +1 -0
- package/dist/tools/code_tools.js +191 -0
- package/dist/tools/code_tools.js.map +1 -0
- package/dist/tools/computer_tools.d.ts +59 -0
- package/dist/tools/computer_tools.d.ts.map +1 -0
- package/dist/tools/computer_tools.js +348 -0
- package/dist/tools/computer_tools.js.map +1 -0
- package/dist/tools/file_tools.d.ts +51 -0
- package/dist/tools/file_tools.d.ts.map +1 -0
- package/dist/tools/file_tools.js +424 -0
- package/dist/tools/file_tools.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +13 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/shell_tools.d.ts +39 -0
- package/dist/tools/shell_tools.d.ts.map +1 -0
- package/dist/tools/shell_tools.js +163 -0
- package/dist/tools/shell_tools.js.map +1 -0
- package/dist/tools/web_tools.d.ts +31 -0
- package/dist/tools/web_tools.d.ts.map +1 -0
- package/dist/tools/web_tools.js +117 -0
- package/dist/tools/web_tools.js.map +1 -0
- package/dist/voice/index.d.ts +41 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +58 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/light_daemon.d.ts +197 -0
- package/dist/voice/light_daemon.d.ts.map +1 -0
- package/dist/voice/light_daemon.js +746 -0
- package/dist/voice/light_daemon.js.map +1 -0
- package/dist/voice/setup.d.ts +63 -0
- package/dist/voice/setup.d.ts.map +1 -0
- package/dist/voice/setup.js +443 -0
- package/dist/voice/setup.js.map +1 -0
- package/dist/voice/stt.d.ts +148 -0
- package/dist/voice/stt.d.ts.map +1 -0
- package/dist/voice/stt.js +361 -0
- package/dist/voice/stt.js.map +1 -0
- package/dist/voice/tts.d.ts +168 -0
- package/dist/voice/tts.d.ts.map +1 -0
- package/dist/voice/tts.js +698 -0
- package/dist/voice/tts.js.map +1 -0
- package/dist/voice/voice_loop.d.ts +101 -0
- package/dist/voice/voice_loop.d.ts.map +1 -0
- package/dist/voice/voice_loop.js +463 -0
- package/dist/voice/voice_loop.js.map +1 -0
- package/dist/voice/wake_word.d.ts +153 -0
- package/dist/voice/wake_word.d.ts.map +1 -0
- package/dist/voice/wake_word.js +458 -0
- package/dist/voice/wake_word.js.map +1 -0
- package/dist/voice/wav.d.ts +99 -0
- package/dist/voice/wav.d.ts.map +1 -0
- package/dist/voice/wav.js +306 -0
- package/dist/voice/wav.js.map +1 -0
- package/dist/web/broker.d.ts +33 -0
- package/dist/web/broker.d.ts.map +1 -0
- package/dist/web/broker.js +58 -0
- package/dist/web/broker.js.map +1 -0
- package/dist/web/open_browser.d.ts +2 -0
- package/dist/web/open_browser.d.ts.map +1 -0
- package/dist/web/open_browser.js +42 -0
- package/dist/web/open_browser.js.map +1 -0
- package/dist/web/server.d.ts +54 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +831 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/static/app.js +4121 -0
- package/dist/web/static/index.html +2281 -0
- package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
- package/dist/web/static/js/a11y.js +165 -0
- package/dist/web/static/js/admin.js +2145 -0
- package/dist/web/static/js/assistant.js +475 -0
- package/dist/web/static/js/calendar/reminders.js +114 -0
- package/dist/web/static/js/calendar/utils.js +166 -0
- package/dist/web/static/js/calendar.js +3348 -0
- package/dist/web/static/js/censor.js +356 -0
- package/dist/web/static/js/chat.js +4546 -0
- package/dist/web/static/js/chatRenderer.js +2294 -0
- package/dist/web/static/js/chatStream.js +276 -0
- package/dist/web/static/js/codeRunner.js +398 -0
- package/dist/web/static/js/color/hex.js +14 -0
- package/dist/web/static/js/colorPicker.js +453 -0
- package/dist/web/static/js/compare/icons.js +77 -0
- package/dist/web/static/js/compare/index.js +1482 -0
- package/dist/web/static/js/compare/models.js +104 -0
- package/dist/web/static/js/compare/panes.js +817 -0
- package/dist/web/static/js/compare/probe.js +78 -0
- package/dist/web/static/js/compare/scoreboard.js +223 -0
- package/dist/web/static/js/compare/selector.js +1335 -0
- package/dist/web/static/js/compare/state.js +58 -0
- package/dist/web/static/js/compare/stream.js +695 -0
- package/dist/web/static/js/compare/vote.js +254 -0
- package/dist/web/static/js/cookbook-diagnosis.js +694 -0
- package/dist/web/static/js/cookbook-hwfit.js +1888 -0
- package/dist/web/static/js/cookbook.js +2093 -0
- package/dist/web/static/js/cookbookDownload.js +633 -0
- package/dist/web/static/js/cookbookProgressSignal.js +29 -0
- package/dist/web/static/js/cookbookRunning.js +3506 -0
- package/dist/web/static/js/cookbookServe.js +2128 -0
- package/dist/web/static/js/document.js +9731 -0
- package/dist/web/static/js/documentLibrary.js +3376 -0
- package/dist/web/static/js/dragSort.js +265 -0
- package/dist/web/static/js/editor/ai-inpaint.js +374 -0
- package/dist/web/static/js/editor/ai-models.js +273 -0
- package/dist/web/static/js/editor/ai-rembg.js +230 -0
- package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
- package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
- package/dist/web/static/js/editor/build/controls.js +366 -0
- package/dist/web/static/js/editor/build/popups.js +112 -0
- package/dist/web/static/js/editor/build/right-panel.js +200 -0
- package/dist/web/static/js/editor/build/toolbar.js +73 -0
- package/dist/web/static/js/editor/build/topbar.js +131 -0
- package/dist/web/static/js/editor/build/transform-popup.js +109 -0
- package/dist/web/static/js/editor/canvas-coords.js +21 -0
- package/dist/web/static/js/editor/canvas-events.js +197 -0
- package/dist/web/static/js/editor/canvas-transforms.js +132 -0
- package/dist/web/static/js/editor/checkerboard.js +24 -0
- package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
- package/dist/web/static/js/editor/composite-helpers.js +83 -0
- package/dist/web/static/js/editor/filters/blur.js +118 -0
- package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
- package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
- package/dist/web/static/js/editor/fx/filter-string.js +38 -0
- package/dist/web/static/js/editor/fx/histogram.js +67 -0
- package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
- package/dist/web/static/js/editor/harmonize-masks.js +116 -0
- package/dist/web/static/js/editor/history-panel.js +176 -0
- package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
- package/dist/web/static/js/editor/layer-helpers.js +131 -0
- package/dist/web/static/js/editor/layer-panel.js +600 -0
- package/dist/web/static/js/editor/mask-utils.js +91 -0
- package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
- package/dist/web/static/js/editor/slider-ux.js +191 -0
- package/dist/web/static/js/editor/snap.js +107 -0
- package/dist/web/static/js/editor/state.js +257 -0
- package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
- package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
- package/dist/web/static/js/editor/tools/clone.js +81 -0
- package/dist/web/static/js/editor/tools/crop.js +137 -0
- package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
- package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
- package/dist/web/static/js/editor/tools/lasso.js +65 -0
- package/dist/web/static/js/editor/tools/move.js +79 -0
- package/dist/web/static/js/editor/tools/stroke.js +123 -0
- package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
- package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
- package/dist/web/static/js/editor/tools/transform-session.js +381 -0
- package/dist/web/static/js/editor/tools/wand.js +46 -0
- package/dist/web/static/js/editor/wire-import.js +148 -0
- package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
- package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
- package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
- package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
- package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
- package/dist/web/static/js/editor/wire-topbar.js +188 -0
- package/dist/web/static/js/emailInbox.js +1245 -0
- package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
- package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
- package/dist/web/static/js/emailLibrary/state.js +34 -0
- package/dist/web/static/js/emailLibrary/utils.js +202 -0
- package/dist/web/static/js/emailLibrary.js +5217 -0
- package/dist/web/static/js/emojiPicker.js +311 -0
- package/dist/web/static/js/escMenuStack.js +102 -0
- package/dist/web/static/js/fileHandler.js +295 -0
- package/dist/web/static/js/gallery.js +2835 -0
- package/dist/web/static/js/galleryEditor.js +3798 -0
- package/dist/web/static/js/group.js +955 -0
- package/dist/web/static/js/init.js +405 -0
- package/dist/web/static/js/keyboard-shortcuts.js +292 -0
- package/dist/web/static/js/langIcons.js +187 -0
- package/dist/web/static/js/markdown/tableRow.js +19 -0
- package/dist/web/static/js/markdown.js +806 -0
- package/dist/web/static/js/memory.js +1418 -0
- package/dist/web/static/js/modalManager.js +1552 -0
- package/dist/web/static/js/modalSnap.js +782 -0
- package/dist/web/static/js/model/matchKey.js +19 -0
- package/dist/web/static/js/modelPicker.js +695 -0
- package/dist/web/static/js/modelSort.js +33 -0
- package/dist/web/static/js/models.js +642 -0
- package/dist/web/static/js/notes.js +5054 -0
- package/dist/web/static/js/package.json +1 -0
- package/dist/web/static/js/platform.js +47 -0
- package/dist/web/static/js/presets.js +1110 -0
- package/dist/web/static/js/providers.js +93 -0
- package/dist/web/static/js/rag.js +177 -0
- package/dist/web/static/js/research/jobs.js +335 -0
- package/dist/web/static/js/research/panel.js +1233 -0
- package/dist/web/static/js/researchSynapse.js +225 -0
- package/dist/web/static/js/search-chat.js +201 -0
- package/dist/web/static/js/search.js +52 -0
- package/dist/web/static/js/section-management.js +260 -0
- package/dist/web/static/js/sessions.js +3108 -0
- package/dist/web/static/js/settings.js +4991 -0
- package/dist/web/static/js/sidebar-layout.js +573 -0
- package/dist/web/static/js/signature.js +504 -0
- package/dist/web/static/js/skills.js +1908 -0
- package/dist/web/static/js/slashAutocomplete.js +267 -0
- package/dist/web/static/js/slashCommands.js +6099 -0
- package/dist/web/static/js/spinner.js +406 -0
- package/dist/web/static/js/storage.js +123 -0
- package/dist/web/static/js/tasks.js +2699 -0
- package/dist/web/static/js/theme.js +2086 -0
- package/dist/web/static/js/tileManager.js +386 -0
- package/dist/web/static/js/tourAutoplay.js +133 -0
- package/dist/web/static/js/tourHints.js +179 -0
- package/dist/web/static/js/tts-ai.js +521 -0
- package/dist/web/static/js/ui.js +1288 -0
- package/dist/web/static/js/util/ordinal.js +13 -0
- package/dist/web/static/js/voiceRecorder.js +283 -0
- package/dist/web/static/js/windowDrag.js +326 -0
- package/dist/web/static/js/windowResize.js +233 -0
- package/dist/web/static/landing.html +824 -0
- package/dist/web/static/lib/docx.umd.min.js +20116 -0
- package/dist/web/static/lib/highlight.min.js +1213 -0
- package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
- package/dist/web/static/lib/mammoth.browser.min.js +21 -0
- package/dist/web/static/lib/qrcode.min.js +8 -0
- package/dist/web/static/lib/xlsx.full.min.js +24 -0
- package/dist/web/static/login.html +582 -0
- package/dist/web/static/manifest.json +15 -0
- package/dist/web/static/style.css +35733 -0
- package/dist/web/static/sw.js +144 -0
- package/dist/workflows/engine.d.ts +162 -0
- package/dist/workflows/engine.d.ts.map +1 -0
- package/dist/workflows/engine.js +625 -0
- package/dist/workflows/engine.js.map +1 -0
- package/dist/workflows/index.d.ts +8 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +14 -0
- package/dist/workflows/index.js.map +1 -0
- 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 = '✓ Downloaded!';
|
|
1265
|
+
setTimeout(() => { newExp.innerHTML = '⤓ 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
|
+
}
|