mirror of
https://github.com/logseq/logseq.git
synced 2026-05-19 10:22:37 +00:00
feat: redesign asset-picker customize banner — single click target
Resting state restructured for clearer affordance + better
discoverability:
- Avatar tile demoted from <button> to <div aria-hidden> — it's a
live preview, not a control. Removes the redundant second toggle.
- Banner demoted from <button> to <div aria-hidden>. The chevron-down
is gone — it was miscueing as a <select>-style dropdown, and the
morph-in-place transition meant it never lived long enough to read
as a disclosure rotation anyway.
- New invisible <button.cb-row-trigger> overlays the resting row
edge-to-edge (covering the zone's 18px padding) so the entire
surface is one accessible click target. Mounted only when collapsed
(`when-not expanded?`) — when expanded, the dropdown chips inside
`.cb-rows` are unparented so there's no nested-interactive risk.
- aria-label composes scope + descriptor + verb so screen readers
announce the current state on focus ("Custom · Image, circle.
Customize avatar."). aria-controls links to the rows panel id;
panel gets role="region" + aria-label for the disclosure pair.
- Right-side "Edit" text label replaces the chevron — muted gray-09
at rest, brightens to gray-12 when the row is hovered.
Zone background swapped to solid theme tokens (no alpha veil):
- Rest: var(--lx-gray-01) — most recessed surface in both themes
- Hover: var(--lx-gray-03) — two-step lift via :has(.cb-row-trigger:hover)
- 120ms cubic-bezier(0.32, 0.72, 0, 1) on background-color only
Solid colors compose more reliably across light + dark themes than
stacked alphas, and avoid the content-clipping issue that would
happen if the trigger overlay carried a near-opaque background of
its own. The :has() pattern means the hover paints on the same
element that already backs the content, so the avatar / banner-text
/ Edit label naturally show through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3755,7 +3755,17 @@
|
||||
(.closest ".asset-picker"))]
|
||||
(when-let [btn (.querySelector cnt ".is-ghost-highlighted")]
|
||||
(util/stop e)
|
||||
(.click btn)))))))})]]]
|
||||
(.click btn)))))))})
|
||||
;; Rounded-circle clear button (matches icon-search). Visible
|
||||
;; only when the input has content; shares the same on-brand
|
||||
;; affordance instead of letting the browser render its native
|
||||
;; cancel-X (hidden via CSS).
|
||||
(when-not (string/blank? search-q)
|
||||
[:a.x {:on-click (fn [_]
|
||||
(reset! *search-q "")
|
||||
(update-web-query! "")
|
||||
(some-> (rum/deref *search-input-ref) (.focus)))}
|
||||
(shui/tabler-icon "x" {:size 14})])]]]
|
||||
|
||||
;; Body - scrollable content area with top/bottom margin
|
||||
(let [;; Get recently used asset UUIDs and resolve to asset entities
|
||||
@@ -4091,35 +4101,44 @@
|
||||
;; conditional render would mount/unmount on toggle and CSS would
|
||||
;; have nothing to interpolate from.
|
||||
[:div.avatar-customize-zone {:data-expanded (when expanded? "true")}
|
||||
;; Single click target spanning the resting row (avatar +
|
||||
;; meta + Edit). Sits as an invisible overlay above
|
||||
;; `.cb-content` so the avatar and banner text remain a
|
||||
;; clean visual layer underneath while clicks anywhere on
|
||||
;; the row hit one accessible-named <button>. Disabled
|
||||
;; (pointer-events: none, opacity: 0) in the expanded
|
||||
;; state so the dropdown chips inside `.cb-rows` are
|
||||
;; reachable without being nested under a parent button.
|
||||
;; aria-controls links to the expanded panel id below.
|
||||
(when-not expanded?
|
||||
[:button.cb-row-trigger
|
||||
{:type "button"
|
||||
:on-click #(swap! (::customize-expanded? state) not)
|
||||
:aria-label (str scope-label " · " descriptor ". Customize avatar.")
|
||||
:aria-expanded expanded?
|
||||
:aria-controls "asset-picker-cb-rows"}])
|
||||
[:div.cb-content
|
||||
[:button.cb-avatar-trigger
|
||||
{:type "button"
|
||||
:on-click #(swap! (::customize-expanded? state) not)
|
||||
:aria-label "Customize avatar"
|
||||
:aria-expanded expanded?}
|
||||
[:div.cb-avatar-trigger
|
||||
{:aria-hidden "true"}
|
||||
[:div.preview-avatar
|
||||
(icon fallback-preview-icon {:size 56})]]
|
||||
[:div.cb-meta-stage
|
||||
;; Resting state: the entire banner is one click target
|
||||
;; that toggles the expanded customize panel. Layout:
|
||||
;; [scope · descriptor] [chevron-down]
|
||||
;; State-as-label — `Default · Letters, circle` /
|
||||
;; `Custom · Briefcase, rounded`. Avatar tile remains a
|
||||
;; second click target with the same toggle behavior; the
|
||||
;; chevron rotates 180° on expand.
|
||||
[:button.cb-banner
|
||||
{:type "button"
|
||||
:on-click #(swap! (::customize-expanded? state) not)
|
||||
:aria-label "Customize avatar"
|
||||
:aria-expanded expanded?
|
||||
:tab-index (if expanded? -1 0)}
|
||||
;; Resting state: visible banner content. Click target
|
||||
;; lives on `.cb-row-trigger` above (covers this entire
|
||||
;; row). `.cb-banner` is now a presentation-only div —
|
||||
;; not interactive, no aria-expanded — so the row reads
|
||||
;; as one button to assistive tech.
|
||||
[:div.cb-banner
|
||||
{:aria-hidden "true"}
|
||||
[:div.banner-text
|
||||
[:span.banner-scope scope-label]
|
||||
[:span.banner-sep "·"]
|
||||
[:span.banner-descriptor descriptor]]
|
||||
[:span.banner-chevron
|
||||
(shui/tabler-icon "chevron-down" {:size 12})]]
|
||||
[:span.banner-edit "Edit"]]
|
||||
[:div.cb-rows
|
||||
{:id "asset-picker-cb-rows"
|
||||
:role "region"
|
||||
:aria-label "Avatar customization options"}
|
||||
[:div.cb-row
|
||||
[:span.cb-label "Shape"]
|
||||
(shui/dropdown-menu
|
||||
|
||||
@@ -94,11 +94,14 @@
|
||||
> .x {
|
||||
@apply flex items-center justify-center;
|
||||
@apply absolute right-2 top-1/2 -translate-y-1/2;
|
||||
@apply w-5 h-5 rounded-full;
|
||||
@apply opacity-60 hover:opacity-100;
|
||||
@apply transition-opacity duration-150;
|
||||
background-color: var(--rx-gray-06);
|
||||
color: var(--rx-gray-11);
|
||||
@apply w-5 h-5 rounded-full cursor-pointer;
|
||||
@apply transition-colors duration-150;
|
||||
background-color: var(--rx-gray-07);
|
||||
color: var(--rx-gray-12);
|
||||
}
|
||||
|
||||
> .x:hover {
|
||||
background-color: var(--rx-gray-08);
|
||||
}
|
||||
|
||||
.ui__input {
|
||||
@@ -873,7 +876,7 @@
|
||||
|
||||
/* Search input matching icon picker */
|
||||
.asset-picker-search {
|
||||
@apply py-1;
|
||||
@apply py-1 pr-3;
|
||||
@apply flex-shrink-0;
|
||||
background-color: var(--lx-gray-03);
|
||||
/* Grey line at bottom of header section, matching icon picker */
|
||||
@@ -891,9 +894,41 @@
|
||||
@apply focus:bg-gray-03 !h-8;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Rounded-circle clear button — mirrors the icon-search affordance
|
||||
(icon.css:94-104). Higher contrast than the icon-search variant
|
||||
(gray-07 → gray-08 on hover, gray-12 glyph) because it sits on
|
||||
the asset-picker's slightly lighter input background. */
|
||||
> .x {
|
||||
@apply flex items-center justify-center;
|
||||
@apply absolute right-2 top-1/2 -translate-y-1/2;
|
||||
@apply w-5 h-5 rounded-full cursor-pointer;
|
||||
@apply transition-colors duration-150;
|
||||
background-color: var(--rx-gray-07);
|
||||
color: var(--rx-gray-12);
|
||||
}
|
||||
|
||||
> .x:hover {
|
||||
background-color: var(--rx-gray-08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* `:type "search"` (added for semantics + mobile keyboard hint) makes
|
||||
WebKit/Chrome render a native blue cancel-X inside the input. The
|
||||
icon picker already provides its own rounded-circle clear button
|
||||
(`a.x` at icon.cljs:6842 in icon-search; clearing in asset-picker
|
||||
happens via Escape). Hide the native chrome so we have a single,
|
||||
on-brand clear affordance. */
|
||||
.cp__emoji-icon-picker input[type="search"]::-webkit-search-cancel-button,
|
||||
.asset-picker input[type="search"]::-webkit-search-cancel-button,
|
||||
.cp__emoji-icon-picker input[type="search"]::-webkit-search-decoration,
|
||||
.asset-picker input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Grid layout - 5 columns for larger, scannable images */
|
||||
.asset-picker-grid {
|
||||
@apply grid gap-2 px-3 pt-1 pb-3;
|
||||
@@ -1959,9 +1994,14 @@
|
||||
The expanded-state gradient (`::before`) layers on top — its peak
|
||||
opacity is capped at 0.55 so this veil still reads through under
|
||||
the gradient. */
|
||||
background:
|
||||
linear-gradient(rgb(0 0 0 / 0.15), rgb(0 0 0 / 0.15)),
|
||||
var(--lx-gray-02, var(--rx-gray-02));
|
||||
/* Solid theme-token background — no alpha veil. Resting state uses
|
||||
gray-01 (the most-recessed surface in the gray scale, page-bg-ish
|
||||
in both themes) so the banner reads as "tucked into" the popover
|
||||
rather than a competing surface. Hover lifts one step to gray-02
|
||||
via `:has()` below; the transition is on background-color so
|
||||
state changes interpolate cleanly across light + dark themes. */
|
||||
background-color: var(--lx-gray-01, var(--rx-gray-01));
|
||||
transition: background-color 120ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
border-bottom: 1px solid var(--lx-gray-05, var(--rx-gray-05));
|
||||
isolation: isolate;
|
||||
/* Isolate the band's layout from siblings: when the rail expands or
|
||||
@@ -1984,6 +2024,53 @@
|
||||
--avatar-size: 56px;
|
||||
}
|
||||
|
||||
/* Whole-row click target. Invisible overlay sitting above the resting
|
||||
row so the entire avatar-customize-zone surface (edge to edge,
|
||||
including its 18px horizontal padding) is one accessible <button>.
|
||||
Spans only the collapsed row's height; the Reset/Done rail and the
|
||||
expanded controls sit below and are reached without going through
|
||||
this trigger. Mounted only in the collapsed state
|
||||
(the `when-not expanded?` guard in the component), so there's no
|
||||
nested-interactive risk when the chips render below.
|
||||
|
||||
Hover deepens the existing dark veil (zone's base is gray-02 +
|
||||
rgba(0,0,0,0.15)) rather than lifting it — fits the zone's
|
||||
"quietly-recessed banner" design intent. Compatible with both light
|
||||
and dark themes via a theme-portable black overlay. */
|
||||
.cb-row-trigger {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px; /* matches collapsed .cb-content min-height */
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1; /* above the visual content layer for clicks */
|
||||
transition: background-color 120ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.cb-row-trigger:focus-visible {
|
||||
/* Inset outline so the focus ring stays within the zone boundary
|
||||
instead of bleeding past the popover edge. */
|
||||
outline: 2px solid var(--lx-accent-09, var(--rx-accent-09));
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Hover state: lift the zone two steps up the gray scale (gray-01 →
|
||||
gray-03). Solid color, no alpha layer — both themes get a clean
|
||||
shift. Driven by `:has()` on the invisible row-trigger so the
|
||||
hover fires off the actual click target, not off ambient hovers
|
||||
of the rail-wrap or the avatar tile. */
|
||||
.avatar-customize-zone:has(.cb-row-trigger:hover) {
|
||||
background-color: var(--lx-gray-03, var(--rx-gray-03));
|
||||
}
|
||||
|
||||
/* Brighten the Edit label when the row is hovered. */
|
||||
.avatar-customize-zone:has(.cb-row-trigger:hover) .banner-edit {
|
||||
color: var(--lx-gray-12, var(--rx-gray-12));
|
||||
}
|
||||
|
||||
/* Gradient lives on a pseudo-element so opacity can crossfade independently
|
||||
of the wrapper's solid base background. translateZ(0) promotes the layer
|
||||
to its own compositor row so the fade composes on the GPU and doesn't
|
||||
@@ -2045,14 +2132,12 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* Avatar trigger — the only click target that toggles expand/collapse.
|
||||
Resets the native button chrome so the avatar reads as a plain tile.
|
||||
The trigger animates width/height in lock with --avatar-size so the
|
||||
avatar morphs from 18px (compact banner) to 56px (expanded tile)
|
||||
inside `contain: layout`. */
|
||||
/* Avatar tile — decorative preview, no longer interactive. The single
|
||||
click target is `.cb-row-trigger` (invisible overlay) which spans
|
||||
the whole resting row. The tile still morphs width/height in lock
|
||||
with --avatar-size so the avatar grows from 20px (compact banner)
|
||||
to 56px (expanded tile) inside `contain: layout`. */
|
||||
.cb-avatar-trigger {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
@@ -2071,12 +2156,6 @@
|
||||
transition: width 200ms cubic-bezier(0.32, 0.72, 0, 1),
|
||||
height 200ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--lx-accent-09, var(--rx-accent-09));
|
||||
outline-offset: 2px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.preview-avatar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -2152,15 +2231,12 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Compact banner — resting state. 32px-tall row, the *entire row* is a
|
||||
single click target (`<button>`) that toggles the expanded customize
|
||||
panel. Layout: state-as-label on the left (`Default · Letters,
|
||||
circle`), decorative chevron-down on the right (rotates 180° on
|
||||
expand). Avatar tile is a parallel click target with the same toggle
|
||||
behavior — both routes converge on `aria-expanded`. */
|
||||
/* Compact banner — resting state visual layer. 32px-tall row showing
|
||||
state-as-label on the left (`Default · Letters, circle`) and a
|
||||
muted "Edit" affordance on the right. NOT a click target — the
|
||||
single accessible button is `.cb-row-trigger` (overlay) which
|
||||
covers the avatar + this banner together. */
|
||||
.avatar-customize-zone .cb-banner {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -2175,12 +2251,6 @@
|
||||
transition: opacity 80ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.avatar-customize-zone .cb-banner:focus-visible {
|
||||
outline: 2px solid var(--lx-accent-09, var(--rx-accent-09));
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.avatar-customize-zone[data-expanded="true"] .cb-banner {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -2225,20 +2295,21 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Chevron — decorative cue that the row expands. Rotates 180° on
|
||||
expand. Not a separate click target; the parent `<button>` owns the
|
||||
action. */
|
||||
.avatar-customize-zone .cb-banner .banner-chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* "Edit" affordance — right-aligned muted text. Replaces the earlier
|
||||
chevron-down (which falsely implied "popover opens below" or
|
||||
"stays-visible disclosure that rotates"; this row morphs in place
|
||||
and the banner content fades out entirely on expand, so a rotating
|
||||
chevron never lived long enough to read as feedback). The label is
|
||||
muted by default and brightens when the user hovers the row (via
|
||||
the sibling-combinator rule on `.cb-row-trigger:hover`). */
|
||||
.avatar-customize-zone .cb-banner .banner-edit {
|
||||
flex-shrink: 0;
|
||||
color: var(--lx-gray-11, var(--rx-gray-11));
|
||||
transition: transform 200ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.avatar-customize-zone[data-expanded="true"] .cb-banner .banner-chevron {
|
||||
transform: rotate(180deg);
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
color: var(--lx-gray-09, var(--rx-gray-09));
|
||||
transition: color 120ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.avatar-customize-zone .preview-title {
|
||||
|
||||
Reference in New Issue
Block a user