Two follow-up tweaks after the asset-picker chrome theming pass:
1. `.license-badge` (the "Free for any use" pill in the web-image hover
preview card) used raw var(--lx-gray-04) for bg — invalid in OG,
making the pill background completely transparent. Switch to
@apply bg-gray-04 for the themed chain. Color also gains the
--ls-primary-text-color middle step. The same raw-var pattern was
present on `.web-images-error` — fixed in the same pass.
2. The shui/separator between the asset-picker's tabrow and search
input used the shadcn default `bg-border opacity-50`, which rendered
washed-out teal in OG (~#003947 at 50% alpha). Apply the
`icon-picker-separator` class so it picks up the same themed
--ls-border-color (#0e5263, full opacity) as the icon picker.
To make this work across both pickers, `.icon-picker-separator` is
moved out of the `.cp__emoji-icon-picker` parent block to top-level
(now shared with `.asset-picker`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Asset-picker chrome rendered with neutral grays in OG turquoise:
- topbar/tabrow used raw var(--lx-gray-03) — invalid in OG → transparent
- segmented control bg used var(--lx-gray-06, --rx-gray-06) — neutral
- active segment used raw var(--lx-gray-01) — invalid in OG → unset
- customize zone used var(--lx-gray-01, --rx-gray-01) — fell through to
--ls-primary-background-color (#002b36) which is the SAME color as the
page bg behind the popover, making the band look like a hole punched
through the picker
- gray-05 borders + gray-10/11/12 text fell to neutral
Fixes:
- .asset-picker-tabrow bg → @apply bg-gray-03 (matches icon picker's
tabs-section via the full Tailwind themed chain)
- .segmented-control bg → @apply bg-gray-06 (themed quaternary teal)
- .segment[data-active] bg → @apply bg-gray-01 (carved-in feel against
the lighter control)
- .avatar-customize-zone bg → color-mix(secondary-bg 90%, black) — a
recessed-but-still-distinct band that doesn't bleed into page bg in OG
- All gray-05 borders gain --ls-border-color middle step
- All gray-10/11/12 text gains --ls-primary-/--ls-secondary-text-color
middle steps
Radix themes unaffected: --lx-gray-N still wins step 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The regular icon/emoji grid animates outline-width 0 → 2px AND
outline-offset 0 → 2px in parallel when .is-highlighted lands on a
tile, producing a satisfying scale-up feel as the ring grows outward.
The Custom tab tiles used a baseline of `2px solid transparent` with
outline-offset -2px (inset) and only transitioned outline-color, so
arrow-key navigation through Text/Avatar/Image only showed a fade.
Visually inconsistent with the rest of the picker.
- Baseline: `outline: 0 solid transparent; outline-offset: 0`
- Highlighted: `outline: 2px solid accent-09; outline-offset: 2px`
- Transition: extended to include outline-width and outline-offset
(matches the per-cell transition in the regular grid)
Outline now grows outward (positive offset) on highlight, scaling up
the ring rather than fading it in place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Arrow-key navigation through Text/Avatar/Image tiles flashed the
browser's default :focus-visible outline around the entire
<button.custom-tab-item> (preview + label) for one frame before the
.is-highlighted class applied our intended preview-only ring. Visually
choppy and inconsistent with the keyboard-nav style of the regular
icon/emoji grid (which doesn't trigger DOM focus on individual tiles).
Override :focus / :focus-visible on .custom-tab-item to remove the
outline and box-shadow. The .is-highlighted .custom-tab-item-preview
rule (already defined below) remains the canonical keyboard-nav
indicator — accessible focus is preserved, just rendered on the 48x48
preview tile rather than wrapping the whole column.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five issues in the Custom tab fixed in one pass.
1. Hover bg of the 48x48 tile preview (`.custom-tab-item-preview:hover`)
was raw var(--lx-gray-05, var(--rx-gray-05)) — neutral gray in OG.
Inject --ls-border-color middle step (matches the regular grid's
hover, also updated earlier this session).
2. Keyboard-highlighted preview bg used bright var(--lx-accent-04),
swamping content. Replace with the same color-mix-diluted quaternary
teal used by the regular grid's ghost/highlighted tiles. Accent-09
outline still carries the "I am selected" signal.
3. Tile labels ("Text", "Avatar", "Image") used raw gray-11/12 — neutral
in OG. Add --ls-primary-text-color and --ls-secondary-text-color
middle steps for label resting/hover respectively (matches the rest
of the picker text-theming work this session).
4. Image tile's dashed border was raw var(--rx-gray-08) — neutral. Use
the lx-gray-08 / --ls-border-color / rx-gray-08 chain. Same chain
applied to the image-placeholder branch in the `icon` fn (cljs:691)
so the page-icon's `Pick an image` preview matches.
5. Text tile preview wasn't picking up the user's selected color — the
`icon` fn only wraps its output in .ls-icon-color-wrap when
`:color?` is truthy, and the Custom-tab text branch was calling it
without that opt. Pass `:color? true` so the SVG's
`fill: currentColor` resolves to the chosen color (verified live:
green preset now shows on the WO preview).
Avatar and Image tile color application unchanged — those paths use
explicit bg/color inline styles, not the currentColor wrapper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ghost (#094b5a) and hover (#0e5263) were only ~4% apart in lightness —
visually indistinguishable on a teal picker bg. Drop ghost to 50%
alpha via color-mix with transparent, which mixes the quaternary
teal half-and-half with the picker bg behind it. Ghost pulls two
shades closer to the picker bg, hover (still full --ls-border-color)
gets a real visual lift.
color-mix(srgb) is Chrome 111+ / Safari 16.2+; safe in Logseq
Electron and modern web targets.
Same Tailwind chain token so Radix themes still receive their
themed gray (just at half intensity), preserving the muted-ghost
aesthetic there too.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two tweaks to picker tile interaction states:
1. .is-highlighted (arrow-key selected): bg dropped from --lx-accent-04
(bright cyan in OG) to @apply !bg-gray-04 (themed quaternary teal —
same value as ghost). Bright accent-09 outline still carries the
"I am selected" signal. Line icons (especially green) now remain
readable instead of getting drowned by the accent-tinted bg.
2. Button :hover: was raw var(--lx-gray-05, var(--rx-gray-05)) — falls
to neutral gray in OG and is identical to ghost's bg in Radix.
Inject --ls-border-color (#0e5263, one shade lighter than the
ghost's #094b5a) as the themed middle step so hover actually pops
above ghost in OG. Radix themes still use --lx-gray-05 step 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "No results found" empty state rendered neutral grays in OG
(container/icon at rx-gray-08, title at rx-gray-10, subtitle at
rx-gray-08) on top of a teal picker background — out of theme.
Inject `--ls-primary-text-color` (#a4b5b6 muted teal-gray in OG) as
the themed middle step across all three rules. Layer visual hierarchy
via per-child opacity (cannot set on the container — would multiply
into children and flatten the contrast):
- icon: opacity 0.5 (decorative aid, most muted)
- title: opacity 1 (most prominent line)
- subtitle: opacity 0.7 (themed but slightly muted)
Radix themes unaffected (--lx-gray-N still wins step 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related tweaks to the picker topbar/search chrome:
1. The horizontal separators (`.icon-picker-separator` between tabs and
search input, and `.search-section`/`.asset-picker-search` bottom
border) were raw `var(--lx-gray-N, var(--rx-gray-N))` — neutral gray
in OG, out of theme. Inject `--ls-border-color` as the middle step
(#0e5263 in OG, a themed teal that matches the picker hue).
2. Drop the `:focus-within { border-bottom-color: var(--lx-accent-09) }`
accent on `.search-section` and `.asset-picker-search`. The search
input has no validation state to communicate, and the search icon
already brightens on focus (opacity-50 → opacity-75). The blue
accent line was redundant chrome.
Radix themes unaffected (--lx-gray-N still wins step 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Placeholder fix: previous commit gave placeholder the same themed color
as typed text, collapsing the visual hierarchy. Restore differentiation
by keeping the themed color (--ls-primary-text-color in OG) AND adding
opacity: 0.6 — placeholder recedes against full-strength typed text,
themed in every theme.
(X) clear button fix: bg was raw var(--lx-gray-07, var(--rx-gray-07)),
glyph raw var(--lx-gray-12, var(--rx-gray-12)). In OG both fell to
neutral grays.
- bg → @apply bg-gray-07 (Tailwind chain includes
--ls-quaternary-background-color → #094b5a themed teal in OG)
- hover bg → manual chain with --ls-border-color (#0e5263, one step
lighter; gray-08 chain has no --ls-* middle)
- glyph color → manual chain with --ls-secondary-text-color (#dfdfdf
themed light; gray-12 chain has no --ls-* middle)
Same treatment applied to the asset-picker (X) for consistency. Radix
themes preserved (--lx-gray-N still wins step 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Section headers, search placeholders, and tab labels rendered as neutral
gray (rx-gray-11/12) in OG because Tailwind's gray-11/gray-12 chains
stop at `--lx-gray-N, --rx-gray-N` (no `--ls-*-text-color` step). On a
teal picker background, neutral gray reads as out-of-theme washout.
Inject `--ls-primary-text-color` (#a4b5b6 in OG — muted teal-gray) as
the themed middle step for label text:
- section-header inline styles (icon.cljs, 8 sites)
- .tab-item resting state (icon.css)
- .ui__input::placeholder for icon & asset pickers
For the brighter active-tab state and its underline, use
`--ls-secondary-text-color` (#dfdfdf in OG) — same role, lighter tone.
Radix themes unaffected: `--lx-gray-N` still wins step 1 of the chain.
Themes without either lx-gray-N or ls-*-text-color (older custom themes)
still fall to neutral `--rx-gray-N` for safety.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ghost-tile outline still rendered neutral gray (rx-gray-08) in OG
because Tailwind's gray-08 fallback chain is only two steps deep
(`--lx-gray-08 → --rx-gray-08`) — no `--ls-*` intermediate, unlike
gray-01 through gray-07.
Inject `--ls-border-color` manually as the middle step. In OG that
resolves to #0e5263 (a themed teal border, one shade lighter than the
quaternary-teal ghost bg #094b5a), giving a subtle outline that sits
in the same hue family as the tile. Radix themes are unaffected
(--lx-gray-08 still wins step 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous fallback form `var(--lx-gray-04, var(--rx-gray-04))` only
had two steps, so in OG turquoise (where `--lx-gray-04` is unset) the
ghost tile collapsed to `--rx-gray-04` (neutral hsl(0,0%,15.8%) gray)
on a teal picker background — out of theme harmony.
Switch to `@apply !bg-gray-04`, whose Tailwind chain is three steps
deep: `var(--lx-gray-04, var(--ls-quaternary-background-color, var(--rx-gray-04)))`.
OG's `--ls-quaternary-background-color` (#094b5a, a darker teal)
now wins, and the ghost tile sits in the same color family as the
picker chrome. Radix-* themes are unaffected (their `--lx-gray-04`
still wins step 1).
Outline color (`--lx-gray-08`) keeps the existing 2-step fallback
since Tailwind's chain for gray-08+ doesn't include an `--ls-*`
intermediate. A separate, larger pass would be needed to theme
outlines/foreground grays.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements .claude/plans/icon-picker-theme-fit-bulletproof.md. Three
interrelated fixes that together make every picker surface render
correctly across themes (Radix-* and OG turquoise) without breaking
the 9-column grid.
1) Scrollbar opt-out (restores 9-col grid in OG)
- Add `scrollbar-color: auto` to `.cp__emoji-icon-picker > .bd` and
`.asset-picker > .bd`. The OG theme's `:root { scrollbar-color }`
rule sets explicit thumb/track colors, which forces WebKit out of
macOS overlay-scrollbar mode into classic 15px-wide scrollbars.
The 9-px overhead collapsed the row math (9 tiles × 36 + 8 gaps × 4
= 356px) past the available 349px. With `auto` the browser returns
to 6px overlay → 358px available → 9 tiles fit.
2) Theme-adaptive color tokens (fixes pink-section-headers + accent
in selected states)
- 59 picker-scoped `var(--rx-gray-N)` references rewritten to
`var(--lx-gray-N, var(--rx-gray-N))`, matching the existing
fallback pattern used elsewhere in the file. Bare `var(--rx-gray-N)`
was theme-agnostic; the wrapped form lets Radix themes (where
`--lx-gray-N` is set) pick up the themed value while OG keeps the
neutral gray as fallback. Same render in OG today, headroom for
future theme refinements.
- 6 `var(--rx-blue-09)` references in selected states (asset picker
image selection, text picker gallery selection) → `var(--lx-accent-09)`.
Selected items now honor the user's accent in every theme (cyan in
OG, blue/pink/etc. in Radix themes).
- 8 bare `var(--lx-gray-11)` inline styles in icon.cljs rewritten with
the proper fallback. Fixes the pink-section-header bug: when a
color swatch is picked, `.pane-section { color: var(--ls-color-icon-preset) }`
cascades. The bare `var(--lx-gray-11)` was invalid in OG → property
unset → section-header inherited the icon-preset color and turned
pink/green/etc. The fallback keeps the property valid → header
stays themed gray.
3) Structurally borderless edge-to-edge search input
- `.search-section` background switched from raw `var(--lx-gray-03)`
(invalid in OG → transparent) to `@apply bg-gray-03` (Tailwind
utility with full fallback chain through `--ls-tertiary-background-color`).
Section is now themed in every theme.
- `.tabs-section` same treatment.
- `.ui__input` inside `.search-input` / `.asset-picker-search` is now
structurally borderless: `bg-transparent rounded-none`. No more
visible rounded-rect pill; the input is just text + cursor sitting
on the section's themed background. Edge-to-edge feel is now real
(theme-agnostic by construction), not faked via color matching.
- `.search-section:focus-within` and `.asset-picker-search:focus-within`
swap their `border-bottom-color` to `var(--lx-accent-09)` to provide
themed focus indication on the section (the input no longer has its
own focus ring). Matches the CMD-K palette pattern.
Verified live in OG turquoise theme: 9 cols, 1 scrollbar, themed
section bg (#08404f), transparent input, cyan focus border, themed
gray headers regardless of icon-preset color. Verified across All /
Emojis / Icons tabs and the reaction picker.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous single-scroller commit (3364f7bdbf) removed `h-[358px]` from
`.pane-section.has-virtual-list`, assuming `:custom-scroll-parent` alone
would carry the layout. It doesn't: when the popup container provides no
max-height (e.g., reaction picker under a block toolbar / comment thread,
or full picker opened fresh on the Emojis/Icons tab), the chain collapses
to zero — Virtuoso hasn't rendered yet → parent has no content → Virtuoso
measures 0 viewport → renders 0 items → never resolves. The picker shrunk
to ~86px tall with the entire grid missing.
The fixed height and `:custom-scroll-parent` are complementary, not
redundant:
- `h-[358px]` anchors the flex chain so Virtuoso has something to measure
on first render.
- `:custom-scroll-parent` keeps Virtuoso from creating its own scrollbar,
preserving the 9-column grid.
Restoring `h-[358px] overflow-y-visible` with the `searching-result h-auto`
override fixes every surface (All / Emojis / Icons / search / reaction
picker) at 442 picker / 360 .bd / 9 cols / 1 scrollbar.
Also adds defensive `(= :emoji (:type icon))` guards to the two remaining
reaction-picker call sites (block.cljs and comments.cljs) for symmetry
with content.cljs and ui.cljs. Required because comments.cljs didn't yet
require `frontend.handler.notification`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The reaction picker and the full picker's Emojis/Icons tabs all nested
two scrollbars (outer `.bd` + inner Virtuoso scroller). The pair ate
~12px of horizontal space, collapsing the 9-column grid to 8. The
reaction picker additionally rendered a redundant "Emojis · N" header
above its Virtuoso-Header-hosted "Recently used" section, with no
visual separator and the wrong order.
- pane-section: pass `:custom-scroll-parent` to Virtuoso unconditionally
(was gated on `searching?`). Virtuoso defers scrolling to the nearest
`.bd-scroll` ancestor in every mode, so `.bd` is the sole scrollbar.
- icon.css: drop the fixed `h-[358px]` on `.pane-section.has-virtual-list`.
The pane grows to Virtuoso's reported list height; `.bd-scroll` catches
the overflow.
- emojis-cp: render "Recently used" and "Emojis" as sibling pane-sections
(same shape as `all-pane`) instead of nesting recents in Virtuoso's
Header slot. Fixes header ordering and gives the two sections a natural
divider (the Emojis section-header).
- Reaction-picker call sites (block.cljs, comments.cljs, content.cljs,
ui.cljs): replace the inline emoji-only opts with `(merge
reaction-picker-opts ...)` for consistency.
Verified live across every surface (All / Emojis / Icons / Custom /
search / reaction picker): exactly one scrollbar, 9 columns, correct
header order.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add `pt-1` to `.cp__emoji-icon-picker > .bd` so the icon grid (and the
"Recently used" header) doesn't sit flush against the search input.
Asset-picker already had `py-1` on `.bd`, but its avatar-customize
banner negates that margin to keep the expanded-state gradient flush
against the topbar — add a 4px gap to the first `.pane-section`
following the banner so the grid doesn't read crammed against the
banner's bottom border.
Two structurally-distinct dropdown surfaces leak the popup's p-1 padding
in different ways, so they need different fixes:
- Top-level (shui/popup-show! → ui__dropdown-menu-content): icon-search
is a direct child, and its outer class swaps with @*view
(cp__emoji-icon-picker / asset-picker / text-picker). Apply `-m-1` to
all three view classes so switching tabs via "< Back" doesn't jump.
- Sub-content (block-context-menu "Set icon" / "Add reaction"): the
caller wraps the picker in an extra `[:div.p-1]`, stacking two p-1
layers. Pass `{:class "!p-0"}` to the sub-content (matching the
precedent at icon.cljs:4615-4625) and keep the inner wrapper for
breathing room.
Also drop the orphan `.ui__dropdown-menu-sub-content .cp__emoji-icon-picker
{ -m-2 }` rule that would overshoot under the new `!p-0` math.
Reaction pickers (comments + block reactions + editor toolbar +
context menu) only need a search input and an emoji grid — tabs are
redundant (only emoji allowed), color picker is meaningless (emojis
aren't tintable), trash button has no icon to delete.
Two fixes wrapped together:
1. `:tabs [[:emoji ...]]` was unused — `icon-search` filters by
`:allowed-tabs`, not `:tabs`. Switching reaction call sites to
`:allowed-tabs [:emoji]` actually hides Custom/Icons/All tabs
from the strip.
2. Add a `:hide-topbar?` opt. When true, the whole `.tabs-section`
(tab strip + color picker + trash button) and the separator
below collapse — the picker becomes search input + emoji grid.
The invisible `tab-observer` and `keyboard-nav-controller` lift
out of the topbar div so they keep working regardless. The
`.content-pane`'s `role="tabpanel"` + `aria-labelledby` are
gated on topbar visibility — no tab to label means no tabpanel
role.
Reuse the existing-but-orphan `:icon/search-emojis` key (already in
24 language dicts) as the placeholder + aria-label when topbar is
hidden. No new translation work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three related polish fixes for the icon-picker asset search:
1. Exclude :image from the icon-picker's "Recently used" section. The
data still lives in :ui/ls-icons-used-v2, but rendering :image
alongside icons/emojis creates broken `photo-off` tiles (assets
that were deleted/moved) and conceptually doesn't fit — image
recall has its own home in the asset-picker's "Recently used
assets" row. Filter applied at both render sites: compute-flat-
items and the parallel all-cp path.
2. Drop the `.pane-section` wrapper class from the assets-search-
section. `.pane-section` sets `color: var(--ls-color-icon-preset)`
to tint Tabler SVGs via currentColor — useful for icons/emojis,
but it polluted the image tiles' hover state: the undefined
`--rx-accent-09` border-color rule fell back to currentColor,
which inside .pane-section resolved to the user's picked icon
color. Bypassing .pane-section restores the asset-picker's
neutral cascade.
3. Switch image-asset-item + web-image-item from `border` to
`outline` for hover/focus/selected states. A `border` with
`border-radius` + `overflow-hidden` compressed the inner image's
visible radius below the outer border's curve, making rounded
corners look mismatched between the tile and its image. Outline
draws outside the element without compressing inner content and
follows border-radius automatically, so curves stay consistent
across all shapes (rounded, circle, rounded-rect).
Drive-by: the outline shorthand needs an explicit `currentColor`
fallback inside var() because shorthand parsing fails entirely when
the referenced custom property is undefined (which `--rx-accent-09`
is in this codebase). Without the fallback, outline-style reverts
to its initial `none` and the ring doesn't render at all. The
prior `border-color` longhand didn't have this problem because
longhand var() invalidation falls back to the property's own
initial value (currentColor), which still renders.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The search input already promised "Search emojis, icons, assets" but
only delivered emojis + icons. Add an Assets section to search results
that surfaces local image assets matching the query, so users can pick
an existing image directly from the icon-picker without drilling into
the asset-picker view.
Layout matches the asset-picker exactly: 64x68px tiles in a 5-column
grid, below the Icons section. Section is hidden when no assets match.
Reuses image-asset-item with a new :variant :search opt:
- Tooltip kept (filenames are unreadable on 64px tiles), shortened
to 200ms delay for fast search intent vs 400ms browse intent
- Ghost-retry icon hidden in search context (broken tiles just look
empty rather than show an ugly refresh affordance); the silent
retry logic still runs underneath
- Animation transitions dropped to avoid jank during rapid typing
Implementation notes:
- search-assets filters a per-picker-session cache (::loaded-assets,
loaded once on mount via sync + async paths mirroring the asset-
picker). No DB query on keystroke.
- compute-flat-items adds an Assets section with :cols 5; the existing
move-grid-highlight already supports per-section column counts so
arrow-key nav across 9-col icons → 5-col assets works for free.
- Kbd shortcut extended: Alt+Meta+4 toggles the Assets section
(extending the existing 1/2/3 pattern).
- Avatar-fallback sub-picker opts out via new :no-assets? true — its
job is picking an icon/emoji as a fallback glyph, not an image.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three small fixes from the same review pass:
- colors/bounded-memoize: hits were O(n) because the LRU promote
rebuilt the order vector via (vec (remove …)). Replace LRU with a
plain memo cache that drops half on overflow. Hits become O(1)
map lookup. The working set for adjust-for-contrast and muted-tint
(~200 entries) stays under the 256 cap in normal use, so eviction
is rare and the LRU bookkeeping was pure overhead.
- handler/icon-color: add-recent! and clear! now wrap their
storage writes in try/catch like get-recents already does.
Safari private-browsing and QuotaExceeded would otherwise throw
from React effect cleanups; the recents list is a nice-to-have,
silent drop is the right failure mode.
- ui/tab-items: add :type "button" so a tab can't accidentally
submit a form. Brings parity with the sibling segmented-control
fn. No current call site puts tabs inside a form, but cheap
safety belt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "FIXME: somehow those icons don't work" comment hid a real
diagnosis: csk/->Camel_Snake_Case lowercases consecutive caps like
"AB" to "Ab", so the label round-trips back to "IconAb" instead of
the actual "IconAB" tabler export. The renderer's reverse lookup
misses, the icon renders empty.
Document the mechanism, why the filter is correctly scoped to the
three affected names, and why other consecutive-cap exports
(IconEPassport, IconSTurnDown) don't need the same treatment.
The real fix would store the original tabler key alongside the
label so lookup doesn't go through CSK at all, but that's a
rendering-path refactor; the filter is the right local stopgap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two no-input surfaces in the color picker pane used the same demo hex
(#a1b2c3 / #A1B2C3) — the hex input's placeholder ghost text and
react-colorful's SV pad starting position. Inconsistent casing on
two literals separated by ~60 lines.
Extract as a private constant with a docstring explaining the demo-
value intent. Canonicalize on lowercase to match the rest of the
codebase's hex convention; the input's placeholder renders visually
identical either way since browsers don't case-discriminate hex.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related cleanups for the lightbox extension.
1. Rapid double-click guard. preview-images! did not check whether a
lightbox was already open before creating a new one. Two synchronous
clicks each created a lightbox + attached window listeners, then the
second assignment to window.photoLightbox orphaned the first instance
— its swallow filter and Escape handler stayed bound, soft-breaking
outside clicks until reload. Early-return if pswp is active.
2. Docstring fix. Previous wording claimed the Escape handler is
"identical to PhotoSwipe's own" when no popper is open. It isn't —
it stopImmediatePropagation's first, which preempts other global
Escape hooks (notably :editor/escape-editing). That behavior is
actually desired so closing a preview doesn't also exit block-edit
mode. Reword to match reality.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shui/avatar-image was called without an :alt prop, which leaves the
underlying <img> with no alt attribute — some screen readers fall
back to announcing the URL or filename.
The avatar is always rendered next to its label (page/block title),
which the screen reader already announces. Passing the title as alt
would double-announce; the right call is explicit empty alt to mark
the image as decorative per WCAG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The tab strip already had role=tab + aria-selected + roving tabindex,
but the content area lacked role=tabpanel and ids, and the tabs lacked
aria-controls. Screen readers couldn't associate the panel with the
active tab label.
ui/tab-items: add :tab-id-prefix and :panel-id opts. When set, each
button emits id=\"<prefix>-tab-<id>\" and aria-controls=<panel-id>.
Defaults to nil so other callers (none today) stay byte-identical.
icon-search panel: id=\"icon-picker-panel\", role=\"tabpanel\",
aria-labelledby pointing at the active tab's id (dynamic via @*tab).
No tabindex=0 on the panel — its content always has focusable
descendants so an extra stop would be redundant per APG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The cond that strips a picker-emitted icon down to persistable fields was
duplicated across block.cljs (page-title), property/value.cljs (default-
icon-row), and views.cljs (table row picker). Three near-identical
branches in three places.
Extract to `icon-component/icon-data-for-storage` — a pure fn paralleling
`normalize-icon` but for write paths instead of render paths. Each call
site collapses to a single function call.
Drive-by: property/value.cljs's image branch previously dropped `:id`
where block.cljs kept it. The unified helper uses the more complete
shape, which is harmless for existing image storage (extra field) and
matches what the other write paths emit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The renderer's fire-and-forget <unlink-asset hook fires for any tx
carrying deleted-assets, including:
- bulk graph sync downloads (:sync-download-graph?) — another device's
retraction would wipe the local copy of an asset we may still want
- RTC remote applies (:rtc-tx?, :rtc-download-graph?)
- undo/redo replays (:undo?, :redo?)
- multi-tab fan-out (every connected tab's renderer received the same
deleted-assets payload and raced to unlink the same path)
Add a tx-meta gate so the unlink only fires when the deletion is a
fresh, local user action. The bulk-sync case is the load-bearing one
(real data-loss risk). The undo/redo flags match codebase convention
elsewhere but note that real undo-preserves-asset support needs a
trash-folder pattern, not just a gate — flagged with a TODO.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The per-block doseq path (avatar/text/clear) used to issue N independent
transactions through the worker — partial mid-loop failures left the
selection in a mixed state, no shared undo step, and N reactive re-renders.
Wrap the doseq in one ui-outliner-tx/transact! with op-tag
:set-block-properties. The inner handler calls each open their own
transact! but short-circuit when an outer binding is active (per the
macro at frontend/modules/outliner/ui.cljc), so all N ops collapse into a
single atomic DB transaction, one undo step, and one re-render cascade.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The per-block icon-data builder explicitly select-keys'd only colors,
which silently reverted rounded-rect avatars to circles and dropped
text alignment/mode choices when applying an icon to multiple selected
blocks. Add the missing keys and rename the local from `colors` to
`styling` to match the broader intent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Section-collapse shortcut now uses util/meta-key? so Ctrl+Alt+1/2/3
works on Win/Linux (Mac unchanged: ⌥⌘1/2/3).
- Move `delete-mode (if del-btn? :remove :hidden)` default out of `:or`
and into the let body in asset-picker and text-picker — CLJS evaluates
:or defaults in the outer scope, so the sibling `del-btn?` reference
was an undeclared var.
- Add ^:large-vars/cleanup-todo to asset-picker for parity with
icon-search.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When an entity inherits a class default-icon but has no own override,
the trash button now writes `{:type :none}` directly with one click,
labelled "Hide inherited icon". Recovery is via the page-title Restore
affordance. Previously the trash button disappeared in this state,
leaving no way to opt out of inheritance.
Also fixes the icon-picker on-chosen wrapper to be variadic and forward
the action keyword — without this, asset-picker / text-picker delete
clicks silently dropped `:remove-entirely` and the entity wrote `nil`
instead of `:none`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan v2 specified all three trash sites should share the cond/dropdown
pattern driven by delete-mode. The first implementation only updated
the outer icon-search trash (icon.cljs:7155); the inner asset-picker
(icon.cljs:3887) and text-picker (icon.cljs:6334) kept the
single-click behavior, so users who opened the picker on an avatar/
image icon (which auto-routes to asset-picker) couldn't reach the
"Remove entirely" option.
Changes:
- Add `delete-mode` to asset-picker and text-picker prop destructuring
(with :or default `(if del-btn? :remove :hidden)` for back-compat).
- Replace `(when del-btn? (shui/button …))` with the same cond block
used at the outer trash — :hidden, :remove, :two-option branches.
- Thread `:delete-mode delete-mode` from icon-search to both sub-pickers.
- Change the sub-picker `:on-delete` shims to 1-arg `(fn [& [action]] …)`
so the action keyword chosen in the sub-picker's own dropdown flows
through to the parent on-chosen.
Verified live: open picker on a tagged page with class default-icon +
own override (Dr. Robert Whitfield #Person) → asset-picker view opens
→ trash button has aria-haspopup="menu" and tooltip "Remove icon…" →
real mouse click opens dropdown with [↩ Revert to default] / [🗑 Remove
entirely].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The trash-button render switched the picker render path between
:hidden, :remove, and :two-option modes via (case delete-mode ...).
This caused an "Objects are not valid as a React child" runtime error
on icon-free pages — even though :hidden branch returned nil, the
parent dialog's render still threw.
Reproduced bisect: same logic with (cond (= delete-mode :hidden) ...)
renders cleanly. The case form alone — with keyword tests and a Radix
dropdown in one branch — leaked a keyword into React's child tree.
Swap to `cond` and add a code comment explaining the gotcha so a
future cleanup doesn't re-introduce the case form.
Verified live: Add icon on the icon-free "wowie" page now opens the
picker cleanly with no error boundary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The icon-picker's "Remove entirely" dropdown option writes
{:type :none} to suppress inheritance. Without a counterpart UI, users
had no way back — the trash button hides itself when the entity is
already suppressed.
In page.cljs:242's db-page-title-actions, detect the :none sentinel
and swap the affordance:
- Entity has no icon (nil / unresolvable): label "Add icon" → opens picker
- Entity is suppressed via :type :none: label "Restore icon" with ↩ prefix → one-click retract
Single affordance slot, contextual behavior. The label change makes
the user's situation legible: they see WHAT will happen ("Restore",
not "Add") and can act with one click — no picker round-trip needed.
Verified live: page "yolo" was at :type :none. After reload, affordance
label reads "Restore icon" with arrow-back-up prefix. Click → property
retracted → label flips to "Add icon".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the new delete UX per .claude/plans/delete-button-bulletproof.md.
In icon-search at line 7023, the trash button now renders conditionally
on `delete-mode`:
- :hidden — not rendered
- :remove — single-click immediate retract (today's behavior)
- :two-option — opens a Radix dropdown with two labeled items:
[↩ Revert to default] → retract :logseq.property/icon
[🗑 Remove entirely] → write {:type :none} sentinel
Trash glyph stays consistent in every mode that shows it; the `…`
suffix in the tooltip + aria-haspopup="menu" signal the menu without a
chevron. Dropdown items have icon prefixes + text labels.
`on-chosen` extended to a 3-arity `(e icon-value action)` where action
is :remove | :revert | :remove-entirely | nil. The page-title on-chosen
in block.cljs:3964 branches:
- :remove-entirely → writes :type :none (suppresses inheritance)
- :revert / :remove / nil → retracts (lets inheritance resume)
Existing single-arg `(on-chosen nil)` callers still work via variadic
tail. Asset-picker and text-picker trash shims forward :revert when
delete-mode is :two-option (their trash stays single-click; revert is
the safe default that lets class default reappear on tagged pages).
Verified live: outer picker on Dr. Robert Whitfield (tagged #Person
with class default-icon avatar):
- Click trash → dropdown opens with both items
- Choose Revert → photo override retracted, class default avatar renders
- aria-haspopup="menu", tooltip "Remove icon…"
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Classify the picker's delete affordance into :remove | :two-option |
:hidden based on entity state + property scope:
:two-option — has own icon AND class inheritance would kick in
:remove — has own icon, no inheritance source
:hidden — no own icon, OR entity is :type :none (suppressed)
Reads :block/tags + :logseq.property.class/default-icon so the picker
re-renders when a class's default-icon changes (db-mixins/query
registers them as deps).
Bind a reactive `delete-mode` alongside the existing `del-btn?` in
icon-search. del-btn? becomes derived from delete-mode for back-compat
with downstream callers; delete-mode will drive the dropdown render in
the next commit.
No UI change yet — this is the pure data layer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Trash clicks left optimistic atoms intact:
- asset-picker's ::pending-icon could be re-injected by an in-flight
<save-image-asset! resolving AFTER delete, leaving a phantom
image-placeholder
- :ui/icon-hover-preview lingered as a ghost overlay
- ::asset-picker-initial-mode persisted, pinning the next picker
reopen to the deleted icon's mode
- *upload-status banner stayed visible mid-upload deletes
Add a reset-picker-transient-state! helper near the preview helpers in
icon.cljs and wire it into all four trash sites:
- asset-picker (icon.cljs:3882, inside the asset-picker component) —
clears its own *pending-icon and *upload-status
- icon-search shims at :6788 and :6821 (delegated from inner pickers)
and outer trash at :7027 — clear *asset-picker-initial-mode + the
global hover-preview
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
block.cljs:3922's `or` chain treated `{:type :none}` as truthy, so the
.ls-page-icon div was rendered with an empty shui ghost button inside
(the icon renderer at icon.cljs:638+ has no `:type :none` branch and
returns nil). Visible as a gray 38×38 rounded box next to the page title
after the user deleted an icon, preventing the title from flowing
flush-left.
Filter the user-data branches (own, inherited-default-icon, tag icons)
through icon-component/renderable-icon? so `{:type :none}` and other
unrenderable values produce nil and the slot is omitted entirely. The
hardcoded class/property fallbacks (`{:type :tabler-icon :id "hash"}`,
`…"letter-p"}`) bypass the predicate since they are guaranteed
renderable once `js/tablerIcons` loads.
Belt-and-suspenders: also defends against legacy `:type :none` values
already persisted in user DBs from prior deletes.
Verified live: page with persisted `:type :none` now renders title
flush-left, no .ls-page-icon div in the DOM.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
icon-row was modified earlier in this branch (c85e8e5588) to add
< rum/reactive db-mixins/query mixins so a model/sub-block call could
refresh the entity reactively. Those mixins turn rum/defc into a class
component, but the component still called hooks/use-effect! — which
internally calls React.useEffect and is only valid inside a function
component's body. The result: an Invalid-hook-call exception every time
icon-row rendered, surfacing as a "Something wrong, please try again"
toast when the user clicked to add a page icon.
Convert rum/defc → rum/defcs (class component, takes state as first
arg) and replace the hooks/use-effect! cleanup with a :will-unmount
lifecycle. The original effect returned a cleanup that restored the
cursor when `editing?` was true on unmount; lifecycle-based unmount is
the natural equivalent.
Verified live: page reloads, picker can render icon-row without
throwing — zero hook errors in console.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>