Commit Graph

24617 Commits

Author SHA1 Message Date
scheinriese
868a9fb100 fix(asset-picker): theme license pill + share separator with icon picker
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>
2026-05-21 15:33:27 +02:00
scheinriese
7512070919 fix(asset-picker): theme topbar, segmented control, and customize zone
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>
2026-05-21 15:26:15 +02:00
scheinriese
ecaf1bc97b fix(icon): match Custom-tab keyboard-nav scale-up to the regular grid
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>
2026-05-21 14:57:13 +02:00
scheinriese
1ffafc22bc fix(icon): suppress browser focus outline on Custom-tab tiles
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>
2026-05-21 14:50:12 +02:00
scheinriese
63c7236f74 fix(icon): theme Custom tab tiles + apply user icon color to text preview
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>
2026-05-21 14:31:04 +02:00
scheinriese
9a36f996f7 fix(icon): dilute ghost-highlight bg via color-mix for clearer hover lift
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>
2026-05-21 14:17:12 +02:00
scheinriese
1c1fd10368 fix(icon): subtler highlighted bg + themed hover lift
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>
2026-05-21 14:13:29 +02:00
scheinriese
cfeb8a3638 fix(icon): theme search empty-state via --ls-primary-text-color
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>
2026-05-21 14:07:31 +02:00
scheinriese
ace3317721 fix(icon): theme topbar separator + drop accent focus on search
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>
2026-05-21 14:04:20 +02:00
scheinriese
6fb90c5804 fix(icon): re-mute placeholder + theme the (X) clear button
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>
2026-05-21 13:59:16 +02:00
scheinriese
b563a4681e fix(icon): theme picker label text via --ls-*-text-color middle step
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>
2026-05-21 13:54:09 +02:00
scheinriese
a115660d4b fix(icon): themed ghost-highlight outline via --ls-border-color middle step
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>
2026-05-21 13:48:27 +02:00
scheinriese
724242118d fix(icon): themed ghost-highlight tile via Tailwind utility chain
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>
2026-05-21 13:45:36 +02:00
scheinriese
9ca1a71ea4 fix(icon): theme-fit hardening — scrollbar, color tokens, edge-to-edge search
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>
2026-05-21 13:40:02 +02:00
scheinriese
8b90e11fc3 fix(icon): restore has-virtual-list fixed height to prevent picker collapse
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>
2026-05-21 12:28:37 +02:00
scheinriese
3364f7bdbf fix(icon): single-scroller layout — restore 9-column grid across all picker modes
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>
2026-05-21 11:20:53 +02:00
scheinriese
d48eea2629 style(icon): breathing room between search input and first content row
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.
2026-05-20 03:39:36 +02:00
scheinriese
e967718a3d fix(icon): unify padding strategy across picker surfaces
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.
2026-05-20 03:29:44 +02:00
scheinriese
06540afa27 feat(icon): emoji-only minimal picker for reactions
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>
2026-05-20 02:51:06 +02:00
scheinriese
e3d9c54651 Merge remote-tracking branch 'origin/master' into feat/unified-icon-picker
# Conflicts:
#	deps/db/src/logseq/db/frontend/schema.cljs
#	src/main/frontend/components/block.cljs
#	src/main/frontend/components/content.cljs
#	src/main/frontend/components/selection.cljs
#	src/main/frontend/worker/db/migrate.cljs
2026-05-20 02:17:43 +02:00
scheinriese
7cbc9ab65c fix(icon): polish asset-search results — recents filter, color cascade, outline ring
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>
2026-05-20 02:04:13 +02:00
scheinriese
0d7020abc1 feat(icon): asset search results in icon-picker
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>
2026-05-20 00:54:48 +02:00
scheinriese
871de7eed1 perf+fix: bounded-memoize hot-path, storage try/catch, tab-items button type
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>
2026-05-19 23:53:49 +02:00
scheinriese
b9fd1583a7 docs(icon): replace FIXME with root-cause explanation for filtered tabler icons
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>
2026-05-19 23:33:59 +02:00
scheinriese
85cf295edc refactor(icon): extract placeholder-hex constant for color picker
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>
2026-05-19 23:28:52 +02:00
scheinriese
fc2f683dc6 fix(lightbox): guard against rapid double-click + clarify Escape docstring
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>
2026-05-19 23:18:22 +02:00
scheinriese
b698957e23 fix(a11y): mark avatar image as decorative with explicit empty alt
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>
2026-05-19 23:07:39 +02:00
scheinriese
546cdfc6ee fix(icon): wire WAI-ARIA tab/tabpanel linkage on icon-picker
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>
2026-05-19 23:02:45 +02:00
scheinriese
800ce16da9 refactor(icon): extract icon-data-for-storage helper
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>
2026-05-19 22:53:12 +02:00
scheinriese
8efba90795 fix(pipeline): gate asset unlink on fresh local delete txs only
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>
2026-05-19 22:43:41 +02:00
scheinriese
0334d162ef fix(selection): atomic batch icon writes via single outliner transact!
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>
2026-05-19 22:26:21 +02:00
scheinriese
6f1afa4cca fix(selection): preserve avatar :shape and text :alignment / :mode in batch writes
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>
2026-05-19 21:53:34 +02:00
scheinriese
308ab8fb92 fix(icon): cross-platform meta-key, lift broken :or defaults, flag asset-picker
- 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>
2026-05-19 21:47:48 +02:00
Tienson Qin
d68cbce311 enhance(ux): able to edit #Comments title 2026-05-20 03:09:53 +08:00
Tienson Qin
22609130c4 fix: undo lost created-by 2026-05-20 03:06:00 +08:00
Tienson Qin
553f15a4cd fix: comment icon alignment 2026-05-20 02:51:20 +08:00
Tienson Qin
8e48079bfb fix: comment issues 2026-05-20 02:33:06 +08:00
scheinriese
041b63675b feat(icon): :suppress mode for single-click hide of inherited icon
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>
2026-05-19 17:57:52 +02:00
scheinriese
f257a7b30c feat(icon): extend two-option dropdown to asset-picker + text-picker
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>
2026-05-19 17:06:56 +02:00
scheinriese
cfedc6b2b8 fix(icon): use cond instead of case for delete-mode dispatch
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>
2026-05-19 16:55:23 +02:00
scheinriese
6c75090f94 feat(page): Restore icon affordance for suppressed entities
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>
2026-05-19 16:22:23 +02:00
scheinriese
989b12b811 feat(icon): two-option delete dropdown in icon-picker
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>
2026-05-19 16:20:22 +02:00
scheinriese
3983ee4cf1 feat(icon): add compute-delete-mode helper
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>
2026-05-19 16:16:06 +02:00
scheinriese
40113cffb5 fix(icon): reset transient picker state on trash click
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>
2026-05-19 16:14:15 +02:00
scheinriese
575fd5e916 i18n(icon): add keys for delete dropdown + restore affordance
5 new keys for the two-option delete UX:
- :icon/remove-icon          "Remove icon"           ; immediate-delete tooltip
- :icon/remove-icon-options  "Remove icon…"          ; dropdown trigger tooltip
- :icon/revert-to-default    "Revert to default"     ; dropdown item #1
- :icon/remove-entirely      "Remove entirely"       ; dropdown item #2
- :icon/restore-icon         "Restore icon"          ; page-title affordance when :type :none

Both en.edn and zh-cn.edn updated per logseq-i18n skill Rule 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:11:27 +02:00
scheinriese
6710230988 fix(block): suppress empty icon slot when value is non-renderable
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>
2026-05-19 16:10:12 +02:00
Tienson Qin
29854b9708 enhance: tag comment blocks 2026-05-19 22:08:02 +08:00
Tienson Qin
4afc96114e chore: remove :block/content usage from publish worker 2026-05-19 20:57:08 +08:00
scheinriese
51c6b25b95 fix(property/value): icon-row hook crash when picker opens
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>
2026-05-19 14:13:35 +02:00
Tienson Qin
f94c241f43 disable ssl by default 2026-05-19 20:10:51 +08:00