diff --git a/bb.edn b/bb.edn index adc7a02..e95c7c3 100644 --- a/bb.edn +++ b/bb.edn @@ -16,7 +16,15 @@ (let [adapter (slurp "dev/theme-adapter.js")] (spit "dev/replicant/public/theme-adapter.js" adapter) (spit "dev/squint/public/theme-adapter.js" adapter) - (println "Copied theme-adapter.js to dev targets")))} + (println "Copied theme-adapter.js to dev targets")) + (let [lr (slurp "dev/css-live-reload.js")] + (spit "dev/replicant/public/css-live-reload.js" lr) + (spit "dev/squint/public/css-live-reload.js" lr) + (println "Copied css-live-reload.js to dev targets")))} + + watch-theme + {:doc "Watch src/ui/*.css and tokens.edn, rebuild theme on changes" + :task (load-file "scripts/watch-theme.bb")} test {:doc "Run all unit tests" @@ -125,7 +133,7 @@ session "ui-dev"] (shell {:continue true} "tmux kill-session -t" session) (shell "tmux new-session -d -s" session - (str "PORT=" hport " bb dev-hiccup")) + (str "bash -c 'bb watch-theme & PORT=" hport " bb dev-hiccup'")) (shell "tmux split-window -h -t" session (str "bash -c 'cd dev/replicant && SHADOW_HTTP_PORT=" rport " npx shadow-cljs watch app'")) (shell "tmux split-window -v -t" session diff --git a/dev/css-live-reload.js b/dev/css-live-reload.js new file mode 100644 index 0000000..b8891dc --- /dev/null +++ b/dev/css-live-reload.js @@ -0,0 +1,42 @@ +// CSS Live Reload — polls /theme.css and hot-swaps the stylesheet on change. +// No full page reload needed. Included automatically in dev pages. +(function() { + 'use strict'; + var POLL_MS = 500; + var link = document.querySelector('link[rel="stylesheet"][href*="theme"]'); + if (!link) return; + + var prevHash = 0; + + function djb2(str) { + var h = 5381; + for (var i = 0; i < str.length; i++) { + h = ((h << 5) + h + str.charCodeAt(i)) | 0; + } + return h; + } + + function check() { + fetch('/theme.css', { cache: 'no-store' }) + .then(function(r) { return r.text(); }) + .then(function(css) { + var h = djb2(css); + if (prevHash && h !== prevHash) { + // Clone link and swap — avoids FOUC + var next = link.cloneNode(); + next.href = '/theme.css?t=' + Date.now(); + link.parentNode.insertBefore(next, link.nextSibling); + next.onload = function() { + link.parentNode.removeChild(link); + link = next; + }; + console.log('[css-live-reload] theme.css updated'); + } + prevHash = h; + }) + .catch(function() { /* ignore network errors */ }); + } + + check(); + setInterval(check, POLL_MS); +})(); diff --git a/dev/hiccup/src/dev/hiccup.clj b/dev/hiccup/src/dev/hiccup.clj index da9d0f2..da6a4b9 100644 --- a/dev/hiccup/src/dev/hiccup.clj +++ b/dev/hiccup/src/dev/hiccup.clj @@ -503,6 +503,7 @@ [:script (h/raw theme-persistence-script)]] [:body [:script {:src "/theme-adapter.js" :defer true}] + [:script {:src "/css-live-reload.js" :defer true}] (sidebar/sidebar-layout {} (app-sidebar active-page port) (sidebar/sidebar-overlay {}) @@ -534,6 +535,11 @@ :headers {"Content-Type" "application/javascript"} :body (slurp "dev/theme-adapter.js")} + (= path "/css-live-reload.js") + {:status 200 + :headers {"Content-Type" "application/javascript"} + :body (slurp "dev/css-live-reload.js")} + (resolve-page path) {:status 200 :headers {"Content-Type" "text/html; charset=utf-8"} diff --git a/dev/replicant/public/index.html b/dev/replicant/public/index.html index 2e0b2a2..00c75ed 100644 --- a/dev/replicant/public/index.html +++ b/dev/replicant/public/index.html @@ -47,5 +47,6 @@
+ diff --git a/dev/squint/index.html b/dev/squint/index.html index 4024506..1da024d 100644 --- a/dev/squint/index.html +++ b/dev/squint/index.html @@ -47,5 +47,6 @@
+ diff --git a/scripts/watch-theme.bb b/scripts/watch-theme.bb new file mode 100644 index 0000000..d5d94f0 --- /dev/null +++ b/scripts/watch-theme.bb @@ -0,0 +1,44 @@ +#!/usr/bin/env bb +;; Watch src/ui/*.css and src/theme/tokens.edn for changes. +;; On change, rebuild dist/theme.css and copy to dev targets. + +(require '[babashka.fs :as fs] + '[clojure.java.io :as io] + '[clojure.string :as str] + '[ui.css.gen :as gen]) + +(defn source-files [] + (concat (map str (fs/glob "src/ui" "*.css")) + ["src/theme/tokens.edn"])) + +(defn get-mtimes [] + (into {} + (keep (fn [p] + (when (fs/exists? p) + [p (fs/last-modified-time p)]))) + (source-files))) + +(defn rebuild! [] + (gen/build-theme! {:input "src/theme/tokens.edn" :output "dist/theme.css"}) + (let [css (slurp "dist/theme.css")] + (spit "dev/replicant/public/theme.css" css) + (io/make-parents "dev/squint/public/theme.css") + (spit "dev/squint/public/theme.css" css))) + +(defn changed-filenames [prev curr] + (->> (keys curr) + (filter (fn [p] (not= (get prev p) (get curr p)))) + (map (fn [p] (last (str/split p #"/")))))) + +(println "[watch-theme] Watching src/ui/*.css and src/theme/tokens.edn ...") +(loop [prev (get-mtimes)] + (Thread/sleep 500) + (let [curr (get-mtimes)] + (when (not= prev curr) + (println (str "[watch-theme] Changed: " (str/join ", " (changed-filenames prev curr)))) + (try + (rebuild!) + (println "[watch-theme] Rebuilt and copied.") + (catch Exception e + (println (str "[watch-theme] ERROR: " (.getMessage e)))))) + (recur curr)))