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>
Resting state restructured for clearer affordance + better
discoverability:
- Avatar tile demoted from <button> to <div aria-hidden> — it's a
live preview, not a control. Removes the redundant second toggle.
- Banner demoted from <button> to <div aria-hidden>. The chevron-down
is gone — it was miscueing as a <select>-style dropdown, and the
morph-in-place transition meant it never lived long enough to read
as a disclosure rotation anyway.
- New invisible <button.cb-row-trigger> overlays the resting row
edge-to-edge (covering the zone's 18px padding) so the entire
surface is one accessible click target. Mounted only when collapsed
(`when-not expanded?`) — when expanded, the dropdown chips inside
`.cb-rows` are unparented so there's no nested-interactive risk.
- aria-label composes scope + descriptor + verb so screen readers
announce the current state on focus ("Custom · Image, circle.
Customize avatar."). aria-controls links to the rows panel id;
panel gets role="region" + aria-label for the disclosure pair.
- Right-side "Edit" text label replaces the chevron — muted gray-09
at rest, brightens to gray-12 when the row is hovered.
Zone background swapped to solid theme tokens (no alpha veil):
- Rest: var(--lx-gray-01) — most recessed surface in both themes
- Hover: var(--lx-gray-03) — two-step lift via :has(.cb-row-trigger:hover)
- 120ms cubic-bezier(0.32, 0.72, 0, 1) on background-color only
Solid colors compose more reliably across light + dark themes than
stacked alphas, and avoid the content-clipping issue that would
happen if the trigger overlay carried a near-opaque background of
its own. The :has() pattern means the hover paints on the same
element that already backs the content, so the avatar / banner-text
/ Edit label naturally show through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Asset deletion (three independent failures stacked):
- Unlink file on asset-block retract via the existing <unlink-asset
helper at handler/assets.cljs (modules/outliner/pipeline.cljs:74-80).
The block was commented out; deleted-assets was computed but never
consumed, so files leaked indefinitely.
- Reactive existence check in image-icon-cp / avatar-image-cp via
state/sub :db/latest-transacted-entity-uuids. model/sub-block can't
drive this — worker/react.cljs:63-67 calls (d/entity db-after id),
which returns nil for retracted entities, so [::block id] is never
emitted in affected-keys and subscriptions never fire on deletion.
- Force-remount Avatar root via :key on asset-missing? toggle. Radix's
Avatar primitive tracks image-loading status in context; once
AvatarImage reported 'loaded', the status sticks even after the image
unmounts, so AvatarFallback stays hidden.
PR-review cleanup (no behavior change):
- Strip 17+ [DEBUG ...] console statements across icon, block, views,
two editor handlers, and assets.
- Close content.cljs preview gap: pass :preview-target-db-id +
:property to icon-search so the block bullet observes hover preview
consistently with every other surface.
- Scope-correct close-fallback-menu! — use dissoc-icon-preview-field!
instead of unconditional state/set-state! nil.
- Delete dead code: heal-dangling-asset-icon (raced lazy hydrate),
asset-uuid->entity, rgb-string->hex, name->hex (named + css),
format-pairs.
- Hoist icon-grid-cols / custom-tab-cols magic numbers (9 sites).
- Search inputs: add :type "search" and :aria-label (both picker +
asset-picker).
- Replace hardcoded #6B7280 with (colors/variable :gray :09).
- Reconcile recents-cap doc drift in storage.cljc + recents-lane
docstring (cap lives in handler/icon-color/max-recents).
- Remove orphan-asset cleanup from outliner/core save-block-inner. Root
cause (migration 65.27's :url ref-type) is fixed by 65.28/65.29; all
asset writers are atomic (save-image-asset!, new-asset-block,
build-new-asset), and the cleanup ran on every save with a silent-
retract failure mode.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`get-node-icon` (icon.cljs:783) references `derive-abbreviated`,
defined later at line 2235 — added to the existing `declare` for
the other derive helpers to silence the `:undeclared-var` warning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`get-node-icon`'s `:text` branch always derived per-instance values
via `derive-initials`, ignoring the class default's `:mode` field
(stored at text-picker commit time, one of "initials" /
"abbreviated" / "custom"). Result: setting a class default to
Abbreviated "Course" still rendered each row's icon as 2-char
initials (e.g. "M2" for "Math 201") instead of the abbreviated
form ("Math 201" itself, or "Math" for longer titles).
Now dispatches per-instance derivation on `:mode`:
- initials → `derive-initials` of each row's title (unchanged)
- abbreviated → `derive-abbreviated` (existing helper, with
graceful fallback to initials when abbreviation collapses to
the same string)
- custom → propagate the class's literal `:value` verbatim.
Custom is an intentional override — every row shows the same
glyph rather than re-deriving.
Color and alignment also flow through inheritance now.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hover-preview architecture:
- Split scope into primary (`:db-id`/`:db-ids` + `:property`) and
inheritor (`:inheritor-db-ids` + `:inheritor-property`). Class
default-icon edits now preview live on inheriting instance rows
while leaving the class's own page-title icon untouched.
- Tile + color hovers compose via additive merge (own-field-only
dissoc on mouse-out); grid mouse-leave skips clear when heading
into a Radix popper, so moving from a tile to the color swatches
no longer wipes the tile preview.
- Custom-tab tiles read `:ui/icon-hover-preview` reactively so
hovering a color swatch tints the Text/Avatar tile previews in
lockstep with the page-title.
Inheritor fetch:
- Tag instances aren't eagerly hydrated into the main-thread conn,
so `(:block/_tags class)` returns empty at component render time.
Both `default-icon-row` (for the reset affordance count) and
`icon-search` (for hover-preview broadcast) async-fetch via
`db-async/<get-tag-objects` against the worker, mirroring the
existing precedent at value.cljs:1131.
Asset picker:
- Auto-route URL paste in the search input to `<save-url-asset!`
with restore-on-failure (paste content never lost).
- Asset-grid tiles crop to the avatar's rounded-rect when shape is
set, via a `data-avatar-shape` attribute on the picker root.
- Banner avatar tile always renders the fallback config (color +
shape + initials/icon/emoji) even when an image is layered, so
the customize controls preview the layer they actually edit.
- Shape descriptor "Rounded" → "Rectangle" for clarity.
CMD-K:
- Drop `block-title-with-icon` from list-item — the dedicated icon
slot already renders the resolved icon via `get-node-icon-cp`,
so the inline render double-printed for blocks with own icons.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Optimistic ::pending-icon in asset-picker — wraps on-chosen so the
avatar tile renders the just-committed value during the ~30-50ms
SharedWorker entity-write window. Cleared in :will-remount when
current-icon refreshes. Mirrors icon-picker's pending-icon pattern.
- Asset-picker tile previews color-swatch hovers (in addition to icon
hovers) by overlaying the previewed color onto committed-icon.
- Single-row table-cell icon click + batch selection toolbar both
pass the resolved (inheritance-aware) icon so icon-search auto-routes
to the asset-picker for instance rows of avatar-default classes.
- Avatar customize-zone gets a dark alpha veil background; expanded
gradient temporarily off (bottom stop pre-adapted via color-mix so
it can be restored). Compact em-emoji glyph clamped to 11px to
prevent the 30px emoji-mart inline span from overflowing the tile.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Hover-preview state now carries `:property` (`:logseq.property/icon` or
`:logseq.property.class/default-icon`) so two pickers editing different
fields of the same entity don't leak previews into each other. Threaded
through icon-picker → icon-search → asset-picker; readers in
icon-picker-trigger-icon, get-node-icon-cp, and the asset-picker band
tile gate on it. `get-node-icon-cp` defaults to `:logseq.property/icon`
so existing sidebar/cmdk/breadcrumb callers don't change behavior.
- icon-search's reactive entity-icon override now reads `(get property)`
instead of hardcoded `:logseq.property/icon`. Without this, committing
via the Default Icon picker wrote `:logseq.property.class/default-icon`
but the override kept returning the unchanged `:logseq.property/icon`,
freezing the asset-picker tile + Fallback chip at the pre-edit state
while the page-icon still updated.
- Property-value icon-row + default-icon-row now thread their entity
db-id and property scope into the picker, fixing the `preview-target-
db-id=nil` case that previously gated all live-preview broadcasts off
for property-field pickers.
- Fallback dropdown is controlled-open via a `::fallback-menu-open?`
rum/local; commit fns close the entire chain (parent dropdown +
Radix-cascaded sub-content) so a tile pick auto-dismisses the menu.
- Keyboard nav re-renders icon-search via `(rum/react *highlighted-
index)` so its phantom `icon-hover-effects` component receives a fresh
`current-id` and broadcasts on arrow-key navigation, matching mouse
hover behavior.
- Dropdown menu items use the canonical shui pattern from
`deps/shui/src/logseq/shui/demo.cljs`: tabler icon as the first child
with `scale-90 pr-1 opacity-80` utility classes. Letter-case for the
Letters item, circle-dashed for Icon…, circle / square-rounded for
the Shape options.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Hover/focus on Shape and Fallback dropdown rows broadcasts a wrapped
avatar to :ui/icon-hover-preview so the page-title trigger and the
band's own tile preview the change live; commit fires on click,
dropdown close clears via on-mouse-leave on the content wrapper.
`:on-focus` (not `:on-mouse-enter`) — Radix DropdownMenuItem uses
pointermove → item.focus() for highlighted state, so onFocus fires
reliably for both keyboard and mouse and works for the item under
the cursor at open time.
- Stopped clobbering the avatar's :data :backgroundColor when the
preview state has no explicit :color (icon-picker-trigger-icon and
get-node-icon-cp). The earlier `(or (:color preview) "inherit")`
fallback turned shape/fallback-only previews into a chip-less, bare
letters render in the page-title.
- Sub-picker's icon grid now inherits the parent avatar's color via a
new `:initial-color` prop on icon-search. Since `:color-btn? false`
suppresses the swatch (whose useEffect normally sets
`--ls-color-icon-preset`), `:did-mount` now applies the variable +
`icon-colored` class directly to the picker root.
- Left sidebar `.page-icon` was rendering icon-fallback avatars
off-center: a `.left-sidebar-inner .ui__icon` rule (16px / mr-2 /
opacity-70) was cascading 4 levels deep into the avatar tree and
shifting the SVG glyph left via outer-margin participation in the
fallback's flex centering. Scoped both `.page-icon .ui__icon` and
`.left-sidebar-inner .ui__icon` so they no longer apply to
avatar-internal icons (`:not(.ui__avatar *)` / `:not(.page-icon *)`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Banner redesign: scope · descriptor (Default · Letters, circle), whole row
expands the customize panel, decorative chevron rotates, 12px medium typography
matching Linear/Notion metadata patterns
- Compact-state avatar matches sidebar's .page-icon (20×20, 10px / 500),
avatar-fallback class added so the existing sidebar CSS rule activates
- Class-default-icon inheritance flows to the page-title (was reading the
tag's own page icon instead of :logseq.property.class/default-icon)
- Emoji as a third :fallback-type alongside :letters and :icon; humanizer
drives the chip label and banner copy ("Briefcase filled", "White check mark")
- Fallback's "Icon…" opens as a Radix sub-menu (matching content.cljs's
"Add reaction" pattern), with All/Icons/Emojis tabs, no redundant color or
delete buttons, and a synchronous focus handoff in onOpenAutoFocus so the
search input is ready to type the moment the sub-menu opens
- Live preview broadcasts wrapped avatars to :ui/icon-hover-preview so both
the page-title trigger and the band's tile mirror the hovered fallback
- Fixed cb-rail-wrap collapse via explicit height transition (grid 0fr was
bleeding ~30px); chip ellipsis logic now lets long icon names ("Align box
bottom left filled") fit without wrapping the structural label
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a Fallback dimension to avatar icons (:letters | :icon) with
inheritance through `:logseq.property.class/default-icon`. When the
fallback is :icon, the avatar renders the chosen tabler icon instead
of initials, color-inheriting from the contrast-adjusted muted tint.
Highlights:
- normalize-icon enforces an invariant on both fast and slow paths:
a :fallback-type :icon without a non-blank :fallback-icon degrades
to :letters, so the renderer never has to guard.
- get-node-icon extends class-default select-keys to propagate
:fallback-type and :fallback-icon alongside :shape.
- Both the text-only avatar branch and avatar-image-cp dispatch on
:fallback-type. Tabler icons render at ~55% of avatar size with
the fallback's contrast-adjusted color.
- New Fallback row in the customize band with a ghost-chip dropdown.
Selecting "Letters" commits inline; selecting "Icon…" opens a
constrained sub-picker (only the Icons tab) anchored on the click.
- Generic addition: icon-search now accepts an :allowed-tabs prop
that filters the tab strip and seeds *tab to the first allowed
entry. Useful beyond fallback (any caller wanting a scoped picker).
- Reset link now clears Shape AND Fallback together, dropping any
dormant :fallback-icon. Disabled when state matches default.
Polish:
- Avatar font-size scales up past 32px (16px @ 40px tile, 22px @ 56px
tile, ~40% of size for larger). Page-icon and band preview now
read as proper avatars instead of small text in big circles.
- Customize band labels match Settings panel style (text-sm
font-medium leading-5 opacity-70).
- preview-meta gets min-height: 56px + justify-content: center so
the resting "Title / subtitle" sits centered against the avatar.
- Ghost-chip aesthetic: dropdown chips drop background and border at
rest (1px transparent border keeps geometry stable), revealing
fill on hover and Radix's data-state="open". Label and value share
the same 14px text-sm so each row reads as one "Shape: Circle"
line — Linear/Notion settings panel register.
- Tighter row geometry: 26px chip × 2 + 4px gap = 56px exact match
against the avatar tile. No more overflow tail under the avatar.
Tests: 16 new assertions across 6 deftest groups (legacy default,
round-trip, :icon→:letters degradation, blank-icon edge case,
top-level legacy keys, full multi-field coexistence). 26 total
icon test assertions, all passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a Shape dimension to avatar icons (circle | rounded-rect) with
inheritance through `:logseq.property.class/default-icon`. Surfaced via
a tap-the-preview customize band in the asset-picker (avatar mode) with
a Shape dropdown and a keymap-style Reset/Done rail. Animates open/close.
Highlights:
- normalize-icon defaults `:shape :circle` for legacy data on both the
slow path and the already-shaped fast path.
- get-node-icon extends class-default `select-keys` to propagate :shape.
- avatar.tsx accepts `data-shape`; CSS in icon.css drives the radius via
`[data-shape="rounded-rect"]` selectors (avoiding Tailwind JIT issues
with conditional arbitrary-value classes).
- Customize band: preview tile + Shape dropdown + Reset/Done rail. All
blocks always rendered so CSS transitions can interpolate height,
gradient, and the cue badge crossfade. Layout matches Paper artboard
99K-1 / 97A-1 (344px inner content inside 380px band, inset rail
separator, gradient flush against topbar).
- Fixes `keep-popup?` plumbing at three forwarding wrappers (asset-
picker, icon-search, icon-picker) and the topmost on-chosen handler
in property/value.cljs. Single click now produces a single write
instead of the prior triple-write race.
- icon-row (property/value.cljs) and icon-search (icon.cljs) both made
reactive via model/sub-block — so in-popup commits update the picker
preview/chip live, not just the page-header avatar.
- Lazy `*text-measure-ctx` so the namespace loads in the Node test
runner (was previously blocking all icon tests).
- New `.lx-toolbar-action` / `.lx-toolbar-reset-link` utility CSS
mirrors Settings → Keymap shortcut popover footer styling.
- 10 new test assertions for shape default, preservation, fast-path
handling, and field independence.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an icon prefix to the Name cell of class-instance tables and a
batch Set icon button to the row-selection action bar. Avatar/text
initials are derived per row, so batch-applying an avatar to multiple
rows yields each row's own initials while preserving picker colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Avatar fallback rendering: replace the prior 31.4%-alpha background
treatment, which silently rendered dark picks (#1a3d60 etc.) at ~1.1:1
contrast on dark surfaces. The new pipeline computes a hue-preserving
muted tint for the bg via OKLCh L bisection (~1.5:1 vs page surface)
and runs the picked color through adjust-for-contrast (3.0 target) for
the initials. Picked color passes through as text whenever it reads;
only genuinely-illegible picks get lifted, and the lift stays close
to the picked hue. Works for both custom hex picks and Radix theme
tokens (var(--rx-...)) via a new colors/->hex CSS-color resolver.
The 3.0 target — instead of WCAG 4.5 body-text — treats avatar initials
as decorative identifiers (matches Slack/Linear/GitHub practice) and
lets vivid hues like tomato and red pass through without desaturating.
Class default-icon row: new property row that uses the unified icon
picker in default-icon mode, where Text/Image tiles commit immediately
rather than drilling into sub-pickers (per-instance inheritance derives
the actual face from each instance's title). Setting a class's page
icon now auto-syncs into default-icon when default-icon is empty so
instances inherit it; clearing the page icon only clears default-icon
if the two were synced.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The floating URL+Upload buttons are the last UI in the picker still
pushing users toward an explicit URL-name dialog; paste and Web Image
flows already auto-derive names. Replace them with a thin static
footer ("Tip: Drop an image, paste a link, or browse · ⌘V"), delete
the url-asset-pane popover entirely, and fix URL-paste naming to use
extract-filename-from-url instead of clipboard-{timestamp}. Phones
get a parallel hint where each verb is a real control (iOS Safari
won't deliver paste events to non-input popover roots).
Also: title+caption styling for the Wikipedia tooltip to match the
color-picker contrast tooltip, and remove the 56px grid bottom
padding that previously cleared the floating bar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The icon-picker's color swatch only lived at level 1 — to recolor an
avatar from inside the asset-picker the user had to click Back, recolor,
then drill into Custom > Avatar again. Surface the same trigger in the
asset-picker topbar so the round trip collapses to one click.
- Trigger renders only when mode = :avatar; image assets aren't tinted
so showing it in Image mode would be misleading. Toggling segment to
Image dismisses an open color popover by id (:asset-picker-color) so
it doesn't orphan over an unrelated topbar.
- The color-picker component itself is unchanged besides accepting an
optional :popup-id opt and threading it into shui/popup-show!. The
asset-picker observes the same `*color` rum atom the icon-picker
topbar uses, so backing out reflects the new color in the parent
immediately — no state duplication.
- Hover-preview wired through preview-target-db-id, same as the parent.
- Layout: bundles color + trash inside `.asset-picker-topbar-actions`
in the topbar's right grid cell. Class is intentionally distinct from
`.asset-picker-actions` (the floating bottom bar with "Add image via
URL" / "Upload image"); reusing the latter would inherit its
`position: absolute; bottom: 0` and yank the topbar group off-screen.
Forward-declares `color-picker` near the asset-picker so the
top-to-bottom CLJS compile resolves the call site at line 3375 even
though the definition lives at line 5042. Without the declare, CLJS
emits a direct property reference that's undefined at runtime, the
`(color-picker …)` call throws, and the entire `.asset-picker-topbar-
actions` subtree fails to mount (which manifested as both the color
trigger AND the trash going missing).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds four asset-scoped properties so Logseq can keep the provenance of an
image alongside the file:
- :logseq.property.asset/source-url — Wikimedia descriptionurl, Wikipedia
page, or any user-supplied source link. Queryable; this is the join
key the asset-picker uses to dedup web-image hits against the local
library.
- :logseq.property.asset/source-name — short label ("Wikimedia Commons",
"Wikipedia", etc.). Queryable.
- :logseq.property.asset/license — license slug (CC-BY-SA-3.0 etc.).
Queryable.
- :logseq.property.asset/attribution — full credit string for embed/
export contexts. Not queryable.
Schema bumped 65.26 → 65.29 with three migrations:
- 65.27: register the four new properties.
- 65.28: an in-development DB hit a build that registered source-url
with :type :url (a ref-typed schema), which made datascript treat
plain URL strings as tempid lookups and blocked every asset save with
"Tempids used only as value in transaction". Coerces the entity's
:logseq.property/type back to :string.
- 65.29: 65.28 corrected the type but left :db/valueType :db.type/ref on
the same entity. In Logseq's datascript fork the :db/valueType on a
:db/ident-keyed entity IS the live schema, so the attribute stayed
ref-typed and string URLs continued to fail. Retracts :db/valueType
to release the lock; mirrors logseq.outliner.property's existing
ref→non-ref pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Web-image dedup turned into an annotation. Tiles whose source-url already
matches a local asset stay in the row but swap their globe overlay for a
deep-forest-green disc with a white check. Click on a saved tile routes
through the existing asset (no re-download, no orphan duplicate). Keeps
the row at full width even when 3 of 5 hits are already saved, and avoids
the "broken search" feel of the previous filter.
Disc color is a fixed#137333 rather than a Radix token because the
green scale's step 9 is intentionally similar across themes — pairing it
with a flipping icon color leaves white washed out in dark mode and
near-black on green in light mode. Hardcoded deep green is the simplest
single-token answer that gives white the contrast it needs in both modes.
Broken-image state (image-icon-cp's :error? branch) reworked from a heavy
filled bg-gray-04 block to a bordered placeholder-style tile matching the
"pick an image" affordance — solid border (vs dashed for "awaiting
input"), translucent fill, muted photo-off glyph at 0.45x size. Same
chrome family, distinct enough to read as "broken/persistent" rather
than "empty/inviting".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- icon dispatch + renderable-icon? trust the asset-uuid string instead of
probing the renderer-side entity, so a fresh navigation to a page whose
icon points at an asset renders via the filesystem loader instead of
silently rendering nothing while DataScript catches up.
- <load-asset-url! widens the retry window when the error is db-worker
not-yet-initialized (15x500ms) vs real failures (3x1s unchanged).
- asset-picker hides web-image suggestions whose source-url already
matches a saved asset, so the same image no longer shows up in both
Web images and Available assets. Pull queries extended to surface
:logseq.property.asset/source-url, and the keyboard-nav flat list
filters in lockstep with the visible UI.
- Custom-tab Text/Avatar/Image preview thumbnails get aria-hidden so
screen readers and find-by-text read just the label rather than
"STAvatar"/"STText".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The page-icon now previews any tile the user is hovering or keyboard-
navigating in the picker — mirroring the existing color preview but
extended to icon, emoji, text, avatar, and image-placeholder types. The
preview state is keyed by `:db-id` and reads through the existing
`:ui/icon-hover-preview` slot.
Custom-tab tiles route to the asset-picker on the right tab and commit
a placeholder synchronously: Avatar commits the initials avatar and
opens the Avatar tab; Image commits an `:image-placeholder` (rendered
as a plus inside a dashed rounded square — the same affordance Logseq
uses for "no icon yet, click to add") and opens the Image tab. The
asset-picker accepts an `:initial-mode` prop that overrides its
heuristic seed.
Mouse-clicking a tab now also focuses the search input so ArrowDown
flows into the grid (the previous code only updated `*focus-region`,
leaving DOM focus on the tab button so the keydown listener never
fired).
A `pending-icon` `rum/use-state` in `icon-picker` holds the just-
committed icon-value during the ~15ms SharedWorker round-trip between
the DB write and the entity update propagating back through the
reactive read chain. Without this, every commit visibly flashed the
old icon for one render cycle. The state auto-clears via `use-effect`
on `[icon-value]` deps (Logseq's `hooks/use-effect!` uses Clojure
value equality, so map content changes fire correctly).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The page-title icon container used `.flex.self-start` and relied on each
icon type's own intrinsic baseline (font glyph for emoji, SVG bottom
anchor for tabler, etc.), with `margin-top: 8px` and `pb-[1.5px]`
band-aids compensating for one type at a time. Replaced with a single
flex-center 38×38 button rule, scoped to `.ls-page-title .ls-page-icon`,
so every type centers on the same visual axis. Drops the per-type pixel
overrides, the mobile-specific `:size 28` (mobile font is unchanged from
desktop, so the override was a band-aid), and the inline `1lh` style.
Bumps the icon-to-title gap from `gap-2` to `gap-3` (12px) for breathing
room.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The color-picker's :after-close! callback was gated on @*highlighted-index,
so opening the color picker without first navigating to an icon left focus
nowhere when the popover closed. The capture-phase keydown listener only
fires for keys whose target is inside the icon-picker container, so the
picker would appear open but reject every keystroke until the user clicked
the search input. Fall back to focusing the search input (and resetting
:focus-region to :search) when there's no highlighted tile to return to.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tab/Shift+Tab cycles three lanes (swatches → hex input → recents) and
arrow keys cross between them: hex input ↑ jumps to the active swatch,
↓ jumps to the first recent; the swatches grid's bottom row hops into
the hex input when the pane is open. The recents lane gets roving
tabindex with 2D nav across its two flex-wrapped rows, and react-
colorful's pad/hue sliders are stripped from the Tab order (mouse/
touch only) so keyboard users never land on the pad. The collapsed
pane is marked `inert` to keep its hidden controls out of focus.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a hex/HSV picker pane to the icon-picker color popover, plus a
per-graph recents lane and cross-theme contrast adjustment. Hex input
supports named-color autocomplete (148 CSS + 949 XKCD) with a ghost
suffix that Tab/Right-arrow accepts. Picked colors that fail WCAG 3:1
against either light or dark background are auto-shifted in OKLCH per
mode; recents and the trigger swatch render as half-pie splits when the
two modes diverge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Multiple keyboard-navigation fixes for the unified icon-picker:
- After committing a color, return DOM focus to the highlighted tile via
Radix's onCloseAutoFocus instead of trying to focus during the swatch
click (Radix's FocusScope traps focus while the popover is mounted, so
any synchronous .focus() is silently undone). Routed through a new
:after-close! kwarg on color-picker.
- focus-grid! now moves DOM focus to the active tile alongside updating
*highlighted-index, so activeElement and .is-highlighted always
coincide (WAI-APG roving-focus pattern, no double focus ring).
- Stable refs in icon-search and asset-picker: created once in
:will-mount, stored in component state. rum/create-ref produces a new
RefObject every render in defcs, which left popover-captured closures
holding orphaned refs whose .current was nil after later renders.
- Search results now filter by tab content type while keeping the query
active across tabs: :all shows both, :emoji only emoji matches, :icon
only icon matches. Mirrors the gate in compute-flat-items so the
visible grid and keyboard-nav flat-items list stay in sync.
- emojis-cp / icons-cp now forward the full opts to pane-section so
highlight, ghost-highlight, focus-region, and wave propagate on the
no-search :emoji / :icon tab paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the user hovers or commits a swatch, the icons in the picker grid
transition to the new color in a staggered wave from top-left to
bottom-right rather than snapping in lockstep — borrowed from Linear's
icon picker.
Mechanically: each cell renderer (icon-cp / emoji-cp / text-cp /
avatar-cp) stamps its grid position as inline `--r` / `--c` custom
properties, sourced from `pane-section`'s render loop. The existing
`--ls-color-icon-preset` cascade already carries the chosen color
down to the cells via currentColor inheritance; we add `color
320ms cubic-bezier(.4,0,.2,1)` to the per-button transition with
`transition-delay: calc(var(--c) * 22ms + var(--r) * 36ms)`, so
each cell starts its glide on its own clock.
Two design decisions worth flagging:
- **Hover preview drives the wave too**, not just commit. CSS
transitions hold the old value during their `delay` window and
retarget cleanly when the property changes mid-flight, so rapid
cursor sweeps gracefully chase without flicker — far cells just
hold steady until the user settles.
- **Material's "standard" easing** (`cubic-bezier(0.4, 0, 0.2, 1)`)
over `ease-out`: each cell pauses, glides through the middle, and
settles. The pause makes the stagger legible — neighbors are
visibly "starting" while predecessors are mid-glide, which is
what reads as a wave rather than a snap-and-fade.
`prefers-reduced-motion: reduce` strips the color transition so the
grid snaps instantly for users who opt out of animation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The color picker had two bugs on avatar icons:
**Hover preview only updated the text, not the circle background.**
`get-node-icon-cp` propagates the preview via the wrapper's CSS
`color`, which inherits down to the avatar's text but not its inline
`background-color`. Same shape inside `icon-picker-trigger-icon`,
which mutated only `[:data :color]`. Both now also mutate
`[:data :backgroundColor]` for `:avatar` icons so the circle previews
together with the text.
**Clicking a swatch did not apply the new color.** color-picker's
`set-color!` is React state; the useEffect that propagates to the
external `*color` atom runs after the synchronous `on-select!`
callback. The icon-picker's `on-chosen` wrapper re-applies `@*color`
to `m`, so the freshly-picked color was overwritten with the previous
one (avatars defaulted to gray-09, so it looked like clicks were
ignored entirely). Sync the atom with `(reset! *color c)` before
calling on-chosen so the wrapper sees the new value. Same fix
incidentally addresses the same bug for already-colored regular
icons.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The color swatches popover now behaves like a proper WAI-APG radio
group: the popover lands focus on the currently-selected swatch on
open (deferred a tick past Radix's onOpenAutoFocus so we override it),
ArrowLeft/Right/Up/Down + Home/End rove with wrap-around, and the
trigger keeps roving tabindex so Tab in/out walks one stop instead of
nine.
While the user is engaging with the palette (mouse-hovering anywhere
in the group OR any swatch focused), non-active siblings drop to 0.55
opacity. Three stay bright: hovered, focused, and currently-selected.
At rest the whole row reads at full saturation — engagement-only
muting matches macOS/Figma/Linear and avoids hurting first-impression
discoverability.
Focus halo gets a styling pass: the system blue outline is replaced by
a colored ring with the same anatomy as the selected ring (inset edge
+ bg gap + outer halo) but thinner (2.5px vs 3.5px) and in the global
accent so focus is never the swatch's own hue — keeps focused vs.
selected legibly distinct on, e.g., the orange swatch where the ring
would otherwise blend.
Each swatch carries a tooltip (hover + keyboard focus, 300ms delay,
matching the asset-picker's web-image-item) with its color name. The
"no color" swatch gets a two-tier tooltip — "Default" + "Inherits the
surrounding text color" — to communicate why someone might want it
(theme-aware behavior in custom themes).
Note on the tooltip wiring: rum/with-key on a shui lsui-wrap React
element strips children to nil, so keys are passed in the props map
where React.createElement extracts them natively.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The icon-picker topbar now ropes the color swatch and trash button into
the same arrow-key rove as the tabs (data-topbar-stop + the controller's
:topbar region). Tabs auto-activate when arrow-roved over (e.detail = 0
discriminates programmatic clicks from real ones, so the on-change
handler keeps focus inside the topbar instead of bouncing to search).
Topbar focus styling is unified across both pickers so back / tabs /
segments / color / trash all paint the same blue ring (the trio of
browser-default outline, custom --lx-accent-09 outline, and shui's
--ring box-shadow no longer disagrees). Trash and inactive tabs need
opacity:1 on :focus-visible so the base opacity-60 doesn't dim the
ring into a muted glow.
Tile-to-tile transition no longer animates outline-color: animating it
from `currentColor` (e.g. red on a tinted icon) briefly tinted the
focus ring red before the accent color landed. Width and offset still
glide so the ring grows/shrinks smoothly; color snaps instantly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Avatar/Image selector isn't a tab bar — both modes show the same
content (the asset grid); only the resulting icon's :type/shape
differs. Switch to a pilled segmented control with radiogroup ARIA
semantics so the affordance reads as "pick one value" rather than
"navigate between sections".
Adds a generic `ui/segmented-control` helper alongside `tab-items`,
plus shared `.segmented-control` styling. Topbar gets a fixed 40px
height to match the icon-picker.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the icon-picker's grid model: a flat-items list + per-section
column descriptors flow into a shared keyboard-nav controller. The
controller is generalized to an options map so callers pass their own
container selector, escape behavior, and (optionally) a topbar selector
for heterogeneous toolbars. Asset-picker wires it for back/tabs/trash
rove via data-topbar-stop, search-input shortcuts (Up/Down/Tab/Esc/
Enter-for-ghost), and tile/row highlight + ghost-highlight props on
recently-used, web images, available assets, and zero-state action rows.
Hoists .is-highlighted / .is-ghost-highlighted CSS out of the
icon-picker scope so both pickers share it, and adds an inset-ring
treatment for the full-width zero-state rows. The mid-load "Loading
assets" spinner is dropped — the web-image skeletons cover the gap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Align section headers across asset-picker and icon-picker at 12px
from the edge by dropping .pane-section's px-2. Grid padding becomes
px-3 pt-1 pb-3 so headers sit closer to their grids.
- Hide "Available assets · 0" when no assets exist; the zero-state
rows speak for themselves. Header reappears during search so "· 0"
still conveys "no matches".
- Drop row-title to text-sm, bump row-chevron to gray-10 (no opacity),
and move the empty-row separator from gray-04 to gray-06 for dark-
mode legibility.
- Pin the web-images info icon to the right edge (margin-left: auto +
margin-right: 12px) and remove its opacity layer.
- Let the web-image external-badge float past the circular avatar clip
by dropping overflow-hidden on .web-image-item (img clips itself).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reorder to Paste clipboard first, then Upload. Drop the standalone URL
row now that the clipboard row accepts pasted URL text. Switch the
shortcut badge to combo style and trigger its press animation on both
keystroke and click. CSS uses :has() to cancel ancestor padding so hover
backgrounds reach the picker edges without a horizontal scrollbar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Avatar/image rendering checks that the referenced asset entity still
exists before calling into the async image component. Without the
guard, the picker crashed inside avatar-image-cp while loading a ghost
uuid. Add a self-heal effect that rewrites dangling icon values on
mount: :avatar degrades to text-only, :image clears entirely.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace "No images yet" placeholder with three tappable rows when the
asset list is empty: Upload from computer, Paste image URL, and
(when supported) Paste from clipboard with a ⌘V hint. Floating
action bar hides while rows are shown.
- Accept Finder / Explorer file pastes. A sync ClipboardEvent reader
inspects clipboardData.files/items, since navigator.clipboard.read()
cannot see OS file references. Global paste listener attached to
the picker root.
- Route URL downloads through Electron IPC (:httpRequest) in desktop
builds to bypass renderer CORS. Add opt-in :structured flag to the
IPC handler so it returns {:status :ok :headers :data} without
breaking existing body-only callers. Sniff magic bytes to detect
image vs HTML vs unknown when server Content-Type is unreliable.
- Classify download failures by HTTP status and error kind. New
url-save-error-copy maps :html-page, :not-image, :too-large,
:unknown, :http-status (with 401/402/403/404/429 specifics),
:empty, :cors, and :network to actionable user copy. Browser-build
CORS rejections now get an honest "browser blocked cross-origin"
message instead of "check your connection".
- Log failures via js/console.error in both IPC and fetch paths for
future debuggability.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live tab control at the top of the asset-picker flips between circular
avatar previews and uncropped image previews. Switching modes persists
an in-place `:type` flip on an existing asset-backed icon, and future
picks are saved with the active mode's `:type`.
Extracted the icon-picker's tab-bar markup into a reusable `ui/tab-items`
helper and unscoped `.tabs-section` / `.tab-item` CSS so both pickers
share the underline style. Web-image thumbnails now default to
`object-fit: contain` in Image mode (was always `cover`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old (memoize (fn [] (debounce …))) built a fresh debouncer on
every render, so each keystroke got its own timer and debouncing
never actually happened — partial-prefix queries all fired, producing
the race condition that request-id guards. Move the debounce into
component state via :will-mount (not :init, because rum/local only
installs the underlying atom during :will-mount) so a single timer
persists across renders.
Two related bugs around the web-images section:
1. Fast typing produced overlapping fetches where a late 'do' response
could overwrite results for 'donald trump'. Stamp each request with
a generation id in a component-local atom and drop responses whose
id no longer matches.
2. The section unmounted during transition states — while the parent's
debounced query hadn't caught up yet, or after the user typed but
before loading? flipped to true — so the layout below jumped up,
then back down when skeletons appeared. Compute a pending? flag
from both conditions, keep the section mounted whenever loading? or
pending?, and mirror avatar-mode on skeletons so circle-mode loads
land in the exact spot the circular skeletons occupied. CSS mirror
the same geometry (transparent 2px border + avatar-mode 50% radius)
on .web-image-placeholder.
'inherit' is a CSS-layer fallback written into --ls-color-icon-preset
so icons without an explicit color still render with something. It's
not a real color and must not leak into React state or the persisted
preset — otherwise the swatches show a phantom selection ring and the
value round-trips through storage. Filter it at both entry points:
color-picker's use-state initializer and icon-search's :init when
reading icon-value color + stored preset.
Also initializes color from the current icon's color first (falling
back to stored preset) so opening the picker against an already-
colored icon highlights the correct swatch.
The global .ui__icon svg { filter: brightness(.8) } dark-theme clamp
was desaturating icons rendered with an explicit color (e.g. cyan-10
looked visibly muted versus the swatch it was picked from). Add an
icon-colored class anywhere an effective color is present (and not the
'inherit' CSS sentinel), and use it to opt out of the filter where
color IS the affordance — inline icons, picker previews, and the live
color-picker preview on the picker root. Sidebar opacity-70 is left
intact since it's intentional visual hierarchy.
Deleting an icon writes a :none sentinel rather than removing the
property, so del-btn? checks that only tested for truthy icon values
kept the delete button visible on empty slots. Teach both the block
call site (icon picker trigger) and icon-search itself to treat :none
as absent. icon-search additionally recomputes del-btn? reactively
from the live entity via db-mixins/query so keep-popup? flows (e.g.
picking a color that writes the icon for the first time) update the
button in place instead of staying stale.
Stored icon values can fall out of sync with what the picker can
render — e.g. data saved against a phantom tabler utility export
before the picker added a filter. Add a renderable-icon? predicate
and use it to (1) drop dead entries from recently-used so the grid
doesn't render empty tiles, and (2) fall back to the "Set icon"
button on page titles whose stored icon can't resolve.
@tabler/icons-react exports utility helpers (e.g. createReactComponent)
alongside the IconFoo components. The picker keyed off every export, so
these utilities showed up as phantom entries in search, rendered empty,
and — if picked — were written back as unresolvable icon values.
The 48x48 preview boxes had a lot of empty margin around the 24px
icons — especially after dropping the background/border. Bumping to
32px fills the tiles in a way that feels consistent with the densely-
packed icon/emoji grids. Image placeholder scales with it: 28x28→32x32
box, 16→20 camera glyph.
Matches the hover pattern used by icon/emoji tiles: background-color
change instead of a border outline, and no opacity fade on the whole
tile. Drops the default background/border on the preview so unselected
tiles read as plain icons rather than framed controls. Label color
steps up to gray-12 on hover so the tile feels active.
The narrower transition list (background-color, outline-color) replaces
transition-all — the outline-color channel is what lets the
is-highlighted ring fade in without a discrete style jump.
Wire the Custom tab into the picker's arrow-key navigation: Text, Avatar,
and Image tiles now gain a blue ring when highlighted by keyboard and
commit on Enter, matching Icons/Emojis.
custom-tab-cp destructures the already-plumbed :highlighted-id from opts
and emits data-item-id + conditional .is-highlighted on each button. CSS
neutralizes the generic button.is-highlighted (which would paint the
label column) and moves the ring onto the 48x48 preview child. A
baseline transparent outline avoids the currentColor flash when the ring
appears.
Hovering a color swatch live-previews that color on both the picker grid
icons and the page-title icon, letting the user imagine the choice before
committing. Click commits as before; mouse-leave or popup close reverts.
- color-picker now tracks local hover state and splits its effect: a
display-only effect (deps [effective-color]) updates the CSS var on
hover and commit; a commit effect (deps [color]) persists to storage.
An unmount cleanup clears external preview state.
- New :on-hover! / :on-hover-end! kwargs are opt-in; threaded through
icon-picker -> icon-search via :preview-target-db-id from block.cljs.
- get-node-icon-cp subscribes to :ui/icon-hover-preview so sidebar /
inline icons update reactively.
- icon-picker-trigger-icon is a small reactive sub-component so the
page-title trigger updates without forcing the hook-using parent into
a class component. It pre-normalizes icon-value before applying the
preview color so normalize-icon's early-exit doesn't strip :data:value.
- :focus-visible outline on swatches gives keyboard users the same
preview affordance as mouse hover.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The visibility check only handled nil icons, not the {:type :none} value
written when the user explicitly deletes an icon. Treat both as "no icon".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rectangular shui/button color picker with round swatch circles:
- Trigger button shows filled circle (with color) or slashed empty circle
- Popup renders swatches as 24px circles with hover scale and active press
- Selected swatch gets accent-colored ring via box-shadow
- "No color" swatch uses CSS-only diagonal slash (bottom-left to top-right)
- Remove duplicate .color-picker styles from tab-bar and text-picker-actions
- Move "none" to first position in color array for natural scanning order
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>