Add delete button and Selected section to asset picker

- Add delete button in topbar (matching icon-picker styling with red hover)
- Add "Selected" section showing currently chosen image with blue outline
- Add :simple? option to section-header to hide count/chevron
- Floating action buttons now use gradient fade instead of solid background
- Use object-fit: cover for avatar mode, contain for regular images
- Symmetric 8px padding in pane-section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
scheinriese
2026-02-01 03:42:47 +01:00
parent 0e139db6e2
commit f8316292e9
2 changed files with 117 additions and 65 deletions

View File

@@ -732,19 +732,21 @@
(defonce *section-states (atom {}))
(rum/defc section-header
[{:keys [title count total-count expanded? keyboard-hint on-toggle input-focused?]}]
[{:keys [title count total-count expanded? keyboard-hint on-toggle input-focused? simple?]}]
[:div.section-header.text-xs.py-1.5.px-3.flex.justify-between.items-center.gap-2.bg-gray-02.h-8
{:style {:color "var(--lx-gray-11)"}}
;; Left: Title · total-count · Chevron
[:div.flex.items-center.gap-1.cursor-pointer.select-none
{:on-click on-toggle}
;; Left: Title · total-count · Chevron (chevron and count hidden in simple mode)
[:div.flex.items-center.gap-1.select-none
(when-not simple? {:class "cursor-pointer"
:on-click on-toggle})
[:span.font-bold title]
(when (or total-count count)
(when (and (not simple?) (or total-count count))
[:<>
[:span "·"]
[:span {:style {:font-size "0.7rem"}}
(or total-count count)]])
(ui/icon (if expanded? "chevron-down" "chevron-right") {:size 14})]
(when-not simple?
(ui/icon (if expanded? "chevron-down" "chevron-right") {:size 14}))]
[:div.flex-1] ; Spacer
@@ -1041,7 +1043,7 @@
"Renders a single image asset thumbnail in the asset picker grid.
When avatar-context is provided, renders circular previews and returns avatar data.
Returns nil if asset file doesn't exist (ghost asset)."
[state asset {:keys [on-chosen avatar-context]}]
[state asset {:keys [on-chosen avatar-context selected?]}]
(let [url @(::url state)
error? @(::error state)
asset-type (:logseq.property.asset/type asset)
@@ -1052,7 +1054,8 @@
(when-not error?
[:button.image-asset-item
{:title asset-title
:class (when avatar-mode? "avatar-mode")
:class (util/classnames [{:avatar-mode avatar-mode?
:selected selected?}])
:on-click (fn [e]
(let [image-data {:asset-uuid (str asset-uuid)
:asset-type asset-type}]
@@ -1094,7 +1097,7 @@
(p/catch (fn [_err]
(reset! *loading? false))))))
state)}
[state {:keys [on-chosen on-back avatar-context]}]
[state {:keys [on-chosen on-back on-delete del-btn? current-icon avatar-context]}]
(let [*search-q (::search-q state)
*loading? (::loading? state)
*loaded-assets (::loaded-assets state)
@@ -1102,6 +1105,14 @@
;; Use cached assets if available, otherwise try to get them
assets (or (rum/react *loaded-assets) [])
search-q @*search-q
;; Extract current image UUID from the icon (works for both :image and :avatar with image)
current-asset-uuid (or (get-in current-icon [:data :asset-uuid])
(when (= :image (:type current-icon))
(get-in current-icon [:data :asset-uuid])))
;; Find the current asset from the list
current-asset (when current-asset-uuid
(some #(when (= (str (:block/uuid %)) current-asset-uuid) %)
assets))
;; Filter assets by search query
filtered-assets (if (string/blank? search-q)
assets
@@ -1150,7 +1161,12 @@
[:button.back-button
{:on-click on-back}
(shui/tabler-icon "chevron-left" {:size 16})
[:span "Back"]]]
[:span "Back"]]
;; Delete button (aligned to right)
(when del-btn?
(shui/button {:variant :outline :size :sm :data-action "del"
:on-click on-delete}
(shui/tabler-icon "trash" {:size 17})))]
(shui/separator {:class "my-0 opacity-50"})
[:div.asset-picker-search
[:div.search-input
@@ -1161,33 +1177,47 @@
:auto-focus true
:on-change #(reset! *search-q (util/evalue %))})]]]
;; Section header (matching icon picker style)
[:div.asset-picker-section-header
[:div.section-title
[:span.font-bold "Images"]
[:span.font-medium (str " · " asset-count)]]]
;; Body - scrollable content area with top/bottom margin
[:div.bd.bd-scroll
;; "Current" section - shows currently selected image (only when not searching)
(when (and current-asset (string/blank? search-q))
[:div.pane-section
(section-header {:title "Selected"
:simple? true})
[:div.asset-picker-current
{:class (when avatar-mode? "avatar-mode")}
(image-asset-item current-asset {:on-chosen on-chosen
:avatar-context avatar-context
:selected? true})]])
;; Asset grid
[:div.asset-picker-grid
{:class (when avatar-mode? "avatar-mode")}
(cond
loading?
[:div.flex.flex-col.items-center.justify-center.h-32.text-gray-08
[:div.animate-spin (shui/tabler-icon "loader-2" {:size 32})]
[:span.text-sm.mt-2 "Loading assets..."]]
;; "Images" section
[:div.pane-section
(section-header {:title "Images"
:count asset-count
:expanded? true})
(seq filtered-assets)
(for [asset filtered-assets]
(rum/with-key
(image-asset-item asset {:on-chosen on-chosen
:avatar-context avatar-context})
(str (:block/uuid asset))))
;; Asset grid
[:div.asset-picker-grid
{:class (when avatar-mode? "avatar-mode")}
(cond
loading?
[:div.flex.flex-col.items-center.justify-center.h-32.text-gray-08
[:div.animate-spin (shui/tabler-icon "loader-2" {:size 32})]
[:span.text-sm.mt-2 "Loading assets..."]]
:else
[:div.flex.flex-col.items-center.justify-center.h-32.text-gray-08
(shui/tabler-icon "photo-off" {:size 32})
[:span.text-sm.mt-2 "No image assets found"]
[:span.text-xs.mt-1 "Upload an image to get started"]])]
(seq filtered-assets)
(for [asset filtered-assets]
(rum/with-key
(image-asset-item asset {:on-chosen on-chosen
:avatar-context avatar-context
:selected? (= (str (:block/uuid asset)) current-asset-uuid)})
(str (:block/uuid asset))))
:else
[:div.flex.flex-col.items-center.justify-center.h-32.text-gray-08
(shui/tabler-icon "photo-off" {:size 32})
[:span.text-sm.mt-2 "No image assets found"]
[:span.text-xs.mt-1 "Upload an image to get started"]])]]]
;; Action buttons (floating at bottom)
[:div.asset-picker-actions
@@ -1456,6 +1486,9 @@
((:on-chosen opts) e icon-data)
(reset! *view :icon-picker))
:on-back #(reset! *view :icon-picker)
:on-delete #(on-chosen nil)
:del-btn? del-btn?
:current-icon normalized-icon-value
:avatar-context (when (= :avatar (:type normalized-icon-value))
normalized-icon-value)})
;; Level 1: Icon Picker view

View File

@@ -99,7 +99,7 @@
}
.pane-section {
@apply pl-2 overflow-y-auto h-full;
@apply px-2 overflow-y-auto h-full;
color: var(--ls-color-icon-preset);
@@ -271,6 +271,16 @@
min-height: 320px;
max-height: 440px;
background-color: var(--lx-gray-02);
/* Body content - 4px padding top/bottom */
> .bd {
@apply py-1;
}
/* Sections - 8px padding on both sides (symmetric) */
.pane-section {
@apply px-2;
}
}
/* Topbar wrapper for easier inspection */
@@ -278,9 +288,9 @@
@apply flex-shrink-0;
}
/* Back button as link-style button */
/* Back button row with delete button on right */
.asset-picker-back {
@apply flex-shrink-0;
@apply flex-shrink-0 flex items-center justify-between;
/* py-2 + content height matches icon-picker topbar */
padding-top: 8px;
padding-bottom: 7px;
@@ -301,6 +311,12 @@
color: var(--rx-gray-12);
}
}
/* Delete button - matching icon picker styling */
.ui__button[data-action=del] {
@apply !w-6 !h-6 overflow-hidden rounded-md opacity-60;
@apply hover:text-red-rx-09 hover:opacity-90;
}
}
/* Search input matching icon picker */
@@ -326,26 +342,6 @@
}
}
/* Section header matching icon picker */
.asset-picker-section-header {
@apply flex items-center justify-between px-4 py-1;
@apply text-[10px] tracking-tight;
@apply flex-shrink-0;
color: var(--rx-gray-11);
.section-title {
@apply flex items-center gap-0.5;
}
.section-actions {
@apply flex items-center gap-2;
.shortcut {
@apply text-xs;
}
}
}
/* Grid layout - 5 columns for larger, scannable images */
.asset-picker-grid {
@apply grid gap-2 p-3;
@@ -381,7 +377,7 @@
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
border-radius: 4px;
}
@@ -399,22 +395,45 @@
img {
@apply rounded-full;
object-fit: cover;
}
> div {
@apply rounded-full;
}
}
/* Selected state - blue outline */
&.selected {
border-color: var(--rx-blue-09);
box-shadow: 0 0 0 1px var(--rx-blue-09);
}
}
/* Action buttons - floating at bottom */
/* Current selection section - single item display */
.asset-picker-current {
@apply px-3 py-2;
@apply flex-shrink-0;
.image-asset-item {
width: 64px;
height: 64px;
padding-bottom: 0; /* Override aspect ratio hack for fixed size */
}
}
/* Action buttons - floating at bottom with fade effect */
.asset-picker-actions {
@apply flex items-center justify-center gap-2 px-3 py-2;
@apply absolute bottom-2 left-1/2 -translate-x-1/2;
@apply rounded-lg;
background-color: var(--lx-gray-03);
border: 1px solid var(--rx-gray-06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
@apply flex items-center justify-end gap-2;
@apply absolute bottom-0 left-0 right-0;
@apply py-3 pr-3;
background: linear-gradient(
to bottom,
transparent 0%,
color-mix(in srgb, var(--lx-gray-03) 30%, transparent) 30%,
color-mix(in srgb, var(--lx-gray-03) 70%, transparent) 60%,
var(--lx-gray-03) 100%
);
.secondary-button {
@apply flex items-center gap-1.5 px-2.5 py-1.5 rounded-md;