From f8316292e9dac92f1c38a49707deb823ff7a4271 Mon Sep 17 00:00:00 2001 From: scheinriese Date: Sun, 1 Feb 2026 03:42:47 +0100 Subject: [PATCH] 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 --- src/main/frontend/components/icon.cljs | 101 ++++++++++++++++--------- src/main/frontend/components/icon.css | 81 ++++++++++++-------- 2 files changed, 117 insertions(+), 65 deletions(-) diff --git a/src/main/frontend/components/icon.cljs b/src/main/frontend/components/icon.cljs index 04a423cc78..dc8402b9e2 100644 --- a/src/main/frontend/components/icon.cljs +++ b/src/main/frontend/components/icon.cljs @@ -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 diff --git a/src/main/frontend/components/icon.css b/src/main/frontend/components/icon.css index 52e42e1620..dc19e145fb 100644 --- a/src/main/frontend/components/icon.css +++ b/src/main/frontend/components/icon.css @@ -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;