mirror of
https://github.com/logseq/logseq.git
synced 2026-05-16 08:52:20 +00:00
feat: replace asset-picker action bar with footer hint
The floating URL+Upload buttons are the last UI in the picker still
pushing users toward an explicit URL-name dialog; paste and Web Image
flows already auto-derive names. Replace them with a thin static
footer ("Tip: Drop an image, paste a link, or browse · ⌘V"), delete
the url-asset-pane popover entirely, and fix URL-paste naming to use
extract-filename-from-url instead of clipboard-{timestamp}. Phones
get a parallel hint where each verb is a real control (iOS Safari
won't deliver paste events to non-input popover roots).
Also: title+caption styling for the Wikipedia tooltip to match the
color-picker contrast tooltip, and remove the 56px grid bottom
padding that previously cleared the floating bar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -934,14 +934,6 @@
|
||||
"Maximum allowed size for URL assets (10MB)"
|
||||
(* 10 1024 1024))
|
||||
|
||||
(defn- valid-url?
|
||||
"Check if string is a valid HTTP/HTTPS URL"
|
||||
[url]
|
||||
(and (string? url)
|
||||
(not (string/blank? url))
|
||||
(or (string/starts-with? url "http://")
|
||||
(string/starts-with? url "https://"))))
|
||||
|
||||
(defn- valid-image-content-type?
|
||||
"Check if content-type header indicates an image"
|
||||
[content-type]
|
||||
@@ -1091,37 +1083,6 @@
|
||||
nil)))))
|
||||
(p/resolved nil)))))))))))
|
||||
|
||||
(declare <download-url-asset-via-ipc)
|
||||
|
||||
(defn- <validate-url-asset
|
||||
"Validate URL. Returns promise with {:content-type :size :url} or rejects."
|
||||
[url]
|
||||
(if (util/electron?)
|
||||
;; Electron: we can't get just headers via :httpRequest, so do a full GET
|
||||
;; (cached by the renderer anyway) and synthesize from sniff results.
|
||||
(-> (<download-url-asset-via-ipc url)
|
||||
(p/then (fn [{:keys [content-type size]}]
|
||||
{:content-type content-type :size size :url url})))
|
||||
;; Browser: keep current HEAD implementation.
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(-> (js/fetch url #js {:method "HEAD"
|
||||
:mode "cors"
|
||||
:credentials "omit"})
|
||||
(.then (fn [^js response]
|
||||
(if (.-ok response)
|
||||
(let [content-type (.get (.-headers response) "content-type")
|
||||
content-length (.get (.-headers response) "content-length")
|
||||
size (when content-length (js/parseInt content-length 10))]
|
||||
(resolve {:content-type content-type
|
||||
:size size
|
||||
:url url}))
|
||||
(reject (ex-info "Failed to fetch URL"
|
||||
{:kind :http :status (.-status response)})))))
|
||||
(.catch (fn [err]
|
||||
(reject (ex-info "Network error"
|
||||
{:kind :network :error (.-message err)})))))))))
|
||||
|
||||
(defn- <download-url-asset-via-fetch
|
||||
"Browser path: uses js/fetch. Subject to CORS."
|
||||
[url]
|
||||
@@ -2507,7 +2468,9 @@
|
||||
(shui/tabler-icon "info-circle" {:size 14})])
|
||||
(shui/tooltip-content
|
||||
{:side "top" :show-arrow true}
|
||||
[:span "Images from Wikipedia Commons. Check licensing before commercial use."])))]
|
||||
[:div
|
||||
[:div.text-sm.font-medium "Images from Wikipedia Commons"]
|
||||
[:div.text-xs.opacity-70.mt-1 "Check licensing before commercial use."]])))]
|
||||
|
||||
;; Image grid (or inline network-error message)
|
||||
(when web-expanded?
|
||||
@@ -2551,7 +2514,7 @@
|
||||
web-id)))]))])))
|
||||
|
||||
;; ============================================================================
|
||||
;; URL Asset Pane (Popover content for "Add asset via URL")
|
||||
;; URL Asset Save Error Copy
|
||||
;; ============================================================================
|
||||
|
||||
(defn- url-save-error-copy
|
||||
@@ -2596,122 +2559,6 @@
|
||||
;; Default — preserve ex-message for unclassified errors
|
||||
(or (ex-message err) "Failed to download image."))))
|
||||
|
||||
(rum/defcs url-asset-pane < rum/reactive
|
||||
(rum/local "" ::url)
|
||||
(rum/local "" ::name)
|
||||
(rum/local nil ::error)
|
||||
(rum/local false ::loading?)
|
||||
(rum/local nil ::validated?)
|
||||
[state {:keys [on-close on-asset-added]}]
|
||||
(let [*url (::url state)
|
||||
*name (::name state)
|
||||
*error (::error state)
|
||||
*loading? (::loading? state)
|
||||
*validated? (::validated? state)
|
||||
url @*url
|
||||
asset-name @*name
|
||||
error @*error
|
||||
loading? @*loading?
|
||||
validated? @*validated?
|
||||
url-valid? (valid-url? url)
|
||||
can-save? (and url-valid?
|
||||
(not (string/blank? asset-name))
|
||||
(not loading?))
|
||||
|
||||
;; Validate URL on blur
|
||||
validate-url!
|
||||
(fn []
|
||||
(when (valid-url? url)
|
||||
(reset! *loading? true)
|
||||
(reset! *error nil)
|
||||
(-> (<validate-url-asset url)
|
||||
(p/then (fn [{:keys [content-type size]}]
|
||||
(cond
|
||||
(not (valid-image-content-type? content-type))
|
||||
(reset! *error "URL does not point to a supported image format")
|
||||
|
||||
(and size (> size max-url-asset-size))
|
||||
(reset! *error (str "Image exceeds " (/ max-url-asset-size 1024 1024) "MB size limit"))
|
||||
|
||||
:else
|
||||
(do
|
||||
(reset! *validated? true)
|
||||
;; Auto-extract filename if empty
|
||||
(when (string/blank? @*name)
|
||||
(reset! *name (extract-filename-from-url url)))))))
|
||||
(p/catch (fn [err] (reset! *error (url-save-error-copy err))))
|
||||
(p/finally #(reset! *loading? false)))))
|
||||
|
||||
;; Save handler
|
||||
handle-save!
|
||||
(fn []
|
||||
(reset! *loading? true)
|
||||
(reset! *error nil)
|
||||
(let [repo (state/get-current-repo)]
|
||||
(-> (<save-url-asset! repo url asset-name)
|
||||
(p/then (fn [asset-entity]
|
||||
(when asset-entity
|
||||
(on-asset-added asset-entity))
|
||||
(on-close)))
|
||||
(p/catch (fn [err]
|
||||
(let [copy (url-save-error-copy err)]
|
||||
(reset! *error nil)
|
||||
(shui/toast! copy :error))))
|
||||
(p/finally #(reset! *loading? false)))))]
|
||||
|
||||
[:div.url-asset-pane
|
||||
;; URL input
|
||||
[:div.form-group
|
||||
[:label "URL"]
|
||||
(shui/input
|
||||
{:placeholder "https://example.com/image.png"
|
||||
:value url
|
||||
:auto-focus true
|
||||
:on-change (fn [e]
|
||||
(reset! *url (util/evalue e))
|
||||
(reset! *validated? false)
|
||||
(reset! *error nil))
|
||||
:on-blur validate-url!
|
||||
:on-key-down (fn [^js e]
|
||||
(when (= 13 (.-keyCode e))
|
||||
(validate-url!)))})]
|
||||
|
||||
;; Name input
|
||||
[:div.form-group
|
||||
[:label "Name"]
|
||||
(shui/input
|
||||
{:placeholder "image"
|
||||
:value asset-name
|
||||
:on-change #(reset! *name (util/evalue %))})]
|
||||
|
||||
;; Format note
|
||||
[:div.format-note
|
||||
"Supported: PNG, JPG, GIF, WebP, SVG, BMP"
|
||||
[:br]
|
||||
"Max size: 10MB"]
|
||||
|
||||
;; Error display
|
||||
(when error
|
||||
[:div.error-message error])
|
||||
|
||||
;; Action buttons
|
||||
[:div.pane-footer
|
||||
(shui/button
|
||||
{:variant :outline
|
||||
:size :sm
|
||||
:on-click on-close}
|
||||
"Cancel")
|
||||
(shui/button
|
||||
{:variant :default
|
||||
:size :sm
|
||||
:disabled (not can-save?)
|
||||
:on-click handle-save!}
|
||||
(if loading?
|
||||
[:span.flex.items-center.gap-1
|
||||
[:span.animate-spin (shui/tabler-icon "loader-2" {:size 14})]
|
||||
"Saving..."]
|
||||
"Save"))]]))
|
||||
|
||||
;; ============================================================================
|
||||
;; Multi-File Upload Preview
|
||||
;; ============================================================================
|
||||
@@ -2819,7 +2666,6 @@
|
||||
(rum/local true ::loading?) ;; Start with loading state
|
||||
(rum/local nil ::loaded-assets) ;; Cached assets loaded async
|
||||
(rum/local nil ::web-query-debounced) ;; Debounced web search query
|
||||
(rum/local false ::popover-open?) ;; Track if any popover is open
|
||||
(rum/local :avatar ::mode) ;; :avatar | :image — live tab state, seeded in :will-mount
|
||||
(rum/local nil ::paste-handler) ;; Holds latest clipboard-paste closure for the DOM listener
|
||||
;; Keyboard-nav state, parallels the icon-picker's model.
|
||||
@@ -2919,7 +2765,6 @@
|
||||
*loading? (::loading? state)
|
||||
*loaded-assets (::loaded-assets state)
|
||||
*web-query-debounced (::web-query-debounced state)
|
||||
*popover-open? (::popover-open? state)
|
||||
;; Keyboard-nav state
|
||||
*focus-region (::focus-region state)
|
||||
*highlighted-index (::highlighted-index state)
|
||||
@@ -2929,7 +2774,6 @@
|
||||
web-images (rum/react *web-images-result)
|
||||
highlighted-idx (rum/react *highlighted-index)
|
||||
loading? (rum/react *loading?)
|
||||
popover-open? (rum/react *popover-open?)
|
||||
;; Use cached assets if available, otherwise try to get them
|
||||
assets (or (rum/react *loaded-assets) [])
|
||||
search-q @*search-q
|
||||
@@ -3227,24 +3071,6 @@
|
||||
:label (or (:block/title asset-entity) "")
|
||||
:data image-data}))))
|
||||
|
||||
;; Open the URL-paste popover anchored to the clicked element.
|
||||
open-url-pane!
|
||||
(fn [^js e]
|
||||
(reset! *popover-open? true)
|
||||
(shui/popup-show!
|
||||
(.-target e)
|
||||
(fn [{:keys [id]}]
|
||||
(url-asset-pane
|
||||
{:on-close (fn []
|
||||
(reset! *popover-open? false)
|
||||
(shui/popup-hide! id))
|
||||
:on-asset-added on-url-asset-entity-added}))
|
||||
{:align :end
|
||||
:side "top"
|
||||
:content-props {:class "url-asset-pane-popup"
|
||||
:sideOffset 8}
|
||||
:on-after-hide (fn [] (reset! *popover-open? false))}))
|
||||
|
||||
;; Read the system clipboard and route to upload / URL-save / toast.
|
||||
handle-clipboard-paste
|
||||
(fn self
|
||||
@@ -3261,7 +3087,7 @@
|
||||
:url
|
||||
(let [repo (state/get-current-repo)
|
||||
url (:url result)
|
||||
asset-name (str "clipboard-" (.now js/Date))]
|
||||
asset-name (extract-filename-from-url url)]
|
||||
(-> (<save-url-asset! repo url asset-name)
|
||||
(p/then (fn [asset-entity]
|
||||
(when asset-entity
|
||||
@@ -3364,11 +3190,6 @@
|
||||
;; only) and the trash button. Bundling them under one grid slot
|
||||
;; keeps the topbar's three-column layout (back / segment / actions)
|
||||
;; intact when the color trigger appears or disappears.
|
||||
;; Class name is intentionally NOT `.asset-picker-actions` — that
|
||||
;; class is already used for the floating bottom action bar
|
||||
;; ("Add image via URL" / "Upload image", icon.css:1000) which
|
||||
;; sets `position: absolute; bottom: 0`. Reusing it here would
|
||||
;; punt the topbar group off-screen.
|
||||
[:div.asset-picker-topbar-actions
|
||||
;; Color trigger — Avatar mode only. Mirrors the icon-picker's
|
||||
;; topbar trigger (same component, same `*color` atom) so backing
|
||||
@@ -3686,10 +3507,9 @@
|
||||
[:div.row-subtitle "Browse or drop a file in"]]
|
||||
[:div.row-chevron (shui/tabler-icon "chevron-right" {:size 16})]]]))])]])
|
||||
|
||||
;; Hidden file input lives at the top level so both the floating Upload
|
||||
;; button and the empty-state "Upload from computer" row reference it via
|
||||
;; <label for="asset-upload-input">, regardless of whether the floating
|
||||
;; actions bar is rendered.
|
||||
;; Hidden file input lives at the top level so both the empty-state
|
||||
;; "Add from your computer" row and the footer-hint "browse" link can
|
||||
;; reference it via <label for="asset-upload-input">.
|
||||
[:input#asset-upload-input.hidden
|
||||
{:type "file"
|
||||
:accept "image/*"
|
||||
@@ -3698,40 +3518,43 @@
|
||||
(let [files (array-seq (.-files (.-target e)))]
|
||||
(handle-upload files)))}]
|
||||
|
||||
;; Action buttons (floating at bottom) - only when we have assets or are
|
||||
;; loading. Zero-state replaces this bar with the empty-state rows above.
|
||||
;; Footer hint — only when we have assets or are loading. Zero-state
|
||||
;; replaces this bar with the empty-state rows above.
|
||||
(when (or loading? (seq @*loaded-assets))
|
||||
[:div.asset-picker-actions
|
||||
(shui/button
|
||||
{:variant :outline
|
||||
:size :sm
|
||||
:on-click (fn [^js e] (open-url-pane! e))}
|
||||
(shui/tabler-icon "link" {:size 16})
|
||||
[:span "Add image via URL"])
|
||||
(shui/button
|
||||
{:variant (if popover-open? :secondary :default)
|
||||
:size :sm
|
||||
:as-child true}
|
||||
[:label {:for "asset-upload-input"}
|
||||
[:span "Upload image"]])
|
||||
|
||||
;; Mobile camera button
|
||||
(when (util/mobile?)
|
||||
(shui/button
|
||||
{:variant :secondary
|
||||
:size :sm
|
||||
:as-child true}
|
||||
[:label
|
||||
[:input.hidden
|
||||
{:type "file"
|
||||
:accept "image/*"
|
||||
:capture "environment"
|
||||
:on-change (fn [e]
|
||||
(let [files (array-seq (.-files (.-target e)))]
|
||||
(handle-upload files)))}]
|
||||
[:div.flex.items-center.gap-2
|
||||
(shui/tabler-icon "camera" {:size 16})
|
||||
[:span "Take photo"]]]))])]))
|
||||
[:div.asset-picker-footer-hint
|
||||
[:span.tip-label "Tip:"]
|
||||
[:span.tip-body
|
||||
(if (util/mobile?)
|
||||
;; Phone: every verb is a real control. iOS Safari won't deliver
|
||||
;; paste events to the popover root reliably, so paste-a-link is
|
||||
;; a button that calls handle-clipboard-paste directly (mirrors
|
||||
;; the empty-state clipboard-row).
|
||||
[:<>
|
||||
[:button.tip-link
|
||||
{:type "button"
|
||||
:on-click (fn [_] (handle-clipboard-paste))}
|
||||
"Paste a link"]
|
||||
", "
|
||||
[:label.tip-link {:for "asset-upload-input" :tab-index 0} "browse"]
|
||||
", or "
|
||||
[:label.tip-link {:tab-index 0}
|
||||
[:input.hidden
|
||||
{:type "file"
|
||||
:accept "image/*"
|
||||
:capture "environment"
|
||||
:on-change (fn [e]
|
||||
(let [files (array-seq (.. e -target -files))]
|
||||
(handle-upload files)))}]
|
||||
"take a picture"]]
|
||||
;; Desktop / iPad: passive hint. Drop + paste rely on the
|
||||
;; existing global handlers attached to the picker root.
|
||||
[:<>
|
||||
"Drop an image, paste a link, or "
|
||||
[:label.tip-link {:for "asset-upload-input" :tab-index 0} "browse"]
|
||||
" "
|
||||
[:span.tip-sep "·"]
|
||||
" "
|
||||
(shui/shortcut "mod+v" {:style :combo})])]])]))
|
||||
|
||||
(defn open-image-asset-picker!
|
||||
"Opens the asset picker popup for selecting an image icon.
|
||||
|
||||
@@ -823,12 +823,7 @@
|
||||
/* Right-side group: holds the (avatar-mode-only) color trigger and the
|
||||
trash button. Flex-row aligns them on a single baseline; the gap
|
||||
mirrors the icon-picker's `.tab-actions gap-1` so the two topbars
|
||||
read as the same chrome at different drill levels.
|
||||
|
||||
Distinct class from `.asset-picker-actions` (the floating bottom bar
|
||||
in this same picker). Sharing the class clobbered both — the bottom
|
||||
bar's `position: absolute; bottom: 0;` flung this topbar group off
|
||||
to the picker's footer. */
|
||||
read as the same chrome at different drill levels. */
|
||||
@apply flex items-center gap-1;
|
||||
|
||||
/* Delete button - matching icon picker styling */
|
||||
@@ -906,15 +901,12 @@
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
min-height: 120px;
|
||||
align-content: start;
|
||||
/* Add padding at bottom for floating action buttons */
|
||||
padding-bottom: 56px;
|
||||
|
||||
/* Recently used row - single row, no extra padding */
|
||||
&.recently-used-row,
|
||||
&.web-images-row {
|
||||
@apply flex-none overflow-visible;
|
||||
min-height: auto;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,37 +993,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Action buttons - floating at bottom with fade effect */
|
||||
.asset-picker-actions {
|
||||
@apply flex items-center justify-end gap-2;
|
||||
@apply absolute bottom-0 left-0 right-0;
|
||||
@apply py-3 pr-3;
|
||||
@apply transition-opacity duration-200;
|
||||
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%
|
||||
);
|
||||
/* Footer hint - thin static row at bottom when assets exist.
|
||||
Mirrors the cmdk dropdown's .hints pattern: bg-gray-03 with a
|
||||
border-top divider, "Tip:" label in font-medium gray-12, prose
|
||||
in gray-11, inline links in accent-11. */
|
||||
.asset-picker-footer-hint {
|
||||
@apply flex w-full items-center px-3 py-2 gap-1.5 text-sm;
|
||||
border-top: 1px solid var(--lx-gray-05);
|
||||
background-color: var(--lx-gray-03);
|
||||
|
||||
/* Style shui buttons inside action bar */
|
||||
.ui__button {
|
||||
@apply gap-1.5 transition-colors duration-150;
|
||||
.tip-label {
|
||||
@apply font-medium;
|
||||
color: var(--lx-gray-12);
|
||||
}
|
||||
|
||||
/* Make label inside upload button flex */
|
||||
> label {
|
||||
@apply flex items-center gap-1.5 cursor-pointer;
|
||||
}
|
||||
.tip-body {
|
||||
@apply flex flex-row flex-wrap items-center gap-1;
|
||||
color: var(--lx-gray-11);
|
||||
}
|
||||
|
||||
/* Override button variant transparency */
|
||||
&.as-secondary {
|
||||
@apply bg-secondary;
|
||||
}
|
||||
.tip-link,
|
||||
button.tip-link {
|
||||
cursor: pointer;
|
||||
color: var(--lx-accent-11);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
&.as-classic {
|
||||
@apply bg-primary;
|
||||
}
|
||||
.tip-link:hover,
|
||||
button.tip-link:hover {
|
||||
color: var(--lx-accent-12);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tip-link:focus-visible {
|
||||
outline: 2px solid var(--lx-accent-09);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tip-sep {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,45 +1131,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* URL Asset Pane - popover for adding images from URL */
|
||||
.url-asset-pane-popup {
|
||||
@apply overflow-visible;
|
||||
}
|
||||
|
||||
.url-asset-pane {
|
||||
@apply flex flex-col gap-3 p-3;
|
||||
width: 320px;
|
||||
background-color: var(--lx-gray-02);
|
||||
|
||||
.form-group {
|
||||
@apply flex flex-col gap-1;
|
||||
|
||||
label {
|
||||
@apply text-xs font-medium;
|
||||
color: var(--rx-gray-11);
|
||||
}
|
||||
|
||||
.ui__input {
|
||||
@apply text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.format-note {
|
||||
@apply text-xs leading-relaxed;
|
||||
color: var(--rx-gray-09);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply text-xs px-2 py-1.5 rounded;
|
||||
color: var(--rx-red-11);
|
||||
background-color: var(--rx-red-03);
|
||||
}
|
||||
|
||||
.pane-footer {
|
||||
@apply flex justify-end gap-2 pt-1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Text Picker (Level 2 view) - matches asset-picker structure
|
||||
============================================================================ */
|
||||
|
||||
Reference in New Issue
Block a user