feat: scope hover-preview by property + sub-menu auto-close + dropdown glyphs

- 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>
This commit is contained in:
scheinriese
2026-05-08 23:38:55 +02:00
parent 980138dfa8
commit b95335cc6d
4 changed files with 240 additions and 111 deletions

View File

@@ -3275,6 +3275,12 @@
:del-btn? (boolean (and icon' (not= (:type icon') :none)))
:page-title (:block/title block)
:preview-target-db-id (:db/id block)
;; Page-title scope — explicit so it doesn't
;; rely on the icon-picker's default. Makes
;; the contract visible at the call site
;; alongside the parallel default-icon-row
;; in property/value.cljs.
:property :logseq.property/icon
:button-opts (when (:page-title? config)
;; Drop shui's default sizing/padding so the
;; CSS rule on .ls-page-title .ls-page-icon

View File

@@ -58,7 +58,12 @@
(.getContext canvas "2d"))))
(declare normalize-icon derive-initials derive-avatar-initials
<search-wikipedia-image <save-url-asset! open-image-asset-picker!)
<search-wikipedia-image <save-url-asset! open-image-asset-picker!
;; Used by `asset-picker`'s avatar fallback sub-picker — declared
;; up front because `icon-search` itself is defined far below
;; (after asset-picker), so the call site at the avatar fallback
;; needs a forward declare to satisfy the cljs compiler.
icon-search)
(def ^:private icon-name-acronyms
"All-caps tokens that should stay uppercase when humanizing tabler icon
@@ -765,9 +770,15 @@
;; Hover preview from icon-picker — overrides node's icon and/or
;; color while the user is hovering tiles in the picker. The state
;; can carry `:icon` (full normalized item override), `:color`
;; (color override), or both.
;; (color override), or both. `:property` scopes the preview so a
;; Default-Icon-scoped hover doesn't leak into surfaces that
;; render the page-icon (sidebar, cmdk, breadcrumb, etc.).
;; Defaults to `:logseq.property/icon` because that's what every
;; existing caller of this fn renders.
preview-property (or (:property opts) :logseq.property/icon)
preview (state/sub :ui/icon-hover-preview)
preview-active? (and preview
(= preview-property (:property preview))
(or (= (:db-id preview) (:db/id entity))
(contains? (:db-ids preview) (:db/id entity))))
preview-icon (when preview-active? (:icon preview))
@@ -2865,6 +2876,14 @@
(rum/local nil ::web-query-debounced) ;; Debounced web search query
(rum/local :avatar ::mode) ;; :avatar | :image — live tab state, seeded in :will-mount
(rum/local false ::customize-expanded?) ;; Avatar customize band open/closed
;; Controlled open state for the Fallback dropdown menu (the chip's
;; menu that contains Letters / Icon… options). Controlled so we can
;; programmatically close the whole menu chain after a sub-picker
;; commit (Radix cascade-closes the sub-content when the parent
;; closes). Without this, the user picks an icon in the sub-picker
;; and the Fallback dropdown stays open until they manually click
;; out — confusing and slow.
(rum/local false ::fallback-menu-open?)
(rum/local nil ::paste-handler) ;; Holds latest clipboard-paste closure for the DOM listener
;; Keyboard-nav state, parallels the icon-picker's model.
(rum/local :search ::focus-region) ;; :search | :grid
@@ -2924,6 +2943,26 @@
(when @*asset-picker-open?
(reset! *loading? false))))))
;; Pre-warm the customize-band's expanded layout. The
;; first expand from compact dropped frames because the
;; expanded layout (cb-rail-wrap visible at 39px, avatar
;; at 56px, cb-content at 14px padding + 14px gap) had
;; never been computed before the user clicked. We
;; briefly set `data-expanded` (with a `data-prewarming`
;; sibling that suppresses every transition under the
;; zone — see icon.css), force a synchronous reflow via
;; `offsetHeight`, then revert. The browser keeps the
;; expanded layout's box geometry warm; the user's first
;; real click hits a hot cache and the morph runs at 60fps.
(when-let [^js zone (some-> (rum/dom-node state)
(.querySelector ".avatar-customize-zone"))]
(.setAttribute zone "data-prewarming" "")
(.setAttribute zone "data-expanded" "true")
(.-offsetHeight zone)
(.removeAttribute zone "data-expanded")
(.-offsetHeight zone)
(.removeAttribute zone "data-prewarming"))
;; Attach a paste listener to the picker root so ⌘V anywhere in
;; the modal routes through the render-time clipboard handler.
(let [node (rum/dom-node state)
@@ -2958,7 +2997,8 @@
state)}
[state {:keys [on-chosen on-back on-delete del-btn? current-icon avatar-context page-title
*color preview-target-db-id preview-target-db-ids]}]
*color preview-target-db-id preview-target-db-ids property]
:or {property :logseq.property/icon}}]
(let [*search-q (::search-q state)
*loading? (::loading? state)
*loaded-assets (::loaded-assets state)
@@ -3415,7 +3455,7 @@
:on-hover! (when (or preview-target-db-id (seq preview-target-db-ids))
(fn [c]
(state/set-state! :ui/icon-hover-preview
(cond-> {:color c}
(cond-> {:color c :property property}
preview-target-db-id (assoc :db-id preview-target-db-id)
(seq preview-target-db-ids) (assoc :db-ids (set preview-target-db-ids))))))
:on-hover-end! (when (or preview-target-db-id (seq preview-target-db-ids))
@@ -3625,6 +3665,14 @@
hover-preview (state/sub :ui/icon-hover-preview)
hover-icon (when (and hover-preview
(= preview-target-db-id (:db-id hover-preview))
;; Gate on property scope so the
;; tile only reflects previews from
;; the picker that's editing *this*
;; field. Without it, opening the
;; Default Icon picker would tint
;; the page-title's separately-mounted
;; asset-picker tile (and vice versa).
(= property (:property hover-preview))
(= :avatar (get-in hover-preview [:icon :type])))
(:icon hover-preview))
;; Committed (hover-free) base: what the avatar would
@@ -3642,24 +3690,39 @@
(on-chosen nil
(assoc-in preview-icon [:data :shape] new-shape)
true))
;; All three commit fns clear `:ui/icon-hover-preview`
;; (so the asset-picker tile stops reading a stale
;; hover-icon and falls back to the freshly-committed
;; value) and close the Fallback menu chain via the
;; controlled `::fallback-menu-open?` atom. Closing the
;; parent menu Radix-cascades the close into the
;; sub-content too, so a tile-pick in the sub-picker
;; dismisses the entire menu — matching the standard
;; "click an item to commit and dismiss" pattern.
close-fallback-menu! (fn []
(state/set-state! :ui/icon-hover-preview nil)
(reset! (::fallback-menu-open? state) false))
set-fallback-letters! (fn []
(on-chosen nil
(-> preview-icon
(assoc-in [:data :fallback-type] :letters)
(update :data dissoc :fallback-icon))
true))
true)
(close-fallback-menu!))
set-fallback-icon! (fn [icon-name]
(on-chosen nil
(-> preview-icon
(assoc-in [:data :fallback-type] :icon)
(assoc-in [:data :fallback-icon] icon-name))
true))
true)
(close-fallback-menu!))
set-fallback-emoji! (fn [emoji-id]
(on-chosen nil
(-> preview-icon
(assoc-in [:data :fallback-type] :emoji)
(assoc-in [:data :fallback-icon] emoji-id))
true))
true)
(close-fallback-menu!))
;; Live preview while hovering Shape / Fallback dropdown
;; rows. Broadcast a synthetic avatar (committed config
;; with the hovered field changed) into
@@ -3670,18 +3733,13 @@
;; fallback sub-picker uses on tile hover.
preview-shape-on-hover!
(fn [shape]
(js/console.log "[DEBUG band shape-hover] FIRED shape=" (pr-str shape)
"preview-target-db-id=" preview-target-db-id)
(when preview-target-db-id
(state/set-state! :ui/icon-hover-preview
{:db-id preview-target-db-id
:icon (assoc-in committed-icon [:data :shape] shape)})
(js/console.log "[DEBUG band shape-hover] AFTER set-state, value="
(pr-str (:ui/icon-hover-preview @state/state)))))
:property property
:icon (assoc-in committed-icon [:data :shape] shape)})))
preview-fallback-on-hover!
(fn [fb-type]
(js/console.log "[DEBUG band fallback-hover] FIRED fb-type=" (pr-str fb-type)
"preview-target-db-id=" preview-target-db-id)
(when preview-target-db-id
(let [base committed-icon
;; For Letters: drop fallback-icon so the avatar
@@ -3704,6 +3762,7 @@
(or current-fb-icon "circle-dashed"))))]
(state/set-state! :ui/icon-hover-preview
{:db-id preview-target-db-id
:property property
:icon next-icon}))))
clear-preview-on-leave!
(fn []
@@ -3780,14 +3839,7 @@
:aria-label "Customize avatar"
:aria-expanded expanded?}
[:div.preview-avatar
(icon preview-icon {:size 56})
[:div.preview-cue
;; Both glyphs always rendered, stacked; CSS shows the
;; active one (and crossfades) based on data-expanded.
[:span.preview-cue-glyph.preview-cue-pencil
(shui/tabler-icon "pencil" {:size 11})]
[:span.preview-cue-glyph.preview-cue-check
(shui/tabler-icon "check" {:size 11})]]]]
(icon preview-icon {:size 56})]]
[:div.cb-meta-stage
;; Resting state: the entire banner is one click target
;; that toggles the expanded customize panel. Layout:
@@ -3843,17 +3895,30 @@
;; mouseenter is suppressed for the item already
;; under the cursor at open time, which made the
;; first hover silently no-op.
;; Leading tabler icons follow the codebase's canonical
;; dropdown-menu-item pattern (see deps/shui/src/logseq/
;; shui/demo.cljs:18-50): tabler icon as the first
;; child with `scale-90 pr-1 opacity-80` so the icon
;; reads slightly smaller and dimmer than the label.
(shui/dropdown-menu-item
{:on-click #(set-shape! :circle)
:on-focus #(preview-shape-on-hover! :circle)}
(shui/tabler-icon "circle" {:class "scale-90 pr-1 opacity-80"})
"Circle")
(shui/dropdown-menu-item
{:on-click #(set-shape! :rounded-rect)
:on-focus #(preview-shape-on-hover! :rounded-rect)}
(shui/tabler-icon "square-rounded" {:class "scale-90 pr-1 opacity-80"})
"Rectangle")))]
[:div.cb-row
[:span.cb-label "Fallback"]
(shui/dropdown-menu
;; Controlled open state — see `::fallback-menu-open?`
;; comment above. The map of props goes as the first
;; arg to dropdown-menu, before the trigger/content
;; children.
{:open @(::fallback-menu-open? state)
:on-open-change #(reset! (::fallback-menu-open? state) %)}
(shui/dropdown-menu-trigger
{:as-child true}
[:button.cb-chip
@@ -3899,9 +3964,16 @@
(shui/dropdown-menu-content
{:align "end"
:on-mouse-leave clear-preview-on-leave!}
;; Same canonical pattern as Shape — tabler icon
;; first, label after. `letter-case` is the closest
;; tabler glyph to the chip's "Aa" cue. `circle-dashed`
;; on the sub-trigger matches the placeholder we
;; broadcast on hover when no fallback-icon has been
;; committed yet, so menu and live-preview agree.
(shui/dropdown-menu-item
{:on-click set-fallback-letters!
:on-focus #(preview-fallback-on-hover! :letters)}
(shui/tabler-icon "letter-case" {:class "scale-90 pr-1 opacity-80"})
"Letters")
;; Sub-menu pattern (matches content.cljs's "Add reaction"
;; → emoji picker): "Icon…" expands to the side instead
@@ -3912,6 +3984,7 @@
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
{:on-focus #(preview-fallback-on-hover! :icon)}
(shui/tabler-icon "circle-dashed" {:class "scale-90 pr-1 opacity-80"})
"Icon…")
;; `dropdown-menu-sub-content` ships with `p-1` baked
;; into shui's popup-core defaults, AND content.cljs's
@@ -3953,10 +4026,7 @@
;; emojis route to set-fallback-emoji!
;; (which writes :fallback-type :emoji)
;; and tabler icons route to the
;; existing :icon path. `(:type item)`
;; is `:icon` or `:tabler-icon` for
;; tabler tiles and `:emoji` for emoji
;; tiles after normalization.
;; existing :icon path.
(let [glyph-id (or (get-in icon [:data :value])
(:id icon))]
(cond
@@ -3992,6 +4062,13 @@
:page-title page-title
:preview-target-db-id preview-target-db-id
:hover-wrap-fn fallback-hover-wrap-fn
;; Propagate the asset-picker's property scope to
;; the sub-picker so its hover broadcasts go to
;; the same surface (e.g. when editing the class
;; default-icon, the sub-picker's icon-grid hovers
;; reach only the Default Icon field, not the
;; page-title icon).
:property property
;; Suppress the picker's own color swatch and
;; delete button — the parent asset-picker
;; already owns both for the whole avatar, and
@@ -5889,8 +5966,16 @@
0)))
s)}
[state {:keys [on-chosen del-btn? icon-value page-title preview-target-db-id preview-target-db-ids
allowed-tabs hover-wrap-fn color-btn?]
:or {color-btn? true}
allowed-tabs hover-wrap-fn color-btn? property]
:or {color-btn? true
;; `property` scopes hover-preview broadcasts so two pickers
;; on the same entity but different properties (e.g.
;; page-title's `:logseq.property/icon` and class
;; default-icon's `:logseq.property.class/default-icon`)
;; don't leak previews into each other's surfaces. Default
;; to `:logseq.property/icon` because that's the dominant
;; case; callers editing other fields pass their own.
property :logseq.property/icon}
:as opts}]
;; `color-btn?` defaults to true so existing call sites are unchanged;
;; the avatar fallback sub-picker passes `:color-btn? false` because
@@ -5956,18 +6041,28 @@
:custom-image {:type :image-placeholder
:id "image-placeholder"}}
preview-targets-set? (or preview-target-db-id (seq preview-target-db-ids))
preview-base-target (cond-> {}
;; Every preview write carries `:property` so readers in
;; different fields editing the same entity (page-title icon vs
;; class default-icon) can isolate by property. The default of
;; `:logseq.property/icon` matches the existing implicit scope of
;; sidebar / cmdk / page-title rendering — so unscoped callers
;; keep working as before.
preview-base-target (cond-> {:property property}
preview-target-db-id (assoc :db-id preview-target-db-id)
(seq preview-target-db-ids) (assoc :db-ids (set preview-target-db-ids)))
clear-tile-hover!
(fn []
(when preview-targets-set?
;; Stale-db-id guard: if a different picker has since taken over
;; the slot, don't clear its preview. Prevents cleanup-races
;; between pickers opening on different blocks back-to-back.
;; Stale-db-id-and-property guard: if a different picker has
;; since taken over the slot, don't clear its preview.
;; Prevents cleanup-races between pickers on different blocks
;; or on the same block but different properties (e.g. opening
;; the Default Icon picker right after the page-title picker
;; on the same class).
(let [current (:ui/icon-hover-preview @state/state)
mine? (or (= preview-target-db-id (:db-id current))
(= (set preview-target-db-ids) (:db-ids current)))]
mine? (and (= property (:property current))
(or (= preview-target-db-id (:db-id current))
(= (set preview-target-db-ids) (:db-ids current))))]
(when (or (nil? current) mine?)
(state/set-state! :ui/icon-hover-preview nil)))))
broadcast-tile-hover!
@@ -6000,8 +6095,12 @@
;; closure and goes stale across keep-popup? flows (e.g. picking a color
;; on an inherited icon, which writes the icon to the entity for the first
;; time). Treat the :none sentinel (set on delete) as "no icon".
;; `property` selects the right entity attribute — `:logseq.property/icon`
;; for page-icon pickers, `:logseq.property.class/default-icon` for the
;; class default-icon picker. Without this, a default-icon picker would
;; read the unrelated `:logseq.property/icon` and stay stale across commits.
del-btn? (if preview-target-db-id
(let [icon (some-> (model/sub-block preview-target-db-id) :logseq.property/icon)]
(let [icon (some-> (model/sub-block preview-target-db-id) (get property))]
(and icon (not= (:type icon) :none)))
del-btn?)
;; Same staleness problem applies to icon-value itself. shui's popup
@@ -6015,7 +6114,18 @@
;; downstream asset-picker's preview tile + Shape chip + body grid.
icon-value (if preview-target-db-id
(or (some-> (model/sub-block preview-target-db-id)
:logseq.property/icon)
;; Pick the right entity attribute based on
;; scope — `:logseq.property/icon` for the
;; page-icon picker, `:logseq.property.class/
;; default-icon` for the class default-icon
;; picker. The Default Icon commit writes to
;; the latter, so without this lookup the
;; reactive override returned the unrelated
;; `:logseq.property/icon` (often nil or the
;; old page-icon value) and the asset-picker
;; tile + Fallback chip stayed frozen at the
;; pre-edit state.
(get property))
icon-value)
icon-value)
normalized-icon-value (normalize-icon icon-value)
@@ -6045,16 +6155,26 @@
(when (:type icon-item) (add-used-item! icon-item)))))
*focus-region (::focus-region state)
*highlighted-index (::highlighted-index state)
;; Use `rum/react` (not bare deref) so changes to
;; `*highlighted-index` from `keyboard-nav-controller` arrow-key
;; handlers re-render icon-search. Without this, `highlighted-id`
;; below was computed once at popup-show and never refreshed —
;; the phantom `icon-hover-effects` component (which broadcasts
;; the highlighted item to `:ui/icon-hover-preview`) never saw a
;; fresh `current-id`, so keyboard nav silently no-op'd while
;; mouse hover worked. One subscription is enough; the other
;; reads can stay as bare derefs once the component is hooked up.
highlighted-idx (rum/react *highlighted-index)
section-states @*section-states
{flat-items :items sections :sections} (compute-flat-items @*tab result section-states)
highlighted-id (when-let [idx @*highlighted-index]
highlighted-id (when-let [idx highlighted-idx]
(when (< idx (count flat-items))
(:id (nth flat-items idx))))
highlighted-section (when-let [idx @*highlighted-index]
highlighted-section (when-let [idx highlighted-idx]
(when-let [si (section-for-index idx sections)]
(:label (nth sections si))))
ghost-highlighted-id (when (and (= @*focus-region :search)
(nil? @*highlighted-index)
(nil? highlighted-idx)
(pos? (count flat-items)))
(:id (first flat-items)))
ghost-highlighted-section (when ghost-highlighted-id
@@ -6113,7 +6233,12 @@
;; here lets the asset-picker's color trigger drive the
;; same preview state.
:preview-target-db-id preview-target-db-id
:preview-target-db-ids preview-target-db-ids})
:preview-target-db-ids preview-target-db-ids
;; Property scope for hover-preview isolation —
;; flows down so asset-picker's own preview writes
;; (Shape/Fallback hovers, color swatch) carry the
;; same scope and target only the right surfaces.
:property property})
:text-picker
;; Level 2: Text Picker view
@@ -6206,9 +6331,9 @@
;; host React hooks (class component via rum/reactive mixin).
(icon-hover-effects
{:current-id highlighted-id
:current-item (when (and @*highlighted-index
(< @*highlighted-index (count flat-items)))
(nth flat-items @*highlighted-index))
:current-item (when (and highlighted-idx
(< highlighted-idx (count flat-items)))
(nth flat-items highlighted-idx))
:broadcast! broadcast-tile-hover!
:clear! clear-tile-hover!})
@@ -6311,7 +6436,7 @@
:on-hover! (when (or preview-target-db-id (seq preview-target-db-ids))
(fn [c]
(state/set-state! :ui/icon-hover-preview
(cond-> {:color c}
(cond-> {:color c :property property}
preview-target-db-id (assoc :db-id preview-target-db-id)
(seq preview-target-db-ids) (assoc :db-ids (set preview-target-db-ids))))))
:on-hover-end! (when (or preview-target-db-id (seq preview-target-db-ids))
@@ -6453,10 +6578,17 @@
(rum/defc icon-picker-trigger-icon < rum/reactive
"Reactive sub-component so the trigger icon re-renders on hover-preview changes
without forcing the parent (which uses React hooks) into a class component."
[icon-value preview-target-db-id icon-props]
(let [preview (when preview-target-db-id (state/sub :ui/icon-hover-preview))
preview-active? (and preview (= (:db-id preview) preview-target-db-id))
without forcing the parent (which uses React hooks) into a class component.
`property` scopes the preview match — defaults to `:logseq.property/icon`
so existing callers keep their behavior; property/value.cljs's
`default-icon-row` passes `:logseq.property.class/default-icon` so its
trigger only reflects previews from a Default-Icon-scoped picker."
[icon-value preview-target-db-id icon-props & [property]]
(let [property (or property :logseq.property/icon)
preview (when preview-target-db-id (state/sub :ui/icon-hover-preview))
preview-active? (and preview
(= (:db-id preview) preview-target-db-id)
(= (:property preview) property))
preview-icon (when preview-active? (:icon preview))
;; Source: previewed icon (cross-type swap) or the committed value.
;; Both go through the same color-overlay path below.
@@ -6484,7 +6616,8 @@
(icon effective-icon-value (merge {:color? true} icon-props))))
(rum/defc icon-picker
[icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts button-opts page-title preview-target-db-id default-icon?]}]
[icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts button-opts page-title preview-target-db-id default-icon? property]
:or {property :logseq.property/icon}}]
(let [*trigger-ref (rum/use-ref nil)
;; Optimistic post-commit override. Holds the just-committed
;; icon-value during the ~15ms SharedWorker round-trip between
@@ -6524,7 +6657,8 @@
:page-title page-title
:del-btn? del-btn?
:preview-target-db-id preview-target-db-id
:default-icon? default-icon?})))]
:default-icon? default-icon?
:property property})))]
(hooks/use-effect!
(fn []
(when initial-open?
@@ -6564,5 +6698,5 @@
(if has-icon?
(if (vector? effective-icon-value) ; hiccup
effective-icon-value
(icon-picker-trigger-icon effective-icon-value preview-target-db-id icon-props))
(icon-picker-trigger-icon effective-icon-value preview-target-db-id icon-props property))
(or empty-label "Empty"))))))

View File

@@ -1981,8 +1981,15 @@
gap: 10px;
padding: 0;
min-height: 32px;
transition: padding 200ms cubic-bezier(0.32, 0.72, 0, 1),
gap 200ms cubic-bezier(0.32, 0.72, 0, 1);
/* Padding and gap snap rather than transition. Earlier this row
animated 2 layout properties concurrently with 4 more across
`.cb-avatar-trigger` and `.cb-rail-wrap` — six layout-triggering
properties on three nested elements per frame. Even on Chrome the
work is non-trivial; on browsers with weaker GPU acceleration
(Dia, etc.) it drops frames perceptibly. Snapping these two is a
~14px height + 4px gap jump at t=0, which the avatar morph
immediately covers — effectively invisible during user testing,
but cuts ~33% of per-frame layout work. */
}
.avatar-customize-zone[data-expanded="true"] .cb-content {
@@ -2006,9 +2013,15 @@
applies its own padding-top, so we drop this margin to 0 to avoid
double-spacing. */
margin-top: 6px;
/* Animate only width + height (the visible morph). The 6px → 0px
margin-top shift snaps because (a) on browsers that struggle with
layout-heavy per-frame work, every property dropped is one fewer
reflow, and (b) the snap happens at t=0 while the avatar starts
growing — the size morph hides the position snap. `will-change`
deliberately omitted: for layout properties it doesn't help and
can confuse some compositors into spurious layer creation. */
transition: width 200ms cubic-bezier(0.32, 0.72, 0, 1),
height 200ms cubic-bezier(0.32, 0.72, 0, 1),
margin-top 200ms cubic-bezier(0.32, 0.72, 0, 1);
height 200ms cubic-bezier(0.32, 0.72, 0, 1);
&:focus-visible {
outline: 2px solid var(--lx-accent-09, var(--rx-accent-09));
@@ -2028,50 +2041,6 @@
width: 100% !important;
height: 100% !important;
}
.preview-cue {
position: absolute;
bottom: -3px;
right: -3px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--lx-gray-02, var(--rx-gray-02));
border: 1px solid var(--lx-gray-05, var(--rx-gray-05));
color: var(--lx-gray-11, var(--rx-gray-11));
/* Hide the cue in compact state — at 18px the cue would crowd the
small avatar and add no information (the "Use icon" CTA on the
right is the affordance for compact state). It fades back in as
the avatar grows on expand. */
opacity: 0;
transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1),
background-color 160ms cubic-bezier(0.32, 0.72, 0, 1),
border-color 160ms cubic-bezier(0.32, 0.72, 0, 1),
color 160ms cubic-bezier(0.32, 0.72, 0, 1);
}
&:hover .preview-cue {
border-color: var(--lx-gray-07, var(--rx-gray-07));
}
/* Both glyphs (pencil + check) overlap in the cue badge. Each crossfades
based on the parent zone's data-expanded attribute. transform pops them
onto their own layer so they don't repaint the avatar tile during the
swap. */
.preview-cue-glyph {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 160ms cubic-bezier(0.32, 0.72, 0, 1);
}
.preview-cue-pencil { opacity: 1; }
.preview-cue-check { opacity: 0; }
}
/* Expanded state: drop the compact-state margin-top so the 56px avatar
@@ -2106,16 +2075,6 @@
height: 11px !important;
}
.avatar-customize-zone[data-expanded="true"] .preview-cue {
background: var(--lx-accent-09, var(--rx-accent-09));
border-color: var(--lx-gray-02, var(--rx-gray-02));
color: white;
opacity: 1;
}
.avatar-customize-zone[data-expanded="true"] .preview-cue-pencil { opacity: 0; }
.avatar-customize-zone[data-expanded="true"] .preview-cue-check { opacity: 1; }
/* The right column hosts both .cb-banner (resting) and .cb-rows (expanded)
on top of each other. Only the "active" child claims layout space —
the other is `position: absolute` so it sits over the active one
@@ -2410,6 +2369,7 @@
}
}
/* Reset/Done rail — only shown when expanded; sits below the row stack
separated by a 1px divider that runs the band's full width. */
/* Rail wrapper — height collapses to 0 in resting state, transitions to
@@ -2454,6 +2414,21 @@
opacity: 1;
}
/* Pre-warming pass — see asset-picker `:did-mount` (icon.cljs ~2900).
On first mount we briefly set `data-expanded` so the browser computes
the expanded layout once (caching it for the user's first real
click); during that pass we suppress every transition under the zone
so the user doesn't see a flash. The `*::before` pseudo-element
(gradient backdrop) needs the same treatment so its opacity fade
doesn't run during the warm-up. */
.avatar-customize-zone[data-prewarming],
.avatar-customize-zone[data-prewarming] *,
.avatar-customize-zone[data-prewarming] *::before,
.avatar-customize-zone[data-prewarming] *::after {
transition: none !important;
animation: none !important;
}
.avatar-customize-zone .cb-rail {
display: flex;
align-items: center;

View File

@@ -146,12 +146,18 @@
(icon-component/icon-search
{:on-chosen on-chosen!
:icon-value icon
:del-btn? (some? icon)})
:del-btn? (some? icon)
:preview-target-db-id (:db/id block)
;; This row edits `:logseq.property/icon` — the same scope the
;; page-title icon uses. Hover broadcasts target both surfaces.
:property :logseq.property/icon})
[:div.col-span-3.flex.flex-row.items-center.gap-2
(icon-component/icon-picker icon-value
{:disabled? config/publishing?
:del-btn? (some? icon-value)
:on-chosen on-chosen!})])))
:on-chosen on-chosen!
:preview-target-db-id (:db/id block)
:property :logseq.property/icon})])))
(rum/defc default-icon-row < rum/reactive
"Renders the Default Icon property for classes.
@@ -213,6 +219,14 @@
:on-chosen on-chosen
:page-title page-title
:default-icon? true
:preview-target-db-id (:db/id block)
;; Scope the hover-preview to the
;; default-icon property so this row's
;; picker doesn't leak previews into
;; the page-title icon (which subscribes
;; to `:logseq.property/icon`-scoped
;; previews on the same class entity).
:property :logseq.property.class/default-icon
:icon-props {:size 20}})]))
(defn select-type?