fix: avatar fallback colors update instantly on theme toggle

The avatar fallback hex (background + text) is computed in JS by
`avatar-fallback-style` → `colors/read-bg-var` → `getComputedStyle`,
so its output is a snapshot of the current theme's CSS variables at
render time. The result is written into the element's inline style.
On `tt` (theme toggle) the snapshot stayed frozen until something
unrelated triggered a re-render, leaving avatars carrying the wrong
theme's tone — too bright in dark mode, too dark in light mode.

Two coordinated changes:

1. state/set-theme-mode! now stamps `data-theme` + body classes
   synchronously *before* mutating `:ui/theme`. The previous flow
   left the DOM update inside theme.cljs's `use-effect!`, which
   fires AFTER React's render commit — so subscribers re-rendering
   on the state change still read the old theme's CSS vars. The
   theme.cljs effect remains as an idempotent safety net plus the
   side effects (plugin hook, custom-theme application).

2. `avatar-image-cp` and `get-node-icon-cp` subscribe to `:ui/theme`.
   The subscribed value is discarded — the subscription's job is to
   tick Rum's dependency graph so the component re-renders on toggle
   and recomputes `avatar-fallback-style` against the (now fresh)
   CSS variables. Combined with the synchronous DOM update above,
   the read-bg-var snapshot is correct on the first render after
   toggle, no second tick required.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
scheinriese
2026-05-12 17:31:42 +02:00
parent e30384d6c5
commit c540a92ffa
2 changed files with 43 additions and 1 deletions

View File

@@ -478,6 +478,15 @@
;; the design rationale (subscribing to the per-tx atom is
;; what catches retractions; `model/sub-block` doesn't).
_latest-tx (state/sub :db/latest-transacted-entity-uuids)
;; Re-render on theme toggle so `avatar-fallback-style` re-
;; samples `--ls-primary-background-color` from the new
;; cascade. The avatar's inline `style` is computed in JS
;; from a `getComputedStyle` snapshot — without a Rum
;; subscription on `:ui/theme`, the snapshot stays frozen
;; after a theme toggle (state.cljs:1283) and the avatar
;; renders with the previous theme's surface tone until
;; some unrelated re-render fires.
_theme (state/sub :ui/theme)
asset-entity (when (and asset-uuid (string? asset-uuid))
(try (db/entity [:block/uuid (uuid asset-uuid)])
(catch :default _ nil)))
@@ -893,7 +902,16 @@
(rum/defc get-node-icon-cp < rum/reactive db-mixins/query
[node-entity opts]
(let [;; Get fresh entity using db/sub-block to make it reactive to property changes
(let [;; Re-render on theme toggle so the bare-avatar branch inside
;; `(icon ...)` (text/letters/emoji avatars) re-samples the
;; live `--ls-primary-background-color` via `read-bg-var`
;; when computing `avatar-fallback-style`. Without this sub,
;; the inline `style` hexes baked in at the previous render
;; stay frozen across theme toggles — none of the other
;; subscriptions in this component (`:ui/icon-hover-preview`,
;; `model/sub-block`) tick on `:ui/theme` changes.
_theme (state/sub :ui/theme)
;; Get fresh entity using db/sub-block to make it reactive to property changes
fresh-entity (when-let [db-id (:db/id node-entity)]
(or (model/sub-block db-id) node-entity))
entity (or fresh-entity node-entity)

View File

@@ -1244,6 +1244,29 @@ Similar to re-frame subscriptions"
(set-edit-content! edit-input-id content)
(set-editor-last-pos! new-pos)))
(defn- apply-theme-to-dom!
"Synchronously stamp `data-theme` + body classes onto the document
so CSS variables re-resolve to the new theme *before* the state
mutation below triggers React subscribers. Without this, subscribers
that compute hex values from CSS vars at render time (e.g. avatar
fallback colors via `colors/read-bg-var` → `getComputedStyle`)
would re-render with the OLD theme's resolved vars because
`theme.cljs`'s `use-effect!` only sets these attributes *after*
the render commit. That effect remains as a safety net (it's
idempotent — same setAttribute) and continues to handle the
plugin-hook + custom-theme side effects."
[mode]
(when (exists? js/document)
(let [^js doc js/document.documentElement
^js cls (.-classList doc)
^js cls-body (.-classList js/document.body)]
(.setAttribute doc "data-theme" mode)
(if (= mode "dark")
(do (.add cls "dark")
(doto cls-body (.remove "white-theme" "light-theme") (.add "dark-theme")))
(do (.remove cls "dark")
(doto cls-body (.remove "dark-theme") (.add "white-theme" "light-theme")))))))
(defn set-theme-mode!
([mode] (set-theme-mode! mode (:ui/system-theme? @state)))
([mode system-theme?]
@@ -1253,6 +1276,7 @@ Similar to re-frame subscriptions"
(util/set-theme-dark)))
(when (mobile-util/native-platform?)
(mobile-util/set-native-interface-style! mode system-theme?))
(apply-theme-to-dom! mode)
(set-state! :ui/theme mode)
(storage/set :ui/theme mode)))