mirror of
https://github.com/logseq/logseq.git
synced 2026-06-01 19:01:22 +00:00
feat: legible avatar fallbacks + class default-icon row
Avatar fallback rendering: replace the prior 31.4%-alpha background treatment, which silently rendered dark picks (#1a3d60 etc.) at ~1.1:1 contrast on dark surfaces. The new pipeline computes a hue-preserving muted tint for the bg via OKLCh L bisection (~1.5:1 vs page surface) and runs the picked color through adjust-for-contrast (3.0 target) for the initials. Picked color passes through as text whenever it reads; only genuinely-illegible picks get lifted, and the lift stays close to the picked hue. Works for both custom hex picks and Radix theme tokens (var(--rx-...)) via a new colors/->hex CSS-color resolver. The 3.0 target — instead of WCAG 4.5 body-text — treats avatar initials as decorative identifiers (matches Slack/Linear/GitHub practice) and lets vivid hues like tomato and red pass through without desaturating. Class default-icon row: new property row that uses the unified icon picker in default-icon mode, where Text/Image tiles commit immediately rather than drilling into sub-pickers (per-instance inheritance derives the actual face from each instance's title). Setting a class's page icon now auto-syncs into default-icon when default-icon is empty so instances inherit it; clearing the page icon only clears default-icon if the two were synced. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,24 @@
|
||||
(pick js/document.documentElement))
|
||||
parse-css-color->hex)))
|
||||
|
||||
(defn ->hex
|
||||
"Resolve any inline CSS color string to its current-theme hex value:
|
||||
- hex strings pass through unchanged
|
||||
- `var(--name)` references are looked up via the cascade on body
|
||||
(so theme tokens like Radix `--rx-orange-10` resolve to whichever
|
||||
light/dark hex is active right now)
|
||||
- rgb(...)/hsl(...) literals are parsed
|
||||
Returns nil for blank/unresolvable input."
|
||||
[css-color]
|
||||
(when (string? css-color)
|
||||
(let [v (string/trim css-color)]
|
||||
(cond
|
||||
(string/blank? v) nil
|
||||
(string/starts-with? v "#") v
|
||||
(string/starts-with? v "var(")
|
||||
(some-> (re-find #"var\(\s*(--[\w-]+)" v) second read-bg-var)
|
||||
:else (parse-css-color->hex v)))))
|
||||
|
||||
(defn get-accent-color
|
||||
[]
|
||||
(when-let [color (some-> js/document.documentElement
|
||||
@@ -301,6 +319,77 @@
|
||||
adjust-for-contrast
|
||||
(bounded-memoize adjust-for-contrast* 256))
|
||||
|
||||
;; Chroma envelope for muted tints. Picked hue is preserved; chroma is
|
||||
;; clamped low so the tint reads as "atmospheric hint of hue" rather than
|
||||
;; a saturated chip. Floor keeps near-grayscale picks from collapsing to
|
||||
;; pure gray (so a black/white pick still tints the badge differently
|
||||
;; than an unset one).
|
||||
(def ^:private muted-tint-max-chroma 0.05)
|
||||
(def ^:private muted-tint-min-chroma 0.02)
|
||||
|
||||
(defn- muted-tint*
|
||||
[picked-hex surface-hex target-vs-surface]
|
||||
(let [picked-rgb (hex->rgb picked-hex)
|
||||
surface-rgb (hex->rgb surface-hex)]
|
||||
(if (or (nil? picked-rgb) (nil? surface-rgb))
|
||||
picked-hex
|
||||
(let [[_ pC pH] (oklab->oklch (rgb->oklab picked-rgb))
|
||||
;; Near-grayscale picks (white, black, neutrals) carry no
|
||||
;; meaningful hue — atan2(0,0) returns 0 (red-ish), which
|
||||
;; would otherwise paint every gray pick the same warm tint.
|
||||
;; Skip the chroma floor in that case so the bg stays
|
||||
;; neutral. Threshold is generous; anything visibly chromatic
|
||||
;; sits well above 0.01.
|
||||
achromatic? (< pC 0.01)
|
||||
chroma (if achromatic?
|
||||
0.0
|
||||
(-> pC
|
||||
(min muted-tint-max-chroma)
|
||||
(max muted-tint-min-chroma)))
|
||||
surface-dark? (< (relative-luminance surface-rgb) 0.5)
|
||||
target-L (if surface-dark? 1.0 0.0)
|
||||
[surface-L _ _] (oklab->oklch (rgb->oklab surface-rgb))]
|
||||
(loop [lo (min surface-L target-L)
|
||||
hi (max surface-L target-L)
|
||||
best-hex picked-hex
|
||||
i 0]
|
||||
(if (>= i 12)
|
||||
best-hex
|
||||
(let [mid (/ (+ lo hi) 2)
|
||||
candidate-hex (oklch->hex [mid chroma pH])
|
||||
candidate-rgb (hex->rgb candidate-hex)
|
||||
ratio (if candidate-rgb
|
||||
(contrast-ratio candidate-rgb surface-rgb)
|
||||
1.0)]
|
||||
(cond
|
||||
(>= ratio target-vs-surface)
|
||||
;; meets target — pull L back toward surface so the tint
|
||||
;; stays as subtle as it can while still being visible
|
||||
(let [[new-lo new-hi]
|
||||
(if surface-dark?
|
||||
[lo mid]
|
||||
[mid hi])]
|
||||
(recur new-lo new-hi candidate-hex (inc i)))
|
||||
|
||||
surface-dark?
|
||||
(recur mid hi best-hex (inc i))
|
||||
:else
|
||||
(recur lo mid best-hex (inc i))))))))))
|
||||
|
||||
(def ^{:doc "Mute `picked-hex` into a subtle, hue-preserving tint that sits
|
||||
just above `surface-hex` in contrast. Hue is taken from picked, chroma
|
||||
is clamped low (atmospheric, not saturated), and L is bisected toward
|
||||
the surface to land at exactly `target-vs-surface` (default ~1.5:1).
|
||||
|
||||
Use for the bg of a colored badge whose foreground will display the
|
||||
picked color itself — the muted bg gives the badge presence without
|
||||
competing with the foreground's hue. For text on top, run the picked
|
||||
color through `adjust-for-contrast` against the muted bg.
|
||||
|
||||
Memoized via a 256-entry LRU cache keyed on [picked surface target]."}
|
||||
muted-tint
|
||||
(bounded-memoize muted-tint* 256))
|
||||
|
||||
;; Canonical light/dark page surfaces. Used by `adjust-for-both-themes`
|
||||
;; to derive both rendering hexes regardless of the active theme — so
|
||||
;; the picker can show users what their pick will look like in BOTH
|
||||
|
||||
@@ -3212,12 +3212,35 @@
|
||||
(db-property-handler/set-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property/icon
|
||||
icon-data))
|
||||
icon-data)
|
||||
;; For classes, auto-sync the page-icon into
|
||||
;; default-icon when default-icon is empty so
|
||||
;; instances inherit it. If default-icon is already
|
||||
;; set we leave it alone to avoid clobbering a
|
||||
;; carefully configured tag default.
|
||||
(when (and (ldb/class? block)
|
||||
(nil? (:logseq.property.class/default-icon block)))
|
||||
(db-property-handler/set-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property.class/default-icon
|
||||
icon-data)))
|
||||
;; del — set :none to override inheritance (prevents auto-fetch re-trigger)
|
||||
(db-property-handler/set-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property/icon
|
||||
{:type :none})))
|
||||
(do
|
||||
(db-property-handler/set-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property/icon
|
||||
{:type :none})
|
||||
;; For classes, only clear default-icon if it
|
||||
;; matches the page-icon being removed — i.e. the
|
||||
;; two were synced. If default-icon was set
|
||||
;; independently, preserve it.
|
||||
(let [default-icon (:logseq.property.class/default-icon block)]
|
||||
(when (and (ldb/class? block)
|
||||
default-icon
|
||||
(= default-icon icon'))
|
||||
(db-property-handler/remove-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property.class/default-icon))))))
|
||||
:del-btn? (boolean (and icon' (not= (:type icon') :none)))
|
||||
:page-title (:block/title block)
|
||||
:preview-target-db-id (:db/id block)
|
||||
|
||||
@@ -57,30 +57,55 @@
|
||||
(declare normalize-icon derive-initials derive-avatar-initials
|
||||
<search-wikipedia-image <save-url-asset! open-image-asset-picker!)
|
||||
|
||||
(defn- convert-bg-color-to-rgba
|
||||
"Convert background color to rgba format with opacity ~0.314.
|
||||
Handles hex colors, CSS variables, and rgba colors."
|
||||
[backgroundColor]
|
||||
(cond
|
||||
;; Hex color - convert to rgba with opacity
|
||||
(and (string? backgroundColor)
|
||||
(string/starts-with? backgroundColor "#")
|
||||
(= (count (string/replace backgroundColor #"^#" "")) 6))
|
||||
(let [hex (string/replace backgroundColor #"^#" "")
|
||||
r (js/parseInt (subs hex 0 2) 16)
|
||||
g (js/parseInt (subs hex 2 4) 16)
|
||||
b (js/parseInt (subs hex 4 6) 16)]
|
||||
(str "rgba(" r "," g "," b ",0.314)"))
|
||||
;; Already rgba - update opacity to 0.314
|
||||
(and (string? backgroundColor)
|
||||
(string/includes? backgroundColor "rgba"))
|
||||
(string/replace backgroundColor #",\s*[\d.]+\)$" ",0.314)")
|
||||
;; CSS variable - use color-mix to apply opacity
|
||||
(and (string? backgroundColor)
|
||||
(string/starts-with? backgroundColor "var("))
|
||||
(str "color-mix(in srgb, " backgroundColor " 31.4%, transparent)")
|
||||
;; Default: use as-is (might be a color name or other format)
|
||||
:else backgroundColor))
|
||||
(defn- avatar-fallback-style
|
||||
"Build the inline :style map for an avatar fallback chip (the colored
|
||||
circle holding initials).
|
||||
|
||||
Design intent: a muted, hue-preserving tint behind crisp picked-color
|
||||
text — so the picked hue shows through on the initials, with the bg
|
||||
acting as atmospheric framing rather than competing for attention.
|
||||
|
||||
- bg gets `muted-tint`: same hue as picked, low chroma, L bisected to
|
||||
~1.5:1 contrast vs the page surface. Always visibly distinct from
|
||||
the surface, never as saturated as the picked color itself.
|
||||
- text uses the picked color directly when it reads against the muted
|
||||
bg; falls back to `adjust-for-contrast … 3.0` to lift L only when
|
||||
needed (e.g. dark picks on a dark surface where the picked color
|
||||
would still be invisible against its own muted tint).
|
||||
|
||||
The 3.0 target — rather than 4.5:1 body-text — treats avatar initials
|
||||
as decorative identifiers (Slack/Linear/GitHub all do similar). With
|
||||
4.5 the lift triggered for hues like tomato/red whose picked color
|
||||
sits at ~3.5:1 against their own muted bg, and OKLCh L bisection
|
||||
toward white desaturates as it climbs — turning vivid red into dusty
|
||||
pink. 3.0 lets those hues pass through as-is while still safely
|
||||
lifting truly-dark picks (#1a3d60 etc.) into legibility.
|
||||
|
||||
`bg` and `color` may be hex literals OR theme tokens (`var(--rx-...)`),
|
||||
since the picker offers both a custom hex picker and a Radix-token
|
||||
palette. Both are resolved to current-theme hex via `colors/->hex` so
|
||||
they hit the same OKLCh pipeline; otherwise preset picks would skip
|
||||
muting and render as flat saturated discs (bg = text = same token).
|
||||
|
||||
Earlier iteration used a 31.4% alpha treatment, which silently rendered
|
||||
dark picks at ~1.1:1 vs surface — invisible. The next iteration went
|
||||
solid bg + auto-lifted text, which inverted the hierarchy and made
|
||||
vivid picks read as a single solid disc. This version restores the
|
||||
original intent with deterministic OKLCh math."
|
||||
[{:keys [font-size bg color]}]
|
||||
(let [bg-hex (colors/->hex bg)
|
||||
color-hex (colors/->hex (or color bg))
|
||||
page-bg (when bg-hex
|
||||
(colors/read-bg-var "--ls-primary-background-color"))
|
||||
bg' (if (and bg-hex page-bg)
|
||||
(colors/muted-tint bg-hex page-bg 1.5)
|
||||
bg)
|
||||
color' (if (and color-hex bg-hex page-bg)
|
||||
(colors/adjust-for-contrast color-hex bg' 3.0)
|
||||
(or color bg))]
|
||||
(cond-> {:font-size font-size :font-weight "500"}
|
||||
bg' (assoc :background-color bg')
|
||||
color' (assoc :color color'))))
|
||||
|
||||
(defn- get-asset-type-from-db
|
||||
"Get asset type from DB using a direct Datalog query.
|
||||
@@ -319,12 +344,9 @@
|
||||
:style {:object-fit "cover"}}))
|
||||
;; Fallback (shows while loading or on error)
|
||||
(shui/avatar-fallback
|
||||
{:style (cond-> {:font-size font-size
|
||||
:font-weight "500"}
|
||||
explicit-bg
|
||||
(assoc :background-color (convert-bg-color-to-rgba explicit-bg))
|
||||
explicit-color
|
||||
(assoc :color explicit-color))}
|
||||
{:style (avatar-fallback-style {:font-size font-size
|
||||
:bg explicit-bg
|
||||
:color explicit-color})}
|
||||
display-text))))
|
||||
|
||||
(defn measure-text-width
|
||||
@@ -509,12 +531,9 @@
|
||||
(shui/avatar
|
||||
{:style {:width size :height size}}
|
||||
(shui/avatar-fallback
|
||||
{:style (cond-> {:font-size font-size
|
||||
:font-weight "500"}
|
||||
explicit-bg
|
||||
(assoc :background-color (convert-bg-color-to-rgba explicit-bg))
|
||||
explicit-color
|
||||
(assoc :color explicit-color))}
|
||||
{:style (avatar-fallback-style {:font-size font-size
|
||||
:bg explicit-bg
|
||||
:color explicit-color})}
|
||||
display-text)))))
|
||||
|
||||
;; Image with asset — let image-icon-cp resolve via the filesystem
|
||||
@@ -1663,7 +1682,6 @@
|
||||
(colors/variable :gray :09))
|
||||
my-id (:id icon-item)
|
||||
display-text (subs avatar-value 0 (min 3 (count avatar-value)))
|
||||
bg-color-rgba (convert-bg-color-to-rgba backgroundColor)
|
||||
item-shape {:type :avatar
|
||||
:data {:value avatar-value
|
||||
:backgroundColor backgroundColor
|
||||
@@ -1685,10 +1703,9 @@
|
||||
(shui/avatar
|
||||
{:class "w-7 h-7"}
|
||||
(shui/avatar-fallback
|
||||
{:style {:background-color bg-color-rgba
|
||||
:font-size "12px"
|
||||
:font-weight "500"
|
||||
:color color}}
|
||||
{:style (avatar-fallback-style {:font-size "12px"
|
||||
:bg backgroundColor
|
||||
:color color})}
|
||||
display-text))]))
|
||||
|
||||
(defn render-item
|
||||
@@ -2067,6 +2084,10 @@
|
||||
on-chosen (:on-chosen opts)
|
||||
highlighted-id (:highlighted-id opts)
|
||||
on-tile-hover! (:on-tile-hover! opts)
|
||||
;; In default-icon mode (used by tag class default-icon row), Text and
|
||||
;; Image tiles commit immediately rather than drilling into sub-pickers.
|
||||
;; Avatar still opens the asset-picker since avatars need an image.
|
||||
default-icon? (:default-icon? opts)
|
||||
;; Mouse-hover preview broadcast: pass the synthesized preview item
|
||||
;; the page-icon should render for each button. Keyboard hover
|
||||
;; broadcasts `:custom-*` markers and relies on icon-search's
|
||||
@@ -2079,27 +2100,31 @@
|
||||
{:data-item-id "custom-text"
|
||||
:tabIndex "-1"
|
||||
:class (when (= "custom-text" highlighted-id) "is-highlighted")
|
||||
:on-click #(reset! *view :text-picker)
|
||||
:on-click (if default-icon?
|
||||
#(when on-chosen (on-chosen % text-item))
|
||||
#(reset! *view :text-picker))
|
||||
:on-mouse-over (fn [] (some-> on-tile-hover! (apply [text-item])))}
|
||||
[:div.custom-tab-item-preview {:aria-hidden "true"}
|
||||
(icon text-item {:size 32})]
|
||||
[:span.custom-tab-item-label "Text"]])
|
||||
|
||||
;; Avatar option — commits the synthesized initials avatar immediately
|
||||
;; (`keep-popup? true` keeps the icon-picker mounted) and lands on the
|
||||
;; asset-picker's Avatar tab. The user can then pick / upload an image
|
||||
;; to use as the avatar background, or back out and the initials avatar
|
||||
;; stays as the committed icon. Mirrors the visual continuity the user
|
||||
;; gets from the hover preview.
|
||||
;; Avatar option. In page-icon context: commits the synthesized initials
|
||||
;; avatar immediately and lands on the asset-picker's Avatar tab so the
|
||||
;; user can pick a face image. In default-icon (class) context: commits
|
||||
;; only the type-without-image; each instance auto-derives its own
|
||||
;; initials from its own title via get-node-icon, so binding a specific
|
||||
;; face would be the wrong shape for the class default.
|
||||
(when avatar-item
|
||||
[:button.custom-tab-item
|
||||
{:data-item-id "custom-avatar"
|
||||
:tabIndex "-1"
|
||||
:class (when (= "custom-avatar" highlighted-id) "is-highlighted")
|
||||
:on-click (fn [e]
|
||||
(when on-chosen (on-chosen e avatar-item true))
|
||||
(reset! *asset-picker-initial-mode :avatar)
|
||||
(reset! *view :asset-picker))
|
||||
:on-click (if default-icon?
|
||||
#(when on-chosen (on-chosen % avatar-item))
|
||||
(fn [e]
|
||||
(when on-chosen (on-chosen e avatar-item true))
|
||||
(reset! *asset-picker-initial-mode :avatar)
|
||||
(reset! *view :asset-picker)))
|
||||
:on-mouse-over (fn [] (some-> on-tile-hover! (apply [avatar-item])))}
|
||||
[:div.custom-tab-item-preview {:aria-hidden "true"}
|
||||
(icon avatar-item {:size 32})]
|
||||
@@ -2113,10 +2138,14 @@
|
||||
{:data-item-id "custom-image"
|
||||
:tabIndex "-1"
|
||||
:class (when (= "custom-image" highlighted-id) "is-highlighted")
|
||||
:on-click (fn [e]
|
||||
(when on-chosen (on-chosen e image-placeholder-item true))
|
||||
(reset! *asset-picker-initial-mode :image)
|
||||
(reset! *view :asset-picker))
|
||||
:on-click (if default-icon?
|
||||
;; Default-icon context: commit placeholder and close.
|
||||
;; Per-instance images are auto-derived elsewhere.
|
||||
(fn [e] (when on-chosen (on-chosen e image-placeholder-item)))
|
||||
(fn [e]
|
||||
(when on-chosen (on-chosen e image-placeholder-item true))
|
||||
(reset! *asset-picker-initial-mode :image)
|
||||
(reset! *view :asset-picker)))
|
||||
:on-mouse-over (fn [] (some-> on-tile-hover! (apply [image-placeholder-item])))}
|
||||
[:div.custom-tab-item-preview {:aria-hidden "true"}
|
||||
[:span.image-tile-placeholder
|
||||
@@ -5750,7 +5779,7 @@
|
||||
(icon effective-icon-value (merge {:color? true} icon-props))))
|
||||
|
||||
(rum/defc icon-picker
|
||||
[icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts button-opts page-title preview-target-db-id]}]
|
||||
[icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts button-opts page-title preview-target-db-id default-icon?]}]
|
||||
(let [*trigger-ref (rum/use-ref nil)
|
||||
;; Optimistic post-commit override. Holds the just-committed
|
||||
;; icon-value during the ~15ms SharedWorker round-trip between
|
||||
@@ -5789,7 +5818,8 @@
|
||||
:icon-value normalized-icon-value
|
||||
:page-title page-title
|
||||
:del-btn? del-btn?
|
||||
:preview-target-db-id preview-target-db-id})))]
|
||||
:preview-target-db-id preview-target-db-id
|
||||
:default-icon? default-icon?})))]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when initial-open?
|
||||
|
||||
@@ -141,6 +141,68 @@
|
||||
:del-btn? (some? icon-value)
|
||||
:on-chosen on-chosen!})])))
|
||||
|
||||
(rum/defc default-icon-row < rum/reactive
|
||||
"Renders the Default Icon property for classes.
|
||||
Uses a single icon-picker button that opens the universal icon search popup
|
||||
supporting all icon types (icon, emoji, avatar, text, image)."
|
||||
[block _editing?]
|
||||
(let [block (or (model/sub-block (:db/id block)) block)
|
||||
own-value (:logseq.property.class/default-icon block)
|
||||
inherited-value (when-not own-value
|
||||
(some :logseq.property.class/default-icon
|
||||
(ldb/get-class-extends block)))
|
||||
;; Subscribe to parent entities for reactivity when inheriting
|
||||
_ (when-not own-value
|
||||
(doseq [parent (ldb/get-class-extends block)]
|
||||
(model/sub-block (:db/id parent))))
|
||||
current-value (or own-value inherited-value)
|
||||
page-title (:block/title block)
|
||||
;; Enrich type-only values for display preview.
|
||||
;; Without this, normalize-icon produces {:data {:value nil}} for
|
||||
;; type-only maps like {:type :avatar}, causing invisible buttons.
|
||||
display-value (when current-value
|
||||
(case (:type current-value)
|
||||
:avatar (if (get-in current-value [:data :value])
|
||||
current-value
|
||||
(assoc current-value :data
|
||||
{:value (icon-component/derive-avatar-initials (or page-title ""))}))
|
||||
:text (if (get-in current-value [:data :value])
|
||||
current-value
|
||||
(assoc current-value :data
|
||||
{:value (icon-component/derive-initials (or page-title ""))}))
|
||||
:image (if (get-in current-value [:data :asset-uuid])
|
||||
current-value
|
||||
(assoc current-value :data {:empty? true}))
|
||||
current-value))
|
||||
on-chosen (fn [_e icon]
|
||||
(if icon
|
||||
(let [icon-data (cond
|
||||
(= :text (:type icon)) {:type :text :data (:data icon)}
|
||||
(= :avatar (:type icon)) {:type :avatar :data (:data icon)}
|
||||
(= :image (:type icon)) {:type :image :data (:data icon)}
|
||||
:else (select-keys icon [:type :id :color]))]
|
||||
(property-handler/set-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property.class/default-icon
|
||||
icon-data))
|
||||
(property-handler/remove-block-property!
|
||||
(:db/id block)
|
||||
:logseq.property.class/default-icon)))]
|
||||
[:div.flex.flex-row.items-center.gap-2.w-full.cursor-pointer
|
||||
{:on-click (fn [^js e]
|
||||
;; Delegate clicks on the surrounding row to the icon-picker
|
||||
;; button, but skip if the button itself was clicked
|
||||
(when-not (some-> (.-target e) (.closest "button"))
|
||||
(when-let [btn (some-> (.-currentTarget e) (.querySelector "button"))]
|
||||
(.click btn))))}
|
||||
(icon-component/icon-picker display-value
|
||||
{:disabled? config/publishing?
|
||||
:del-btn? (some? current-value)
|
||||
:on-chosen on-chosen
|
||||
:page-title page-title
|
||||
:default-icon? true
|
||||
:icon-props {:size 20}})]))
|
||||
|
||||
(defn select-type?
|
||||
[block property]
|
||||
(let [type (:logseq.property/type property)]
|
||||
@@ -1442,6 +1504,9 @@
|
||||
(= :logseq.property/icon (:db/ident property))
|
||||
(icon-row block editing?)
|
||||
|
||||
(= :logseq.property.class/default-icon (:db/ident property))
|
||||
(default-icon-row block editing?)
|
||||
|
||||
(and (= type :number) (not editing?) (not closed-values?))
|
||||
(single-number-input block property value (:table-view? opts))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user