penpot/frontend/src/app/main/refs.cljs
Luis de Dios 98daf1592d
✨ Apply tokens to stroke per side (#10913)
* ♻️ Derive per-side stroke widths from the side values

The per-stroke `:stroke-per-side` boolean only gated whether the
renderer looked at the four side widths, and the CSS generator used it
to decide whether to emit one `border-width` or four. Comparing the
sides is enough, so drop the attribute from the shape schema and from
the stroke attribute list.

The WASM property and upload bridges and `stroke-per-side-widths` now
derive the per-side widths from the values alone.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Declare per-side stroke width token attributes

Replace the single `:stroke-width` token attribute with
`:stroke-width-top`, `:stroke-width-right`, `:stroke-width-bottom`
and `:stroke-width-left`, add `per-side-stroke-width-keys`, and map
the new attributes to the strokes shape attribute and to the
dimensions token type.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Unapply only the token of the changed stroke side

A stroke change that reports a single per-side sub-attribute now
resolves to that side's token only. A plain `:stroke-width` change
still resolves to every side, and a change with no sub-attribute
resolves to all width keys plus the color.

Add a regression test that tokens on untouched sides survive a change
to another side.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add a predicate for per-side stroke shapes

Boards and rectangles support independent stroke widths per side.
Expose `per-side-stroke-shape?` so callers can gate the per-side UI,
and cover the supported and unsupported shape types with a test.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add stroke side width materialization helper

`materialize-stroke-side-widths` concretizes the four per-side width
keys from a stroke: edited sides take the new value, the others keep
their current width (0 when there is no stroke), and `:stroke-width`
mirrors the top side for legacy consumers.

This pulls the logic already duplicated in the token apply path into a
shared helper, ready for the direct-edit path.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Apply a stroke width token to every side

`update-stroke-width` now writes the four per-side width keys, both
when the shape already has a stroke and when it gets a new default
one, so the applied-token bookkeeping matches the stroke attributes.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Apply a stroke width token to a single side

Add `update-stroke-width-side`, which changes only the sides named in
`attributes` on the first stroke of each shape. The remaining sides
keep their current width (0 when the shape had no stroke) and all side
keys are materialized through the shared helper, so consumers never
fall back to `:stroke-width`.

Route the per-side token keys to the new function and update the
apply, remap and component tests.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Complete a partially applied per-side token on toggle

When explicit attributes come from an input or a plugin call, toggle
the token off only if it already covers every target attribute on every
selected shape. A partial per-side application is completed instead of
removed. The token pill keeps the previous any-attribute behavior.

Add tests for both the completion and the full removal.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Offer per-side stroke width actions in the token menu

Boards and rectangles get a stroke width submenu with an all-sides
action and one action per side; other shapes keep the single global
action. The global action targets every per-side attribute so the
design tab keeps showing the token on each side.

Add the pill labels for the new attributes, the menu test, and the
`workspace.tokens.stroke-width` string.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Persist the per-side stroke preference

Add `:stroke-per-side` to the user profile props schema and expose it
through a derived `stroke-per-side` ref. The design tab will read the
preference from here instead of a per-stroke attribute.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add per-side stroke width helpers to the stroke menu

Add `stroke-width-all-attrs` and `per-side-stroke-available?`, which
checks the feature flag and that a single board or rectangle, or a
uniform multi-selection of them, is selected. Use it instead of the
inline shape-type check and drop the old per-side toggle handler.

Cover both helpers with a test.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add per-side stroke width token inputs to the design tab

Turn the four side width fields into token inputs with detach actions
and a `:multiple` mixed value when the sides differ. The per-side
toggle now reads the persisted profile preference instead of a
per-stroke attribute, so previous per-side edits survive.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Materialize stroke sides on direct width edit

The stroke menu per-side handler only wrote the edited side key and,
for the top side, the global `:stroke-width`. A stroke holding just
`:stroke-width` made every consumer fall back to the global value, so
editing one side changed all the others.

Add a `change-stroke-side-width` event that materializes the four side
keys through the shared helper and reports only the edited key as
changed, so tokens on untouched sides are not unapplied. Route the menu
handler through it.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Make stroke width fields non nullable

Drop `:nillable` from the global and per-side stroke width inputs and
use `:multiple` for the mixed state, so an empty field no longer
represents a null width.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Fix the numeric-input props schema key

The schema declared `:applied-token`, but the component body and every
caller use `:applied-token-name`, so the prop was never validated.
Rename the schema entry to match.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add token-disabled support to the numeric input

The design-system numeric input accepts `:token-disabled` and
`:token-tooltip`; the token button is disabled and shows the reason.
Scope the disabled input style to `input:disabled` so a disabled token
button no longer dims the whole field. Pass both props through the
token wrapper.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Disable token controls below the first fill or stroke

Design tokens only apply to the first fill or stroke of a shape. Add
`tokens-allowed-position?` and mark the fill and stroke lists with
`tokens-first-only`, so later entries disable their token controls and
explain why. The colorpicker opens on the direct color tab and disables
the token tab for those rows.

Cover the helper with a test and add the new translation.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Refactor colorpicker style switcher to DS radio buttons

Replace the legacy `components/radio-buttons` markup in the colorpicker
with the design system `radio-buttons*`, using its declarative options
API. Switching between direct color and token mode now passes string
values, as the DS component expects.

The previous keyword values broke the round trip back to color mode:
the DOM stringifies keywords with a leading colon, so the value never
matched `:direct-color`. Using plain strings keeps the conversion
clean.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add playwright tests

* ✨ Scope per-side stroke controls to each stroke

Give every stroke row its own expanded state instead of sharing one
profile-wide preference. The state lives in `:workspace-local`, keyed by
`[ids index]`, so it survives selecting another shape and coming back but
resets on reload.

Remove the `:stroke-per-side` profile prop and its ref. The ref now derives
from `:workspace-local`.

Update the Playwright spec to expand the controls per stroke through the
toggle, and assert that strokes toggle independently, that the state resets
on reload, and that it survives switching shapes.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Keep stroke tokens when editing or removing later strokes

The token unapply logic decided which tokens to clear from the shape
using only the changed sub-attributes, without knowing which stroke was
edited. Since stroke tokens only live on the first stroke, editing or
removing a later stroke cleared the first stroke's tokens.

Add a `:changed-item-index` option to `generate-update-shapes` and skip
unapplying fill/stroke tokens when the changed item is not the first.
The stroke color, attrs, side-width and remove events now report the
index they touch.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Ignore token shortcuts when tokens are disabled for input

The numeric input opened the token dropdown on `{` regardless of
`token-disabled?`, so inputs that cannot hold tokens (for example,
strokes after the first one) still opened it, and typing `{token}` plus
`}` could apply a token there.

Extract the key handling into `token-shortcut`, which returns nil when
tokens are disabled, and use it for both `{` and `}`.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Gate per-side stroke tokens on the WASM renderer

The token context menu offered per-side stroke width actions whenever
the feature flag was on and the shape was a board or rectangle, without
checking the renderer. The classic renderer only draws the single
`:stroke-width`, so applying a per-side token there wrote inert data,
the token pill reported it, and the stroke changed appearance when the
WASM renderer was later enabled.

Add `per-side-stroke-enabled?` (flag + WASM renderer) and use it from
both the design tab and the token context menu. Thread the renderer
flag into the context menu through `:render-wasm`.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Keep first-stroke tokens when reordering later strokes

Fill and stroke tokens only ever live on the first item of the
collection. When a stroke update arrives without a changed item
index (for example reordering the second and third strokes), the
unapply logic assumed the first item had been edited and removed
every stroke token from the shape.

Compare the first item before and after the update instead: when
no item index is given, unapply only if the first item actually
changed. Reordering later strokes now leaves the first stroke and
its tokens untouched, while moving the first stroke away still
detaches them. Explicit item edits keep their previous behavior.

AI-assisted-by: deepseek-v4-flash
2026-09-25 13:09:55 +02:00

693 lines
18 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.main.refs
"A collection of derived refs."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cph]
[app.common.files.tokens :as cfo]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.config :as cf]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.tokens.selected-set :as dwts]
[app.main.store :as st]
[app.main.streams :as ms]
[beicon.v2.core :as rx]
[clojure.string :as str]
[okulary.core :as l]))
;; ---- Global refs
(def route
(l/derived (l/key :route) st/state))
(def router
(l/derived (l/key :router) st/state))
(def profile
(l/derived (l/key :profile) st/state))
(def custom-shortcuts
(l/derived (fn [state] (get-in state [:profile :props :custom-shortcuts])) st/state))
(def current-page-id
(l/derived (l/key :current-page-id) st/state))
(def team
(l/derived dsh/lookup-team st/state))
(def project
(l/derived (fn [state]
(let [project-id (:current-project-id state)
projects (:projects state)]
(get projects project-id)))
st/state))
(def permissions
(l/derived (l/key :permissions) team))
(def teams
(l/derived (l/key :teams) st/state))
(def exception
(l/derived :exception st/state))
(def threads
(l/derived :comment-threads st/state))
(def share-links
(l/derived :share-links st/state))
(def export
(l/derived :export st/state))
(def persistence
(l/derived :persistence st/state))
(def projects
(l/derived :projects st/state))
(def files
(l/derived :files st/state))
(def file
(l/derived (fn [state]
(let [file-id (:current-file-id state)
files (:files state)]
(get files file-id)))
st/state))
(def shared-files
"A derived state that points to the current list of shared
files (without the content, only summary)"
(l/derived :shared-files st/state))
(defn select-libraries
"Find between all the given files, those who are libraries of the file-id.
Also include the file-id file itself.
Return a map of id -> library."
[files file-id]
(persistent!
(reduce-kv (fn [result id file]
(if (or (= id file-id)
(= (:library-of file) file-id))
(assoc! result id file)
result))
(transient {})
files)))
;; NOTE: for performance reasons, prefer derefing refs/files and then
;; use with-memo mechanism with `select-libraries` this will avoid
;; executing the select-libraries reduce-kv on each state change and
;; only execute it when files are changed. This ref exists for
;; backward compatibility with the code, but it is considered
;; DEPRECATED and all new code should not use it and old code should
;; be gradually migrated to more efficient approach
(def libraries
"A derived state that contains the currently loaded shared
libraries with all its content; including the current file"
(l/derived (fn [state]
(let [files (get state :files)
file-id (get state :current-file-id)]
(select-libraries files file-id)))
st/state))
(defn extract-selected-files
[files selected]
(let [get-file #(get files %)
sim-file #(select-keys % [:id :name :project-id :is-shared])
xform (comp (keep get-file)
(map sim-file))]
(->> (sequence xform selected)
(d/index-by :id))))
(def selected-files
(l/derived (fn [state]
(let [selected (get state :selected-files)
files (get state :files)]
(extract-selected-files files selected)))
st/state))
(def selected-project
(l/derived :selected-project st/state))
(def dashboard-local
(l/derived :dashboard-local st/state))
(def render-state
(l/derived :render-state st/state))
(def render-context-lost?
(l/derived :lost render-state))
(def workspace-local
(l/derived :workspace-local st/state))
(def stroke-per-side
"Per-stroke expansion state of the design tab per-side controls, keyed by
`[ids index]`. This is ephemeral UI state: it survives shape and page
switches during the session but resets on reload."
(l/derived (fn [state] (get-in state [:workspace-local :stroke-per-side])) st/state))
(def workspace-global
(l/derived :workspace-global st/state))
(def mcp
(l/derived :mcp st/state))
(def workspace-drawing
(l/derived :workspace-drawing st/state))
(def workspace-tokens
"All tokens related ephimeral state"
(l/derived :workspace-tokens st/state))
(def workspace-selrect
(let [a (atom nil)]
(rx/sub! ms/workspace-selrect #(reset! a %))
a))
;; WARNING: Don't use directly from components, this is a proxy to
;; improve performance of selected-shapes and
(def ^:private selected-shapes-data
(l/derived
(fn [state]
(let [objects (dsh/lookup-page-objects state)
selected (dm/get-in state [:workspace-local :selected])]
{:objects objects :selected selected}))
st/state
(fn [v1 v2]
(and (identical? (:objects v1) (:objects v2))
(= (:selected v1) (:selected v2))))))
(def selected-shapes
(l/derived
(fn [{:keys [objects selected]}]
(dsh/process-selected objects selected))
selected-shapes-data =))
(defn make-selected-ref
[id]
(l/derived #(contains? % id) selected-shapes))
(def export-in-progress?
(l/derived :export-in-progress? export))
(def export-error?
(l/derived :export-error? export))
(def export-progress
(l/derived :export-progress export))
(def exports
(l/derived :exports export))
(def export-detail-visibililty
(l/derived :export-detail-visibililty export))
(def export-widget-visibililty
(l/derived :export-widget-visibililty export))
(def export-health
(l/derived :export-health export))
(def selected-zoom
(l/derived :zoom workspace-local))
(def selected-drawing-tool
(l/derived :tool workspace-drawing))
(def current-drawing-shape
(l/derived :object workspace-drawing))
(def selected-edition
(l/derived :edition workspace-local))
(def workspace-edit-path
(l/derived :edit-path workspace-local))
(def current-transform
(l/derived :transform workspace-local))
(def options-mode
(l/derived :options-mode workspace-local))
(def options-mode-global
(l/derived :options-mode workspace-global))
(def default-font
(l/derived :default-font workspace-global))
(def inspect-expanded
(l/derived :inspect-expanded workspace-local))
(def workspace-vport
(l/derived :vport workspace-local))
(def vbox
(l/derived :vbox workspace-local))
(def current-hover
(l/derived :hover workspace-local))
(def context-menu
(l/derived :context-menu workspace-local))
(def token-context-menu
(l/derived :token-context-menu workspace-local))
;; page item that it is being edited
(def editing-page-item
(l/derived :page-item workspace-local))
;; set of pages selected in the sitemap (multi-selection)
(def selected-pages
(l/derived :selected-pages workspace-local))
(def current-hover-ids
(l/derived :hover-ids context-menu))
(def workspace-layout
(l/derived :workspace-layout st/state))
(def snap-pixel?
(l/derived #(contains? % :snap-pixel-grid) workspace-layout))
(def rulers?
(l/derived #(contains? % :rulers) workspace-layout))
;; FIXME: rename to current-file-data
(def workspace-data
"Currently working file data on workspace"
(l/derived dsh/lookup-file-data st/state))
(def workspace-file-colors
(l/derived (fn [{:keys [id] :as data}]
(some-> (:colors data) (update-vals #(assoc % :file-id id))))
workspace-data
=))
(def recent-colors
"Recent colors for the currently selected file"
(l/derived (fn [state]
(when-let [file-id (:current-file-id state)]
(dm/get-in state [:recent-colors file-id])))
st/state))
(def recent-fonts
"Recent fonts for the currently selected file"
(l/derived (fn [state]
(when-let [file-id (:current-file-id state)]
(dm/get-in state [:recent-fonts file-id])))
st/state))
(def workspace-file-typography
(l/derived :typographies workspace-data))
(def workspace-presence
(l/derived :workspace-presence st/state))
(def workspace-page
"Ref to currently active page on workspace"
(l/derived dsh/lookup-page st/state))
(def workspace-page-flows
(l/derived #(-> % :flows not-empty) workspace-page))
(def workspace-page-guides
(l/derived :guides workspace-page))
(defn workspace-page-object-by-id
[page-id shape-id]
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
(def workspace-page-objects
(l/derived dsh/lookup-page-objects st/state identical?))
(def workspace-read-only?
(l/derived :read-only? workspace-global))
(def workspace-paddings-selected
(l/derived :paddings-selected workspace-global))
(def workspace-gap-selected
(l/derived :gap-selected workspace-global))
(def workspace-margins-selected
(l/derived :margins-selected workspace-global))
(defn object-by-id
[id]
(l/derived #(get % id) workspace-page-objects))
(defn objects-by-id
[ids]
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =))
(defn parents-by-ids
[ids]
(l/derived
(fn [objects]
(let [parent-ids (into #{} (keep #(get-in objects [% :parent-id])) ids)]
(into [] (keep #(get objects %)) parent-ids)))
workspace-page-objects =))
(defn shape-parents
[id]
(l/derived
(fn [objects]
(into []
(keep (d/getf objects))
(cph/get-parent-ids objects id)))
workspace-page-objects =))
(defn children-objects
[id]
(l/derived
(fn [objects]
(->> (dm/get-in objects [id :shapes])
(into [] (keep (d/getf objects)))))
workspace-page-objects =))
(defn all-children-objects
[id]
(l/derived
(fn [objects]
(let [children-ids (cph/get-children-ids objects id)]
(into [] (keep (d/getf objects)) children-ids)))
workspace-page-objects =))
(def workspace-frames
(l/derived ctt/get-frames workspace-page-objects =))
(def workspace-editor
(l/derived :workspace-editor st/state))
(def workspace-editor-state
(l/derived :workspace-editor-state st/state))
(def workspace-v2-editor-state
(l/derived :workspace-v2-editor-state st/state))
(def workspace-modifiers
(l/derived :workspace-modifiers st/state))
(def workspace-wasm-editor-styles
(l/derived :workspace-wasm-editor-styles st/state))
(def workspace-wasm-modifiers
(let [a (atom nil)]
(rx/sub! ms/wasm-modifiers #(reset! a %))
a))
(def ^:private workspace-modifiers-with-objects
(l/derived
(fn [state]
{:modifiers (get state :workspace-modifiers)
:objects (dsh/lookup-page-objects state)})
st/state
(fn [a b]
(and (identical? (:modifiers a) (:modifiers b))
(identical? (:objects a) (:objects b))))))
(def workspace-frame-modifiers
(l/derived
(fn [{:keys [modifiers objects]}]
(reduce (fn [result [id modifiers]]
(let [shape (get objects id)
frame-id (:frame-id shape)]
(cond
(cph/frame-shape? shape)
(assoc-in result [id id] modifiers)
(some? frame-id)
(assoc-in result [frame-id id] modifiers)
:else
result)))
{}
modifiers))
workspace-modifiers-with-objects))
(defn workspace-modifiers-by-frame-id
[frame-id]
(l/derived #(get % frame-id) workspace-frame-modifiers =))
(def workspace-clipboard-style
(l/derived :clipboard-style workspace-global))
(defn select-bool-children [id]
(l/derived #(dsh/select-bool-children % id) st/state =))
(defn is-child-selected?
[id]
(l/derived
(fn [{:keys [selected objects]}]
(let [children (cph/get-children-ids objects id)]
(some #(contains? selected %) children)))
selected-shapes-data
=))
(def workspace-focus-selected
(l/derived :workspace-focus-selected st/state))
(defn workspace-get-flex-child
[ids]
(l/derived
(fn [state]
(let [objects (dsh/lookup-page-objects state)]
(into []
(comp (map (d/getf objects))
(filter (partial ctl/flex-layout-immediate-child? objects)))
ids)))
st/state =))
;; ---- Token refs
(def tokens-lib
(l/derived dsh/lookup-tokens-lib st/state))
(def tokens-status
(l/derived dsh/lookup-tokens-status st/state))
(def workspace-token-theme-groups
(l/derived (d/nilf ctob/get-theme-groups) tokens-lib))
(defn workspace-token-theme
[id]
(l/derived
(fn [lib]
(when lib
(ctob/get-theme lib id)))
tokens-lib))
(def workspace-token-theme-tree-no-hidden
(l/derived (fn [lib]
(or
(some-> lib
(ctob/delete-theme ctob/hidden-theme-id)
(ctob/get-theme-tree))
[]))
tokens-lib))
(def workspace-token-themes
(l/derived #(or (some-> % ctob/get-themes) []) tokens-lib))
(def workspace-token-themes-no-hidden
(l/derived #(remove ctob/hidden-theme? %) workspace-token-themes))
(def selected-token-set-id
(l/derived (l/key :selected-token-set-id) workspace-tokens))
(def workspace-ordered-token-sets
(l/derived #(or (some-> % ctob/get-sets) []) tokens-lib))
(def workspace-token-sets-tree
(l/derived (d/nilf ctob/get-set-tree) tokens-lib))
(def workspace-active-theme-ids
(l/derived (d/nilf ctos/get-active-theme-ids) tokens-status))
(def workspace-all-tokens-map
(l/derived (d/nilf ctob/get-all-tokens-map) tokens-lib))
;; TODOstatus ver una forma eficiente de hacer un derived de lib y status a la vez
(defn token-sets-at-path-all-active
[group-path]
(l/derived
(fn [status]
(when status
(cfo/sets-at-path-all-active? status @(l/derived identity tokens-lib) group-path)))
tokens-status))
;; FIXME: deprecated, it should not be implemented with ref (still used in form)
(def workspace-active-theme-sets-tokens
(l/derived (fn [[status lib]]
(if (and status lib)
(cfo/get-tokens-in-active-sets status lib)
{}))
[tokens-status tokens-lib]))
(def workspace-token-in-selected-set
(fn [token-id]
(l/derived
#(dwts/get-token-in-selected-set % token-id)
st/state)))
(def workspace-all-tokens-in-selected-set
(l/derived #(or (dwts/get-all-tokens-in-selected-set %) {}) st/state))
(def plugins-permissions-peek
(l/derived (fn [state]
(dm/get-in state [:plugins-permissions-peek :data]))
st/state))
;; ---- Viewer refs
(defn get-viewer-objects
[state page-id]
(dm/get-in state [:viewer :pages page-id :objects]))
(defn lookup-viewer-objects-by-id
[page-id]
(l/derived #(get-viewer-objects % page-id) st/state =))
(def viewer-data
(l/derived (l/key :viewer) st/state))
(def viewer-file
(l/derived :file viewer-data))
(def viewer-thumbnails
(l/derived :thumbnails viewer-file))
(def viewer-project
(l/derived :project viewer-data))
(def viewer-state
(l/derived :viewer st/state))
(def viewer-local
(l/derived :viewer-local st/state))
(def viewer-overlays
(l/derived :viewer-overlays st/state))
(def comment-threads
(l/derived :comment-threads st/state))
(def comments-local
(l/derived :comments-local st/state))
(def profiles
(l/derived :profiles st/state))
(def viewer-fullscreen?
(l/derived (fn [state]
(dm/get-in state [:viewer-local :fullscreen?]))
st/state))
(def viewer-zoom-type
(l/derived (fn [state]
(dm/get-in state [:viewer-local :zoom-type]))
st/state))
(defn- resolved-uri?
"Returns true if the uri is already a fully resolved URI (blob or data)."
[uri]
(and (string? uri)
(or (str/starts-with? uri "blob:")
(str/starts-with? uri "data:"))))
(defn workspace-thumbnail-by-id
[object-id]
(l/derived
(fn [state]
(when-let [entry (dm/get-in state [:thumbnails object-id])]
(cond-> entry
(and (:uri entry)
(not (resolved-uri? (:uri entry))))
(update :uri cf/resolve-media))))
st/state))
(def workspace-text-modifier
(l/derived :workspace-text-modifier st/state))
(defn workspace-text-modifier-by-id [id]
(l/derived #(get % id) workspace-text-modifier =))
(defn is-layout-child?
[ids]
(l/derived
(fn [objects]
(->> ids
(map (d/getf objects))
(some (partial ctl/any-layout-immediate-child? objects))))
workspace-page-objects))
(defn all-layout-child?
[ids]
(l/derived
(fn [objects]
(->> ids
(map (d/getf objects))
(every? (partial ctl/any-layout-immediate-child? objects))))
workspace-page-objects =))
(defn flex-layout-child?
[ids]
(l/derived
(fn [objects]
(->> ids
(map (d/getf objects))
(every? (partial ctl/flex-layout-immediate-child? objects))))
workspace-page-objects =))
(defn grid-layout-child?
[ids]
(l/derived
(fn [objects]
(->> ids
(map (d/getf objects))
(every? (partial ctl/grid-layout-immediate-child? objects))))
workspace-page-objects =))
(def colorpicker
(l/derived :colorpicker st/state))
(def workspace-grid-edition
(l/derived :workspace-grid-edition st/state))
(defn workspace-grid-edition-id
[id]
(l/derived #(get % id) workspace-grid-edition))
(def workspace-preview-blend
(l/derived :workspace-preview-blend st/state))
(defn workspace-preview-blend-by-id [id]
(l/derived (l/key id) workspace-preview-blend =))
(def specialized-panel
(l/derived :specialized-panel st/state))
(def updating-library
(l/derived :updating-library st/state))
(def progress
(l/derived :progress st/state))
(def access-tokens
(l/derived :access-tokens st/state))
(def access-token-created
(l/derived :access-token-created st/state))