Skip to content

feat: unified icon picker with emoji, tabler, text, avatar, image#12706

Open
scheinriese wants to merge 187 commits into
masterfrom
feat/unified-icon-picker
Open

feat: unified icon picker with emoji, tabler, text, avatar, image#12706
scheinriese wants to merge 187 commits into
masterfrom
feat/unified-icon-picker

Conversation

@scheinriese
Copy link
Copy Markdown
Contributor

@scheinriese scheinriese commented May 22, 2026

Summary

  • New unified icon picker. Five icon types in one popover: emoji, tabler, custom text (initials, abbreviated, or custom mode with color and alignment), avatar (with shape, letter or icon fallback, color), and image upload.
  • Multi-block batch operations: select blocks then open the picker from the selection toolbar; one pick writes to every selected block in a single transaction. Avatar and Text icons share color and shape while each block keeps its own initials derived from its title; emoji, tabler, and image apply uniformly.
  • Emoji-only variant (reaction-picker-opts) powers block reactions, the new comments feature, and the p r shortcut. Strips tabs, topbar, color picker, and Custom tab; keeps the Recently used lane at the top.
  • Asset picker sub-popover with Web images, Recently used, and Available assets sections. Section-collapse shortcuts (⌥⌘1/2/3/4) with hover-reveal hints.
  • Default-icon inheritance for tag classes. Every page tagged with a class inherits the class's icon unless it sets its own.
  • Render surfaces re-fit to the canonical 20-slot scale: inline [[page-refs]], table-view rows, list and gallery rows, lightbox preview, sidebar wrapper.
  • Bounded LRU color memoization, theme-aware DOM stamping, full keyboard navigation with ARIA semantics. OG, OG-turquoise, and Radix grey themes in light and dark.
  • Full Chinese i18n (122 new keys in en.edn, mirrored in zh-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 in get-node-icon (components/icon.cljs).

Rules

  • Instance's own :logseq.property/icon wins.
  • {:type :none} on the instance explicitly hides and skips inheritance.
  • Otherwise walk :block/tags sorted by :db/id. Oldest-attached tag with a default-icon wins.
  • A class without its own default-icon inherits from its ancestor via :logseq.property.class/extends.
  • Avatar and Text defaults act as templates: each instance derives initials from its own :block/title while inheriting color, shape, mode, and fallback type from the class.
  • Image defaults render an empty placeholder per instance until that instance picks a real asset.

Example. Set #Person's Default Icon to a blue avatar. The class page shows a blue "PE". Tagging Ada Lovelace with #Person displays a blue "AL", no per-page setup. Ada's icon-picker Remove offers Revert to default (retract override) or Remove entirely (writes {:type :none}; recoverable via the page-title "Restore icon" affordance). Setting a class page's own icon also writes default-icon if empty; clearing it clears default-icon only when the two were in sync, so opening #Person and picking an icon Just Works.

Migrations

Schema bumps 65.29 to 65.36. All idempotent and additive. No existing data is rewritten or removed.

Version Purpose
65.32 Adds :logseq.property.class/default-icon
65.33 Adds four asset properties: source-url, source-name, license, attribution
65.34 Corrective: fixes a property-type mismatch in 65.33 (:url to :string)
65.35 Corrective: retracts a leftover :db/valueType :db.type/ref that 65.34 missed
65.36 Orphan cleanup (wikidata-id, property-key-width) plus catch-up re-registration of master's :assignee and :agent/session-id for dev DBs whose kv was stuck on pre-merge numbering

65.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 dropped wikidata-id or property-key-width idents 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 :type and fall back to the emoji/glyph branch when missing. No flag, no opt-in.

Changes

  • Picker core: components/icon.cljs and icon.css. Five-tab popover, shared section state, keyboard nav, customize zone.
  • Color system: 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.
  • Schema and migrations: worker/db/migrate.cljs, deps/db/.../property.cljs, deps/db/.../schema.cljs. See Migrations above.
  • Block, page, property surfaces: 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.
  • Views, cmdk, sidebars: 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.
  • Theme and global state: ui.cljs, components/theme.cljs, state.cljs, util.cljc. Live icon-color preview, hover-preview writer, UI helpers.
  • Handlers and pipeline: 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.
  • shadcn primitives: packages/ui/@/components/ui/{avatar,tooltip}.tsx. Avatar fallback and tooltip arrow/offset tweaks.
  • i18n, tests, config: dicts/{en,zh-cn}.edn, test/.../icon_test.cljs (new), typos.toml.
Test plan (click to expand)

Picker entry, tabs, shortcuts

  • Click any page-title icon. Cycle All / Emojis / Icons / Custom; each tab populates Recents and section headers; tab underline tracks the active tab.
  • Focus the search input: glyph lifts and brightens.
  • Hover a section header ("Recently used", "Web images"): shortcut hint (⌥⌘2 etc.) appears after a 250ms delay.
  • Press ⌥⌘1/⌥⌘2/⌥⌘3/⌥⌘4: each toggles its section. State persists across reopens. Collapse state from the All tab does NOT leak into Emojis.
  • Hover the (i) info-icon next to "Web images": tooltip explains source/licensing. Clicking (i) opens the tooltip without collapsing the section.

Keyboard navigation

  • In any icon grid, arrow keys reveal a ghost focus ring; Enter commits.
  • Emoji tab: arrow across category boundaries without dead stops.
  • Search input: type "moon", ArrowDown moves ghost focus to the first match; ArrowDown again enters the grid.

Emoji, Tabler, Text

  • Emoji tab: pick any emoji. Page-title icon, left-sidebar Recent, right-sidebar References, and inline [[refs]] all update in lockstep.
  • Icons tab: search "star", pick a tabler glyph. Same lockstep update.
  • Custom → Text: page-title swaps to initials ("Math 201" → "MA"). Hover Initials / Abbreviated / Custom: live preview; mouse-out reverts.
  • Custom tile: free-form input up to 8 chars; live update as you type.
  • Alignment control (left/center/right) shifts SVG text-anchor within the 20px slot.

Color picker

  • Pick from the 8-swatch row: live recolor; Recently used lane updates (max 14, per-graph localStorage).
  • Rainbow tile: type #3b82f6, drag SV pad and hue slider; icon recolors during drag.
  • Near-black hex: contrast indicator (split-circle) at the input's right edge; hover shows light/dark-adjusted hexes.
  • Gallery and controls separator uses the canonical color in OG and themed graphs, light and dark.

Avatar + asset picker

  • Custom → Avatar: page icon becomes a default initials avatar. Customize banner expands with sequenced row animation; reverses on collapse.
  • Shape Circle ↔ Rectangle: avatar preview and page icon lockstep.
  • Fallback Letters → Icon sub-picker (All / Icon / Emoji): tabler glyph renders centered, no SVG inflation.
  • Available assets grid and asset picker overall hug content (no 120px / 320px floors).
  • Pick an asset (web image / clipboard paste / local upload): avatar fills, letter or icon fallback underlay shows until load.
  • Avatar/Image segmented control: Image commits as plain :image; aria-label translates in 简体中文.
  • Reset → Circle + Letters; Done collapses without resetting.

Default-icon inheritance

  • Set Default Icon on #Person (any type). Instance with no own icon inherits.
  • Set an instance icon: instance wins.
  • Trash on instance → Revert to default falls back to class; Remove entirely writes {:type :none} (recoverable via "Restore icon" on page title).
  • Change the class default: instances without own icons update; instances with own icons stay.
  • Two classes with defaults: oldest-attached wins (:block/tags by :db/id).
  • Setting an icon on the class page itself auto-syncs into default-icon when empty.

Batch + reactions

  • Select 3+ blocks with different titles. Open the picker from the selection toolbar's smile button. Pick an Avatar color/shape; all rows show the same bubble color and shape with per-block initials. Single undo reverts all at once.
  • Selecting #Institution rows seeds the picker on the avatar tab (auto-routed from the first row's resolved icon).
  • Trigger reactions from every entry point: block + button, comment + button, context menu Add reaction, and p r shortcut. Each opens the emoji-only picker with the Recently used lane on top, no tabs row, no color picker.
  • p r shortcut with multiple blocks selected: one pick toggles the reaction on every selected block.

Render surfaces (inline refs, table, list, gallery, sidebars, cmdk)

  • Inline [[page-ref]]: 20×20 with translateY(-1px) lift. Spot-check H1 (28px) and H6 (12px).
  • Table row: avatar 20px, tabler 16px (no inflation).
  • List and gallery rows: 20-slot scale, gap-2 between bullet and title.
  • Left-sidebar Recents and right-sidebar References preserve avatar shape and color; tabler glyphs not oversized. Recoloring updates the sidebar entry (reads [:data :color]).
  • Block status/history popover with avatar author icon: sidebar scale, no fallback flash.
  • Cmd+K results: emoji native; tabler 14px in gray chip; text 20px colored, no chip; avatar 20px, no chip. Inherited icon shows class default. Breadcrumb segments hide icons.

Migration, theming, i18n

  • Existing graph: schema bumps cleanly to 65.36, no console errors, pre-existing icons unchanged.
  • Pre-merge dev graph (kv stuck at 65.30-65.34): 65.36 catch-up re-registers :assignee and :agent/session-id, re-runs the property-type coercion, deletes the wikidata-id / property-key-width orphans.
  • Cycle OG / OG-turquoise / Radix-grey in light and dark. Spot-check separators, customize-band recess, search focus, alignment-toggle selected state.
  • Settings → Language → 简体中文: all picker chrome translated, including Avatar/Image aria-labels.

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

scheinriese and others added 30 commits March 17, 2026 13:38
…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>
scheinriese and others added 3 commits May 22, 2026 13:59
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>
@github-actions github-actions Bot added the :type/feature New feature label May 22, 2026
scheinriese and others added 17 commits May 22, 2026 15:28
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>
@scheinriese scheinriese marked this pull request as ready for review May 23, 2026 02:41
@scheinriese scheinriese requested a review from tiensonqin May 23, 2026 02:41
@scheinriese
Copy link
Copy Markdown
Contributor Author

@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.

researcher-demo-pr.zip

scheinriese and others added 3 commits May 24, 2026 13:52
"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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +57 to +82
(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))
")
Comment on lines +17 to +45
(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"))))))
Comment on lines +303 to +336
(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))
Comment on lines +119 to +135
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))
Comment on lines +259 to +267
(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))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants