feat: unified icon picker with emoji, tabler, text, avatar, image#12706
feat: unified icon picker with emoji, tabler, text, avatar, image#12706scheinriese wants to merge 187 commits into
Conversation
…pport Introduces a comprehensive icon picker system supporting 5 icon types: emoji, tabler icons, text (SVG viewBox), avatar, and image assets. Key features: - Text icons rendered as crisp SVG with smart text splitting - Image asset picker with grid view, drag-and-drop upload - Wikipedia Commons image search with license confirmation - URL-based asset import with validation - Recently used assets tracking - Unified icon format with normalize-icon for consistent storage - DB migrations for default-icon, wikidata-id, property-key-width properties - Context menu "Set icon" / "Change icon" entry points - Bordered tooltip arrows for web image info - Photo-icon sizing in CMD+K and page title contexts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bump sidebar icon sizes to 20px, increase avatar font-size in sidebar, fix page title avatar dimensions, refine icon picker tabs and layout, add keyboard focus styles for tab items, and prevent bold layout shift. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enhance unified icon picker: refine asset-picker section navigation, improve avatar/image icon handling in on-chosen callbacks, add asset alignment support for images, and update icon rendering for block and page contexts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporary debug logging to trace how :logseq.property.asset/type "jpg" leaks onto regular text blocks during icon picker avatar→tabler-icon switching. Logs at: save-image-asset!, wrap-parse-block, save-block-inner!, api-insert-new-block!, and icon on-chosen callback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ty states - Show section shortcut hints (⌥⌘1/2/3) only when focus is on grid/tabs, hidden when typing in search - Restore ⌥⌘1/2/3 to toggle section collapse (was incorrectly switching tabs); remove tab-switching shortcuts - Scope keyboard highlight to active section only, preventing duplicate highlights when same emoji appears in Recently used and Emojis - Unify hover shape from circle to rounded-rect (8px) matching keyboard focus, with consistent visual hierarchy (hover < focus < ghost) - Add "No results found" empty state with search-off icon and subtitle - Show ghost asset placeholder for missing image files instead of hiding them entirely - Unify Emojis/Icons virtual list height (358px) to match All tab, eliminating height jump when switching between browsing tabs - Bump picker max-height to 442px to accommodate taller virtual lists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sible When search filters results to a single category, the chevron toggle and keyboard shortcut hints are unnecessary — collapse the only visible section makes no sense. Uses simple header mode instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s overlay Allow users to drag image files directly onto the icon picker popup to set as icon, bypassing the Custom > Image tab navigation. Features a frosted glass overlay with backdrop blur, radial edge glow using theme-aware accent colors, decorative corner brackets, and scroll locking during drag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Context-aware empty states: "No images yet" (first use) vs "No matching images" (search) with appropriate icons (photo vs search-off) - Add .asset-picker-empty CSS class with grid-column: 1/-1 to fix text wrapping bug where empty state was confined to a single 65px grid column - Refine drag overlay: frosted glass backdrop, corner bracket indicators, file type subtitle (PNG, JPG, SVG, GIF, WebP), shared CSS between pickers - Lock scroll on drag-active to prevent content shifting under overlay Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y leaks Ghost assets now retry URL resolution 3 times with 1s delay, show a clickable refresh icon for manual retry, and handle invalidated blob URLs via img on-error. Orphaned asset properties (type without checksum+size) are auto-retracted on save, and block rendering requires all 3 asset properties to prevent false asset treatment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Asset queries now require checksum (filtering orphaned entries) and deduplicate by checksum to prevent showing duplicate thumbnails. Changed async query to transact-db? false so deleted assets don't get re-transacted from worker DB back into frontend. Added debug logging across asset URL resolution, picker mounting, and view lifecycle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rruption - Merge load-image-url!, try-load-image-with-extensions!, and duplicate <load-asset-url! into a single unified <load-asset-url! with retry logic, extension guessing, and per-component stale guard - Fix concurrent asset loads dropping by replacing global *load-id-counter with per-component ::load-id atoms - Add on-error handler to image-icon-cp <img> tag for graceful fallback - Wire up collapsible sections in asset-picker using rum/react on *section-states (fixes reactivity with @ plain deref) - Restore correct } placement in cond-> forms (emoji-cp, text-cp, avatar-cp) that clj-paren-repair had corrupted by absorbing test/expr pairs into maps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and live persistence - Add derive-abbreviated, normalize-word-boundaries, and abbreviated-stop-words for smart title abbreviation (e.g. "Software Engineer" → "Soft Eng") - Replace single preview with gallery row of 3 selectable style tiles - Add ::mode and ::deleted? state atoms; persist mode in icon data - Live-persist on gallery click, alignment change, and text input (300ms debounce) - Guard will-unmount with ::deleted? to prevent re-persisting after delete - Add gallery CSS (.text-picker-gallery, -item, -preview, -label) and color-picker nested styles in .text-picker-actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass the icon-search *color atom value as :selected-color to text-picker, so the text-picker's color initializes from the tab-bar selection when no existing icon color is saved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rectangular shui/button color picker with round swatch circles: - Trigger button shows filled circle (with color) or slashed empty circle - Popup renders swatches as 24px circles with hover scale and active press - Selected swatch gets accent-colored ring via box-shadow - "No color" swatch uses CSS-only diagonal slash (bottom-left to top-right) - Remove duplicate .color-picker styles from tab-bar and text-picker-actions - Move "none" to first position in color array for natural scanning order Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The visibility check only handled nil icons, not the {:type :none} value
written when the user explicitly deletes an icon. Treat both as "no icon".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hovering a color swatch live-previews that color on both the picker grid icons and the page-title icon, letting the user imagine the choice before committing. Click commits as before; mouse-leave or popup close reverts. - color-picker now tracks local hover state and splits its effect: a display-only effect (deps [effective-color]) updates the CSS var on hover and commit; a commit effect (deps [color]) persists to storage. An unmount cleanup clears external preview state. - New :on-hover! / :on-hover-end! kwargs are opt-in; threaded through icon-picker -> icon-search via :preview-target-db-id from block.cljs. - get-node-icon-cp subscribes to :ui/icon-hover-preview so sidebar / inline icons update reactively. - icon-picker-trigger-icon is a small reactive sub-component so the page-title trigger updates without forcing the hook-using parent into a class component. It pre-normalizes icon-value before applying the preview color so normalize-icon's early-exit doesn't strip :data:value. - :focus-visible outline on swatches gives keyboard users the same preview affordance as mouse hover. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire the Custom tab into the picker's arrow-key navigation: Text, Avatar, and Image tiles now gain a blue ring when highlighted by keyboard and commit on Enter, matching Icons/Emojis. custom-tab-cp destructures the already-plumbed :highlighted-id from opts and emits data-item-id + conditional .is-highlighted on each button. CSS neutralizes the generic button.is-highlighted (which would paint the label column) and moves the ring onto the 48x48 preview child. A baseline transparent outline avoids the currentColor flash when the ring appears.
Matches the hover pattern used by icon/emoji tiles: background-color change instead of a border outline, and no opacity fade on the whole tile. Drops the default background/border on the preview so unselected tiles read as plain icons rather than framed controls. Label color steps up to gray-12 on hover so the tile feels active. The narrower transition list (background-color, outline-color) replaces transition-all — the outline-color channel is what lets the is-highlighted ring fade in without a discrete style jump.
The 48x48 preview boxes had a lot of empty margin around the 24px icons — especially after dropping the background/border. Bumping to 32px fills the tiles in a way that feels consistent with the densely- packed icon/emoji grids. Image placeholder scales with it: 28x28→32x32 box, 16→20 camera glyph.
@tabler/icons-react exports utility helpers (e.g. createReactComponent) alongside the IconFoo components. The picker keyed off every export, so these utilities showed up as phantom entries in search, rendered empty, and — if picked — were written back as unresolvable icon values.
Stored icon values can fall out of sync with what the picker can render — e.g. data saved against a phantom tabler utility export before the picker added a filter. Add a renderable-icon? predicate and use it to (1) drop dead entries from recently-used so the grid doesn't render empty tiles, and (2) fall back to the "Set icon" button on page titles whose stored icon can't resolve.
Deleting an icon writes a :none sentinel rather than removing the property, so del-btn? checks that only tested for truthy icon values kept the delete button visible on empty slots. Teach both the block call site (icon picker trigger) and icon-search itself to treat :none as absent. icon-search additionally recomputes del-btn? reactively from the live entity via db-mixins/query so keep-popup? flows (e.g. picking a color that writes the icon for the first time) update the button in place instead of staying stale.
The global .ui__icon svg { filter: brightness(.8) } dark-theme clamp
was desaturating icons rendered with an explicit color (e.g. cyan-10
looked visibly muted versus the swatch it was picked from). Add an
icon-colored class anywhere an effective color is present (and not the
'inherit' CSS sentinel), and use it to opt out of the filter where
color IS the affordance — inline icons, picker previews, and the live
color-picker preview on the picker root. Sidebar opacity-70 is left
intact since it's intentional visual hierarchy.
'inherit' is a CSS-layer fallback written into --ls-color-icon-preset so icons without an explicit color still render with something. It's not a real color and must not leak into React state or the persisted preset — otherwise the swatches show a phantom selection ring and the value round-trips through storage. Filter it at both entry points: color-picker's use-state initializer and icon-search's :init when reading icon-value color + stored preset. Also initializes color from the current icon's color first (falling back to stored preset) so opening the picker against an already- colored icon highlights the correct swatch.
Two related bugs around the web-images section: 1. Fast typing produced overlapping fetches where a late 'do' response could overwrite results for 'donald trump'. Stamp each request with a generation id in a component-local atom and drop responses whose id no longer matches. 2. The section unmounted during transition states — while the parent's debounced query hadn't caught up yet, or after the user typed but before loading? flipped to true — so the layout below jumped up, then back down when skeletons appeared. Compute a pending? flag from both conditions, keep the section mounted whenever loading? or pending?, and mirror avatar-mode on skeletons so circle-mode loads land in the exact spot the circular skeletons occupied. CSS mirror the same geometry (transparent 2px border + avatar-mode 50% radius) on .web-image-placeholder.
The old (memoize (fn [] (debounce …))) built a fresh debouncer on every render, so each keystroke got its own timer and debouncing never actually happened — partial-prefix queries all fired, producing the race condition that request-id guards. Move the debounce into component state via :will-mount (not :init, because rum/local only installs the underlying atom during :will-mount) so a single timer persists across renders.
Live tab control at the top of the asset-picker flips between circular avatar previews and uncropped image previews. Switching modes persists an in-place `:type` flip on an existing asset-backed icon, and future picks are saved with the active mode's `:type`. Extracted the icon-picker's tab-bar markup into a reusable `ui/tab-items` helper and unscoped `.tabs-section` / `.tab-item` CSS so both pickers share the underline style. Web-image thumbnails now default to `object-fit: contain` in Image mode (was always `cover`). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace "No images yet" placeholder with three tappable rows when the
asset list is empty: Upload from computer, Paste image URL, and
(when supported) Paste from clipboard with a ⌘V hint. Floating
action bar hides while rows are shown.
- Accept Finder / Explorer file pastes. A sync ClipboardEvent reader
inspects clipboardData.files/items, since navigator.clipboard.read()
cannot see OS file references. Global paste listener attached to
the picker root.
- Route URL downloads through Electron IPC (:httpRequest) in desktop
builds to bypass renderer CORS. Add opt-in :structured flag to the
IPC handler so it returns {:status :ok :headers :data} without
breaking existing body-only callers. Sniff magic bytes to detect
image vs HTML vs unknown when server Content-Type is unreliable.
- Classify download failures by HTTP status and error kind. New
url-save-error-copy maps :html-page, :not-image, :too-large,
:unknown, :http-status (with 401/402/403/404/429 specifics),
:empty, :cors, and :network to actionable user copy. Browser-build
CORS rejections now get an honest "browser blocked cross-origin"
message instead of "check your connection".
- Log failures via js/console.error in both IPC and fetch paths for
future debuggability.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both properties were declared in property.cljs and registered via migrations (65.31, 65.32) but had zero consumers anywhere in the source. They slipped in from the prep commit (e782310) when the unified-picker scope was reconstructed, intended for follow-up PRs that have not yet landed. Built-ins can never be cleanly removed once shipped, so cleaning before the PR goes out. - Remove `:logseq.property/wikidata-id` declaration + 65.31 migration. - Remove `:logseq.property/property-key-width` declaration + 65.32 migration. - Renumber remaining migrations: old 65.33→new 65.31 (asset properties), old 65.34→new 65.32 (fix-asset-source-url-property-type), old 65.35→new 65.33 (fix-asset-source-url-schema-lock). Docstring comments at migrate.cljs:96/106 updated to reference the new numbers. - Bump schema.cljs version 65.35 → 65.33. Local dev DBs that have already migrated past 65.30 need to be wiped; Logseq's migration runner refuses to load DBs with schema-version higher than the code expects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The asset-picker's segmented control toggling between Avatar and Image modes carried a hardcoded English `:aria-label "Icon rendering mode"`. Every other aria-label in this file goes through `(t :icon.*)`; this one was missed. The two option labels were already translated (`:icon.asset-mode/avatar` / `:icon.asset-mode/image`) — only the group label was bare. Add `:icon.asset-mode/picker-aria-label "Asset type"` (en) / "资产类型" (zh-cn), mirroring the existing `:icon.color/picker-aria-label` convention. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The icon-picker's search-input had a `:focus-within` rule lifting `.ls-icon-search` from opacity-50 → opacity-75 — the active-state signal that replaces a focus ring. The asset-picker's `.asset-picker-search` block mirrored the rest of the chrome but omitted this rule, so its search input gave no visual feedback when focused. Comment in the asset-picker block already documents the lift as the intended active-state signal — this aligns code with that contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI's spell-check (crate-ci/typos) flagged two patterns introduced by this PR: - `daa` inside hex color values (`e6daa6` beige, `daa520` goldenrod) in colors/named.cljs and colors/css.cljs. Pure tokenizer artifact; the hex codes are W3C canonical and can't be renamed. - `unparseable` in 3 docstrings + 1 comment of colors.cljs. Both spellings are valid English; typos prefers `unparsable`. Fix surgically rather than growing the global word allowlist: add the two color-data files to `extend-exclude` (they're pure lookup tables with no prose to spell-check anyway), and rename `unparseable` in colors.cljs to `unparsable`. No code semantics changed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI on PR #12706 surfaced two issues this PR introduced: 1. `frontend.worker.markdown-mirror-test` failed 2 assertions. Root cause: the new property `:logseq.property.asset/source-name` had `:title "Source"`, colliding with test fixtures that create a page literally named "Source" via `db-test/find-page-by-title`. The helper queries `[?b :block/title "Source"]` without filtering property entities, so the property registration leaked into the query result. Rename `:title "Source"` → `"Source name"` to match the property's actual semantics (it's the source's human-readable name, e.g. "Wikipedia"; the URL lives in the sibling `:source-url`). 2. clj-kondo reported 19 PR-introduced warnings (CI gate is strict — 0 errors / 0 warnings). All quality-only, none affect runtime. - colors.cljs: prefix `bg-var-theme-observer` (the side-effecting MutationObserver `defonce`) with `_` so lint sees it as intentional. The `.observe` side-effect stays live. - icon.cljs: prefix 9 unused let-bindings / fn-params with `_`; rename 6 shadowing locals (3 of which were rebinding the namespace's own `icon`/`custom-active?` vars inside fn bodies) to descriptive non-shadowing names like `icon-data`; unwrap 3 redundant `let`/`do` wrappers. Verified: `clj-paren-repair` clean, `clj-kondo --lint` reports 0 errors / 0 warnings on both files, shadow-cljs compile 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…plicate filter Commit 1ad23f3 removed `:logseq.property/wikidata-id` and `:logseq.property/property-key-width` from `built-in-properties` but didn't ship a cleanup migration. Any dev DB that ran the intermediate code (old 65.31 / 65.32) kept the orphan `:db/ident` entities, and the worker's tx validator rejected every subsequent write with "DB write failed with invalid data". - Add migration 65.34 = `:delete-properties` for both orphans. Mirrors the 65.25 pattern that purged `:block/pre-block?` + the embedding HNSW labels. `delete-property` emits a per-usage retract plus `[:db/retractEntity ident]`, so fresh graphs no-op and affected DBs self-heal on next boot. - Bump `db-schema/version` 65.33 → 65.34. - Fix the duplicate-property filter in `upgrade-version!`. The previous `(when (d/entity db k) (assert (str "DB migration: property already exists " k)))` was a no-op — `(assert string)` tests a truthy value and never throws — so duplicates were silently kept and crashed the downstream transact on `:db/ident` upsert conflict. Now mirrors the classes branch immediately below: silently skip when the entity already exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…d-sections)
`build-sections` looped with `(if-let [g (first gs)] …)`, which terminates
on a nil entry instead of skipping it. The `:emoji` branch of
`compute-flat-items` passes `(when show-used? {…})` as its first group;
`show-used?` is only true for the reaction picker, so the regular icon
picker got nil as the first arg, the loop short-circuited, and
`flat-items` came back empty — no ghost-focus highlight on the first
emoji, and the search-input ArrowDown handler no-op'd on `(pos? (count
flat-items))`.
Change to `(if (seq gs) …)` with an explicit nil-skip in the inner
branch, mirroring how the function already handles groups with empty
`:items`. Strictly relaxes the contract — every previously-accepted
shape still produces the same output, plus nil entries now skip
cleanly. Regressed in 3364f7b.
Verified live: Emojis tab now shows `.is-ghost-highlighted` on the
first emoji while search is focused, and ArrowDown advances selection
through the grid identically to the All / Icons / Custom tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`*section-states` (defonce atom at icon.cljs:2170) is a module-global keyed by section label. The same label `"Emojis"` was read by both the All tab's collapsible Emojis section AND the Emojis tab's `compute-flat-items` (icon.cljs:5128). So if the user collapsed Emojis in the All tab — via the chevron or `⌥⌘2` — switching to the Emojis tab would zero `flat-items` even though the visible grid (rendered by `emojis-cp`, which doesn't honor the gate) still showed every emoji. Result: search input had no ghost-highlight on the first emoji, and ArrowDown was a no-op. The Emojis tab's grid header is hidden (`:show-header? has-recents?`) and not collapsible, so reading `section-states` there was accidental coupling, not a feature. The `⌥⌘N` collapse shortcuts are already gated to the `:all` tab (icon.cljs:5453), confirming that collapse is an All-tab affordance. Drop the gate from the `:emoji` branch of `compute-flat-items` for both "Recently used" and "Emojis" labels. The All tab's collapse UX is unchanged (still consults section-states for its own rendering). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapsible section headers (All tab + search results) carry a
keyboard-shortcut hint like "Hide ⌥⌘2". Previously the hint was only
visible when keyboard focus was in the icon grid (focus-region ∈
{:grid :tabs}) — so mouse-only users never discovered the shortcut.
Now the hint also fades in when the cursor hovers over the section
header, teaching the shortcut as the user reaches for the chevron.
Implemented in pure CSS so there's no React state, no re-renders.
- Add `.section-header-hint` class to the hint wrapper.
- `@media (hover: hover) and (pointer: fine)` rule overrides the
`!opacity-0` Tailwind utility on hover, with a 250ms hover-intent
delay so a quick click-through doesn't flash the hint.
Mirrors the `.keyboard-shortcut` reveal pattern in container.css
(left sidebar nav). Touch devices and reduced-motion preferences are
respected via the media query and the existing `transition-all
duration-200` on the hint element.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends the icon-search picker's section-collapse feature to the
asset-picker:
- ⌥⌘1 / ⌥⌘2 / ⌥⌘3 toggle the Recently used / Web images / Available
assets sections. Hints fade in on hover (existing `.section-header`
hover rule already applies) and during keyboard nav.
- `keyboard-nav-controller` now accepts a `:section-shortcuts {keycode
label}` map and gates on `*tab = :all` only when `*tab` is wired
(icon-search), so the asset-picker fires its own shortcuts
unconditionally.
- `web-images-section` accepts a `:focus-region` prop so its
section-header reveals the hint during keyboard nav too, matching the
sibling sections.
Also moves the Web images (i) info icon from far-right to right next to
the title for visual rhythm with the other two sections:
- `section-header` gains a `:title-extra` slot that renders inside the
left cluster. The toggle click handler is split across the title span
and the count/chevron span; the title-extra sibling between them has
no click handler, so clicking the (i) opens its tooltip without
collapsing the section. (stopPropagation on a wrapper proved
unreliable through Radix's tooltip-trigger.)
- Drops the now-unneeded `.section-header-row` wrapper at the Web
images call site and the matching CSS rule.
- `.info-icon` hoisted to file scope; `transform: translateY(2px)`
added so the glyph is optically centered against the bold title
baseline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`.asset-picker-grid` had `min-height: 120px` as a drop-zone /
loading-skeleton floor. The Recently used and Web images variants
explicitly opted out (`.recently-used-row, .web-images-row { min-height:
auto }`), but the bare grid (Available assets) kept the floor.
When a user has exactly 1-2 available assets, the grid still padded to
120px, leaving ~40px of dead space below the last tile and above the
"Tip: Drop an image…" footer.
The Available assets section's `section-header` only renders
`(when (seq assets))` (icon.cljs:4830), so the entire section collapses
when empty — meaning the 120px floor never applies to the actually-empty
state. Dropping it lets the grid hug its content in the few-items state.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`.asset-picker { min-height: 320px }` forced a floor on the popover even
when content was much shorter — e.g. empty graph + collapsed Web images
left ~30px of dead space below "Add from your computer" before the
picker's bottom edge.
The icon-search picker has its own min-height, so this change only
affects the asset-picker. Picker now sizes to content (still clamped by
the existing max-height: 440px). Trade-off: slight resize when toggling
Web images or when assets load, but the picker is anchored to a
trigger that grows downward, so the resize doesn't dislocate the
visible content.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`block-title` in table views (`views.cljs:328`) renders an icon next to each row's title inside a 20×20 `.table-row-icon` wrapper, but passed `:size 16` to `get-node-icon-cp` — which forced both tabler glyphs AND avatars to 16, leaving avatars looking cramped inside their slot. Every other 20-slot surface in the app (inline page-refs, cmdk rows, property/value pickers, breadcrumbs, sidebars) renders avatars at 20 while keeping tabler glyphs at 14-16. The table-row title was the only outlier — and the avatar shrinkage was visible against the bolded inline `[[page-ref]]` style of the same person. Add an `:avatar-size` opt to `get-node-icon-cp` so callers can override only the photo-icon branch (`:avatar` / `:image` types). The table cell now passes `:size 16 :avatar-size 20`: tabler glyphs stay compact at 16, avatars fill the 20×20 slot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
List and Gallery views render each row via `block-container`, inheriting the outliner block bullet's 16×16 slot with `:size 14` glyph. For pages with letter-initials avatar fallback (e.g. "MG"), the two letters wrapped to two lines inside the cramped circle — looks broken. Every other "node icon" surface in the app (inline `[[page-refs]]`, sidebars, table-row icons) already uses the **20×20 slot, 16px non-photo glyph, 20px avatar** pattern. List/Gallery were the outlier. Align list/gallery rows with that pattern, keep outliner blocks untouched: - `block.cljs:2247-2258` — when `(:list-view? config)` or `(:gallery-view? config)`, render the bullet icon at `:size 16 :avatar-size 20`. Outliner blocks stay at 14/avatar-default-20. - `block.cljs:4316-4320` — tag the row with `is-list-view-row` and use `gap-2` (8px) instead of `gap-1` (4px) to match the table-cell spacing. - `block.css:1337+` — under `.is-list-view-row`, grow `.bullet-container` to `1.25em` (20px at the default text size). The inner `.icon-cp-container` already reserves 20px so the glyph centers cleanly. - `packages/ui/@/components/ui/avatar.tsx:48` — add `whitespace-nowrap leading-none` to `.avatar-fallback` as a regression safety net so two-letter monograms never line-break, regardless of tile size. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
23 commits from master since last sync (4bf2430): CLI agent bridge, :logseq.property/assignee + :logseq.property.agent/session-id property definitions, comments-block movability (rename `protected-comment-block?` → `comment-block?` at 3 sites), mobile selection-bar reaction, asset block editing stability, plus skill / docs / test additions. Conflicts resolved: - `deps/db/src/logseq/db/frontend/schema.cljs`: bump version to "65.36" (was 65.34 ours / 65.31 master). - `src/main/frontend/worker/db/migrate.cljs`: both sides added entries at 65.30 and 65.31. Renumber ours to 65.32-65.36 (default-icon → asset props → fix-property-type → fix-schema-lock → 65.36 catch-up). The 65.36 catch-up re-registers master's :assignee and :agent/session-id (no-op if present; rescues dev DBs whose kv was stuck at our pre-merge numbering and would otherwise skip master's 65.30/65.31), re-runs fix-asset-source-url-property-type (idempotent via its `(= :url ...)` guard, covers DBs stuck at old 65.32), and deletes the wikidata-id / property-key-width orphans. Updated docstrings to reflect the new numbering. Auto-merged correctly without manual intervention: - `block.cljs`: master's 4 hunks land in regions ours didn't touch. Predicate rename applies at 3 sites; L2142 keeps the broader `protected-comment-block?` deliberately (cache-key path). - `handler/events/ui.cljs`: master's `reaction-target-block-ids` helper, `[blocks target]` arglist, doseq, and `{:block :blocks :target}` destructure all come through; ours's `(merge icon-component/reaction-picker-opts {:on-chosen …})` is preserved verbatim. - `property.cljs`: master's :assignee, :agent/session-id, namespace allow-list addition; ours's default-icon, asset metadata, comments whitespace cosmetic. Disjoint regions. - `en.edn`, `zh-cn.edn`: disjoint key additions. Verified post-merge: - 0 conflict markers (grep) - `clj-paren-repair` clean on 5 touched files - cljs `(require :reload)` succeeds on schema, migrate, block, events.ui, property - migration table: 28 entries, all version strings unique, last is 65.36 with catch-up payload - `bb lint:worker-and-frontend-separate` passes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Setting a Default Icon on a tag, then opening the Avatar color picker
caused the picker to open expanded (SV pad visible) with the literal
string "var(--rx-gray-09)" in the hex input and a red-underline
"invalid" treatment. The synthesized avatar context was emitting a CSS
variable string as the avatar's :color, which flowed into the picker
as if it were a hex value.
- Avatar default now resolves (colors/variable :gray :09) to its
current-theme hex via colors/->hex before storing in the avatar's
:backgroundColor / :color.
- resolve-color gains a var(...) branch that resolves through ->hex
and returns {:match :var}.
- color-picker-popover normalizes any incoming CSS-var color prop to
hex before computing custom? and seeding the hex input — works for
any built-in or theme CSS variable, not just rx-gray-09.
- :ls-icon-color-preset round-trip is now hex-validated on both
write and read, so non-hex pollution from earlier sessions is
silently discarded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After the prior fix, the color picker still treated preset-swatch picks as "custom" because the comparison was asymmetric: the incoming color was normalized to hex, but the palette values stayed as CSS-var strings, so plain `=` never matched. Picks also never reached localStorage — the hex-only write guard silently dropped CSS-var swatch values. - Add a `same-color?` helper that resolves both sides via colors/->hex (nil-safe, theme-aware). preset-hex? delegates to it; the preset swatch and Default tile `active?` checks call it directly. - Recents lane: thread `color` through props and base `checked?` on the committed color via same-color? (was incorrectly comparing against the live hex-input text field with plain equality). - Palette "Gray" swatch: `:gray :10` → `:gray :09`, matching the avatar default used in five other call sites. - Storage write + read guards accept `var(--rx-…)` palette expressions alongside hex / "inherit", so preset picks now persist with theme-responsive colors instead of being silently dropped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "Clear N customized icon" hint button next to the Default Icon row was visible whenever any tag instance had its own `:logseq.property/icon` set — even when the class default itself was empty. In that state the word "customized" is misleading: an instance with an icon and no default isn't overriding anything, it's just setting its own icon. Gate the button on `current-value` in addition to `(pos? diverged-count)` so the affordance only appears when there's actually a default to fall back to. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three issues in the "Clear N customized icon" confirm flow: - Count stayed at 1 after the customization was cleared. The *all-instances atom was populated once with snapshot maps from <get-tag-objects (a d/pull worker query), so the filter for diverged instances ran against stale property values. The separate model/sub-block doseq subscribed for reactivity but discarded the live entity it returned. Now the cached snapshots are re-hydrated to live entities via model/sub-block per render, combining the subscribe and live-read in one pass. - Confirm modal hid the row preview list when N=1, leaving the user without a visual indicator of which row would be affected (the case where a preview is most useful). Show the list for every N≥1; trailing punctuation is always ":" now. - For N=1 the dict produced "These row will use the default icon instead of its custom one." The determiner is now switched alongside row/rows and its/their, so N=1 reads "This row…". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The confirm modal's affected-row preview used a 18x18 wrapper with `icon-component/icon` at `:size 16`, leaving avatars cramped at 16x16 inside a too-tight slot. Every other node-icon surface (inline page-refs, table-row icons, list/gallery rows, sidebars) already uses the 20-slot pattern: 20x20 wrapper, 16px non-photo glyph, 20px avatar. Switch the preview to `get-node-icon-cp` with `:size 16 :avatar-size 20 :own-icon? true`, and grow the wrapper to 20x20. `:own-icon?` keeps the read scoped to the instance's own `:logseq.property/icon` — the customization that's about to be cleared — instead of falling back through inheritance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@tiensonqin and other potential reviewers: I tested extensively against a synthetic academic-user graph i created a while ago, the same one shown in the demo recordings above. The db.sqlite is attached below if you want to try for yourself. |
"Dr. David Kowalski" used to render avatar initials "DD" because the
naïve token splitter treated "Dr." as the first name. Add a locale-
aware honorific-stripping helper in frontend.context.i18n (English,
German, French, Spanish, Italian seed lists) and call it from
derive-initials, derive-avatar-initials, and derive-abbreviated before
tokenizing. Match is case-insensitive, requires whitespace after the
prefix (so "Drew" doesn't match "Dr"), and accepts the no-period
variant ("Dr Smith" works the same as "Dr. Smith"). Single-pass —
"Prof. Dr. Müller" strips to "Dr. Müller" rather than recursing.
When stripping reduces a multi-word title to a single word, the
existing single-word path takes the first two characters of that
word — so "Mrs. Smith" → "SM" (consistent with how mononyms like
"Madonna" → "MA" already render). Articles ("The", "A", "An") are
NOT stripped — "The Beatles" stays "TB" since the article is part
of the name.
Stored avatars where the user explicitly committed a :value via the
picker keep their stored value; class-default-inherited avatars and
new picks adopt the new logic immediately. Adds the first tests for
derive-initials, derive-avatar-initials, and derive-abbreviated.
Plan: .context/plans/avatar-initials-honorific.md (Phase 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three related bugs in the block context menu's "Add reaction" and "Set icon" sub-menus: 1. Search input would intermittently not auto-focus when the sub-menu opened. Cause: Radix's MenuContent re-focuses itself on pointer-move between menu items, stealing focus from the just-mounted input (~60–100ms after React's autoFocus). The :did-mount setTimeout 0 fallback raced and often lost. Fix: install a capture-phase focusin listener on document for ~300ms after mount and bounce focus back to the search input if it lands anywhere outside the picker. 2. Hovering rapidly between "Add reaction" and "Set icon" could leave both pickers visible at once. Cause: Radix doesn't enforce sibling exclusivity between MenuSubs, and the two sub-menus held independent open state (uncontrolled vs. controlled). Fix: hoist both to a single `open-sub` keyword in the parent menu's use-state; opening one automatically closes the other in the same render. 3. Clicking the color picker dot inside the Set icon picker's topbar opened the popover but closed the icon picker behind it. Cause: the popover portals to body, and Radix's MenuSubContent fires onFocusOutside on us — first with target = parent menu container (from MenuContent's onItemLeave focus-steal), then with target inside the popover. Fix: preventDefault selectively in both onFocusOutside and onPointerDownOutside when the real target (via event.detail.originalEvent.target) is either inside .color-picker-popover or is the parent .ui__dropdown-menu-content itself. Matching the container via .matches (not .closest) leaves clicks on sibling menu items free to close the sub-menu as expected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a unified icon system across Logseq’s UI, expanding icon support beyond emojis to include Tabler icons, custom text, avatars, and images—along with a new color pipeline, class-level default icon inheritance, and additional schema/migration work to support these features.
Changes:
- Added a unified icon picker (full + emoji-only “reaction” variant) and wired it into multiple UI surfaces (page title, sidebars, table/list/gallery rows, CMDK, comments/reactions, selection toolbar batch ops).
- Implemented a new color infrastructure (named color dictionaries, CSS var parsing, contrast adjustments, and caching) plus per-graph “recent colors” localStorage handling.
- Introduced schema/migration updates for class default icons and asset attribution/source metadata, including corrective migrations for prior schema mistakes.
Reviewed changes
Copilot reviewed 46 out of 50 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| typos.toml | Excludes generated color tables from typo checking. |
| src/test/frontend/components/icon_test.cljs | Replaces/expands icon-related tests (avatar shape/fallback, initials/honorific stripping). |
| src/resources/dicts/zh-cn.edn | Adds Chinese i18n keys for icon picker/default-icon UI. |
| src/resources/dicts/en.edn | Adds English i18n keys for icon picker/default-icon UI (incl. pluralization fns). |
| src/main/frontend/worker/db/migrate.cljs | Adds migrations 65.32–65.36 + corrective fixes for asset source-url schema/type issues. |
| src/main/frontend/util.cljc | Re-exports hsl2hex for new color parsing logic. |
| src/main/frontend/ui.cljs | Adds segmented-control UI primitive alongside existing tab UI. |
| src/main/frontend/state.cljs | Centralizes DOM theme stamping via apply-theme-to-dom! and calls it from theme setters. |
| src/main/frontend/spec/storage.cljc | Documents new icon-color recents localStorage key family. |
| src/main/frontend/modules/outliner/pipeline.cljs | Hooks asset deletion to unlink files on local retract txs. |
| src/main/frontend/handler/property.cljs | Allows class default-icon property through property handler allowlist. |
| src/main/frontend/handler/icon_color.cljs | New: bounded per-graph recent icon-color storage in localStorage. |
| src/main/frontend/handler/events/ui.cljs | Switches reaction picker invocations to shared emoji-only opts. |
| src/main/frontend/handler/block.cljs | Removes block-title-with-icon helper (icon now rendered in dedicated slots). |
| src/main/frontend/handler/assets.cljs | Adds <unlink-asset helper for deleting asset files on disk. |
| src/main/frontend/extensions/lightbox.css | New: forces PhotoSwipe pointer-events to work over Radix modal popups. |
| src/main/frontend/extensions/lightbox.cljs | Adds event swallowing/inert handling + robust init/detach for PhotoSwipe over popovers. |
| src/main/frontend/context/i18n.cljs | Adds honorific stripping helper for initials derivation. |
| src/main/frontend/components/views.cljs | Adds per-row icon slot and icon picker on table title cells. |
| src/main/frontend/components/theme.cljs | Uses state/apply-theme-to-dom! and forces root rerender on theme change. |
| src/main/frontend/components/table.css | Styles table row icon hover affordance. |
| src/main/frontend/components/selection.cljs | Adds selection-toolbar batch icon picker w/ per-block avatar/text derivation. |
| src/main/frontend/components/right_sidebar.cljs | Adjusts icon sizing for sidebar entities. |
| src/main/frontend/components/property/value.cljs | Updates icon property editing; adds class default-icon property row and reset overrides UI. |
| src/main/frontend/components/property.cljs | Ensures class default-icon property shows on class pages even when empty. |
| src/main/frontend/components/page.css | Adjusts page title icon-to-title spacing. |
| src/main/frontend/components/page.cljs | Adds restore icon UI when icon suppressed/unrenderable; wires restore to property removal. |
| src/main/frontend/components/left_sidebar.cljs | Normalizes sidebar page icon sizing and minor formatting. |
| src/main/frontend/components/editor.cljs | Normalizes single-icon-per-row layout for search results. |
| src/main/frontend/components/content.cljs | Refactors context menu submenus; keeps set-icon submenu open across color popover interactions. |
| src/main/frontend/components/container.css | Normalizes sidebar icon slot sizing and centering, including emoji/image handling. |
| src/main/frontend/components/cmdk/list_item.cljs | Removes duplicate inline icon rendering; adds hook class for chip suppression. |
| src/main/frontend/components/cmdk/core.cljs | Updates icon resolution calls and minor formatting changes. |
| src/main/frontend/components/cmdk/cmdk.css | Suppresses CMDK icon chip background for photo-style icons using :has(...). |
| src/main/frontend/components/block/comments.cljs | Uses emoji-only reaction picker opts and warns if non-emoji is chosen. |
| src/main/frontend/components/block.css | Large set of icon sizing/alignment rules for inline refs, page title, list/gallery rows, CMDK breadcrumbs. |
| src/main/frontend/components/block.cljs | Integrates class default-icon inheritance into page title rendering; updates list/gallery icon sizing; reaction picker opts. |
| src/main/frontend/colors/named.cljs | New: embedded XKCD color dictionary map + reverse lookup. |
| src/main/frontend/colors/css.cljs | New: embedded CSS named-color map + reverse lookup. |
| src/main/frontend/colors.cljs | Major expansion: CSS color parsing, theme-aware caching, contrast adjustment, name resolution, measurement helpers. |
| scripts/build_color_dicts.bb | New: generator for XKCD named color dictionary source file. |
| pnpm-lock.yaml | Adds react-colorful dependency lock entries. |
| packages/ui/@/components/ui/tooltip.tsx | Adds optional bordered arrow rendering and exports TooltipArrow. |
| packages/ui/@/components/ui/avatar.tsx | Adds shape data prop typing; improves fallback styling classes. |
| package.json | Adds react-colorful dependency. |
| deps/db/src/logseq/db/frontend/schema.cljs | Bumps DB schema version to 65.36. |
| deps/db/src/logseq/db/frontend/property.cljs | Adds class/default-icon + asset attribution/source properties to built-ins. |
| .gitignore | Ignores /.claude/plans/. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| (def ^:private file-template | ||
| "(ns frontend.colors.named | ||
| \"XKCD color survey dataset (~949 colors). | ||
| Public domain (CC0): https://xkcd.com/color/rgb.txt | ||
| Generated by scripts/build_color_dicts.bb -- do not edit by hand.\") | ||
|
|
||
| (def ^:export named-colors | ||
| \"Map of normalized lowercase color name -> 6-char hex (no leading '#'). | ||
| Names with spaces are stored as-is; the resolver normalizes input the | ||
| same way before lookup.\" | ||
| {%PAIRS%}) | ||
|
|
||
| (def ^:private by-hex | ||
| (into {} (map (fn [[n h]] [h n])) named-colors)) | ||
|
|
||
| (defn name->hex | ||
| \"Look up a normalized color name (lowercase, single-spaced). Returns | ||
| 6-char hex without '#' or nil.\" | ||
| [s] | ||
| (get named-colors s)) | ||
|
|
||
| (defn hex->name | ||
| \"Reverse lookup: 6-char lowercase hex (no '#') -> canonical name or nil.\" | ||
| [h] | ||
| (get by-hex h)) | ||
| ") |
| (defn parse-css-color->hex | ||
| "Parse a computed CSS color value (hex / hsl(...) / rgb(...)) into a hex | ||
| string. Returns nil for blank or unparsable input." | ||
| [s] | ||
| (when-let [v (some-> s string/trim not-empty)] | ||
| (cond | ||
| (string/starts-with? v "#") v | ||
| (string/starts-with? v "hsl") | ||
| (let [parts (-> v | ||
| (string/replace #"hsla?\(" "") | ||
| (string/replace ")" "") | ||
| (string/replace "%" "") | ||
| (string/split #"[, ]+"))] | ||
| (when (>= (count parts) 3) | ||
| (let [[h s l] (map js/parseFloat (take 3 parts))] | ||
| (when (and (number? h) (number? s) (number? l)) | ||
| (apply util/hsl2hex [h s l]))))) | ||
| (string/starts-with? v "rgb") | ||
| (let [parts (-> v | ||
| (string/replace #"rgba?\(" "") | ||
| (string/replace ")" "") | ||
| (string/split #"[, ]+"))] | ||
| (when (>= (count parts) 3) | ||
| (let [[r g b] (map #(js/parseInt %) (take 3 parts))] | ||
| (when (and (number? r) (number? g) (number? b)) | ||
| (str "#" | ||
| (.padStart (.toString r 16) 2 "0") | ||
| (.padStart (.toString g 16) 2 "0") | ||
| (.padStart (.toString b 16) 2 "0")))))) |
| (defn- bounded-memoize | ||
| "Wrap f with a memo cache capped at `max-entries`. When the cap is | ||
| exceeded, drops half the entries in arbitrary order. The hot set | ||
| for our callers (adjust-for-contrast, muted-tint) stays well under | ||
| the cap so eviction is rare; LRU bookkeeping on every hit would | ||
| cost more than the occasional recompute it'd save." | ||
| [f max-entries] | ||
| (let [cache (atom {})] | ||
| (fn [& args] | ||
| (let [k args | ||
| c @cache] | ||
| (if-let [hit (find c k)] | ||
| (val hit) | ||
| (let [v (apply f args)] | ||
| (swap! cache | ||
| (fn [c'] | ||
| (let [c'' (assoc c' k v)] | ||
| (if (> (count c'') max-entries) | ||
| (into {} (take (quot max-entries 2) c'')) | ||
| c'')))) | ||
| v)))))) | ||
|
|
||
| (def ^{:doc "Adjust `picked-hex` toward sufficient WCAG contrast against `surface-hex`. | ||
|
|
||
| - Returns picked-hex unchanged if either input is invalid or contrast | ||
| already meets `target-ratio`. | ||
| - Otherwise bisects OKLCh L (preserving hue and chroma; chroma re-clipped | ||
| to sRGB on every step) toward the surface's opposite end until the ratio | ||
| is met or we exhaust 12 iterations. | ||
| - Falls back to white/black at the appropriate extreme if convergence fails. | ||
|
|
||
| Memoized via a 256-entry LRU cache keyed on [picked surface target]."} | ||
| adjust-for-contrast | ||
| (bounded-memoize adjust-for-contrast* 256)) |
| on-chosen! (fn [_e icon & [keep-popup?]] | ||
| (let [blocks (get-operating-blocks block) | ||
| ;; Handle text/avatar icons with :data nested structure | ||
| icon-data (when icon | ||
| (cond | ||
| (= :text (:type icon)) | ||
| {:type :text :data (:data icon)} | ||
|
|
||
| (= :avatar (:type icon)) | ||
| {:type :avatar :data (:data icon)} | ||
|
|
||
| :else | ||
| (select-keys icon [:type :id :color])))] | ||
| (property-handler/batch-set-block-property! | ||
| (map :db/id blocks) | ||
| :logseq.property/icon | ||
| (when icon (select-keys icon [:type :id :color])))) | ||
| (clear-overlay!) | ||
| (when editing? | ||
| (editor-handler/restore-last-saved-cursor!))) | ||
| icon-data)) |
| (defn <unlink-asset | ||
| "Delete an asset's file from disk. Used by the outliner pipeline when an | ||
| asset block is retracted (see modules/outliner/pipeline.cljs). Swallows | ||
| fs errors so a missing file (already deleted, sync mismatch) is a no-op." | ||
| [repo asset-block-id asset-type] | ||
| (let [file-path (path/path-join (config/get-repo-dir repo) | ||
| common-config/local-assets-dir | ||
| (str asset-block-id "." asset-type))] | ||
| (p/catch (fs/unlink! repo file-path {}) (constantly nil)))) |
Summary
reaction-picker-opts) powers block reactions, the new comments feature, and thep rshortcut. Strips tabs, topbar, color picker, and Custom tab; keeps the Recently used lane at the top.[[page-refs]], table-view rows, list and gallery rows, lightbox preview, sidebar wrapper.en.edn, mirrored inzh-cn.edn).Default-icon system (tag-class icon inheritance)
A new class-scoped property
:logseq.property.class/default-icon(migration 65.32) lets a tag class declare an icon every instance inherits. Accepts all five icon types as typed maps ({:type :tabler-icon :id "user"},{:type :avatar :data {…}}). Resolved inget-node-icon(components/icon.cljs).Rules
:logseq.property/iconwins.{:type :none}on the instance explicitly hides and skips inheritance.:block/tagssorted by:db/id. Oldest-attached tag with adefault-iconwins.default-iconinherits from its ancestor via:logseq.property.class/extends.:block/titlewhile inheriting color, shape, mode, and fallback type from the class.Example. Set
#Person's Default Icon to a blue avatar. The class page shows a blue "PE". TaggingAda Lovelacewith#Persondisplays a blue "AL", no per-page setup. Ada's icon-picker Remove offersRevert to default(retract override) orRemove entirely(writes{:type :none}; recoverable via the page-title "Restore icon" affordance). Setting a class page's own icon also writesdefault-iconif empty; clearing it clearsdefault-icononly when the two were in sync, so opening#Personand picking an icon Just Works.Migrations
Schema bumps 65.29 to 65.36. All idempotent and additive. No existing data is rewritten or removed.
:logseq.property.class/default-iconsource-url,source-name,license,attributionproperty-typemismatch in 65.33 (:urlto:string):db/valueType :db.type/refthat 65.34 missedwikidata-id,property-key-width) plus catch-up re-registration of master's:assigneeand:agent/session-idfor dev DBs whosekvwas stuck on pre-merge numbering65.34 and 65.35 patch in-branch iterations of 65.33 that never shipped to master, so released graphs see only the net-correct end state. All 65.36 steps are guarded (idempotent
(= :url ...)coercion, no-op re-registration, dev-only orphan deletion). Only third parties hardcoded against the droppedwikidata-idorproperty-key-widthidents break, both of which were dev-only.Backward compatibility
Pre-PR emoji icons keep rendering. The picker normalizes legacy values to
{:type :emoji :data {:value "..."}}on read; the datom is only rewritten when the user re-picks. New types (:text,:avatar,:image) only exist for entities set after this PR landed. All renderers dispatch on:typeand fall back to the emoji/glyph branch when missing. No flag, no opt-in.Changes
components/icon.cljsandicon.css. Five-tab popover, shared section state, keyboard nav, customize zone.colors.cljs,colors/named.cljs(new),colors/css.cljs(new),scripts/build_color_dicts.bb(new). Canonical palette, named-color lookup, three-step--lx-* / --ls-* / --rx-*fallback throughout.worker/db/migrate.cljs,deps/db/.../property.cljs,deps/db/.../schema.cljs. See Migrations above.components/block.{cljs,css},components/content.cljs,components/property.cljs,components/property/value.cljs,components/selection.cljs,components/page.{cljs,css},extensions/lightbox.{cljs,css},components/block/comments.cljs. Icon resolution in block rendering, inline ref icons, property-value picker, lightbox attribution, selection-toolbar entry.components/views.cljs,cmdk/{list_item,core}.cljs,cmdk/cmdk.css,container.css,table.css,components/editor.cljs,left_sidebar.cljs,right_sidebar.cljs. Table and list view avatar sizing, command palette icons, sidebar wiring.ui.cljs,components/theme.cljs,state.cljs,util.cljc. Live icon-color preview, hover-preview writer, UI helpers.handler/icon_color.cljs(new),modules/outliner/pipeline.cljs,handler/{assets,property,block,events/ui}.cljs,spec/storage.cljc. Icon-color commit handler, outliner pipeline hook, asset and reaction-picker integration.packages/ui/@/components/ui/{avatar,tooltip}.tsx. Avatar fallback and tooltip arrow/offset tweaks.dicts/{en,zh-cn}.edn,test/.../icon_test.cljs(new),typos.toml.Test plan (click to expand)
Picker entry, tabs, shortcuts
⌥⌘2etc.) appears after a 250ms delay.Keyboard navigation
Emoji, Tabler, Text
[[refs]]all update in lockstep.text-anchorwithin the 20px slot.Color picker
localStorage).#3b82f6, drag SV pad and hue slider; icon recolors during drag.Avatar + asset picker
:image; aria-label translates in 简体中文.Default-icon inheritance
#Person(any type). Instance with no own icon inherits.Revert to defaultfalls back to class;Remove entirelywrites{:type :none}(recoverable via "Restore icon" on page title).:block/tagsby:db/id).default-iconwhen empty.Batch + reactions
#Institutionrows seeds the picker on the avatar tab (auto-routed from the first row's resolved icon).+button, comment+button, context menu Add reaction, andp rshortcut. Each opens the emoji-only picker with the Recently used lane on top, no tabs row, no color picker.p rshortcut with multiple blocks selected: one pick toggles the reaction on every selected block.Render surfaces (inline refs, table, list, gallery, sidebars, cmdk)
[[page-ref]]: 20×20 withtranslateY(-1px)lift. Spot-check H1 (28px) and H6 (12px).gap-2between bullet and title.[:data :color]).Migration, theming, i18n
:assigneeand:agent/session-id, re-runs the property-type coercion, deletes the wikidata-id / property-key-width orphans.Demo
pr-cycling-icon-picker-tabs.mp4
pr-default-icon-avatar.mp4
pr-customize-avatar-settings.mp4
pr-pasting-uploading-images.mp4
pr-text-icon-customization.mp4
pr-batch-operation-color.mp4
pr-reaction-picker.mp4