(ns outliner.view.app (:require [reagent.dom :as rdom] [clojure.string :as str] [outliner.boundaries.auth :as auth] [outliner.boundaries.storage :as storage] [outliner.model.state :as state] [outliner.control.dispatch :as dispatch] [outliner.view.components :as components] [outliner.view.events :as events])) ;; --- UI State --- ;; Enhanced save with storage manager (add-watch state/doc-state :storage-watcher (fn [_key _atom old-doc new-doc] (let [{:keys [user sync-status]} @state/sys-state] (when (and (not= sync-status :loading) (not= old-doc new-doc)) ;; Use setTimeout to ensure this runs after the current dispatch is complete (js/setTimeout #(storage/save-state! state/doc-state state/sys-state user) 0))))) ;; View state URL sync (add-watch state/view-state :url-watcher (fn [_ _ old-view new-view] (let [new-zoom (:zoom-id new-view) old-zoom (:zoom-id old-view)] (when (not= new-zoom old-zoom) (let [url (js/URL. js/window.location.href) params (.-searchParams url) current-zoom (.get params "zoom")] (when (not= new-zoom current-zoom) (if new-zoom (.set params "zoom" new-zoom) (.delete params "zoom")) (.pushState js/window.history nil "" (.toString url)))))))) ;; Dark mode body class sync (add-watch state/sys-state :dark-mode-body-watcher (fn [_ _ _ new-state] (if (:dark-mode? new-state) (.add (.-classList js/document.body) "dark-mode") (.remove (.-classList js/document.body) "dark-mode")))) ;; Toast auto-hide (defonce toast-timeout (atom nil)) (add-watch state/view-state :toast-watcher (fn [_ _ old-view new-view] (let [new-toast (:toast new-view) old-toast (:toast old-view)] (when (and new-toast (not= new-toast old-toast)) (when @toast-timeout (js/clearTimeout @toast-timeout)) (reset! toast-timeout (js/setTimeout #(state/hide-toast!) 3000)))))) ;; Browser tab title sync (defn- sync-title [] (let [doc @state/doc-state view @state/view-state zoom-id (:zoom-id view) node (get-in doc [:nodes zoom-id]) title (if (and zoom-id node) (let [t (:text node)] (if (str/blank? t) "Untitled" t)) "Palatium")] (when (not= (.-title js/document) title) (set! (.-title js/document) title)))) (add-watch state/view-state :title-watcher (fn [_ _ _ _] (sync-title))) (add-watch state/doc-state :title-watcher (fn [_ _ _ _] (sync-title))) (sync-title) (auth/init-auth-state) (events/init-events!) (defn app-header [sys-state] (let [dark-mode? (:dark-mode? sys-state)] [:header.app-header [:a.header-left {:href "?" :on-click (components/on-zoom-click nil) :role "button" :tab-index 0 :on-key-down (components/on-kb-action #(components/zoom-or-collapse nil)) :aria-label "Home"} [:img.app-logo {:src "logo.jpg" :alt "Palatium Logo"}] [:h1.app-title "Palatium"]] [:div.header-right [:button.dark-mode-toggle.header-dark-mode-toggle {:on-click #(dispatch/dispatch! [:outliner.command/toggle-dark-mode]) :title "Toggle Dark Mode" :aria-label "Toggle Dark Mode"} (if dark-mode? "☀️" "🌙")] [components/auth-button sys-state]]])) (defn zoom-view [doc-state view-state zoom-id] (let [node (get-in doc-state [:nodes zoom-id]) focused? (= zoom-id (:focused-id view-state))] [:div.zoom-view [:div.zoom-title-row [components/zoom-title-editor zoom-id node focused?] [components/node-whitespace]] (for [child-id (:children node)] ^{:key child-id} [components/node-component child-id])])) (defn main-view [doc-state] [:div.main-view (for [root-id (:root doc-state)] ^{:key root-id} [components/node-component root-id])]) (defn app [] (if (state/html-format?) [components/plain-html-view @state/doc-state (:zoom-id @state/view-state)] [:div.app-main-layout {:on-click events/on-global-click :on-input events/on-global-input} [:div.app-container [app-header @state/sys-state] [components/breadcrumbs @state/doc-state @state/view-state] (if-let [zoom-id (:zoom-id @state/view-state)] [zoom-view @state/doc-state @state/view-state zoom-id] [main-view @state/doc-state])] [components/mobile-toolbar @state/view-state @state/sys-state] [components/toast-notification (:toast @state/view-state) @state/sys-state]])) (rdom/render [app] (.getElementById js/document "app"))