state.cljs and theme.cljs both stamped data-theme + body classes onto
the DOM (in apply-theme-to-dom! and the container's mount effect
respectively). The duplicate setAttribute work is idempotent in
practice but the two code paths drift on edge cases (plugin hook
order, custom-theme application) and obscure who owns the stamp.
Promote state/apply-theme-to-dom! to public and have theme.cljs's
effect call it instead of inlining its own copy. Now: set-theme-mode!
calls it synchronously before set-state! (so subscribers see fresh
DOM on next render); the container effect calls it on mount and on
every theme prop change (so the initial render and any external
:ui/theme writes also stamp the DOM). Effect retains its
custom-theme + plugin-hook + re-render-root! responsibilities.
re-render-root! kept on toggle: bg-var cache invalidation (commit
6b4e5fb910) makes recomputed values correct, but components that
don't subscribe to :ui/theme but DO render avatar/contrast colors
would keep stale inline styles without a forced reconciliation.
Verified live: set-theme-mode! "dark" → data-theme stamped to "dark",
`dark` class added to <html>, body classes updated, end state
consistent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
read-bg-var was called on every render of every colored icon via three
call sites in icon.cljs (avatar-fallback-style, icon color? wrapper,
get-node-icon-cp). On a graph with many tagged pages — page list, cmdk
results, sidebar — that meant hundreds of synchronous getComputedStyle
calls per frame, each forcing a style recalc / layout flush.
Cache values per CSS var name in a defonce atom; a MutationObserver on
documentElement watching data-theme + class invalidates the cache on
theme flips (apply-theme-to-dom! writes both). Observer callbacks fire
at the microtask boundary — before React commits the render scheduled
by the same set-state! that triggered the flip — so subscribers always
see fresh values on the next paint without an explicit invalidation
call from state.cljs.
Cache also holds nil so unset vars don't repeatedly hit getComputedStyle.
Verified live:
- 1001 read-bg-var calls after initial fill: 0 getComputedStyle calls.
- Flip documentElement data-theme attribute: exactly 1 fresh
getComputedStyle call on the next lookup (observer fired, cache
cleared, value recomputed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
attach! sets every Radix popper to inert=true and installs 5 window-
capture listeners (pointerdown/mousedown/click/contextmenu/keydown).
Cleanup was wired only to PhotoSwipe's "destroy" event, but if .init
or .loadAndOpen threw, that event never fired — leaving the listeners
attached and every popper permanently inert (soft-bricked app).
Wrap init+loadAndOpen in try/catch: on failure synchronously call
detach! to roll back inert + remove listeners, clear the window-global
so mobile/navigation's later .destroy call doesn't act on a broken
instance, and rethrow so the underlying error surfaces.
Verified by stubbing PhotoSwipeLightbox to throw inside .init and
calling preview-images!: caught exception bubbles up, inert restored
to false on a probe popper, and window.photoLightbox is null.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cleanup pass following the icon-picker i18n migration. Removes 84 keys
in :settings-page/*, :content/*, :linked-references/*, :text/* that
were imported from a sibling branch but have no (t ...) call sites in
the current codebase, plus 5 stale bare-form shortcut keys
(:editor/copy, :dev/show-{block-data,block-ast,page-data},
:asset/confirm-delete) whose dynamic shortcut-desc lookup goes through
the decorated form :command.<ns>/<leaf> instead.
Verified via:
- ripgrep across src/ and deps/ for each key (no static call sites)
- inspection of shortcut-desc-by-id + decorate-namespace path
- bb lang:validate-translations: down from 94 unused → 5 unused
- 5 remaining unused are master-owned :icon/icons-count,
:icon/matched-count, :icon/search-{all,emojis,icons}; left in place
- bb lang:lint-hardcoded -w: still 0 hardcoded strings
- shadow/compile :app: 0 warnings
Keys existed only in en.edn — no other locale files affected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move icon picker + property default-icon-row + selection action-bar text
through frontend.context.i18n/t per .agents/skills/logseq-i18n. Adds
~93 new keys across :icon/*, :icon.{asset,asset-mode,asset-search,
avatar-band,avatar-fallback,avatar-scope,avatar-tab,clipboard,color,
fallback,mode,section-header,shape,text-picker,text-tab,upload,
web-images}/*, :class.default-icon/* namespaces. Each new key is
defined in both en.edn (source) and zh-cn.edn (required second locale
per skill Rule 3).
Compound sentences kept whole via interpolate-rich-text-node (Rule 4);
dynamic-count messages use Tongue function values (Rule 6); existing
generic keys reused where semantics matched (:ui/cancel, :ui/reset,
:ui/empty, :ui/untitled, :property.status/done, :context-menu/set-icon).
Verified:
- bb lang:lint-hardcoded -w → 0 hardcoded strings
- bb lang:validate-translations → all referenced keys defined
- shadow/compile :app → 0 warnings
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings 99 new master commits forward. 5 file-level conflicts resolved:
- deps/db/src/logseq/db/frontend/schema.cljs — bumped version to "65.32"
to accommodate both sides' migration chains
- src/main/frontend/worker/db/migrate.cljs — both branches defined a
65.26 migration; renumbered master's :logseq.property.repeat/repeat-type
to 65.32 after my 65.26..65.31 chain
- src/main/frontend/components/content.cljs — dropped master's debug
prn (commit "remove debug print"); kept HEAD's indentation
- src/main/frontend/components/icon.cljs — kept HEAD's refactor of
emoji-cp into type-dispatched emoji/icon/text/avatar components
+ render-item dispatcher; master's simpler emoji-cp would break
the item-render call site
- .agents/skills/logseq-task-on-lambda/SKILL.md — took master's
updated skill doc verbatim (not my work area)
Re-added /.claude/plans/ to .gitignore after auto-merge picked
master's rewrite of /.claude → /.claude/settings.local.json and
dropped my prior ignore rule.
Compile verified clean against shadow-cljs :app and :test builds
(0 new warnings). App loads in browser without runtime errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Breadcrumb segments in CMD-K rendered the page's full 20px avatar above
12px breadcrumb text, breaking the line box, competing with the row's
own 20px icon for attention, and producing a "double face" effect when
the same entity appeared as a row icon and a breadcrumb icon nearby.
Hide the segment icon behind the existing .breadcrumb--search-result
modifier so all other surfaces (app header, right sidebar, FSRS, etc.)
keep their breadcrumb icons unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the page-title row wrapped to a column (narrow containers), the
icon stretched to the full column height and centered between title
and wrapped tag, and the #tag landed with a disconnected 17px gap.
Constrain the icon box to one title line via align-self + height, and
collapse the tag's margin-top once the row has wrapped. Nudge the
centered icon 4px down for optical alignment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The maximize button on Web image hover previews showed "The image
cannot be loaded" for any Wikimedia PDF or DJVU result. Two causes:
the upscale regex `#"/\d+px-"` didn't match the `/pageN-NNNpx-` size
token PDFs use, so `src` fell back to the raw `.pdf` URL PhotoSwipe
can't render; and Wikimedia's PdfHandler caps PDF/DJVU thumbs at
1280px wide, so even a corrected upscale to 1600 returned 400.
Drop the leading `/` anchor so the regex matches both regular and
PDF/DJVU thumbs, clamp the upscale target to 1280 when we detect
the `/pageN-` prefix, and fall the probe back to the original
thumb URL on error — beats showing the placeholder when a
particular size isn't cached.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related improvements to the asset picker:
1. Hover preview parity across all asset lanes. Previously the rich
preview card (image + From + license badge) only fired for web-image
search results. Now Recently used and Available assets show the same
card too, gracefully degrading to image + title when source/license
are missing (e.g. locally-uploaded assets that haven't been touched
by migration 65.29 yet).
- `asset->preview-data` normalizes an asset block + blob URL into
the shape `web-image-card-content` consumes. Derives `:source`
from `:source-name` (Wikipedia / Wikimedia Commons regex) so the
existing branches still route.
- `image-asset-item` wraps its button in the same shui/tooltip
pattern web-image-item uses.
- `web-image-card-content` source row now omits when neither
`:source` nor `:source-name` is set (was always rendering a
misleading "From: Web" fallback for non-web items). Title
fallback changed to "Untitled image".
- `license->description` made public so the asset path can derive
the human-readable badge same as Commons results.
- Both asset queries (sync + async) extended to pull
`:logseq.property.asset/source-name` and `:logseq.property.asset/license`.
2. Maximize button now opens the same PhotoSwipe lightbox the #Asset
tag page row-click and inline asset blocks use, replacing the
half-baked positioned-popover that was sized via the deleted
`full-image-view` component and `.full-image-view-popup` CSS.
The lightbox-over-popup scenario surfaced three layering bugs that
the new wrapper handles:
- Body's `pointer-events: none` from Radix `modal: true` cascades
into PhotoSwipe and kills backdrop click — fixed via a one-line
`.pswp { pointer-events: auto !important }` rule in
`lightbox.css` (auto-imported via tailwind.all.css's glob).
- Hover states behind the lightbox activate because the swallow
listener can't suppress CSS `:hover` — fixed by marking Radix
popper wrappers `inert` for the lightbox lifetime (browser spec
suppresses mouseover/pointerover targeting the inert subtree).
- Escape closes both lightbox and picker because both attach
keydown handlers at document level — fixed by a window-capture
keydown handler that intercepts Escape before Radix sees it and
manually calls `pswp.close()`.
For Wikimedia results where `:url` is sometimes a PDF/DJVU source
file (not an image), the maximize handler upscales the
`/NNNpx-FILENAME` segment in `:thumb-url` to `/1600px-` so we
always feed PhotoSwipe a server-rendered JPG. Image dimensions
come from an async `new Image()` probe.
Callers outside the picker (block.cljs, pdf/assets.cljs,
handbooks/core.cljs) are unaffected — `roots` is empty, the
pointer-events override is harmless, the swallow has nothing to
swallow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When running `bb dev:cli export-edn --roundtrip` on a graph with
included test, :build/class-properties sort order kept changing
because built-in properties order is lost on export