mirror of
https://github.com/penpot/penpot.git
synced 2025-05-18 16:26:11 +02:00
🎉 Merge tokens-studio
This commit is contained in:
parent
0cd446421d
commit
b82679deaf
85 changed files with 10581 additions and 168 deletions
352
frontend/src/app/main/data/tokens.cljs
Normal file
352
frontend/src/app/main/data/tokens.cljs
Normal file
|
@ -0,0 +1,352 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.data.tokens
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[app.main.ui.workspace.tokens.token-set :as wtts]
|
||||
[app.main.ui.workspace.tokens.update :as wtu]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.data :as data]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Helpers
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; TODO HYMA: Copied over from workspace.cljs
|
||||
(defn update-shape
|
||||
[id attrs]
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(and (cts/check-shape-attrs! attrs)
|
||||
(uuid? id)))
|
||||
|
||||
(ptk/reify ::update-shape
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS Getters
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-tokens-lib [state]
|
||||
(get-in state [:workspace-data :tokens-lib]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS Actions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn toggle-or-apply-token
|
||||
"Remove any shape attributes from token if they exists.
|
||||
Othewise apply token attributes."
|
||||
[shape token]
|
||||
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
|
||||
(merge {} shape-leftover token-leftover)))
|
||||
|
||||
(defn token-from-attributes [token attributes]
|
||||
(->> (map (fn [attr] [attr (wtt/token-identifier token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn unapply-token-id [shape attributes]
|
||||
(update shape :applied-tokens d/without-keys attributes))
|
||||
|
||||
(defn apply-token-to-attributes [{:keys [shape token attributes]}]
|
||||
(let [token (token-from-attributes token attributes)]
|
||||
(toggle-or-apply-token shape token)))
|
||||
|
||||
(defn apply-token-to-shape
|
||||
[{:keys [shape token attributes] :as _props}]
|
||||
(let [applied-tokens (apply-token-to-attributes {:shape shape
|
||||
:token token
|
||||
:attributes attributes})]
|
||||
(update shape :applied-tokens #(merge % applied-tokens))))
|
||||
|
||||
(defn maybe-apply-token-to-shape
|
||||
"When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape."
|
||||
[{:keys [shape token _attributes] :as props}]
|
||||
(if token
|
||||
(apply-token-to-shape props)
|
||||
shape))
|
||||
|
||||
(defn get-token-data-from-token-id
|
||||
[id]
|
||||
(let [workspace-data (deref refs/workspace-data)]
|
||||
(get (:tokens workspace-data) id)))
|
||||
|
||||
(defn set-selected-token-set-id
|
||||
[id]
|
||||
(ptk/reify ::set-selected-token-set-id
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(wtts/assoc-selected-token-set-id state id))))
|
||||
|
||||
(defn create-token-theme [token-theme]
|
||||
(let [new-token-theme token-theme]
|
||||
(ptk/reify ::create-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-token-theme new-token-theme))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)))))))
|
||||
|
||||
(defn update-token-theme [[group name] token-theme]
|
||||
(ptk/reify ::update-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
prev-token-theme (some-> tokens-lib (ctob/get-theme group name))
|
||||
changes (pcb/update-token-theme (pcb/empty-changes it) token-theme prev-token-theme)]
|
||||
(rx/of
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn toggle-token-theme-active? [group name]
|
||||
(ptk/reify ::toggle-token-theme-active?
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
prev-active-token-themes (some-> tokens-lib
|
||||
(ctob/get-active-theme-paths))
|
||||
active-token-themes (some-> tokens-lib
|
||||
(ctob/toggle-theme-active? group name)
|
||||
(ctob/get-active-theme-paths))
|
||||
active-token-themes' (if (= active-token-themes #{ctob/hidden-token-theme-path})
|
||||
active-token-themes
|
||||
(disj active-token-themes ctob/hidden-token-theme-path))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/update-active-token-themes active-token-themes' prev-active-token-themes))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn delete-token-theme [group name]
|
||||
(ptk/reify ::delete-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (get state :workspace-data)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token-theme group name))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn create-token-set [token-set]
|
||||
(let [new-token-set (merge
|
||||
{:name "Token Set"
|
||||
:tokens []}
|
||||
token-set)]
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-token-set new-token-set))]
|
||||
(rx/of
|
||||
(set-selected-token-set-id (:name new-token-set))
|
||||
(dch/commit-changes changes)))))))
|
||||
|
||||
(defn update-token-set [set-name token-set]
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [prev-token-set (some-> (get-tokens-lib state)
|
||||
(ctob/get-set set-name))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/update-token-set token-set prev-token-set))]
|
||||
(rx/of
|
||||
(set-selected-token-set-id (:name token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn toggle-token-set [{:keys [token-set-name]}]
|
||||
(ptk/reify ::toggle-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
prev-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)
|
||||
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
|
||||
theme (-> (or (some-> prev-theme
|
||||
(ctob/set-sets active-token-set-names))
|
||||
(ctob/make-hidden-token-theme :sets active-token-set-names))
|
||||
(ctob/toggle-set token-set-name))
|
||||
prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/update-active-token-themes #{(ctob/token-theme-path ctob/hidden-token-theme-group ctob/hidden-token-theme-name)} prev-active-token-themes))
|
||||
changes' (if prev-theme
|
||||
(pcb/update-token-theme changes theme prev-theme)
|
||||
(pcb/add-token-theme changes theme))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes')
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn import-tokens-lib [lib]
|
||||
(ptk/reify ::import-tokens-lib
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (get state :workspace-data)
|
||||
update-token-set-change (some-> lib
|
||||
(ctob/get-sets)
|
||||
(first)
|
||||
(:name)
|
||||
(set-selected-token-set-id))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-tokens-lib lib))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
update-token-set-change
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn delete-token-set [token-set-name]
|
||||
(ptk/reify ::delete-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (get state :workspace-data)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token-set token-set-name))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn move-token-set [source-set-name dest-set-name position]
|
||||
(ptk/reify ::move-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
prev-before-set-name (ctob/get-neighbor-set-name tokens-lib source-set-name 1)
|
||||
[source-set-name' dest-set-name'] (if (= :top position)
|
||||
[source-set-name dest-set-name]
|
||||
[source-set-name (ctob/get-neighbor-set-name tokens-lib dest-set-name 1)])
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/move-token-set-before source-set-name' dest-set-name' prev-before-set-name))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn update-create-token
|
||||
[{:keys [token prev-token-name]}]
|
||||
(ptk/reify ::update-create-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [token-set (wtts/get-selected-token-set state)
|
||||
token-set-name (or (:name token-set) "Global")
|
||||
changes (if (not token-set)
|
||||
;; No set created add a global set
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
token-set (ctob/make-token-set :name token-set-name :tokens {(:name token) token})
|
||||
hidden-theme (ctob/make-hidden-token-theme :sets [token-set-name])
|
||||
active-theme-paths (some-> tokens-lib ctob/get-active-theme-paths)
|
||||
add-to-hidden-theme? (= active-theme-paths #{ctob/hidden-token-theme-path})
|
||||
base-changes (pcb/add-token-set (pcb/empty-changes) token-set)]
|
||||
(cond
|
||||
(not tokens-lib) (-> base-changes
|
||||
(pcb/add-token-theme hidden-theme)
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))
|
||||
|
||||
add-to-hidden-theme? (let [prev-hidden-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)]
|
||||
(-> base-changes
|
||||
(pcb/update-token-theme (ctob/toggle-set prev-hidden-theme ctob/hidden-token-theme-path) prev-hidden-theme)))
|
||||
|
||||
:else base-changes))
|
||||
;; Either update or add token to existing set
|
||||
(if-let [prev-token (ctob/get-token token-set (or prev-token-name (:name token)))]
|
||||
(pcb/update-token (pcb/empty-changes) (:name token-set) token prev-token)
|
||||
(pcb/add-token (pcb/empty-changes) (:name token-set) token)))]
|
||||
(rx/of
|
||||
(set-selected-token-set-id token-set-name)
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn delete-token
|
||||
[set-name token-name]
|
||||
(dm/assert! (string? set-name))
|
||||
(dm/assert! (string? token-name))
|
||||
(ptk/reify ::delete-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (get state :workspace-data)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token set-name token-name))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-token
|
||||
[token-name]
|
||||
(dm/assert! (string? token-name))
|
||||
(ptk/reify ::duplicate-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [token (some-> (wtts/get-selected-token-set state)
|
||||
(ctob/get-token token-name)
|
||||
(update :name #(str/concat % "-copy")))]
|
||||
(rx/of
|
||||
(update-create-token {:token token}))))))
|
||||
|
||||
(defn set-token-type-section-open
|
||||
[token-type open?]
|
||||
(ptk/reify ::set-token-type-section-open
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-tokens :open-status token-type] open?))))
|
||||
|
||||
;; === Token Context Menu
|
||||
|
||||
(defn show-token-context-menu
|
||||
[{:keys [position _token-name] :as params}]
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::show-token-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-context-menu] params))))
|
||||
|
||||
(def hide-token-context-menu
|
||||
(ptk/reify ::hide-token-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-context-menu] nil))))
|
||||
|
||||
;; === Token Set Context Menu
|
||||
|
||||
(defn show-token-set-context-menu
|
||||
[{:keys [position _token-set-name] :as params}]
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::show-token-set-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-set-context-menu] params))))
|
||||
|
||||
(def hide-token-set-context-menu
|
||||
(ptk/reify ::hide-token-set-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-set-context-menu] nil))))
|
||||
|
||||
;; === Import Export Context Menu
|
||||
|
||||
(defn show-import-export-context-menu
|
||||
[{:keys [position] :as params}]
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::show-import-export-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :import-export-context-menu] params))))
|
||||
|
||||
(def hide-import-export-set-context-menu
|
||||
(ptk/reify ::hide-import-export-set-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :import-export-set-context-menu] nil))))
|
|
@ -44,7 +44,11 @@
|
|||
|
||||
:layers
|
||||
{:del #{:document-history :assets}
|
||||
:add #{:sitemap :layers}}})
|
||||
:add #{:sitemap :layers}}
|
||||
|
||||
:tokens
|
||||
{:del #{:sitemap :layers :document-history :assets}
|
||||
:add #{:tokens}}})
|
||||
|
||||
(def valid-options-mode
|
||||
#{:design :prototype :inspect})
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
[app.common.files.helpers :as cph]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.tokens.token-set :as wtts]
|
||||
[okulary.core :as l]))
|
||||
|
||||
;; ---- Global refs
|
||||
|
@ -205,6 +207,9 @@
|
|||
(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))
|
||||
|
@ -448,6 +453,65 @@
|
|||
ids)))
|
||||
st/state =))
|
||||
|
||||
;; ---- Token refs
|
||||
|
||||
(def tokens-lib
|
||||
(l/derived :tokens-lib workspace-data))
|
||||
|
||||
(def workspace-token-theme-groups
|
||||
(l/derived (d/nilf ctob/get-theme-groups) tokens-lib))
|
||||
|
||||
(defn workspace-token-theme
|
||||
[group name]
|
||||
(l/derived
|
||||
(fn [lib]
|
||||
(when lib
|
||||
(ctob/get-theme lib group name)))
|
||||
tokens-lib))
|
||||
|
||||
(def workspace-token-theme-tree-no-hidden
|
||||
(l/derived (fn [lib]
|
||||
(or
|
||||
(some-> lib
|
||||
(ctob/delete-theme ctob/hidden-token-theme-group ctob/hidden-token-theme-name)
|
||||
(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-temporary-theme? %) workspace-token-themes))
|
||||
|
||||
(def workspace-selected-token-set-id
|
||||
(l/derived wtts/get-selected-token-set-id st/state))
|
||||
|
||||
(def workspace-ordered-token-sets
|
||||
(l/derived #(or (some-> % ctob/get-sets) []) tokens-lib))
|
||||
|
||||
(def workspace-active-theme-paths
|
||||
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
|
||||
|
||||
(def workspace-active-theme-paths-no-hidden
|
||||
(l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths))
|
||||
|
||||
(def workspace-active-set-names
|
||||
(l/derived (d/nilf ctob/get-active-themes-set-names) tokens-lib))
|
||||
|
||||
(def workspace-active-theme-sets-tokens
|
||||
(l/derived #(or (some-> % ctob/get-active-themes-set-tokens) {}) tokens-lib))
|
||||
|
||||
(def workspace-selected-token-set-token
|
||||
(fn [token-name]
|
||||
(l/derived
|
||||
#(some-> (wtts/get-selected-token-set %)
|
||||
(ctob/get-token token-name))
|
||||
st/state)))
|
||||
|
||||
(def workspace-selected-token-set-tokens
|
||||
(l/derived #(or (wtts/get-selected-token-set-tokens %) {}) st/state))
|
||||
|
||||
;; ---- Viewer refs
|
||||
|
||||
(defn lookup-viewer-objects-by-id
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
content (mf/use-state "")
|
||||
|
||||
disabled? (or (str/blank? @content)
|
||||
(str/empty-or-nil? @content))
|
||||
(str/empty? @content))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
|
@ -159,7 +159,7 @@
|
|||
pos-y (* (:y position) zoom)
|
||||
|
||||
disabled? (or (str/blank? content)
|
||||
(str/empty-or-nil? content))
|
||||
(str/empty? content))
|
||||
|
||||
on-esc
|
||||
(mf/use-fn
|
||||
|
@ -230,7 +230,7 @@
|
|||
(fn [] (on-submit @content)))
|
||||
|
||||
disabled? (or (str/blank? @content)
|
||||
(str/empty-or-nil? @content))]
|
||||
(str/empty? @content))]
|
||||
|
||||
[:div {:class (stl/css :edit-form)}
|
||||
[:& resizing-textarea {:value @content
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(set! *warn-on-infer* false)
|
||||
|
||||
(mf/defc tab-element
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [children]}]
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
(def libraries (mf/create-context nil))
|
||||
(def components-v2 (mf/create-context nil))
|
||||
(def design-tokens (mf/create-context nil))
|
||||
|
||||
(def current-scroll (mf/create-context nil))
|
||||
(def current-zoom (mf/create-context nil))
|
||||
|
|
|
@ -130,3 +130,21 @@
|
|||
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
%base-button-action {
|
||||
--button-bg-color: transparent;
|
||||
--button-fg-color: var(--color-foreground-secondary);
|
||||
|
||||
--button-hover-bg-color: transparent;
|
||||
--button-hover-fg-color: var(--color-accent-primary);
|
||||
|
||||
--button-active-bg-color: var(--color-background-quaternary);
|
||||
|
||||
--button-disabled-bg-color: transparent;
|
||||
--button-disabled-fg-color: var(--color-accent-primary-muted);
|
||||
|
||||
--button-focus-bg-color: transparent;
|
||||
--button-focus-fg-color: var(--color-accent-primary);
|
||||
--button-focus-inner-ring-color: transparent;
|
||||
--button-focus-outer-ring-color: var(--color-accent-primary);
|
||||
}
|
||||
|
|
|
@ -12,9 +12,6 @@
|
|||
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def button-variants (set '("primary" "secondary" "ghost" "destructive")))
|
||||
|
||||
|
||||
(def ^:private schema:icon-button
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
|
@ -22,7 +19,7 @@
|
|||
[:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:aria-label :string]
|
||||
[:variant {:optional true}
|
||||
[:maybe [:enum "primary" "secondary" "ghost" "destructive"]]]])
|
||||
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
|
||||
|
||||
(mf/defc icon-button*
|
||||
{::mf/props :obj
|
||||
|
@ -33,6 +30,7 @@
|
|||
:icon-button-primary (= variant "primary")
|
||||
:icon-button-secondary (= variant "secondary")
|
||||
:icon-button-ghost (= variant "ghost")
|
||||
:icon-button-action (= variant "action")
|
||||
:icon-button-destructive (= variant "destructive")))
|
||||
props (mf/spread-props props {:class class :title aria-label})]
|
||||
[:> "button" props [:> icon* {:id icon :aria-label aria-label}] children]))
|
||||
|
|
|
@ -31,3 +31,7 @@
|
|||
.icon-button-destructive {
|
||||
@extend %base-button-destructive;
|
||||
}
|
||||
|
||||
.icon-button-action {
|
||||
@extend %base-button-action;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
},
|
||||
disabled: { control: "boolean" },
|
||||
variant: {
|
||||
options: ["primary", "secondary", "ghost", "destructive"],
|
||||
options: ["primary", "secondary", "ghost", "destructive", "action"],
|
||||
control: { type: "select" },
|
||||
},
|
||||
},
|
||||
|
@ -59,6 +59,12 @@ export const Ghost = {
|
|||
},
|
||||
};
|
||||
|
||||
export const Action = {
|
||||
args: {
|
||||
variant: "action",
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive = {
|
||||
args: {
|
||||
variant: "destructive",
|
||||
|
|
|
@ -50,6 +50,10 @@
|
|||
(def ^:icon-id align-top "align-top")
|
||||
(def ^:icon-id align-vertical-center "align-vertical-center")
|
||||
(def ^:icon-id arrow "arrow")
|
||||
(def ^:icon-id arrow-up "arrow-up")
|
||||
(def ^:icon-id arrow-down "arrow-down")
|
||||
(def ^:icon-id arrow-left "arrow-left")
|
||||
(def ^:icon-id arrow-right "arrow-right")
|
||||
(def ^:icon-id asc-sort "asc-sort")
|
||||
(def ^:icon-id board "board")
|
||||
(def ^:icon-id boards-thumbnail "boards-thumbnail")
|
||||
|
|
|
@ -32,17 +32,17 @@
|
|||
|
||||
(let [level (or level "1")
|
||||
tag (dm/str "h" level)
|
||||
class (dm/str (or class "") " " (stl/css-case :display-typography (= typography t/display)
|
||||
:title-large-typography (= typography t/title-large)
|
||||
:title-medium-typography (= typography t/title-medium)
|
||||
:title-small-typography (= typography t/title-small)
|
||||
:headline-large-typography (= typography t/headline-large)
|
||||
:headline-medium-typography (= typography t/headline-medium)
|
||||
:headline-small-typography (= typography t/headline-small)
|
||||
:body-large-typography (= typography t/body-large)
|
||||
:body-medium-typography (= typography t/body-medium)
|
||||
:body-small-typography (= typography t/body-small)
|
||||
:code-font-typography (= typography t/code-font)))
|
||||
class (dm/str class " " (stl/css-case :display-typography (= typography t/display)
|
||||
:title-large-typography (= typography t/title-large)
|
||||
:title-medium-typography (= typography t/title-medium)
|
||||
:title-small-typography (= typography t/title-small)
|
||||
:headline-large-typography (= typography t/headline-large)
|
||||
:headline-medium-typography (= typography t/headline-medium)
|
||||
:headline-small-typography (= typography t/headline-small)
|
||||
:body-large-typography (= typography t/body-large)
|
||||
:body-medium-typography (= typography t/body-medium)
|
||||
:body-small-typography (= typography t/body-small)
|
||||
:code-font-typography (= typography t/code-font)))
|
||||
props (mf/spread-props props {:class class})]
|
||||
[:> tag props
|
||||
children]))
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
||||
[app.main.ui.workspace.tokens.modals]
|
||||
[app.main.ui.workspace.viewport :refer [viewport]]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -179,6 +180,7 @@
|
|||
file-ready? (mf/deref file-ready*)
|
||||
|
||||
components-v2? (features/use-feature "components/v2")
|
||||
design-tokens? (features/use-feature "design-tokens/v1")
|
||||
|
||||
background-color (:background-color wglobal)]
|
||||
|
||||
|
@ -207,15 +209,16 @@
|
|||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-page-id) {:value page-id}
|
||||
[:& (mf/provider ctx/components-v2) {:value components-v2?}
|
||||
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
|
||||
[:& (mf/provider ctx/team-permissions) {:value permissions}
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
[:& context-menu]
|
||||
(if ^boolean file-ready?
|
||||
[:& workspace-page {:page-id page-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}]
|
||||
[:& workspace-loader])]]]]]]]]))
|
||||
[:& (mf/provider ctx/design-tokens) {:value design-tokens?}
|
||||
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
|
||||
[:& (mf/provider ctx/team-permissions) {:value permissions}
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
[:& context-menu]
|
||||
(if ^boolean file-ready?
|
||||
[:& workspace-page {:page-id page-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}]
|
||||
[:& workspace-loader])]]]]]]]]]))
|
||||
|
|
|
@ -53,6 +53,25 @@
|
|||
|
||||
;; --- Color Picker Modal
|
||||
|
||||
(defn use-color-picker-css-variables! [node-ref current-color]
|
||||
(mf/with-effect [current-color]
|
||||
(let [node (mf/ref-val node-ref)
|
||||
{:keys [r g b h v]} current-color
|
||||
rgb [r g b]
|
||||
hue-rgb (cc/hsv->rgb [h 1.0 255])
|
||||
hsl-from (cc/hsv->hsl [h 0.0 v])
|
||||
hsl-to (cc/hsv->hsl [h 1.0 v])
|
||||
|
||||
format-hsl (fn [[h s l]]
|
||||
(str/fmt "hsl(%s, %s, %s)"
|
||||
h
|
||||
(str (* s 100) "%")
|
||||
(str (* l 100) "%")))]
|
||||
(dom/set-css-property! node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
|
||||
(mf/defc colorpicker
|
||||
{::mf/props :obj}
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}]
|
||||
|
@ -264,23 +283,7 @@
|
|||
(st/emit! (dc/update-colorpicker data)))
|
||||
|
||||
;; Updates the CSS color variable when there is a change in the color
|
||||
(mf/with-effect [current-color]
|
||||
(let [node (mf/ref-val node-ref)
|
||||
{:keys [r g b h v]} current-color
|
||||
rgb [r g b]
|
||||
hue-rgb (cc/hsv->rgb [h 1.0 255])
|
||||
hsl-from (cc/hsv->hsl [h 0.0 v])
|
||||
hsl-to (cc/hsv->hsl [h 1.0 v])
|
||||
|
||||
format-hsl (fn [[h s l]]
|
||||
(str/fmt "hsl(%s, %s, %s)"
|
||||
h
|
||||
(str (* s 100) "%")
|
||||
(str (* l 100) "%")))]
|
||||
(dom/set-css-property! node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))
|
||||
(use-color-picker-css-variables! node-ref current-color)
|
||||
|
||||
;; Updates color when pixel picker is used
|
||||
(mf/with-effect [picking-color? picked-color picked-color-select]
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
[app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]]
|
||||
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
|
||||
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox]]
|
||||
[app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab]]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -52,8 +53,11 @@
|
|||
mode-inspect? (= options-mode :inspect)
|
||||
project (mf/deref refs/workspace-project)
|
||||
|
||||
design-tokens? (mf/use-ctx muc/design-tokens)
|
||||
|
||||
section (cond (or mode-inspect? (contains? layout :layers)) :layers
|
||||
(contains? layout :assets) :assets)
|
||||
(contains? layout :assets) :assets
|
||||
(contains? layout :tokens) :tokens)
|
||||
|
||||
shortcuts? (contains? layout :shortcuts)
|
||||
show-debug? (contains? layout :debug-panel)
|
||||
|
@ -97,6 +101,9 @@
|
|||
assets-tab
|
||||
(mf/html [:& assets-toolbox {:size (- size 58)}])
|
||||
|
||||
tokens-tab
|
||||
(mf/html [:& tokens-sidebar-tab])
|
||||
|
||||
tabs
|
||||
(if ^boolean mode-inspect?
|
||||
#js [#js {:label (tr "workspace.sidebar.layers")
|
||||
|
@ -107,7 +114,11 @@
|
|||
:content layers-tab}
|
||||
#js {:label (tr "workspace.toolbar.assets")
|
||||
:id "assets"
|
||||
:content assets-tab}])]
|
||||
:content assets-tab}
|
||||
(when design-tokens?
|
||||
#js {:label "Tokens"
|
||||
:id "tokens"
|
||||
:content tokens-tab})])]
|
||||
|
||||
[:& (mf/provider muc/sidebar) {:value :left}
|
||||
[:aside {:ref parent-ref
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
|
||||
(mf/defc asset-section
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [children file-id title section assets-count open?]}]
|
||||
[{:keys [children file-id title section assets-count icon open?]}]
|
||||
(let [children (-> (array/normalize-to-array children)
|
||||
(array/without-nils))
|
||||
|
||||
|
@ -151,7 +151,7 @@
|
|||
(mf/html
|
||||
[:span {:class (stl/css :title-name)}
|
||||
[:span {:class (stl/css :section-icon)}
|
||||
[:& section-icon {:section section}]]
|
||||
[:& (or icon section-icon) {:section section}]]
|
||||
[:span {:class (stl/css :section-name)}
|
||||
title]
|
||||
|
||||
|
|
|
@ -69,11 +69,11 @@
|
|||
(defn group-assets
|
||||
"Convert a list of assets in a nested structure like this:
|
||||
|
||||
{'': [{assetA} {assetB}]
|
||||
'group1': {'': [{asset1A} {asset1B}]
|
||||
'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]}
|
||||
'subgroup12': {'': [{asset12A}]}}
|
||||
'group2': {'subgroup21': {'': [{asset21A}}}}
|
||||
{'': [assetA assetB]
|
||||
'group1': {'': [asset1A asset1B]
|
||||
'subgroup11': {'': [asset11A asset11B asset11C]}
|
||||
'subgroup12': {'': [asset12A]}}
|
||||
'group2': {'subgroup21': {'': [asset21A]}}}
|
||||
"
|
||||
[assets reverse-sort?]
|
||||
(when-not (empty? assets)
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
[app.common.logic.shapes :as cls]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.constants :refer [size-presets]]
|
||||
[app.main.data.tokens :as dt]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
|
@ -24,6 +26,10 @@
|
|||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.core :as wtc]
|
||||
[app.main.ui.workspace.tokens.editable-select :refer [editable-select]]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.main.ui.workspace.tokens.token-types :as wtty]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[clojure.set :refer [rename-keys union]]
|
||||
|
@ -96,6 +102,34 @@
|
|||
selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
||||
selection-parents (mf/deref selection-parents-ref)
|
||||
|
||||
tokens (sd/use-active-theme-sets-tokens)
|
||||
tokens-by-type (mf/use-memo
|
||||
(mf/deps tokens)
|
||||
#(ctob/group-by-type tokens))
|
||||
|
||||
border-radius-tokens (:border-radius tokens-by-type)
|
||||
border-radius-options (mf/use-memo
|
||||
(mf/deps shape border-radius-tokens)
|
||||
#(wtc/tokens->select-options
|
||||
{:shape shape
|
||||
:tokens border-radius-tokens
|
||||
:attributes (wtty/token-attributes :border-radius)}))
|
||||
sizing-tokens (:sizing tokens-by-type)
|
||||
width-options (mf/use-memo
|
||||
(mf/deps shape sizing-tokens)
|
||||
#(wtc/tokens->select-options
|
||||
{:shape shape
|
||||
:tokens sizing-tokens
|
||||
:attributes (wtty/token-attributes :sizing)
|
||||
:selected-attributes #{:width}}))
|
||||
height-options (mf/use-memo
|
||||
(mf/deps shape sizing-tokens)
|
||||
#(wtc/tokens->select-options
|
||||
{:shape shape
|
||||
:tokens sizing-tokens
|
||||
:attributes (wtty/token-attributes :sizing)
|
||||
:selected-attributes #{:height}}))
|
||||
|
||||
flex-child? (->> selection-parents (some ctl/flex-layout?))
|
||||
absolute? (ctl/item-absolute? shape)
|
||||
flex-container? (ctl/flex-layout? shape)
|
||||
|
@ -209,8 +243,20 @@
|
|||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(udw/update-dimensions ids attr value))))
|
||||
(let [token-value (wtc/maybe-resolve-token-value value)
|
||||
undo-id (js/Symbol)]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes ids
|
||||
(if token-value
|
||||
#(assoc-in % [:applied-tokens attr] (:id value))
|
||||
#(d/dissoc-in % [:applied-tokens attr]))
|
||||
{:reg-objects? true
|
||||
:attrs [:applied-tokens]})
|
||||
(udw/update-dimensions ids attr (or token-value value))
|
||||
(dwu/commit-undo-transaction undo-id)))))
|
||||
|
||||
|
||||
|
||||
on-proportion-lock-change
|
||||
(mf/use-fn
|
||||
|
@ -256,7 +302,7 @@
|
|||
(update-fn shape)
|
||||
shape))
|
||||
{:reg-objects? true
|
||||
:attrs [:rx :ry :r1 :r2 :r3 :r4]})))
|
||||
:attrs [:rx :ry :r1 :r2 :r3 :r4 :applied-tokens]})))
|
||||
|
||||
on-switch-to-radius-1
|
||||
(mf/use-fn
|
||||
|
@ -281,11 +327,27 @@
|
|||
(on-switch-to-radius-4)
|
||||
(on-switch-to-radius-1))))
|
||||
|
||||
on-border-radius-token-unapply
|
||||
(mf/use-fn
|
||||
(mf/deps ids change-radius)
|
||||
(fn [token]
|
||||
(let [token-value (wtc/maybe-resolve-token-value token)]
|
||||
(st/emit!
|
||||
(change-radius (fn [shape]
|
||||
(-> (dt/unapply-token-id shape (wtty/token-attributes :border-radius))
|
||||
(ctsr/set-radius-1 token-value))))))))
|
||||
|
||||
on-radius-1-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids change-radius)
|
||||
(fn [value]
|
||||
(st/emit! (change-radius #(ctsr/set-radius-1 % value)))))
|
||||
(let [token-value (wtc/maybe-resolve-token-value value)]
|
||||
(st/emit!
|
||||
(change-radius (fn [shape]
|
||||
(-> (dt/maybe-apply-token-to-shape {:token (when token-value value)
|
||||
:shape shape
|
||||
:attributes (wtty/token-attributes :border-radius)})
|
||||
(ctsr/set-radius-1 (or token-value value)))))))))
|
||||
|
||||
on-radius-multi-change
|
||||
(mf/use-fn
|
||||
|
@ -394,24 +456,34 @@
|
|||
:disabled disabled-width-sizing?)
|
||||
:title (tr "workspace.options.width")}
|
||||
[:span {:class (stl/css :icon-text)} "W"]
|
||||
[:> numeric-input* {:min 0.01
|
||||
:no-validate true
|
||||
:placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--")
|
||||
:on-change on-width-change
|
||||
:disabled disabled-width-sizing?
|
||||
:class (stl/css :numeric-input)
|
||||
:value (:width values)}]]
|
||||
[:& editable-select
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:class (stl/css :token-select)
|
||||
:disabled disabled-width-sizing?
|
||||
:on-change on-width-change
|
||||
:on-token-remove #(on-width-change (wtc/maybe-resolve-token-value %))
|
||||
:options width-options
|
||||
:position :left
|
||||
:value (:width values)
|
||||
:input-props {:type "number"
|
||||
:no-validate true
|
||||
:min 0.01}}]]
|
||||
[:div {:class (stl/css-case :height true
|
||||
:disabled disabled-height-sizing?)
|
||||
:title (tr "workspace.options.height")}
|
||||
[:span {:class (stl/css :icon-text)} "H"]
|
||||
[:> numeric-input* {:min 0.01
|
||||
:no-validate true
|
||||
:placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--")
|
||||
:on-change on-height-change
|
||||
:disabled disabled-height-sizing?
|
||||
:class (stl/css :numeric-input)
|
||||
:value (:height values)}]]
|
||||
[:& editable-select
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:class (stl/css :token-select)
|
||||
:disabled disabled-height-sizing?
|
||||
:on-change on-height-change
|
||||
:on-token-remove #(on-height-change (wtc/maybe-resolve-token-value %))
|
||||
:options height-options
|
||||
:position :right
|
||||
:value (:height values)
|
||||
:input-props {:type "number"
|
||||
:no-validate true
|
||||
:min 0.01}}]]
|
||||
[:button {:class (stl/css-case
|
||||
:lock-size-btn true
|
||||
:selected (true? proportion-lock)
|
||||
|
@ -468,13 +540,16 @@
|
|||
[:div {:class (stl/css :radius-1)
|
||||
:title (tr "workspace.options.radius")}
|
||||
[:span {:class (stl/css :icon)} i/corner-radius]
|
||||
[:> numeric-input*
|
||||
[:& editable-select
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:ref radius-input-ref
|
||||
:min 0
|
||||
:class (stl/css :token-select)
|
||||
:on-change on-radius-1-change
|
||||
:class (stl/css :numeric-input)
|
||||
:value (:rx values)}]]
|
||||
:on-token-remove on-border-radius-token-unapply
|
||||
:options border-radius-options
|
||||
:position :right
|
||||
:value (:rx values)
|
||||
:input-props {:type "number"
|
||||
:min 0}}]]
|
||||
|
||||
@radius-multi?
|
||||
[:div {:class (stl/css :radius-1)
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
|
||||
.size {
|
||||
@include flexRow;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.height,
|
||||
|
@ -186,6 +187,7 @@
|
|||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radius-4 {
|
||||
|
|
183
frontend/src/app/main/ui/workspace/tokens/changes.cljs
Normal file
183
frontend/src/app/main/ui/workspace/tokens/changes.cljs
Normal file
|
@ -0,0 +1,183 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.changes
|
||||
(:require
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.colors :as wdc]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;; Token Updates ---------------------------------------------------------------
|
||||
|
||||
(defn apply-token
|
||||
"Apply `attributes` that match `token` for `shape-ids`.
|
||||
|
||||
Optionally remove attributes from `attributes-to-remove`,
|
||||
this is useful for applying a single attribute from an attributes set
|
||||
while removing other applied tokens from this set."
|
||||
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape] :as _props}]
|
||||
(ptk/reify ::apply-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [tokens (some-> (get-in state [:workspace-data :tokens-lib])
|
||||
(ctob/get-active-themes-set-tokens))]
|
||||
(->> (rx/from (sd/resolve-tokens+ tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [undo-id (js/Symbol)
|
||||
resolved-value (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (wtt/attributes-map attributes token)]
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always (update :applied-tokens merge tokenized-attributes))))
|
||||
(when on-update-shape
|
||||
(on-update-shape resolved-value shape-ids attributes))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn unapply-token
|
||||
"Removes `attributes` that match `token` for `shape-ids`.
|
||||
|
||||
Doesn't update shape attributes."
|
||||
[{:keys [attributes token shape-ids] :as _props}]
|
||||
(ptk/reify ::unapply-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(let [remove-token #(when % (wtt/remove-attributes-for-token attributes token %))]
|
||||
(dwsh/update-shapes
|
||||
shape-ids
|
||||
(fn [shape]
|
||||
(update shape :applied-tokens remove-token))))))))
|
||||
|
||||
(defn toggle-token
|
||||
[{:keys [token-type-props token shapes] :as _props}]
|
||||
(ptk/reify ::on-toggle-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [attributes all-attributes on-update-shape]} token-type-props
|
||||
unapply-tokens? (wtt/shapes-token-applied? token shapes (or all-attributes attributes))
|
||||
shape-ids (map :id shapes)]
|
||||
(if unapply-tokens?
|
||||
(rx/of
|
||||
(unapply-token {:attributes (or all-attributes attributes)
|
||||
:token token
|
||||
:shape-ids shape-ids}))
|
||||
(rx/of
|
||||
(apply-token {:attributes attributes
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape on-update-shape})))))))
|
||||
|
||||
;; Shape Updates ---------------------------------------------------------------
|
||||
|
||||
(defn update-shape-radius-all [value shape-ids]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(when (ctsr/has-radius? shape)
|
||||
(ctsr/set-radius-1 shape value)))
|
||||
{:reg-objects? true
|
||||
:attrs ctt/border-radius-keys}))
|
||||
|
||||
(defn update-opacity [value shape-ids]
|
||||
(when (<= 0 value 1)
|
||||
(dwsh/update-shapes shape-ids #(assoc % :opacity value))))
|
||||
|
||||
(defn update-rotation [value shape-ids]
|
||||
(ptk/reify ::update-shape-rotation
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(udw/trigger-bounding-box-cloaking shape-ids)
|
||||
(udw/increase-rotation shape-ids value)))))
|
||||
|
||||
(defn update-shape-radius-single-corner [value shape-ids attributes]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(when (ctsr/has-radius? shape)
|
||||
(cond-> shape
|
||||
(:rx shape) (ctsr/switch-to-radius-4)
|
||||
:always (ctsr/set-radius-4 (first attributes) value))))
|
||||
{:reg-objects? true
|
||||
:attrs [:rx :ry :r1 :r2 :r3 :r4]}))
|
||||
|
||||
(defn update-stroke-width
|
||||
[value shape-ids]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(when (seq (:strokes shape))
|
||||
(assoc-in shape [:strokes 0 :stroke-width] value)))
|
||||
{:reg-objects? true
|
||||
:attrs [:strokes]}))
|
||||
|
||||
(defn update-color
|
||||
[value shape-ids]
|
||||
(let [color (some->> value
|
||||
(tinycolor/valid-color)
|
||||
(tinycolor/->hex)
|
||||
(str "#"))]
|
||||
(wdc/change-fill shape-ids {:color color} 0)))
|
||||
|
||||
(defn update-shape-dimensions [value shape-ids attributes]
|
||||
(ptk/reify ::update-shape-dimensions
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(when (:width attributes) (dwt/update-dimensions shape-ids :width value))
|
||||
(when (:height attributes) (dwt/update-dimensions shape-ids :height value))))))
|
||||
|
||||
(defn- attributes->layout-gap [attributes value]
|
||||
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
|
||||
(zipmap (repeat value)))]
|
||||
{:layout-gap layout-gap}))
|
||||
|
||||
(defn update-layout-padding [value shape-ids attrs]
|
||||
(dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat value))}))
|
||||
|
||||
(defn update-layout-spacing [value shape-ids attributes]
|
||||
(ptk/reify ::update-layout-spacing
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [layout-shape-ids (->> (wsh/lookup-shapes state shape-ids)
|
||||
(eduction
|
||||
(filter :layout)
|
||||
(map :id)))
|
||||
layout-attributes (attributes->layout-gap attributes value)]
|
||||
(rx/of
|
||||
(dwsl/update-layout layout-shape-ids layout-attributes))))))
|
||||
|
||||
(defn update-shape-position [value shape-ids attributes]
|
||||
(ptk/reify ::update-shape-position
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(map #(dwt/update-position % (zipmap attributes (repeat value))) shape-ids)))))
|
||||
|
||||
(defn update-layout-sizing-limits [value shape-ids attributes]
|
||||
(ptk/reify ::update-layout-sizing-limits
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [props (-> {:layout-item-min-w value
|
||||
:layout-item-min-h value
|
||||
:layout-item-max-w value
|
||||
:layout-item-max-h value}
|
||||
(select-keys attributes))]
|
||||
(dwsl/update-layout-child shape-ids props)))))
|
131
frontend/src/app/main/ui/workspace/tokens/common.cljs
Normal file
131
frontend/src/app/main/ui/workspace/tokens/common.cljs
Normal file
|
@ -0,0 +1,131 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.common
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.shortcuts :as dsc]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
;; Helpers ---------------------------------------------------------------------
|
||||
|
||||
(defn camel-keys [m]
|
||||
(->> m
|
||||
(d/deep-mapm
|
||||
(fn [[k v]]
|
||||
(if (or (keyword? k) (string? k))
|
||||
[(keyword (str/camel (name k))) v]
|
||||
[k v])))))
|
||||
|
||||
(defn direction-select
|
||||
"Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`.
|
||||
|
||||
`direction` accepts `:up` or `:down`."
|
||||
[direction n coll]
|
||||
(let [last-n (dec (count coll))
|
||||
next-n (case direction
|
||||
:up (dec n)
|
||||
:down (inc n))
|
||||
wrap-around-n (cond
|
||||
(neg? next-n) last-n
|
||||
(> next-n last-n) 0
|
||||
:else next-n)]
|
||||
wrap-around-n))
|
||||
|
||||
(defn use-arrow-highlight [{:keys [shortcuts-key options on-select]}]
|
||||
(let [highlighted* (mf/use-state nil)
|
||||
highlighted (deref highlighted*)
|
||||
on-dehighlight #(reset! highlighted* nil)
|
||||
on-keyup (fn [event]
|
||||
(cond
|
||||
(and (kbd/enter? event) highlighted) (on-select (nth options highlighted))
|
||||
(kbd/up-arrow? event) (do
|
||||
(dom/prevent-default event)
|
||||
(->> (direction-select :up (or highlighted 0) options)
|
||||
(reset! highlighted*)))
|
||||
(kbd/down-arrow? event) (do
|
||||
(dom/prevent-default event)
|
||||
(->> (direction-select :down (or highlighted -1) options)
|
||||
(reset! highlighted*)))))]
|
||||
(mf/with-effect [highlighted]
|
||||
(let [shortcuts-key shortcuts-key
|
||||
keys [(events/listen globals/document EventType.KEYUP on-keyup)
|
||||
(events/listen globals/document EventType.KEYDOWN dom/prevent-default)]]
|
||||
(st/emit! (dsc/push-shortcuts shortcuts-key {}))
|
||||
(fn []
|
||||
(doseq [key keys]
|
||||
(events/unlistenByKey key))
|
||||
(st/emit! (dsc/pop-shortcuts shortcuts-key)))))
|
||||
{:highlighted highlighted
|
||||
:on-dehighlight on-dehighlight}))
|
||||
|
||||
(defn use-dropdown-open-state []
|
||||
(let [open? (mf/use-state false)
|
||||
on-open (mf/use-fn #(reset! open? true))
|
||||
on-close (mf/use-fn #(reset! open? false))
|
||||
on-toggle (mf/use-fn #(swap! open? not))]
|
||||
{:dropdown-open? @open?
|
||||
:on-open-dropdown on-open
|
||||
:on-close-dropdown on-close
|
||||
:on-toggle-dropdown on-toggle}))
|
||||
|
||||
;; Components ------------------------------------------------------------------
|
||||
|
||||
(mf/defc dropdown-select
|
||||
[{:keys [id _shortcuts-key options on-close element-ref on-select] :as props}]
|
||||
(let [{:keys [highlighted on-dehighlight]} (use-arrow-highlight props)]
|
||||
[:& dropdown {:show true
|
||||
:on-close on-close}
|
||||
[:> :div {:class (stl/css :dropdown)
|
||||
:on-mouse-enter on-dehighlight
|
||||
:ref element-ref}
|
||||
[:ul {:class (stl/css :dropdown-list)}
|
||||
(for [[index item] (d/enumerate options)]
|
||||
(cond
|
||||
(= :separator item)
|
||||
[:li {:class (stl/css :separator)
|
||||
:key (dm/str id "-" index)}]
|
||||
:else
|
||||
(let [{:keys [label selected? disabled?]} item
|
||||
highlighted? (= highlighted index)]
|
||||
[:li
|
||||
{:key (str id "-" index)
|
||||
:class (stl/css-case :dropdown-element true
|
||||
:is-selected selected?
|
||||
:is-highlighted highlighted?)
|
||||
:data-label label
|
||||
:disabled disabled?
|
||||
:on-click #(on-select item)}
|
||||
[:span {:class (stl/css :label)} label]
|
||||
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
|
||||
|
||||
(mf/defc labeled-input
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [label input-props auto-complete? error? render-right]}]
|
||||
(let [input-props (cond-> input-props
|
||||
:always camel-keys
|
||||
;; Disable auto-complete on form fields for proprietary password managers
|
||||
;; https://github.com/orgs/tokens-studio/projects/69/views/11?pane=issue&itemId=63724204
|
||||
(not auto-complete?) (assoc "data-1p-ignore" true
|
||||
"data-lpignore" true
|
||||
:auto-complete "off"))]
|
||||
[:label {:class (stl/css-case :labeled-input true
|
||||
:labeled-input-error error?)}
|
||||
[:span {:class (stl/css :label)} label]
|
||||
[:& :input input-props]
|
||||
(when render-right
|
||||
[:& render-right])]))
|
115
frontend/src/app/main/ui/workspace/tokens/common.scss
Normal file
115
frontend/src/app/main/ui/workspace/tokens/common.scss
Normal file
|
@ -0,0 +1,115 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.input {
|
||||
@extend .input-element;
|
||||
}
|
||||
|
||||
.labeled-input {
|
||||
@extend .input-element;
|
||||
.label {
|
||||
width: auto;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.labeled-input-error {
|
||||
border: 1px solid var(--status-color-error-500) !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend .button-primary;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
@extend .button-tertiary;
|
||||
height: $s-32;
|
||||
width: $s-28;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
max-height: $s-320;
|
||||
width: 100%;
|
||||
margin-top: $s-4;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0;
|
||||
height: $s-12;
|
||||
}
|
||||
|
||||
.dropdown-element {
|
||||
@extend .dropdown-element-base;
|
||||
color: var(--menu-foreground-color-rest);
|
||||
display: flex;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.label,
|
||||
.value {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-transform: unset;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
flex: 0.6;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@include flexCenter;
|
||||
translate: -$s-4 0;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
visibility: hidden;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--menu-foreground-color);
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
&.is-highlighted {
|
||||
background-color: var(--button-primary-background-color-rest);
|
||||
span {
|
||||
color: var(--button-primary-foreground-color-rest);
|
||||
}
|
||||
.check-icon svg {
|
||||
stroke: var(--button-primary-foreground-color-rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
336
frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
Normal file
336
frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
Normal file
|
@ -0,0 +1,336 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.context-menu
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.tokens :as dt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.changes :as wtch]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[app.main.ui.workspace.tokens.token-types :as wtty]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.timers :as timers]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Actions ---------------------------------------------------------------------
|
||||
|
||||
(defn attribute-actions [token selected-shapes attributes]
|
||||
(let [ids-by-attributes (wtt/shapes-ids-by-applied-attributes token selected-shapes attributes)
|
||||
shape-ids (into #{} (map :id selected-shapes))]
|
||||
{:all-selected? (wtt/shapes-applied-all? ids-by-attributes shape-ids attributes)
|
||||
:shape-ids shape-ids
|
||||
:selected-pred #(seq (% ids-by-attributes))}))
|
||||
|
||||
(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape]}]
|
||||
(let [on-update-shape-fn (or on-update-shape
|
||||
(-> (wtty/get-token-properties token)
|
||||
(:on-update-shape)))
|
||||
{:keys [selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)]
|
||||
(map (fn [attribute]
|
||||
(let [selected? (selected-pred attribute)
|
||||
props {:attributes #{attribute}
|
||||
:token token
|
||||
:shape-ids shape-ids}]
|
||||
|
||||
{:title title
|
||||
:selected? selected?
|
||||
:action (fn []
|
||||
(if selected?
|
||||
(st/emit! (wtch/unapply-token props))
|
||||
(st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-fn)))))}))
|
||||
attributes)))
|
||||
|
||||
(defn all-or-sepearate-actions [{:keys [attribute-labels on-update-shape-all on-update-shape]}
|
||||
{:keys [token selected-shapes]}]
|
||||
(let [attributes (set (keys attribute-labels))
|
||||
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)
|
||||
all-action (let [props {:attributes attributes
|
||||
:token token
|
||||
:shape-ids shape-ids}]
|
||||
{:title "All"
|
||||
:selected? all-selected?
|
||||
:action #(if all-selected?
|
||||
(st/emit! (wtch/unapply-token props))
|
||||
(st/emit! (wtch/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))})
|
||||
single-actions (map (fn [[attr title]]
|
||||
(let [selected? (selected-pred attr)]
|
||||
{:title title
|
||||
:selected? (and (not all-selected?) selected?)
|
||||
:action #(let [props {:attributes #{attr}
|
||||
:token token
|
||||
:shape-ids shape-ids}
|
||||
event (cond
|
||||
all-selected? (-> (assoc props :attributes-to-remove attributes)
|
||||
(wtch/apply-token))
|
||||
selected? (wtch/unapply-token props)
|
||||
:else (-> (assoc props :on-update-shape on-update-shape)
|
||||
(wtch/apply-token)))]
|
||||
(st/emit! event))}))
|
||||
attribute-labels)]
|
||||
(concat [all-action] single-actions)))
|
||||
|
||||
(defn spacing-attribute-actions [{:keys [token selected-shapes] :as context-data}]
|
||||
(let [on-update-shape-padding wtch/update-layout-padding
|
||||
padding-attrs {:p1 "Top"
|
||||
:p2 "Right"
|
||||
:p3 "Bottom"
|
||||
:p4 "Left"}
|
||||
all-padding-attrs (into #{} (keys padding-attrs))
|
||||
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes all-padding-attrs)
|
||||
horizontal-attributes #{:p1 :p3}
|
||||
horizontal-padding-selected? (and
|
||||
(not all-selected?)
|
||||
(every? selected-pred horizontal-attributes))
|
||||
vertical-attributes #{:p2 :p4}
|
||||
vertical-padding-selected? (and
|
||||
(not all-selected?)
|
||||
(every? selected-pred vertical-attributes))
|
||||
padding-items [{:title "All"
|
||||
:selected? all-selected?
|
||||
:action (fn []
|
||||
(let [props {:attributes all-padding-attrs
|
||||
:token token
|
||||
:shape-ids shape-ids}]
|
||||
(if all-selected?
|
||||
(st/emit! (wtch/unapply-token props))
|
||||
(st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-padding))))))}
|
||||
{:title "Horizontal"
|
||||
:selected? horizontal-padding-selected?
|
||||
:action (fn []
|
||||
(let [props {:token token
|
||||
:shape-ids shape-ids}
|
||||
event (cond
|
||||
all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
|
||||
horizontal-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove horizontal-attributes))
|
||||
:else (wtch/apply-token (assoc props
|
||||
:attributes horizontal-attributes
|
||||
:on-update-shape on-update-shape-padding)))]
|
||||
(st/emit! event)))}
|
||||
{:title "Vertical"
|
||||
:selected? vertical-padding-selected?
|
||||
:action (fn []
|
||||
(let [props {:token token
|
||||
:shape-ids shape-ids}
|
||||
event (cond
|
||||
all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
|
||||
vertical-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
|
||||
:else (wtch/apply-token (assoc props
|
||||
:attributes vertical-attributes
|
||||
:on-update-shape on-update-shape-padding)))]
|
||||
(st/emit! event)))}]
|
||||
single-padding-items (->> padding-attrs
|
||||
(map (fn [[attr title]]
|
||||
(let [same-axis-selected? (cond
|
||||
(get horizontal-attributes attr) horizontal-padding-selected?
|
||||
(get vertical-attributes attr) vertical-padding-selected?
|
||||
:else true)
|
||||
selected? (and
|
||||
(not all-selected?)
|
||||
(not same-axis-selected?)
|
||||
(selected-pred attr))]
|
||||
{:title title
|
||||
:selected? selected?
|
||||
:action #(let [props {:attributes #{attr}
|
||||
:token token
|
||||
:shape-ids shape-ids}
|
||||
event (cond
|
||||
all-selected? (-> (assoc props :attributes-to-remove all-padding-attrs)
|
||||
(wtch/apply-token))
|
||||
selected? (wtch/unapply-token props)
|
||||
:else (-> (assoc props :on-update-shape on-update-shape-padding)
|
||||
(wtch/apply-token)))]
|
||||
(st/emit! event))}))))
|
||||
gap-items (all-or-sepearate-actions {:attribute-labels {:column-gap "Column Gap"
|
||||
:row-gap "Row Gap"}
|
||||
:on-update-shape wtch/update-layout-spacing}
|
||||
context-data)]
|
||||
(concat padding-items
|
||||
single-padding-items
|
||||
[:separator]
|
||||
gap-items)))
|
||||
|
||||
(defn sizing-attribute-actions [context-data]
|
||||
(concat
|
||||
(all-or-sepearate-actions {:attribute-labels {:width "Width"
|
||||
:height "Height"}
|
||||
:on-update-shape wtch/update-shape-dimensions}
|
||||
context-data)
|
||||
[:separator]
|
||||
(all-or-sepearate-actions {:attribute-labels {:layout-item-min-w "Min Width"
|
||||
:layout-item-min-h "Min Height"}
|
||||
:on-update-shape wtch/update-layout-sizing-limits}
|
||||
context-data)
|
||||
[:separator]
|
||||
(all-or-sepearate-actions {:attribute-labels {:layout-item-max-w "Max Width"
|
||||
:layout-item-max-h "Max Height"}
|
||||
:on-update-shape wtch/update-layout-sizing-limits}
|
||||
context-data)))
|
||||
|
||||
(def shape-attribute-actions-map
|
||||
(let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width")]
|
||||
{:border-radius (partial all-or-sepearate-actions {:attribute-labels {:r1 "Top Left"
|
||||
:r2 "Top Right"
|
||||
:r4 "Bottom Left"
|
||||
:r3 "Bottom Right"}
|
||||
:on-update-shape-all wtch/update-shape-radius-all
|
||||
:on-update-shape wtch/update-shape-radius-single-corner})
|
||||
:spacing spacing-attribute-actions
|
||||
:sizing sizing-attribute-actions
|
||||
:rotation (partial generic-attribute-actions #{:rotation} "Rotation")
|
||||
:opacity (partial generic-attribute-actions #{:opacity} "Opacity")
|
||||
:stroke-width stroke-width
|
||||
:dimensions (fn [context-data]
|
||||
(concat
|
||||
[{:title "Spacing" :submenu :spacing}
|
||||
{:title "Sizing" :submenu :sizing}
|
||||
:separator
|
||||
{:title "Border Radius" :submenu :border-radius}]
|
||||
(stroke-width context-data)
|
||||
[:separator]
|
||||
(generic-attribute-actions #{:x} "X" (assoc context-data :on-update-shape wtch/update-shape-position))
|
||||
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape wtch/update-shape-position))))}))
|
||||
|
||||
(defn default-actions [{:keys [token selected-token-set-id]}]
|
||||
(let [{:keys [modal]} (wtty/get-token-properties token)]
|
||||
[{:title "Delete Token"
|
||||
:action #(st/emit! (dt/delete-token selected-token-set-id (:name token)))}
|
||||
{:title "Duplicate Token"
|
||||
:action #(st/emit! (dt/duplicate-token (:name token)))}
|
||||
{:title "Edit Token"
|
||||
:action (fn [event]
|
||||
(let [{:keys [key fields]} modal]
|
||||
(st/emit! dt/hide-token-context-menu)
|
||||
(dom/stop-propagation event)
|
||||
(modal/show! key {:x (.-clientX ^js event)
|
||||
:y (.-clientY ^js event)
|
||||
:position :right
|
||||
:fields fields
|
||||
:action "edit"
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:token token})))}]))
|
||||
|
||||
(defn selection-actions [{:keys [type token] :as context-data}]
|
||||
(let [with-actions (get shape-attribute-actions-map (or type (:type token)))
|
||||
attribute-actions (if with-actions (with-actions context-data) [])]
|
||||
(concat
|
||||
attribute-actions
|
||||
(when (seq attribute-actions) [:separator])
|
||||
(default-actions context-data))))
|
||||
|
||||
;; Components ------------------------------------------------------------------
|
||||
|
||||
(def tokens-menu-ref
|
||||
(l/derived :token-context-menu refs/workspace-local))
|
||||
|
||||
(defn- prevent-default
|
||||
[event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event))
|
||||
|
||||
(mf/defc menu-entry
|
||||
{::mf/props :obj}
|
||||
[{:keys [title value on-click selected? children submenu-offset]}]
|
||||
(let [submenu-ref (mf/use-ref nil)
|
||||
hovering? (mf/use-ref false)
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? true)
|
||||
(when-let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(dom/set-css-property! submenu-node "display" "block"))))
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? false)
|
||||
(when-let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(timers/schedule 50 #(when-not (mf/ref-val hovering?)
|
||||
(dom/set-css-property! submenu-node "display" "none"))))))
|
||||
set-dom-node
|
||||
(mf/use-callback
|
||||
(fn [dom]
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (and (some? dom) (some? submenu-node))
|
||||
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
|
||||
[:li
|
||||
{:class (stl/css :context-menu-item)
|
||||
:ref set-dom-node
|
||||
:data-value value
|
||||
:on-click on-click
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
(when selected?
|
||||
[:span {:class (stl/css :icon-wrapper)}
|
||||
[:span {:class (stl/css :selected-icon)} i/tick]])
|
||||
[:span {:class (stl/css :title)} title]
|
||||
(when children
|
||||
[:*
|
||||
[:span {:class (stl/css :submenu-icon)} i/arrow]
|
||||
[:ul {:class (stl/css :token-context-submenu)
|
||||
:ref submenu-ref
|
||||
:style {:display "none"
|
||||
:top 0
|
||||
:left (str submenu-offset "px")}
|
||||
:on-context-menu prevent-default}
|
||||
children]])]))
|
||||
|
||||
(mf/defc menu-tree
|
||||
[{:keys [selected-shapes] :as context-data}]
|
||||
(let [entries (if (seq selected-shapes)
|
||||
(selection-actions context-data)
|
||||
(default-actions context-data))]
|
||||
(for [[index {:keys [title action selected? submenu] :as entry}] (d/enumerate entries)]
|
||||
[:* {:key (str title " " index)}
|
||||
(cond
|
||||
(= :separator entry) [:li {:class (stl/css :separator)}]
|
||||
submenu [:& menu-entry {:title title
|
||||
:submenu-offset (:submenu-offset context-data)}
|
||||
[:& menu-tree (assoc context-data :type submenu)]]
|
||||
:else [:& menu-entry
|
||||
{:title title
|
||||
:on-click action
|
||||
:selected? selected?}])])))
|
||||
|
||||
(mf/defc token-context-menu-tree
|
||||
[{:keys [width] :as mdata}]
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
selected-shapes (into [] (keep (d/getf objects)) selected)
|
||||
token-name (:token-name mdata)
|
||||
token (mf/deref (refs/workspace-selected-token-set-token token-name))
|
||||
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)]
|
||||
[:ul {:class (stl/css :context-list)}
|
||||
[:& menu-tree {:submenu-offset width
|
||||
:token token
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:selected-shapes selected-shapes}]]))
|
||||
|
||||
(mf/defc token-context-menu
|
||||
[]
|
||||
(let [mdata (mf/deref tokens-menu-ref)
|
||||
top (+ (get-in mdata [:position :y]) 5)
|
||||
left (+ (get-in mdata [:position :x]) 5)
|
||||
width (mf/use-state 0)
|
||||
dropdown-ref (mf/use-ref)]
|
||||
(mf/use-effect
|
||||
(mf/deps mdata)
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val dropdown-ref)]
|
||||
(reset! width (.-offsetWidth node)))))
|
||||
[:& dropdown {:show (boolean mdata)
|
||||
:on-close #(st/emit! dt/hide-token-context-menu)}
|
||||
[:div {:class (stl/css :token-context-menu)
|
||||
:ref dropdown-ref
|
||||
:style {:top top :left left}
|
||||
:on-context-menu prevent-default}
|
||||
(when mdata
|
||||
[:& token-context-menu-tree (assoc mdata :offset @width)])]]))
|
103
frontend/src/app/main/ui/workspace/tokens/context_menu.scss
Normal file
103
frontend/src/app/main/ui/workspace/tokens/context_menu.scss
Normal file
|
@ -0,0 +1,103 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.token-context-menu {
|
||||
position: absolute;
|
||||
z-index: $z-index-4;
|
||||
}
|
||||
|
||||
.context-list,
|
||||
.token-context-submenu {
|
||||
@include menuShadow;
|
||||
display: grid;
|
||||
width: $s-240;
|
||||
padding: $s-4;
|
||||
border-radius: $br-8;
|
||||
border: $s-2 solid var(--panel-border-color);
|
||||
background-color: var(--menu-background-color);
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
@include bodySmallTypography;
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.token-context-submenu {
|
||||
position: absolute;
|
||||
padding: $s-4;
|
||||
margin-left: $s-6;
|
||||
}
|
||||
|
||||
.separator {
|
||||
@include bodySmallTypography;
|
||||
margin: $s-6;
|
||||
border-block-start: $s-1 solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $s-28;
|
||||
width: 100%;
|
||||
padding: $s-6;
|
||||
border-radius: $br-8;
|
||||
cursor: pointer;
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
@include bodySmallTypography;
|
||||
color: var(--menu-foreground-color);
|
||||
margin-left: calc(($s-32 + $s-28) / 2);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.icon-wrapper + .title {
|
||||
margin-left: $s-6;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-icon {
|
||||
margin-left: $s-2;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
.title {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
.shortcut {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--menu-border-color-focus);
|
||||
background-color: var(--menu-background-color-focus);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
34
frontend/src/app/main/ui/workspace/tokens/core.cljs
Normal file
34
frontend/src/app/main/ui/workspace/tokens/core.cljs
Normal file
|
@ -0,0 +1,34 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.core
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]))
|
||||
|
||||
;; Helpers ---------------------------------------------------------------------
|
||||
|
||||
(defn resolve-token-value [{:keys [value resolved-value] :as _token}]
|
||||
(or
|
||||
resolved-value
|
||||
(d/parse-double value)))
|
||||
|
||||
(defn maybe-resolve-token-value [{:keys [value] :as token}]
|
||||
(when value (resolve-token-value token)))
|
||||
|
||||
(defn tokens->select-options [{:keys [shape tokens attributes selected-attributes]}]
|
||||
(map
|
||||
(fn [{:keys [name] :as token}]
|
||||
(cond-> (assoc token :label name)
|
||||
(wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true)))
|
||||
tokens))
|
||||
|
||||
(defn tokens-name-map->select-options [{:keys [shape tokens attributes selected-attributes]}]
|
||||
(map
|
||||
(fn [[_k {:keys [name] :as token}]]
|
||||
(cond-> (assoc token :label name)
|
||||
(wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true)))
|
||||
tokens))
|
301
frontend/src/app/main/ui/workspace/tokens/editable_select.cljs
Normal file
301
frontend/src/app/main/ui/workspace/tokens/editable_select.cljs
Normal file
|
@ -0,0 +1,301 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.editable-select
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.shortcuts :as dsc]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.core :as wtc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as timers]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn on-number-input-key-down [{:keys [event min-val max-val set-value!]}]
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
down? (kbd/down-arrow? event)]
|
||||
(when (or up? down?)
|
||||
(dom/prevent-default event)
|
||||
(let [value (-> event dom/get-target dom/get-value)
|
||||
value (or (d/parse-double value) value)
|
||||
increment (cond
|
||||
(kbd/shift? event) (if up? 10 -10)
|
||||
(kbd/alt? event) (if up? 0.1 -0.1)
|
||||
:else (if up? 1 -1))
|
||||
new-value (+ value increment)
|
||||
new-value (cond
|
||||
(and (d/num? min-val) (< new-value min-val)) min-val
|
||||
(and (d/num? max-val) (> new-value max-val)) max-val
|
||||
:else new-value)]
|
||||
(set-value! new-value)))))
|
||||
|
||||
(defn direction-select
|
||||
"Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`.
|
||||
|
||||
`direction` accepts `:up` or `:down`."
|
||||
[direction n coll]
|
||||
(let [last-n (dec (count coll))
|
||||
next-n (case direction
|
||||
:up (dec n)
|
||||
:down (inc n))
|
||||
wrap-around-n (cond
|
||||
(neg? next-n) last-n
|
||||
(> next-n last-n) 0
|
||||
:else next-n)]
|
||||
wrap-around-n))
|
||||
|
||||
(mf/defc dropdown-select [{:keys [position on-close element-id element-ref options on-select]}]
|
||||
(let [highlighted* (mf/use-state nil)
|
||||
highlighted (deref highlighted*)
|
||||
on-keyup (fn [event]
|
||||
(cond
|
||||
(and (kbd/enter? event) highlighted) (on-select (nth options highlighted))
|
||||
(kbd/up-arrow? event) (do
|
||||
(dom/prevent-default event)
|
||||
(->> (direction-select :up (or highlighted 0) options)
|
||||
(reset! highlighted*)))
|
||||
(kbd/down-arrow? event) (do
|
||||
(dom/prevent-default event)
|
||||
(->> (direction-select :down (or highlighted -1) options)
|
||||
(reset! highlighted*)))))]
|
||||
(mf/with-effect [highlighted]
|
||||
(let [keys [(events/listen globals/document EventType.KEYUP on-keyup)
|
||||
(events/listen globals/document EventType.KEYDOWN dom/prevent-default)]]
|
||||
(st/emit! (dsc/push-shortcuts :token {}))
|
||||
(fn []
|
||||
(doseq [key keys]
|
||||
(events/unlistenByKey key))
|
||||
(st/emit! (dsc/pop-shortcuts :token)))))
|
||||
[:& dropdown {:show true
|
||||
:on-close on-close}
|
||||
[:> :div {:class (stl/css-case :custom-select-dropdown true
|
||||
:custom-select-dropdown-right (= position :right)
|
||||
:custom-select-dropdown-left (= position :left))
|
||||
:on-mouse-enter #(reset! highlighted* nil)
|
||||
:ref element-ref}
|
||||
[:ul {:class (stl/css :custom-select-dropdown-list)}
|
||||
(for [[index item] (d/enumerate options)]
|
||||
(cond
|
||||
(= :separator item) [:li {:class (stl/css :separator)
|
||||
:key (dm/str element-id "-" index)}]
|
||||
;; Remove items with missing references
|
||||
(seq (:errors item)) nil
|
||||
:else (let [{:keys [label selected? errors]} item
|
||||
highlighted? (= highlighted index)]
|
||||
[:li
|
||||
{:key (str element-id "-" index)
|
||||
:class (stl/css-case :dropdown-element true
|
||||
:is-selected selected?
|
||||
:is-highlighted highlighted?)
|
||||
:data-label label
|
||||
:disabled (seq errors)
|
||||
:on-click #(on-select item)}
|
||||
[:span {:class (stl/css :label)} label]
|
||||
[:span {:class (stl/css :value)} (wtc/resolve-token-value item)]
|
||||
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
|
||||
|
||||
(mf/defc editable-select
|
||||
[{:keys [value options disabled class on-change placeholder on-blur on-token-remove position input-props] :as params}]
|
||||
(let [{:keys [type]} input-props
|
||||
input-class (:class input-props)
|
||||
state* (mf/use-state {:id (uuid/next)
|
||||
:is-open? false
|
||||
:current-value value
|
||||
:token-value nil
|
||||
:current-item nil
|
||||
:top nil
|
||||
:left nil
|
||||
:bottom nil})
|
||||
state (deref state*)
|
||||
is-open? (:is-open? state)
|
||||
refocus? (:refocus? state)
|
||||
current-value (:current-value state)
|
||||
element-id (:id state)
|
||||
|
||||
min-val (get params :min)
|
||||
max-val (get params :max)
|
||||
|
||||
multiple? (= :multiple value)
|
||||
token (when-not multiple?
|
||||
(-> (filter :selected? options) (first)))
|
||||
|
||||
emit-blur? (mf/use-ref nil)
|
||||
select-wrapper-ref (mf/use-ref)
|
||||
|
||||
toggle-dropdown
|
||||
(mf/use-fn
|
||||
(mf/deps state)
|
||||
#(swap! state* update :is-open? not))
|
||||
|
||||
close-dropdown
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! state* assoc :is-open? false))
|
||||
|
||||
labels-map (->> (map (fn [{:keys [label] :as item}]
|
||||
[label item])
|
||||
options)
|
||||
(into {}))
|
||||
|
||||
set-token-value!
|
||||
(fn [value]
|
||||
(swap! state* assoc :token-value value))
|
||||
|
||||
set-value
|
||||
(fn [value event]
|
||||
(swap! state* assoc
|
||||
:current-value value
|
||||
:token-value value)
|
||||
(when on-change (on-change value event)))
|
||||
|
||||
select-item
|
||||
(mf/use-fn
|
||||
(mf/deps on-change on-blur labels-map)
|
||||
(fn [{:keys [value] :as item}]
|
||||
(swap! state* assoc
|
||||
:current-value value
|
||||
:token-value nil
|
||||
:current-item item)
|
||||
(when on-change (on-change item))
|
||||
(when on-blur (on-blur))))
|
||||
|
||||
handle-change-input
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value)
|
||||
value (or (d/parse-double value) value)]
|
||||
(set-value value event)))
|
||||
|
||||
handle-token-change-input
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value)
|
||||
value (or (d/parse-double value) value)]
|
||||
(set-token-value! value)))
|
||||
|
||||
handle-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps set-value is-open? token)
|
||||
(fn [^js event]
|
||||
(cond
|
||||
token (let [backspace? (kbd/backspace? event)
|
||||
enter? (kbd/enter? event)
|
||||
value (-> event dom/get-target dom/get-value)
|
||||
caret-at-beginning? (zero? (.. event -target -selectionStart))
|
||||
no-text-selected? (str/empty? (.toString (js/document.getSelection)))
|
||||
delete-token? (and backspace? caret-at-beginning? no-text-selected?)
|
||||
replace-token-with-value? (and enter? (seq (str/trim value)))]
|
||||
(cond
|
||||
delete-token? (do
|
||||
(dom/prevent-default event)
|
||||
(on-token-remove token)
|
||||
;; Re-focus the input value of the newly rendered input element
|
||||
(swap! state* assoc :refocus? true))
|
||||
replace-token-with-value? (do
|
||||
(dom/prevent-default event)
|
||||
(on-token-remove token)
|
||||
(handle-change-input event)
|
||||
(set-token-value! nil))
|
||||
:else (set-token-value! value)))
|
||||
(= type "number") (on-number-input-key-down {:event event
|
||||
:min-val min-val
|
||||
:max-val max-val
|
||||
:set-value! set-value}))))
|
||||
|
||||
handle-focus
|
||||
(mf/use-fn
|
||||
(mf/deps refocus?)
|
||||
(fn []
|
||||
(when refocus?
|
||||
(swap! state* dissoc :refocus?))
|
||||
(mf/set-ref-val! emit-blur? false)))
|
||||
|
||||
handle-blur
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(mf/set-ref-val! emit-blur? true)
|
||||
(swap! state* assoc :token-value nil)
|
||||
(timers/schedule
|
||||
200
|
||||
(fn []
|
||||
(when (and on-blur (mf/ref-val emit-blur?)) (on-blur))))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps value current-value)
|
||||
#(when (not= (str value) current-value)
|
||||
(swap! state* assoc :current-value value)))
|
||||
|
||||
(mf/with-effect [is-open?]
|
||||
(let [wrapper-node (mf/ref-val select-wrapper-ref)
|
||||
node (dom/get-element-by-class "checked-element is-selected" wrapper-node)
|
||||
nodes (dom/get-elements-by-class "checked-element-value" wrapper-node)
|
||||
closest (fn [a b] (first (sort-by #(mth/abs (- % b)) a)))
|
||||
closest-value (str (closest options value))]
|
||||
(when is-open?
|
||||
(if (some? node)
|
||||
(dom/scroll-into-view-if-needed! node)
|
||||
(some->> nodes
|
||||
(d/seek #(= closest-value (dom/get-inner-text %)))
|
||||
(dom/scroll-into-view-if-needed!)))))
|
||||
|
||||
(mf/set-ref-val! emit-blur? (not is-open?)))
|
||||
|
||||
|
||||
[:div {:class (dm/str class " " (stl/css-case :editable-select true
|
||||
:editable-select-disabled disabled))}
|
||||
(when-let [{:keys [label value]} token]
|
||||
[:div {:title (str label ": " value)
|
||||
:class (stl/css :token-pill)}
|
||||
(wtc/resolve-token-value token)])
|
||||
(cond
|
||||
token [:& :input (merge input-props
|
||||
{:value (or (:token-value state) "")
|
||||
:type "text"
|
||||
:class input-class
|
||||
:onChange handle-token-change-input
|
||||
:onKeyDown handle-key-down
|
||||
:onFocus handle-focus
|
||||
:onBlur handle-blur})]
|
||||
(= type "number") [:& numeric-input* (merge input-props
|
||||
{:autoFocus refocus?
|
||||
:value (or current-value "")
|
||||
:className input-class
|
||||
:onChange set-value
|
||||
:onFocus handle-focus
|
||||
:onBlur handle-blur
|
||||
:placeholder placeholder})]
|
||||
:else [:& :input (merge input-props
|
||||
{:value (or current-value "")
|
||||
:class input-class
|
||||
:onChange handle-change-input
|
||||
:onKeyDown handle-key-down
|
||||
:onFocus handle-focus
|
||||
:onBlur handle-blur
|
||||
:placeholder placeholder
|
||||
:type type})])
|
||||
|
||||
(when (seq options)
|
||||
[:div {:class (stl/css :dropdown-button)
|
||||
:on-click toggle-dropdown}
|
||||
i/arrow])
|
||||
|
||||
(when (and is-open? (seq options))
|
||||
[:& dropdown-select {:position position
|
||||
:on-close close-dropdown
|
||||
:element-id element-id
|
||||
:element-ref select-wrapper-ref
|
||||
:options options
|
||||
:on-select select-item}])]))
|
155
frontend/src/app/main/ui/workspace/tokens/editable_select.scss
Normal file
155
frontend/src/app/main/ui/workspace/tokens/editable_select.scss
Normal file
|
@ -0,0 +1,155 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.editable-select {
|
||||
@extend .asset-element;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: calc($s-32 - 2px); // Fixes border being clipped by the input field
|
||||
width: 100%;
|
||||
padding: $s-8;
|
||||
border-radius: $br-8;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
&:focus-within {
|
||||
.token-pill {
|
||||
background-color: var(--button-primary-background-color-rest);
|
||||
color: var(--button-primary-foreground-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
@include flexCenter;
|
||||
margin-right: -$s-8;
|
||||
padding-right: $s-8;
|
||||
padding-left: 0;
|
||||
aspect-ratio: 0.8 / 1;
|
||||
width: auto;
|
||||
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
transform: rotate(90deg);
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-dropdown-list {
|
||||
min-width: 150px;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.token-pill {
|
||||
background-color: rgb(94 107 120 / 25%);
|
||||
border-radius: $br-4;
|
||||
padding: $s-2 $s-6;
|
||||
text-overflow: ellipsis;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.token-pill + input {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.custom-select-dropdown-left {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.custom-select-dropdown-right {
|
||||
right: 0;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.custom-select-dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
max-height: $s-320;
|
||||
width: auto;
|
||||
margin-top: $s-4;
|
||||
|
||||
.separator {
|
||||
margin: 0;
|
||||
height: $s-12;
|
||||
}
|
||||
|
||||
.dropdown-element {
|
||||
@extend .dropdown-element-base;
|
||||
color: var(--menu-foreground-color-rest);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.label,
|
||||
.value {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-transform: unset;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
flex: 0.6;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@include flexCenter;
|
||||
translate: -$s-4 0;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
visibility: hidden;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--menu-foreground-color);
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
&.is-highlighted {
|
||||
background-color: var(--button-primary-background-color-rest);
|
||||
span {
|
||||
color: var(--button-primary-foreground-color-rest);
|
||||
}
|
||||
.check-icon svg {
|
||||
stroke: var(--button-primary-foreground-color-rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editable-select-disabled {
|
||||
pointer-events: none;
|
||||
}
|
63
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal file
63
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal file
|
@ -0,0 +1,63 @@
|
|||
(ns app.main.ui.workspace.tokens.errors
|
||||
(:require
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def error-codes
|
||||
{:error.import/json-parse-error
|
||||
{:error/code :error.import/json-parse-error
|
||||
:error/message "Import Error: Could not parse json"}
|
||||
|
||||
:error.import/invalid-json-data
|
||||
{:error/code :error.import/invalid-json-data
|
||||
:error/message "Import Error: Invalid token data in json."}
|
||||
|
||||
:error.import/style-dictionary-reference-errors
|
||||
{:error/code :error.import/style-dictionary-reference-errors
|
||||
:error/fn #(str "Import Error:\n\n" (str/join "\n\n" %))}
|
||||
|
||||
:error.import/style-dictionary-unknown-error
|
||||
{:error/code :error.import/style-dictionary-reference-errors
|
||||
:error/message "Import Error:"}
|
||||
|
||||
:error.token/direct-self-reference
|
||||
{:error/code :error.token/direct-self-reference
|
||||
:error/message "Token has self reference"}
|
||||
|
||||
:error.token/invalid-color
|
||||
{:error/code :error.token/invalid-color
|
||||
:error/fn #(str "Invalid color value: " %)}
|
||||
|
||||
:error.style-dictionary/missing-reference
|
||||
{:error/code :error.style-dictionary/missing-reference
|
||||
:error/fn #(str "Missing token references: " (str/join " " %))}
|
||||
|
||||
:error.style-dictionary/invalid-token-value
|
||||
{:error/code :error.style-dictionary/invalid-token-value
|
||||
:error/fn #(str "Invalid token value: " %)}
|
||||
|
||||
:error/unknown
|
||||
{:error/code :error/unknown
|
||||
:error/message "Unknown error"}})
|
||||
|
||||
(defn get-error-code [error-key]
|
||||
(get error-codes error-key (:error/unknown error-codes)))
|
||||
|
||||
(defn error-with-value [error-key error-value]
|
||||
(-> (get-error-code error-key)
|
||||
(assoc :error/value error-value)))
|
||||
|
||||
(defn error-ex-info [error-key error-value exception]
|
||||
(let [err (-> (error-with-value error-key error-value)
|
||||
(assoc :error/exception exception))]
|
||||
(ex-info (:error/code err) err)))
|
||||
|
||||
(defn has-error-code? [error-key errors]
|
||||
(some #(= (:error/code %) error-key) errors))
|
||||
|
||||
(defn humanize-errors [errors]
|
||||
(->> errors
|
||||
(map (fn [err]
|
||||
(cond
|
||||
(:error/fn err) ((:error/fn err) (:error/value err))
|
||||
(:error/message err) (:error/message err)
|
||||
:else err)))))
|
431
frontend/src/app/main/ui/workspace/tokens/form.cljs
Normal file
431
frontend/src/app/main/ui/workspace/tokens/form.cljs
Normal file
|
@ -0,0 +1,431 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.form
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
;; ["lodash.debounce" :as debounce]
|
||||
[app.common.colors :as c]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.tokens :as dt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.color-bullet :refer [color-bullet]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.workspace.colorpicker :as colorpicker]
|
||||
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
|
||||
[app.main.ui.workspace.tokens.common :as tokens.common]
|
||||
[app.main.ui.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[app.main.ui.workspace.tokens.update :as wtu]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.functions :as uf]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Schemas ---------------------------------------------------------------------
|
||||
|
||||
(def valid-token-name-regexp
|
||||
"Only allow letters and digits for token names.
|
||||
Also allow one `.` for a namespace separator.
|
||||
|
||||
Caution: This will allow a trailing dot like `token-name.`,
|
||||
But we will trim that in the `finalize-name`,
|
||||
to not throw too many errors while the user is editing."
|
||||
#"([a-zA-Z0-9-]+\.?)*")
|
||||
|
||||
(def valid-token-name-schema
|
||||
(m/-simple-schema
|
||||
{:type :token/invalid-token-name
|
||||
:pred #(re-matches valid-token-name-regexp %)
|
||||
:type-properties {:error/fn #(str (:value %) " is not a valid token name.
|
||||
Token names should only contain letters and digits separated by . characters.")}}))
|
||||
|
||||
(defn token-name-schema
|
||||
"Generate a dynamic schema validation to check if a token path derived from the name already exists at `tokens-tree`."
|
||||
[{:keys [tokens-tree]}]
|
||||
(let [path-exists-schema
|
||||
(m/-simple-schema
|
||||
{:type :token/name-exists
|
||||
:pred #(not (wtt/token-name-path-exists? % tokens-tree))
|
||||
:type-properties {:error/fn #(str "A token already exists at the path: " (:value %))}})]
|
||||
(m/schema
|
||||
[:and
|
||||
[:string {:min 1 :max 255}]
|
||||
valid-token-name-schema
|
||||
path-exists-schema])))
|
||||
|
||||
(def token-description-schema
|
||||
(m/schema
|
||||
[:string {:max 2048}]))
|
||||
|
||||
;; Helpers ---------------------------------------------------------------------
|
||||
|
||||
(defn finalize-name [name]
|
||||
(-> (str/trim name)
|
||||
;; Remove trailing dots
|
||||
(str/replace #"\.+$" "")))
|
||||
|
||||
(defn valid-name? [name]
|
||||
(seq (finalize-name (str name))))
|
||||
|
||||
(defn finalize-value [value]
|
||||
(-> (str value)
|
||||
(str/trim)))
|
||||
|
||||
(defn valid-value? [value]
|
||||
(seq (finalize-value value)))
|
||||
|
||||
(defn schema-validation->promise [validated]
|
||||
(if (:errors validated)
|
||||
(p/rejected validated)
|
||||
(p/resolved validated)))
|
||||
|
||||
;; Component -------------------------------------------------------------------
|
||||
|
||||
(defn validate-token-value+
|
||||
"Validates token value by resolving the value `input` using `StyleDictionary`.
|
||||
Returns a promise of either resolved tokens or rejects with an error state."
|
||||
[{:keys [value name-value token tokens]}]
|
||||
(let [;; When creating a new token we dont have a token name yet,
|
||||
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names
|
||||
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
|
||||
(cond
|
||||
(empty? (str/trim value))
|
||||
(p/rejected {:errors [{:error/code :error/empty-input}]})
|
||||
|
||||
(ctob/token-value-self-reference? token-name value)
|
||||
(p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
||||
|
||||
:else
|
||||
(-> (update tokens token-name merge {:value value
|
||||
:name token-name
|
||||
:type (:type token)})
|
||||
(sd/resolve-tokens+)
|
||||
(p/then
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
||||
(cond
|
||||
resolved-value (p/resolved resolved-token)
|
||||
:else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))})))))))))
|
||||
|
||||
(defn use-debonced-resolve-callback
|
||||
"Resolves a token values using `StyleDictionary`.
|
||||
This function is debounced as the resolving might be an expensive calculation.
|
||||
Uses a custom debouncing logic, as the resolve function is async."
|
||||
[name-ref token tokens callback & {:keys [timeout] :or {timeout 160}}]
|
||||
(let [timeout-id-ref (mf/use-ref nil)
|
||||
debounced-resolver-callback
|
||||
(mf/use-fn
|
||||
(mf/deps token callback tokens)
|
||||
(fn [value]
|
||||
(let [timeout-id (js/Symbol)
|
||||
;; Dont execute callback when the timout-id-ref is outdated because this function got called again
|
||||
timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)]
|
||||
(mf/set-ref-val! timeout-id-ref timeout-id)
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(when (not (timeout-outdated-cb?))
|
||||
(-> (validate-token-value+ {:value value
|
||||
:name-value @name-ref
|
||||
:token token
|
||||
:tokens tokens})
|
||||
(p/finally
|
||||
(fn [x err]
|
||||
(when-not (timeout-outdated-cb?)
|
||||
(callback (or err x))))))))
|
||||
timeout))))]
|
||||
debounced-resolver-callback))
|
||||
|
||||
(defonce form-token-cache-atom (atom nil))
|
||||
|
||||
(mf/defc ramp
|
||||
[{:keys [color on-change]}]
|
||||
(let [wrapper-node-ref (mf/use-ref nil)
|
||||
dragging? (mf/use-state)
|
||||
hex->value (fn [hex]
|
||||
(when-let [tc (tinycolor/valid-color hex)]
|
||||
(let [hex (str "#" (tinycolor/->hex tc))
|
||||
[r g b] (c/hex->rgb hex)
|
||||
[h s v] (c/hex->hsv hex)]
|
||||
{:hex hex
|
||||
:r r :g g :b b
|
||||
:h h :s s :v v
|
||||
:alpha 1})))
|
||||
value (mf/use-state (hex->value color))
|
||||
on-change' (fn [{:keys [hex]}]
|
||||
(reset! value (hex->value hex))
|
||||
(when-not (and @dragging? hex)
|
||||
(on-change hex)))]
|
||||
(colorpicker/use-color-picker-css-variables! wrapper-node-ref @value)
|
||||
[:div {:ref wrapper-node-ref}
|
||||
[:& ramp-selector
|
||||
{:color @value
|
||||
:disable-opacity true
|
||||
:on-start-drag #(reset! dragging? true)
|
||||
:on-finish-drag #(reset! dragging? false)
|
||||
:on-change on-change'}]]))
|
||||
|
||||
(mf/defc token-value-or-errors
|
||||
[{:keys [result-or-errors]}]
|
||||
(let [{:keys [errors]} result-or-errors
|
||||
empty-message? (or (nil? result-or-errors)
|
||||
(wte/has-error-code? :error/empty-input errors))
|
||||
message (cond
|
||||
empty-message? (dm/str (tr "workspace.token.resolved-value") "-")
|
||||
errors (->> (wte/humanize-errors errors)
|
||||
(str/join "\n"))
|
||||
:else (dm/str (tr "workspace.token.resolved-value") result-or-errors))]
|
||||
[:> text* {:as "p"
|
||||
:typography "body-small"
|
||||
:class (stl/css-case :resolved-value true
|
||||
:resolved-value-placeholder empty-message?
|
||||
:resolved-value-error (seq errors))}
|
||||
message]))
|
||||
|
||||
(mf/defc form
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [token token-type action selected-token-set-id]}]
|
||||
(let [validate-name? (mf/use-state (not (:id token)))
|
||||
token (or token {:type token-type})
|
||||
color? (wtt/color-token? token)
|
||||
selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
|
||||
active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
|
||||
:interactive? true})
|
||||
token-path (mf/use-memo
|
||||
(mf/deps (:name token))
|
||||
#(wtt/token-name->path (:name token)))
|
||||
selected-set-tokens-tree (mf/use-memo
|
||||
(mf/deps token-path selected-set-tokens)
|
||||
(fn []
|
||||
(-> (ctob/tokens-tree selected-set-tokens)
|
||||
;; Allow setting editing token to it's own path
|
||||
(d/dissoc-in token-path))))
|
||||
|
||||
;; Name
|
||||
name-ref (mf/use-var (:name token))
|
||||
name-errors (mf/use-state nil)
|
||||
validate-name
|
||||
(mf/use-fn
|
||||
(mf/deps selected-set-tokens-tree)
|
||||
(fn [value]
|
||||
(let [schema (token-name-schema {:token token
|
||||
:tokens-tree selected-set-tokens-tree})]
|
||||
(m/explain schema (finalize-name value)))))
|
||||
|
||||
on-update-name-debounced
|
||||
(mf/use-fn
|
||||
(uf/debounce (fn [e]
|
||||
(let [value (dom/get-target-val e)
|
||||
errors (validate-name value)]
|
||||
;; Prevent showing error when just going to another field on a new token
|
||||
(when-not (and validate-name? (str/empty? value))
|
||||
(reset! validate-name? false)
|
||||
(reset! name-errors errors))))))
|
||||
|
||||
on-update-name
|
||||
(mf/use-fn
|
||||
(mf/deps on-update-name-debounced)
|
||||
(fn [e]
|
||||
(reset! name-ref (dom/get-target-val e))
|
||||
(on-update-name-debounced e)))
|
||||
|
||||
valid-name-field? (and
|
||||
(not @name-errors)
|
||||
(valid-name? @name-ref))
|
||||
|
||||
;; Value
|
||||
color (mf/use-state (when color? (:value token)))
|
||||
color-ramp-open? (mf/use-state false)
|
||||
value-input-ref (mf/use-ref nil)
|
||||
value-ref (mf/use-var (:value token))
|
||||
token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value]))
|
||||
set-resolve-value
|
||||
(mf/use-fn
|
||||
(fn [token-or-err]
|
||||
(let [error? (:errors token-or-err)
|
||||
v (if error?
|
||||
token-or-err
|
||||
(:resolved-value token-or-err))]
|
||||
(when color? (reset! color (if error? nil v)))
|
||||
(reset! token-resolve-result v))))
|
||||
on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
|
||||
on-update-value (mf/use-fn
|
||||
(mf/deps on-update-value-debounced)
|
||||
(fn [e]
|
||||
(let [value (dom/get-target-val e)]
|
||||
(reset! value-ref value)
|
||||
(on-update-value-debounced value))))
|
||||
on-update-color (mf/use-fn
|
||||
(mf/deps on-update-value-debounced)
|
||||
(fn [hex-value]
|
||||
(reset! value-ref hex-value)
|
||||
(set! (.-value (mf/ref-val value-input-ref)) hex-value)
|
||||
(on-update-value-debounced hex-value)))
|
||||
|
||||
value-error? (seq (:errors @token-resolve-result))
|
||||
valid-value-field? (and
|
||||
(not value-error?)
|
||||
(valid-value? @token-resolve-result))
|
||||
|
||||
;; Description
|
||||
description-ref (mf/use-var (:description token))
|
||||
description-errors (mf/use-state nil)
|
||||
validate-descripion (mf/use-fn #(m/explain token-description-schema %))
|
||||
on-update-description-debounced (mf/use-fn
|
||||
(uf/debounce (fn [e]
|
||||
(let [value (dom/get-target-val e)
|
||||
errors (validate-descripion value)]
|
||||
(reset! description-errors errors)))))
|
||||
on-update-description
|
||||
(mf/use-fn
|
||||
(mf/deps on-update-description-debounced)
|
||||
(fn [e]
|
||||
(reset! description-ref (dom/get-target-val e))
|
||||
(on-update-description-debounced e)))
|
||||
valid-description-field? (not @description-errors)
|
||||
|
||||
;; Form
|
||||
disabled? (or (not valid-name-field?)
|
||||
(not valid-value-field?)
|
||||
(not valid-description-field?))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-name validate-descripion token resolved-tokens)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
;; We have to re-validate the current form values before submitting
|
||||
;; because the validation is asynchronous/debounced
|
||||
;; and the user might have edited a valid form to make it invalid,
|
||||
;; and press enter before the next validations could return.
|
||||
(let [final-name (finalize-name @name-ref)
|
||||
valid-name?+ (-> (validate-name final-name) schema-validation->promise)
|
||||
final-value (finalize-value @value-ref)
|
||||
final-description @description-ref
|
||||
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)]
|
||||
(-> (p/all [valid-name?+
|
||||
valid-description?+
|
||||
(validate-token-value+ {:value final-value
|
||||
:name-value final-name
|
||||
:token token
|
||||
:tokens resolved-tokens})])
|
||||
(p/finally (fn [result err]
|
||||
;; The result should be a vector of all resolved validations
|
||||
;; We do not handle the error case as it will be handled by the components validations
|
||||
(when (and (seq result) (not err))
|
||||
(st/emit! (dt/update-create-token {:token (ctob/make-token :name final-name
|
||||
:type (or (:type token) token-type)
|
||||
:value final-value
|
||||
:description final-description)
|
||||
:prev-token-name (:name token)}))
|
||||
(st/emit! (wtu/update-workspace-tokens))
|
||||
(modal/hide!))))))))
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dt/delete-token selected-token-set-id (:name token)))))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))]
|
||||
|
||||
[:form {:class (stl/css :form-wrapper)
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(if (= action "edit")
|
||||
(tr "workspace.token.edit-token")
|
||||
(tr "workspace.token.create-token" token-type))]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
;; This should be remove when labeled-imput is modified
|
||||
[:span {:class (stl/css :labeled-input-label)} "Name"]
|
||||
[:& tokens.common/labeled-input {:label "Name"
|
||||
:error? @name-errors
|
||||
:input-props {:default-value @name-ref
|
||||
:auto-focus true
|
||||
:on-blur on-update-name
|
||||
:on-change on-update-name}}]
|
||||
(for [error (->> (:errors @name-errors)
|
||||
(map #(-> (assoc @name-errors :errors [%])
|
||||
(me/humanize))))]
|
||||
[:> text* {:as "p"
|
||||
:key error
|
||||
:typography "body-small"
|
||||
:class (stl/css :error)}
|
||||
error])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
;; This should be remove when labeled-imput is modified
|
||||
[:span {:class (stl/css :labeled-input-label)} "value"]
|
||||
[:& tokens.common/labeled-input {:label "Value"
|
||||
:input-props {:default-value @value-ref
|
||||
:on-blur on-update-value
|
||||
:on-change on-update-value
|
||||
:ref value-input-ref}
|
||||
:render-right (when color?
|
||||
(mf/fnc color-bullet []
|
||||
[:div {:class (stl/css :color-bullet)
|
||||
:on-click #(swap! color-ramp-open? not)}
|
||||
(if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)]
|
||||
[:& color-bullet {:color hex
|
||||
:mini? true}]
|
||||
[:div {:class (stl/css :color-bullet-placeholder)}])]))}]
|
||||
(when @color-ramp-open?
|
||||
[:& ramp {:color (some-> (or @token-resolve-result (:value token))
|
||||
(tinycolor/valid-color))
|
||||
:on-change on-update-color}])
|
||||
[:& token-value-or-errors {:result-or-errors @token-resolve-result}]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
;; This should be remove when labeled-imput is modified
|
||||
[:span {:class (stl/css :labeled-input-label)} "Description"]
|
||||
[:& tokens.common/labeled-input {:label "Description"
|
||||
:input-props {:default-value @description-ref
|
||||
:on-change on-update-description}}]
|
||||
(when @description-errors
|
||||
[:> text* {:as "p"
|
||||
:typography "body-small"
|
||||
:class (stl/css :error)}
|
||||
(me/humanize @description-errors)])]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
[:> button* {:on-click on-cancel
|
||||
:type "button"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:type "submit"
|
||||
:variant "primary"
|
||||
:disabled disabled?}
|
||||
(tr "labels.save")]]]]))
|
85
frontend/src/app/main/ui/workspace/tokens/form.scss
Normal file
85
frontend/src/app/main/ui/workspace/tokens/form.scss
Normal file
|
@ -0,0 +1,85 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
@import "./common.scss";
|
||||
|
||||
.form-wrapper {
|
||||
width: $s-384;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: end;
|
||||
gap: $s-12;
|
||||
padding-block-start: $s-8;
|
||||
}
|
||||
|
||||
.with-delete {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-4;
|
||||
}
|
||||
|
||||
.labeled-input-label {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: $s-4 $s-6;
|
||||
margin-bottom: 0;
|
||||
color: var(--status-color-error-500);
|
||||
}
|
||||
|
||||
.resolved-value {
|
||||
--input-hint-color: var(--color-foreground-primary);
|
||||
margin-bottom: 0;
|
||||
padding: $s-4 $s-6;
|
||||
color: var(--input-hint-color);
|
||||
}
|
||||
|
||||
.resolved-value-placeholder {
|
||||
--input-hint-color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.resolved-value-error {
|
||||
--input-hint-color: var(--status-color-error-500);
|
||||
}
|
||||
|
||||
.color-bullet {
|
||||
margin-right: $s-8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-bullet-placeholder {
|
||||
width: var(--bullet-size, $s-16);
|
||||
height: var(--bullet-size, $s-16);
|
||||
min-width: var(--bullet-size, $s-16);
|
||||
min-height: var(--bullet-size, $s-16);
|
||||
margin-top: 0;
|
||||
background-color: color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
|
||||
border-radius: $br-4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
146
frontend/src/app/main/ui/workspace/tokens/modals.cljs
Normal file
146
frontend/src/app/main/ui/workspace/tokens/modals.cljs
Normal file
|
@ -0,0 +1,146 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.modals
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.tokens.form :refer [form]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Component -------------------------------------------------------------------
|
||||
|
||||
(defn calculate-position
|
||||
"Calculates the style properties for the given coordinates and position"
|
||||
[{vh :height} position x y]
|
||||
(let [;; picker height in pixels
|
||||
h 510
|
||||
;; Checks for overflow outside the viewport height
|
||||
overflow-fix (max 0 (+ y (- 50) h (- vh)))
|
||||
|
||||
x-pos 325]
|
||||
(cond
|
||||
(or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"}
|
||||
(= position :left) {:left (str (- x x-pos) "px")
|
||||
:top (str (- y 50 overflow-fix) "px")}
|
||||
:else {:left (str (+ x 80) "px")
|
||||
:top (str (- y 70 overflow-fix) "px")})))
|
||||
|
||||
(defn use-viewport-position-style [x y position]
|
||||
(let [vport (-> (l/derived :vport refs/workspace-local)
|
||||
(mf/deref))]
|
||||
(-> (calculate-position vport position x y)
|
||||
(clj->js))))
|
||||
|
||||
(mf/defc token-update-create-modal
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
||||
(let [wrapper-style (use-viewport-position-style x y position)
|
||||
close-modal (mf/use-fn
|
||||
(fn []
|
||||
(modal/hide!)))]
|
||||
[:div {:class (stl/css :token-modal-wrapper)
|
||||
:style wrapper-style}
|
||||
[:> icon-button* {:on-click close-modal
|
||||
:class (stl/css :close-btn)
|
||||
:icon i/close
|
||||
:variant "action"
|
||||
:aria-label (tr "labels.close")}]
|
||||
[:& form {:token token
|
||||
:action action
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:token-type token-type}]]))
|
||||
|
||||
;; Modals ----------------------------------------------------------------------
|
||||
|
||||
(mf/defc boolean-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/boolean}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc border-radius-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/border-radius}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc color-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/color}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc stroke-width-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/stroke-width}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc box-shadow-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/box-shadow}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc sizing-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/sizing}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc dimensions-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/dimensions}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc numeric-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/numeric}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc opacity-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/opacity}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc other-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/other}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc rotation-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/rotation}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc spacing-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/spacing}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc string-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/string}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
||||
|
||||
(mf/defc typography-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/typography}
|
||||
[properties]
|
||||
[:& token-update-create-modal properties])
|
24
frontend/src/app/main/ui/workspace/tokens/modals.scss
Normal file
24
frontend/src/app/main/ui/workspace/tokens/modals.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.token-modal-wrapper {
|
||||
@extend .modal-container-base;
|
||||
@include menuShadow;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
z-index: 11;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: $s-6;
|
||||
right: $s-6;
|
||||
}
|
369
frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
Normal file
369
frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
Normal file
|
@ -0,0 +1,369 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.modals.themes
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.tokens :as wdt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.common :refer [labeled-input] :as wtco]
|
||||
[app.main.ui.workspace.tokens.sets :as wts]
|
||||
[app.main.ui.workspace.tokens.sets-context :as sets-context]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc empty-themes
|
||||
[{:keys [set-state]}]
|
||||
(let [create-theme
|
||||
(mf/use-fn
|
||||
(mf/deps set-state)
|
||||
#(set-state (fn [_] {:type :create-theme})))]
|
||||
[:div {:class (stl/css :themes-modal-wrapper)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
|
||||
(tr "workspace.token.themes")]
|
||||
[:div {:class (stl/css :empty-themes-wrapper)}
|
||||
[:div {:class (stl/css :empty-themes-message)}
|
||||
[:> text* {:as "span" :typography "title-medium" :class (stl/css :empty-theme-title)}
|
||||
(tr "workspace.token.no-themes-currently")]
|
||||
[:> text* {:as "span"
|
||||
:class (stl/css :empty-theme-subtitle)
|
||||
:typography "body-medium"}
|
||||
(tr "workspace.token.create-new-theme")]]
|
||||
[:div {:class (stl/css :button-footer)}
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:on-click create-theme}
|
||||
(tr "workspace.token.new-theme")]]]]))
|
||||
|
||||
(mf/defc switch
|
||||
[{:keys [selected? name on-change]}]
|
||||
(let [selected (if selected? :on :off)]
|
||||
[:& radio-buttons {:selected selected
|
||||
:on-change on-change
|
||||
:name name}
|
||||
[:& radio-button {:id :on
|
||||
:value :on
|
||||
:icon i/tick
|
||||
:label ""}]
|
||||
[:& radio-button {:id :off
|
||||
:value :off
|
||||
:icon i/close
|
||||
:label ""}]]))
|
||||
|
||||
(mf/defc themes-overview
|
||||
[{:keys [set-state]}]
|
||||
(let [active-theme-ids (mf/deref refs/workspace-active-theme-paths)
|
||||
themes-groups (mf/deref refs/workspace-token-theme-tree-no-hidden)
|
||||
|
||||
create-theme
|
||||
(mf/use-fn
|
||||
(mf/deps set-state)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(set-state (fn [_] {:type :create-theme}))))]
|
||||
|
||||
[:div {:class (stl/css :themes-modal-wrapper)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
|
||||
(tr "workspace.token.themes")]
|
||||
[:ul {:class (stl/css :theme-group-wrapper)}
|
||||
(for [[group themes] themes-groups]
|
||||
[:li {:key (dm/str "token-theme-group" group)}
|
||||
(when (seq group)
|
||||
[:> heading* {:level 3
|
||||
:class (stl/css :theme-group-label)
|
||||
:typography "body-large"}
|
||||
[:span {:class (stl/css :group-title)}
|
||||
[:> icon* {:id "group"}]
|
||||
group]])
|
||||
[:ul {:class (stl/css :theme-group-rows-wrapper)}
|
||||
(for [[_ {:keys [group name] :as theme}] themes
|
||||
:let [theme-id (ctob/theme-path theme)
|
||||
selected? (some? (get active-theme-ids theme-id))
|
||||
delete-theme
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(st/emit! (wdt/delete-token-theme group name)))
|
||||
on-edit-theme
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(set-state (fn [_] {:type :edit-theme
|
||||
:theme-path [(:id theme) (:group theme) (:name theme)]})))]]
|
||||
[:li {:key theme-id
|
||||
:class (stl/css :theme-row)}
|
||||
[:div {:class (stl/css :theme-row-left)}
|
||||
|
||||
;; FIREEEEEEEEEE THIS
|
||||
[:div {:on-click (fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(st/emit! (wdt/toggle-token-theme-active? group name)))}
|
||||
[:& switch {:name (tr "workspace.token.theme" name)
|
||||
:on-change (constantly nil)
|
||||
:selected? selected?}]]
|
||||
[:> text* {:as "span" :typography "body-medium" :class (stl/css :theme-name)} name]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :theme-row-right)}
|
||||
(if-let [sets-count (some-> theme :sets seq count)]
|
||||
[:> button* {:class (stl/css :sets-count-button)
|
||||
:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-edit-theme}
|
||||
[:div {:class (stl/css :label-wrapper)}
|
||||
[:> text* {:as "span" :typography "body-medium"}
|
||||
(tr "workspace.token.num-sets" sets-count)]
|
||||
[:> icon* {:id "arrow-right"}]]]
|
||||
|
||||
[:> button* {:class (stl/css :sets-count-empty-button)
|
||||
:type "button"
|
||||
:variant "secondary"
|
||||
:on-click on-edit-theme}
|
||||
[:div {:class (stl/css :label-wrapper)}
|
||||
[:> text* {:as "span" :typography "body-medium"}
|
||||
(tr "workspace.token.no-sets")]
|
||||
[:> icon* {:id "arrow-right"}]]])
|
||||
|
||||
[:> icon-button* {:on-click delete-theme
|
||||
:variant "ghost"
|
||||
:aria-label (tr "workspace.token.delete-theme-title")
|
||||
:icon "delete"}]]])]])]
|
||||
|
||||
[:div {:class (stl/css :button-footer)}
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:icon "add"
|
||||
:on-click create-theme}
|
||||
(tr "workspace.token.create-theme-title")]]]))
|
||||
|
||||
(mf/defc theme-inputs
|
||||
[{:keys [theme dropdown-open? on-close-dropdown on-toggle-dropdown on-change-field]}]
|
||||
(let [theme-groups (mf/deref refs/workspace-token-theme-groups)
|
||||
group-input-ref (mf/use-ref)
|
||||
on-update-group (partial on-change-field :group)
|
||||
on-update-name (partial on-change-field :name)]
|
||||
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
|
||||
[:div {:class (stl/css :group-input-wrapper)}
|
||||
(when dropdown-open?
|
||||
[:& wtco/dropdown-select {:id ::groups-dropdown
|
||||
:shortcuts-key ::groups-dropdown
|
||||
:options (map (fn [group]
|
||||
{:label group
|
||||
:value group})
|
||||
theme-groups)
|
||||
:on-select (fn [{:keys [value]}]
|
||||
(set! (.-value (mf/ref-val group-input-ref)) value)
|
||||
(on-update-group value))
|
||||
:on-close on-close-dropdown}])
|
||||
;; TODO: This span should be remove when labeled-input is updated
|
||||
[:span {:class (stl/css :labeled-input-label)} "Theme group"]
|
||||
[:& labeled-input {:label "Group"
|
||||
:input-props {:ref group-input-ref
|
||||
:default-value (:group theme)
|
||||
:on-change (comp on-update-group dom/get-target-val)}
|
||||
:render-right (when (seq theme-groups)
|
||||
(mf/fnc drop-down-button []
|
||||
[:button {:class (stl/css :group-drop-down-button)
|
||||
:type "button"
|
||||
:on-click (fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(on-toggle-dropdown))}
|
||||
[:> icon* {:id "arrow-down"}]]))}]]
|
||||
[:div {:class (stl/css :group-input-wrapper)}
|
||||
;; TODO: This span should be remove when labeled-input is updated
|
||||
[:span {:class (stl/css :labeled-input-label)} "Theme"]
|
||||
[:& labeled-input {:label "Theme"
|
||||
:input-props {:default-value (:name theme)
|
||||
:on-change (comp on-update-name dom/get-target-val)}}]]]))
|
||||
|
||||
(mf/defc theme-modal-buttons
|
||||
[{:keys [close-modal on-save-form disabled?] :as props}]
|
||||
[:*
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click close-modal}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:type "submit"
|
||||
:on-click on-save-form
|
||||
:disabled disabled?}
|
||||
(tr "workspace.token.save-theme")]])
|
||||
|
||||
(mf/defc create-theme
|
||||
[{:keys [set-state]}]
|
||||
(let [{:keys [dropdown-open? _on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state)
|
||||
theme (ctob/make-token-theme :name "")
|
||||
on-back #(set-state (constantly {:type :themes-overview}))
|
||||
on-submit #(st/emit! (wdt/create-token-theme %))
|
||||
theme-state (mf/use-state theme)
|
||||
disabled? (-> (:name @theme-state)
|
||||
(str/trim)
|
||||
(str/empty?))
|
||||
on-change-field (fn [field value]
|
||||
(swap! theme-state #(assoc % field value)))
|
||||
on-save-form (mf/use-callback
|
||||
(mf/deps theme-state on-submit)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(let [theme (-> @theme-state
|
||||
(update :name str/trim)
|
||||
(update :group str/trim)
|
||||
(update :description str/trim))]
|
||||
(when-not (str/empty? (:name theme))
|
||||
(on-submit theme)))
|
||||
(on-back)))
|
||||
close-modal (mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(st/emit! (modal/hide))))]
|
||||
[:div {:class (stl/css :themes-modal-wrapper)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
|
||||
(tr "workspace.token.create-theme-title")]
|
||||
[:form {:on-submit on-save-form}
|
||||
[:div {:class (stl/css :create-theme-wrapper)}
|
||||
[:& theme-inputs {:dropdown-open? dropdown-open?
|
||||
:on-close-dropdown on-close-dropdown
|
||||
:on-toggle-dropdown on-toggle-dropdown
|
||||
:theme theme
|
||||
:on-change-field on-change-field}]
|
||||
|
||||
[:div {:class (stl/css :button-footer)}
|
||||
[:& theme-modal-buttons {:close-modal close-modal
|
||||
:on-save-form on-save-form
|
||||
:disabled? disabled?}]]]]]))
|
||||
|
||||
(mf/defc controlled-edit-theme
|
||||
[{:keys [state set-state]}]
|
||||
(let [{:keys [theme-path]} @state
|
||||
[_ theme-group theme-name] theme-path
|
||||
token-sets (mf/deref refs/workspace-ordered-token-sets)
|
||||
theme (mf/deref (refs/workspace-token-theme theme-group theme-name))
|
||||
on-back #(set-state (constantly {:type :themes-overview}))
|
||||
on-submit #(st/emit! (wdt/update-token-theme [(:group theme) (:name theme)] %))
|
||||
{:keys [dropdown-open? _on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state)
|
||||
theme-state (mf/use-state theme)
|
||||
disabled? (-> (:name @theme-state)
|
||||
(str/trim)
|
||||
(str/empty?))
|
||||
token-set-active? (mf/use-callback
|
||||
(mf/deps theme-state)
|
||||
(fn [set-name]
|
||||
(get-in @theme-state [:sets set-name])))
|
||||
on-toggle-token-set (mf/use-callback
|
||||
(mf/deps theme-state)
|
||||
(fn [set-name]
|
||||
(swap! theme-state #(ctob/toggle-set % set-name))))
|
||||
on-change-field (fn [field value]
|
||||
(swap! theme-state #(assoc % field value)))
|
||||
on-save-form (mf/use-callback
|
||||
(mf/deps theme-state on-submit)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(let [theme (-> @theme-state
|
||||
(update :name str/trim)
|
||||
(update :group str/trim)
|
||||
(update :description str/trim))]
|
||||
(when-not (str/empty? (:name theme))
|
||||
(on-submit theme)))
|
||||
(on-back)))
|
||||
close-modal
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps theme on-back)
|
||||
(fn []
|
||||
(st/emit! (wdt/delete-token-theme (:group theme) (:name theme)))
|
||||
(on-back)))]
|
||||
|
||||
[:div {:class (stl/css :themes-modal-wrapper)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
|
||||
(tr "workspace.token.edit-theme-title")]
|
||||
|
||||
[:form {:on-submit on-save-form}
|
||||
[:div {:class (stl/css :edit-theme-wrapper)}
|
||||
[:button {:on-click on-back
|
||||
:class (stl/css :back-btn)
|
||||
:type "button"}
|
||||
[:> icon* {:id ic/arrow-left :aria-hidden true}]
|
||||
(tr "workspace.token.back-to-themes")]
|
||||
|
||||
[:& theme-inputs {:dropdown-open? dropdown-open?
|
||||
:on-close-dropdown on-close-dropdown
|
||||
:on-toggle-dropdown on-toggle-dropdown
|
||||
:theme theme
|
||||
:on-change-field on-change-field}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :select-sets-message)}
|
||||
(tr "workspace.token.set-selection-theme")]
|
||||
[:div {:class (stl/css :sets-list-wrapper)}
|
||||
|
||||
[:& wts/controlled-sets-list
|
||||
{:token-sets token-sets
|
||||
:token-set-selected? (constantly false)
|
||||
:token-set-active? token-set-active?
|
||||
:on-select on-toggle-token-set
|
||||
:on-toggle-token-set on-toggle-token-set
|
||||
:origin "theme-modal"
|
||||
:context sets-context/static-context}]]
|
||||
|
||||
[:div {:class (stl/css :edit-theme-footer)}
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:icon "delete"
|
||||
:on-click on-delete-token}
|
||||
(tr "labels.delete")]
|
||||
[:div {:class (stl/css :button-footer)}
|
||||
[:& theme-modal-buttons {:close-modal close-modal
|
||||
:on-save-form on-save-form
|
||||
:disabled? disabled?}]]]]]]))
|
||||
|
||||
(mf/defc themes-modal-body
|
||||
[_]
|
||||
(let [themes (mf/deref refs/workspace-token-themes-no-hidden)
|
||||
state (mf/use-state (if (empty? themes)
|
||||
{:type :create-theme}
|
||||
{:type :themes-overview}))
|
||||
set-state (mf/use-callback #(swap! state %))
|
||||
component (case (:type @state)
|
||||
:empty-themes empty-themes
|
||||
:themes-overview (if (empty? themes) empty-themes themes-overview)
|
||||
:edit-theme controlled-edit-theme
|
||||
:create-theme create-theme)]
|
||||
[:& component {:state state
|
||||
:set-state set-state}]))
|
||||
|
||||
(mf/defc token-themes-modal
|
||||
{::mf/wrap-props false
|
||||
::mf/register modal/components
|
||||
::mf/register-as :tokens/themes}
|
||||
[_args]
|
||||
(let [handle-close-dialog (mf/use-callback #(st/emit! (modal/hide)))]
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:> icon-button* {:class (stl/css :close-btn)
|
||||
:on-click handle-close-dialog
|
||||
:aria-label (tr "labels.close")
|
||||
:variant "action"
|
||||
:icon "close"}]
|
||||
[:& themes-modal-body]]]))
|
198
frontend/src/app/main/ui/workspace/tokens/modals/themes.scss
Normal file
198
frontend/src/app/main/ui/workspace/tokens/modals/themes.scss
Normal file
|
@ -0,0 +1,198 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
width: 100%;
|
||||
max-width: $s-468;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.empty-themes-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $s-12;
|
||||
padding: $s-72 0;
|
||||
}
|
||||
|
||||
.themes-modal-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.themes-modal-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
appearance: none;
|
||||
color: var(--color-foreground-secondary);
|
||||
width: fit-content;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: $s-4;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.labeled-input-label {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.button-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $s-6;
|
||||
}
|
||||
|
||||
.edit-theme-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.empty-themes-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.empty-theme-subtitle {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.empty-theme-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.select-sets-message {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.create-theme-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: $s-8;
|
||||
right: $s-6;
|
||||
}
|
||||
|
||||
.theme-group-label {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $s-4;
|
||||
}
|
||||
|
||||
.theme-group-rows-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-6;
|
||||
}
|
||||
|
||||
.theme-group-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.theme-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-12;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theme-row-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-16;
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.theme-row-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-6;
|
||||
}
|
||||
|
||||
.sets-count-button {
|
||||
text-transform: lowercase;
|
||||
padding: $s-6;
|
||||
padding-left: $s-12;
|
||||
}
|
||||
|
||||
.label-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-theme-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.sets-list-wrapper {
|
||||
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
|
||||
border-radius: $s-8;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sets-count-empty-button {
|
||||
text-transform: lowercase;
|
||||
padding: $s-6;
|
||||
padding-left: $s-12;
|
||||
}
|
||||
|
||||
.group-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-4;
|
||||
}
|
||||
|
||||
.edit-theme-inputs-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 0.6fr 1fr;
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.group-drop-down-button {
|
||||
@include buttonStyle;
|
||||
color: var(--color-foreground-secondary);
|
||||
width: $s-24;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0 $s-6;
|
||||
}
|
270
frontend/src/app/main/ui/workspace/tokens/sets.cljs
Normal file
270
frontend/src/app/main/ui/workspace/tokens/sets.cljs
Normal file
|
@ -0,0 +1,270 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.sets
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.tokens :as wdt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.workspace.tokens.sets-context :as sets-context]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn on-toggle-token-set-click [token-set-name]
|
||||
(st/emit! (wdt/toggle-token-set {:token-set-name token-set-name})))
|
||||
|
||||
(defn on-select-token-set-click [name]
|
||||
(st/emit! (wdt/set-selected-token-set-id name)))
|
||||
|
||||
(defn on-update-token-set [set-name token-set]
|
||||
(st/emit! (wdt/update-token-set set-name token-set)))
|
||||
|
||||
(defn on-create-token-set [token-set]
|
||||
(st/emit! (wdt/create-token-set token-set)))
|
||||
|
||||
(mf/defc editing-node
|
||||
[{:keys [default-value on-cancel on-submit]}]
|
||||
(let [ref (mf/use-ref)
|
||||
on-submit-valid (mf/use-fn
|
||||
(fn [event]
|
||||
(let [value (str/trim (dom/get-target-val event))]
|
||||
(if (or (str/empty? value)
|
||||
(= value default-value))
|
||||
(on-cancel)
|
||||
(on-submit value)))))
|
||||
on-key-down (mf/use-fn
|
||||
(fn [event]
|
||||
(cond
|
||||
(kbd/enter? event) (on-submit-valid event)
|
||||
(kbd/esc? event) (on-cancel))))]
|
||||
[:input
|
||||
{:class (stl/css :editing-node)
|
||||
:type "text"
|
||||
:ref ref
|
||||
:on-blur on-submit-valid
|
||||
:on-key-down on-key-down
|
||||
:auto-focus true
|
||||
:default-value default-value}]))
|
||||
|
||||
(mf/defc sets-tree
|
||||
[{:keys [token-set
|
||||
token-set-active?
|
||||
token-set-selected?
|
||||
editing?
|
||||
on-select
|
||||
on-toggle
|
||||
on-edit
|
||||
on-submit
|
||||
on-cancel]
|
||||
:as _props}]
|
||||
(let [{:keys [name _children]} token-set
|
||||
selected? (and set? (token-set-selected? name))
|
||||
visible? (token-set-active? name)
|
||||
collapsed? (mf/use-state false)
|
||||
set? true #_(= type :set)
|
||||
group? false #_(= type :group)
|
||||
editing-node? (editing? name)
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps editing-node?)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(when-not editing-node?
|
||||
(on-select name))))
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps editing-node? name)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not editing-node?
|
||||
(st/emit!
|
||||
(wdt/show-token-set-context-menu
|
||||
{:position (dom/get-client-position event)
|
||||
:token-set-name name})))))
|
||||
|
||||
on-drag
|
||||
(mf/use-fn
|
||||
(mf/deps name)
|
||||
(fn [_]
|
||||
(when-not selected?
|
||||
(on-select name))))
|
||||
|
||||
on-drop
|
||||
(mf/use-fn
|
||||
(mf/deps name)
|
||||
(fn [position data]
|
||||
(st/emit! (wdt/move-token-set (:name data) name position))))
|
||||
|
||||
on-submit-edit
|
||||
(mf/use-fn
|
||||
(mf/deps on-submit token-set)
|
||||
#(on-submit (assoc token-set :name %)))
|
||||
|
||||
on-edit-name
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(let [name (-> (dom/get-current-target e)
|
||||
(dom/get-data "name"))]
|
||||
(on-edit name))))
|
||||
on-toggle-set (fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(on-toggle name))
|
||||
|
||||
on-collapse (mf/use-fn #(swap! collapsed? not))
|
||||
|
||||
|
||||
[dprops dref]
|
||||
(h/use-sortable
|
||||
:data-type "penpot/token-set"
|
||||
:on-drag on-drag
|
||||
:on-drop on-drop
|
||||
:data {:name name}
|
||||
:draggable? true)]
|
||||
[:div {:ref dref
|
||||
:role "button"
|
||||
:class (stl/css-case :set-item-container true
|
||||
:dnd-over (= (:over dprops) :center)
|
||||
:dnd-over-top (= (:over dprops) :top)
|
||||
:dnd-over-bot (= (:over dprops) :bot))
|
||||
:on-click on-click
|
||||
:on-double-click on-edit-name
|
||||
:on-context-menu on-context-menu
|
||||
:data-name name}
|
||||
[:div {:class (stl/css-case :set-item-group group?
|
||||
:set-item-set set?
|
||||
:selected-set selected?)}
|
||||
(when group?
|
||||
[:> icon-button* {:on-click on-collapse
|
||||
:aria-label (tr "labels.collapse")
|
||||
:icon (if @collapsed?
|
||||
"arrow-right"
|
||||
"arrow-down")
|
||||
:variant "action"}])
|
||||
|
||||
[:> icon* {:id (if set? "document" "group")
|
||||
:class (stl/css :icon)}]
|
||||
(if editing-node?
|
||||
[:& editing-node {:default-value name
|
||||
:on-submit on-submit-edit
|
||||
:on-cancel on-cancel}]
|
||||
[:*
|
||||
[:div {:class (stl/css :set-name)} name]
|
||||
(if set?
|
||||
[:button {:on-click on-toggle-set
|
||||
:class (stl/css-case :checkbox-style true
|
||||
:checkbox-checked-style visible?)}
|
||||
(when visible?
|
||||
[:> icon* {:aria-label (tr "workspace.token.select-set")
|
||||
:class (stl/css :check-icon)
|
||||
:size "s"
|
||||
:id ic/tick}])]
|
||||
nil
|
||||
#_(when (and children (not @collapsed?))
|
||||
[:div {:class (stl/css :set-children)}
|
||||
(for [child-id children]
|
||||
[:& sets-tree (assoc props :key child-id
|
||||
{:key child-id}
|
||||
:set-id child-id
|
||||
:selected-set-id selected-token-set-id)])]))])]]))
|
||||
|
||||
(defn warn-on-try-create-token-set-group! []
|
||||
(st/emit! (ntf/show {:content (tr "workspace.token.grouping-set-alert")
|
||||
:notification-type :toast
|
||||
:type :warning
|
||||
:timeout 3000})))
|
||||
|
||||
(mf/defc controlled-sets-list
|
||||
[{:keys [token-sets
|
||||
on-update-token-set
|
||||
token-set-selected?
|
||||
token-set-active?
|
||||
on-create-token-set
|
||||
on-toggle-token-set
|
||||
origin
|
||||
on-select
|
||||
context]
|
||||
:as _props}]
|
||||
(let [{:keys [editing? new? on-edit on-create on-reset] :as ctx} (or context (sets-context/use-context))
|
||||
avoid-token-set-grouping #(str/replace % "/" "-")
|
||||
submit-token
|
||||
#(do
|
||||
;; TODO: We don't support set grouping for now so we rename sets for now
|
||||
(when (str/includes? (:name %) "/")
|
||||
(warn-on-try-create-token-set-group!))
|
||||
(on-create-token-set (update % :name avoid-token-set-grouping))
|
||||
(on-reset))]
|
||||
[:ul {:class (stl/css :sets-list)}
|
||||
(if (and
|
||||
(= origin "theme-modal")
|
||||
(empty? token-sets))
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)}
|
||||
(tr "workspace.token.no-sets-create")]
|
||||
(for [token-set token-sets]
|
||||
(when token-set
|
||||
(let [update-token
|
||||
#(do
|
||||
;; TODO: We don't support set grouping for now so we rename sets for now
|
||||
(when (str/includes? (:name %) "/")
|
||||
(warn-on-try-create-token-set-group!))
|
||||
(on-update-token-set (avoid-token-set-grouping (:name token-set)) (update % :name avoid-token-set-grouping))
|
||||
(on-reset))]
|
||||
[:& sets-tree
|
||||
{:key (:name token-set)
|
||||
:token-set token-set
|
||||
:token-set-selected? (if new? (constantly false) token-set-selected?)
|
||||
:token-set-active? token-set-active?
|
||||
:editing? editing?
|
||||
:on-select on-select
|
||||
:on-edit on-edit
|
||||
:on-toggle on-toggle-token-set
|
||||
:on-submit update-token
|
||||
:on-cancel on-reset}]))))
|
||||
|
||||
(when new?
|
||||
[:& sets-tree
|
||||
{:token-set {:name ""}
|
||||
:token-set-selected? (constantly true)
|
||||
:token-set-active? (constantly true)
|
||||
:editing? (constantly true)
|
||||
:on-select (constantly nil)
|
||||
:on-edit on-create
|
||||
:on-submit submit-token
|
||||
:on-cancel on-reset}])]))
|
||||
|
||||
(mf/defc sets-list
|
||||
[{:keys []}]
|
||||
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
|
||||
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)
|
||||
token-set-selected? (mf/use-fn
|
||||
(mf/deps token-sets selected-token-set-id)
|
||||
(fn [set-name]
|
||||
(= set-name selected-token-set-id)))
|
||||
active-token-set-ids (mf/deref refs/workspace-active-set-names)
|
||||
token-set-active? (mf/use-fn
|
||||
(mf/deps active-token-set-ids)
|
||||
(fn [id]
|
||||
(get active-token-set-ids id)))]
|
||||
[:& controlled-sets-list
|
||||
{:token-sets token-sets
|
||||
:token-set-selected? token-set-selected?
|
||||
:token-set-active? token-set-active?
|
||||
:on-select on-select-token-set-click
|
||||
:origin "set-panel"
|
||||
:on-toggle-token-set on-toggle-token-set-click
|
||||
:on-update-token-set on-update-token-set
|
||||
:on-create-token-set on-create-token-set}]))
|
125
frontend/src/app/main/ui/workspace/tokens/sets.scss
Normal file
125
frontend/src/app/main/ui/workspace/tokens/sets.scss
Normal file
|
@ -0,0 +1,125 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.sets-list {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.set-item-container {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--layer-row-foreground-color);
|
||||
padding-left: $s-20;
|
||||
border: $s-2 solid transparent;
|
||||
|
||||
&.dnd-over-bot {
|
||||
border-bottom: $s-2 solid var(--layer-row-foreground-color-hover);
|
||||
}
|
||||
&.dnd-over-top {
|
||||
border-top: $s-2 solid var(--layer-row-foreground-color-hover);
|
||||
}
|
||||
&.dnd-over {
|
||||
border: $s-2 solid var(--layer-row-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.set-item-set,
|
||||
.set-item-group {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: $s-32;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--layer-row-foreground-color);
|
||||
}
|
||||
|
||||
.set-name {
|
||||
@include textEllipsis;
|
||||
flex-grow: 1;
|
||||
padding-left: $s-2;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: $s-20;
|
||||
height: $s-20;
|
||||
padding-right: $s-4;
|
||||
}
|
||||
|
||||
.checkbox-style {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: $s-16;
|
||||
height: $s-16;
|
||||
margin-inline: $s-6;
|
||||
background-color: var(--input-checkbox-background-color-rest);
|
||||
border: 1px solid var(--input-checkbox-border-color-rest);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.checkbox-checked-style {
|
||||
background-color: var(--input-border-color-active);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.set-item-set:hover {
|
||||
background-color: var(--layer-row-background-color-hover);
|
||||
color: var(--layer-row-foreground-color-hover);
|
||||
box-shadow: -100px 0 0 0 var(--layer-row-background-color-hover);
|
||||
}
|
||||
|
||||
.empty-state-message-sets {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: $s-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
.selected-set {
|
||||
background-color: var(--layer-row-background-color-selected);
|
||||
color: var(--layer-row-foreground-color-selected);
|
||||
box-shadow: -100px 0 0 0 var(--layer-row-background-color-selected);
|
||||
}
|
||||
|
||||
.collapsabled-icon {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
height: $s-24;
|
||||
border-radius: $br-8;
|
||||
&:hover {
|
||||
color: var(--title-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.editing-node {
|
||||
@include textEllipsis;
|
||||
color: var(--layer-row-foreground-color-focus);
|
||||
}
|
||||
|
||||
.editing-node {
|
||||
@include textEllipsis;
|
||||
@include bodySmallTypography;
|
||||
@include removeInputStyle;
|
||||
flex-grow: 1;
|
||||
height: $s-28;
|
||||
padding-left: $s-6;
|
||||
margin: 0;
|
||||
border-radius: $br-8;
|
||||
border: $s-1 solid var(--input-border-color-focus);
|
||||
color: var(--layer-row-foreground-color);
|
||||
}
|
47
frontend/src/app/main/ui/workspace/tokens/sets_context.cljs
Normal file
47
frontend/src/app/main/ui/workspace/tokens/sets_context.cljs
Normal file
|
@ -0,0 +1,47 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.sets-context
|
||||
(:require
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def initial {:editing-id nil
|
||||
:new? false})
|
||||
|
||||
(def context (mf/create-context initial))
|
||||
|
||||
(def static-context
|
||||
{:editing? (constantly false)
|
||||
:new? false
|
||||
:on-edit (constantly nil)
|
||||
:on-create (constantly nil)
|
||||
:on-reset (constantly nil)})
|
||||
|
||||
(mf/defc provider
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [children (unchecked-get props "children")
|
||||
state (mf/use-state initial)]
|
||||
[:& (mf/provider context) {:value state}
|
||||
children]))
|
||||
|
||||
(defn use-context []
|
||||
(let [ctx (mf/use-ctx context)
|
||||
{:keys [editing-id new?]} @ctx
|
||||
editing? (mf/use-callback
|
||||
(mf/deps editing-id)
|
||||
#(= editing-id %))
|
||||
on-edit (mf/use-fn
|
||||
#(swap! ctx assoc :editing-id %))
|
||||
on-create (mf/use-fn
|
||||
#(swap! ctx assoc :editing-id (random-uuid) :new? true))
|
||||
on-reset (mf/use-fn
|
||||
#(reset! ctx initial))]
|
||||
{:editing? editing?
|
||||
:new? new?
|
||||
:on-edit on-edit
|
||||
:on-create on-create
|
||||
:on-reset on-reset}))
|
|
@ -0,0 +1,65 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.sets-context-menu
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.tokens :as wdt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.workspace.tokens.sets-context :as sets-context]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def sets-menu-ref
|
||||
(l/derived :token-set-context-menu refs/workspace-local))
|
||||
|
||||
(defn- prevent-default
|
||||
[event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event))
|
||||
|
||||
(mf/defc menu-entry
|
||||
{::mf/props :obj}
|
||||
[{:keys [title value on-click]}]
|
||||
[:li
|
||||
{:class (stl/css :context-menu-item)
|
||||
:data-value value
|
||||
:on-click on-click}
|
||||
[:span {:class (stl/css :title)} title]])
|
||||
|
||||
(mf/defc menu
|
||||
[{:keys [token-set-name]}]
|
||||
(let [{:keys [on-edit]} (sets-context/use-context)
|
||||
edit-name (mf/use-fn #(on-edit token-set-name))
|
||||
delete-set (mf/use-fn #(st/emit! (wdt/delete-token-set token-set-name)))]
|
||||
[:ul {:class (stl/css :context-list)}
|
||||
[:& menu-entry {:title (tr "labels.rename") :on-click edit-name}]
|
||||
[:& menu-entry {:title (tr "labels.delete") :on-click delete-set}]]))
|
||||
|
||||
(mf/defc sets-context-menu
|
||||
[]
|
||||
(let [mdata (mf/deref sets-menu-ref)
|
||||
top (+ (get-in mdata [:position :y]) 5)
|
||||
left (+ (get-in mdata [:position :x]) 5)
|
||||
width (mf/use-state 0)
|
||||
dropdown-ref (mf/use-ref)
|
||||
token-set-name (:token-set-name mdata)]
|
||||
(mf/use-effect
|
||||
(mf/deps mdata)
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val dropdown-ref)]
|
||||
(reset! width (.-offsetWidth node)))))
|
||||
[:& dropdown {:show (boolean mdata)
|
||||
:on-close #(st/emit! wdt/hide-token-set-context-menu)}
|
||||
[:div {:class (stl/css :token-set-context-menu)
|
||||
:ref dropdown-ref
|
||||
:style {:top top :left left}
|
||||
:on-context-menu prevent-default}
|
||||
[:& menu {:token-set-name token-set-name}]]]))
|
|
@ -0,0 +1,46 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.token-set-context-menu {
|
||||
position: absolute;
|
||||
z-index: $z-index-4;
|
||||
}
|
||||
|
||||
.context-list {
|
||||
@include menuShadow;
|
||||
display: grid;
|
||||
width: $s-240;
|
||||
padding: $s-4;
|
||||
border-radius: $br-8;
|
||||
border: $s-2 solid var(--panel-border-color);
|
||||
background-color: var(--menu-background-color);
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
@include bodySmallTypography;
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $s-28;
|
||||
width: 100%;
|
||||
padding: $s-6;
|
||||
border-radius: $br-8;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
.title {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
360
frontend/src/app/main/ui/workspace/tokens/sidebar.cljs
Normal file
360
frontend/src/app/main/ui/workspace/tokens/sidebar.cljs
Normal file
|
@ -0,0 +1,360 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.sidebar
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.tokens :as dt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.color-bullet :refer [color-bullet]]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.hooks.resize :refer [use-resize-hook]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.main.ui.workspace.tokens.changes :as wtch]
|
||||
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
|
||||
[app.main.ui.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.workspace.tokens.sets :refer [sets-list]]
|
||||
[app.main.ui.workspace.tokens.sets-context :as sets-context]
|
||||
[app.main.ui.workspace.tokens.sets-context-menu :refer [sets-context-menu]]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.main.ui.workspace.tokens.theme-select :refer [theme-select]]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[app.main.ui.workspace.tokens.token-types :as wtty]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]
|
||||
[shadow.resource]))
|
||||
|
||||
(def lens:token-type-open-status
|
||||
(l/derived (l/in [:workspace-tokens :open-status]) st/state))
|
||||
|
||||
(def ^:private download-icon
|
||||
(i/icon-xref :download (stl/css :download-icon)))
|
||||
|
||||
;; Components ------------------------------------------------------------------
|
||||
|
||||
(mf/defc token-pill
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-click token theme-token highlighted? on-context-menu]}]
|
||||
(let [{:keys [name value resolved-value errors]} token
|
||||
errors? (and (seq errors) (seq (:errors theme-token)))]
|
||||
[:button
|
||||
{:class (stl/css-case :token-pill true
|
||||
:token-pill-highlighted highlighted?
|
||||
:token-pill-invalid errors?)
|
||||
:title (cond
|
||||
errors? (sd/humanize-errors token)
|
||||
:else (->> [(str "Token: " name)
|
||||
(str (tr "workspace.token.original-value") value)
|
||||
(str (tr "workspace.token.resolved-value") resolved-value)]
|
||||
(str/join "\n")))
|
||||
:on-click on-click
|
||||
:on-context-menu on-context-menu
|
||||
:disabled errors?}
|
||||
(when-let [color (if (seq (ctob/find-token-value-references (:value token)))
|
||||
(wtt/resolved-value-hex theme-token)
|
||||
(wtt/resolved-value-hex token))]
|
||||
[:& color-bullet {:color color
|
||||
:mini? true}])
|
||||
name]))
|
||||
|
||||
(mf/defc token-section-icon
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [type]}]
|
||||
(case type
|
||||
:border-radius i/corner-radius
|
||||
:numeric [:span {:class (stl/css :section-text-icon)} "123"]
|
||||
:color i/drop-icon
|
||||
:boolean i/boolean-difference
|
||||
:opacity [:span {:class (stl/css :section-text-icon)} "%"]
|
||||
:rotation i/rotation
|
||||
:spacing i/padding-extended
|
||||
:string i/text-mixed
|
||||
:stroke-width i/stroke-size
|
||||
:typography i/text
|
||||
;; TODO: Add diagonal icon here when it's available
|
||||
:dimensions [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
||||
:sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
||||
i/add))
|
||||
|
||||
(mf/defc token-component
|
||||
[{:keys [type tokens selected-shapes token-type-props active-theme-tokens]}]
|
||||
(let [open? (mf/deref (-> (l/key type)
|
||||
(l/derived lens:token-type-open-status)))
|
||||
{:keys [modal attributes all-attributes title]} token-type-props
|
||||
|
||||
on-context-menu (mf/use-fn
|
||||
(fn [event token]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dt/show-token-context-menu {:type :token
|
||||
:position (dom/get-client-position event)
|
||||
:token-name (:name token)}))))
|
||||
|
||||
on-toggle-open-click (mf/use-fn
|
||||
(mf/deps open? tokens)
|
||||
#(st/emit! (dt/set-token-type-section-open type (not open?))))
|
||||
on-popover-open-click (mf/use-fn
|
||||
(fn [event]
|
||||
(mf/deps type title)
|
||||
(let [{:keys [key fields]} modal]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dt/set-token-type-section-open type true))
|
||||
(modal/show! key {:x (.-clientX ^js event)
|
||||
:y (.-clientY ^js event)
|
||||
:position :right
|
||||
:fields fields
|
||||
:title title
|
||||
:action "create"
|
||||
:token-type type}))))
|
||||
|
||||
on-token-pill-click (mf/use-fn
|
||||
(mf/deps selected-shapes token-type-props)
|
||||
(fn [event token]
|
||||
(dom/stop-propagation event)
|
||||
(when (seq selected-shapes)
|
||||
(st/emit!
|
||||
(wtch/toggle-token {:token token
|
||||
:shapes selected-shapes
|
||||
:token-type-props token-type-props})))))
|
||||
tokens-count (count tokens)]
|
||||
[:div {:on-click on-toggle-open-click}
|
||||
[:& cmm/asset-section {:icon (mf/fnc icon-wrapper []
|
||||
[:div {:class (stl/css :section-icon)}
|
||||
[:& token-section-icon {:type type}]])
|
||||
:title title
|
||||
:assets-count tokens-count
|
||||
:open? open?}
|
||||
[:& cmm/asset-section-block {:role :title-button}
|
||||
[:button {:class (stl/css :action-button)
|
||||
:on-click on-popover-open-click}
|
||||
i/add]]
|
||||
(when open?
|
||||
[:& cmm/asset-section-block {:role :content}
|
||||
[:div {:class (stl/css :token-pills-wrapper)}
|
||||
(for [token (sort-by :name tokens)]
|
||||
(let [theme-token (get active-theme-tokens (wtt/token-identifier token))]
|
||||
[:& token-pill
|
||||
{:key (:name token)
|
||||
:token token
|
||||
:theme-token theme-token
|
||||
:highlighted? (wtt/shapes-token-applied? token selected-shapes (or all-attributes attributes))
|
||||
:on-click #(on-token-pill-click % token)
|
||||
:on-context-menu #(on-context-menu % token)}]))]])]]))
|
||||
|
||||
(defn sorted-token-groups
|
||||
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
|
||||
Sort each group alphabetically (by their `:token-key`)."
|
||||
[tokens]
|
||||
(let [tokens-by-type (ctob/group-by-type tokens)
|
||||
{:keys [empty filled]} (->> wtty/token-types
|
||||
(map (fn [[token-key token-type-props]]
|
||||
{:token-key token-key
|
||||
:token-type-props token-type-props
|
||||
:tokens (get tokens-by-type token-key [])}))
|
||||
(group-by (fn [{:keys [tokens]}]
|
||||
(if (empty? tokens) :empty :filled))))]
|
||||
{:empty (sort-by :token-key empty)
|
||||
:filled (sort-by :token-key filled)}))
|
||||
|
||||
(mf/defc themes-header
|
||||
[_props]
|
||||
(let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden)
|
||||
open-modal
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(modal/show! :tokens/themes {})))]
|
||||
[:div {:class (stl/css :themes-wrapper)}
|
||||
[:span {:class (stl/css :themes-header)} (tr "labels.themes")]
|
||||
(if (empty? ordered-themes)
|
||||
[:div {:class (stl/css :empty-theme-wrapper)}
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
|
||||
(tr "workspace.token.no-themes")]
|
||||
[:button {:on-click open-modal
|
||||
:class (stl/css :create-theme-button)}
|
||||
(tr "workspace.token.create-one")]]
|
||||
[:div {:class (stl/css :theme-select-wrapper)}
|
||||
[:& theme-select]
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click open-modal}
|
||||
(tr "labels.edit")]])]))
|
||||
|
||||
(mf/defc add-set-button
|
||||
[{:keys [on-open style]}]
|
||||
(let [{:keys [on-create]} (sets-context/use-context)
|
||||
on-click #(do
|
||||
(on-open)
|
||||
(on-create))]
|
||||
(if (= style "inline")
|
||||
[:button {:on-click on-click
|
||||
:class (stl/css :create-theme-button)}
|
||||
(tr "workspace.token.create-one")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:icon "add"
|
||||
:on-click on-click
|
||||
:aria-label (tr "workspace.token.add set")}])))
|
||||
|
||||
(mf/defc themes-sets-tab
|
||||
[]
|
||||
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
|
||||
open? (mf/use-state true)
|
||||
on-open (mf/use-fn #(reset! open? true))]
|
||||
[:& sets-context/provider {}
|
||||
[:& sets-context-menu]
|
||||
[:div {:class (stl/css :sets-sidebar)}
|
||||
[:& themes-header]
|
||||
[:div {:class (stl/css :sidebar-header)}
|
||||
[:& title-bar {:collapsable true
|
||||
:collapsed (not @open?)
|
||||
:all-clickable true
|
||||
:title (tr "labels.sets")
|
||||
:on-collapsed #(swap! open? not)}
|
||||
[:& add-set-button {:on-open on-open
|
||||
:style "header"}]]]
|
||||
(when @open?
|
||||
[:& h/sortable-container {}
|
||||
[:*
|
||||
(when (empty? token-sets)
|
||||
[:div {:class (stl/css :empty-sets-wrapper)}
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
|
||||
(tr "workspace.token.no-sets-yet")]
|
||||
[:& add-set-button {:on-open on-open
|
||||
:style "inline"}]])
|
||||
[:& sets-list]]])]]))
|
||||
|
||||
(mf/defc tokens-tab
|
||||
[_props]
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
selected-shapes (into [] (keep (d/getf objects)) selected)
|
||||
|
||||
active-theme-tokens (sd/use-active-theme-sets-tokens)
|
||||
|
||||
tokens (sd/use-resolved-workspace-tokens)
|
||||
token-groups (mf/with-memo [tokens]
|
||||
(sorted-token-groups tokens))]
|
||||
[:*
|
||||
[:& token-context-menu]
|
||||
[:& title-bar {:all-clickable true
|
||||
:title "TOKENS"}]
|
||||
[:div.assets-bar
|
||||
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
|
||||
(:empty token-groups))]
|
||||
[:& token-component {:key token-key
|
||||
:type token-key
|
||||
:selected-shapes selected-shapes
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:tokens tokens
|
||||
:token-type-props token-type-props}])]]))
|
||||
|
||||
(mf/defc json-import-button []
|
||||
(let []
|
||||
[:div
|
||||
|
||||
[:button {:class (stl/css :download-json-button)
|
||||
:on-click #(.click (js/document.getElementById "file-input"))}
|
||||
download-icon
|
||||
"Import JSON"]]))
|
||||
|
||||
(mf/defc import-export-button
|
||||
{::mf/wrap-props false}
|
||||
[{:keys []}]
|
||||
(let [show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
|
||||
open-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* true)))
|
||||
|
||||
close-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)))
|
||||
|
||||
input-ref (mf/use-ref)
|
||||
on-import
|
||||
(fn [event]
|
||||
(let [file (-> event .-target .-files (aget 0))]
|
||||
(->> (wapi/read-file-as-text file)
|
||||
(sd/process-json-stream)
|
||||
(rx/subs! (fn [lib]
|
||||
(st/emit! (dt/import-tokens-lib lib)))
|
||||
(fn [err]
|
||||
(js/console.error err)
|
||||
(st/emit! (ntf/show {:content (wte/humanize-errors [(ex-data err)])
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 9000})))))
|
||||
(set! (.-value (mf/ref-val input-ref)) "")))
|
||||
on-export (fn []
|
||||
(let [tokens-blob (some-> (deref refs/tokens-lib)
|
||||
(ctob/encode-dtcg)
|
||||
(clj->js)
|
||||
(js/JSON.stringify nil 2)
|
||||
(wapi/create-blob "application/json"))]
|
||||
(dom/trigger-download "tokens.json" tokens-blob)))]
|
||||
[:div {:class (stl/css :import-export-button-wrapper)}
|
||||
[:input {:type "file"
|
||||
:ref input-ref
|
||||
:style {:display "none"}
|
||||
:id "file-input"
|
||||
:accept ".json"
|
||||
:on-change on-import}]
|
||||
[:button {:class (stl/css :import-export-button)
|
||||
:on-click open-menu}
|
||||
download-icon
|
||||
"Tokens"]
|
||||
[:& dropdown-menu {:show show-menu?
|
||||
:on-close close-menu
|
||||
:list-class (stl/css :import-export-menu)}
|
||||
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
|
||||
:on-click #(.click (mf/ref-val input-ref))}
|
||||
"Import"]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
|
||||
:on-click on-export}
|
||||
"Export"]]]))
|
||||
|
||||
(mf/defc tokens-sidebar-tab
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[_props]
|
||||
(let [{on-pointer-down-pages :on-pointer-down
|
||||
on-lost-pointer-capture-pages :on-lost-pointer-capture
|
||||
on-pointer-move-pages :on-pointer-move
|
||||
size-pages-opened :size}
|
||||
(use-resize-hook :tokens 200 38 400 :y false nil)]
|
||||
[:div {:class (stl/css :sidebar-wrapper)}
|
||||
[:article {:class (stl/css :sets-section-wrapper)
|
||||
:style {"--resize-height" (str size-pages-opened "px")}}
|
||||
[:& themes-sets-tab]]
|
||||
[:article {:class (stl/css :tokens-section-wrapper)}
|
||||
[:div {:class (stl/css :resize-area-horiz)
|
||||
:on-pointer-down on-pointer-down-pages
|
||||
:on-lost-pointer-capture on-lost-pointer-capture-pages
|
||||
:on-pointer-move on-pointer-move-pages}]
|
||||
[:& tokens-tab]
|
||||
[:& import-export-button]]]))
|
196
frontend/src/app/main/ui/workspace/tokens/sidebar.scss
Normal file
196
frontend/src/app/main/ui/workspace/tokens/sidebar.scss
Normal file
|
@ -0,0 +1,196 @@
|
|||
// 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 INC
|
||||
|
||||
@use "../../ds/typography.scss" as *;
|
||||
@import "refactor/common-refactor.scss";
|
||||
@import "./common.scss";
|
||||
|
||||
.sidebar-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
// Overflow on the bottom section can't be done without hardcoded values for the height
|
||||
// This has to be changed from the wrapping sidebar styles
|
||||
height: calc(100vh - #{$s-84});
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sets-section-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: var(--resize-height);
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.tokens-section-wrapper {
|
||||
height: 100%;
|
||||
padding-left: $s-12;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.sets-sidebar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.themes-header {
|
||||
display: block;
|
||||
@include headlineSmallTypography;
|
||||
margin-bottom: $s-8;
|
||||
padding-left: $s-8;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.themes-wrapper {
|
||||
padding: $s-12 0 0 $s-12;
|
||||
}
|
||||
|
||||
.empty-theme-wrapper {
|
||||
padding: $s-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.empty-sets-wrapper {
|
||||
padding: $s-12;
|
||||
padding-inline-start: $s-24;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-left: $s-8;
|
||||
padding-top: $s-12;
|
||||
color: var(--layer-row-foreground-color);
|
||||
}
|
||||
|
||||
.empty-state-message {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.token-pills-wrapper {
|
||||
display: flex;
|
||||
gap: $s-4;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.token-pill {
|
||||
@extend .button-secondary;
|
||||
gap: $s-8;
|
||||
padding: $s-4 $s-8;
|
||||
border-radius: $br-6;
|
||||
font-size: $fs-14;
|
||||
|
||||
&.token-pill-highlighted {
|
||||
color: var(--button-primary-foreground-color-rest);
|
||||
background: var(--button-primary-background-color-rest);
|
||||
}
|
||||
|
||||
&.token-pill-invalid {
|
||||
background-color: var(--button-secondary-background-color-rest);
|
||||
color: var(--status-color-error-500);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.section-text-icon {
|
||||
font-size: $fs-12;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
margin-right: $s-4;
|
||||
// Align better with the label
|
||||
translate: 0px -1px;
|
||||
}
|
||||
|
||||
.import-export-button-wrapper {
|
||||
position: absolute;
|
||||
bottom: $s-12;
|
||||
right: $s-12;
|
||||
}
|
||||
|
||||
.import-export-button {
|
||||
@extend .button-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $s-6 $s-8;
|
||||
text-transform: uppercase;
|
||||
gap: $s-8;
|
||||
|
||||
.download-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-export-menu {
|
||||
@extend .menu-dropdown;
|
||||
top: -#{$s-6};
|
||||
right: 0;
|
||||
translate: 0 -100%;
|
||||
width: $s-192;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.import-export-menu-item {
|
||||
@extend .menu-item-base;
|
||||
cursor: pointer;
|
||||
.open-arrow {
|
||||
@include flexCenter;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.open-arrow {
|
||||
svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-select-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.28fr;
|
||||
gap: $s-6;
|
||||
}
|
||||
|
||||
.themes-button {
|
||||
@extend .button-secondary;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.create-theme-button {
|
||||
@include use-typography("body-small");
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
appearance: none;
|
||||
color: var(--color-accent-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.resize-area-horiz {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-bottom: $s-2 solid var(--resize-area-border-color);
|
||||
cursor: ns-resize;
|
||||
}
|
261
frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
Normal file
261
frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
Normal file
|
@ -0,0 +1,261 @@
|
|||
(ns app.main.ui.workspace.tokens.style-dictionary
|
||||
(:require
|
||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(l/set-level! "app.main.ui.workspace.tokens.style-dictionary" :warn)
|
||||
|
||||
;; === Style Dictionary
|
||||
|
||||
(def setup-style-dictionary
|
||||
"Initiates the StyleDictionary instance.
|
||||
Setup transforms from tokens-studio used to parse and resolved token values."
|
||||
(do
|
||||
(sd-transforms/registerTransforms sd)
|
||||
(.registerFormat sd #js {:name "custom/json"
|
||||
:format (fn [^js res]
|
||||
(.-tokens (.-dictionary res)))})
|
||||
sd))
|
||||
|
||||
(def default-config
|
||||
{:platforms {:json
|
||||
{:transformGroup "tokens-studio"
|
||||
;; Required: The StyleDictionary API is focused on files even when working in the browser
|
||||
:files [{:format "custom/json" :destination "penpot"}]}}
|
||||
:preprocessors ["tokens-studio"]
|
||||
;; Silences style dictionary logs and errors
|
||||
;; We handle token errors in the UI
|
||||
:log {:verbosity "silent"
|
||||
:warnings "silent"
|
||||
:errors {:brokenReferences "console"}}})
|
||||
|
||||
(defn parse-sd-token-color-value
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
|
||||
(defn parse-sd-token-dimensions-value
|
||||
"Parses `value` of a dimensions `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(or
|
||||
(wtt/parse-token-value value)
|
||||
(if-let [references (seq (ctob/find-token-value-references value))]
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
||||
:references references}
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
||||
|
||||
(defn process-sd-tokens
|
||||
"Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure.
|
||||
The `get-origin-token` argument should be a function that takes an
|
||||
`sd-token` and returns the original penpot token, so we can merge
|
||||
the resolved attributes back in.
|
||||
|
||||
The `sd-token` will have references in `value` replaced with the computed value as a string.
|
||||
Here's an example for a `sd-token`:
|
||||
```js
|
||||
{
|
||||
name: 'token.with.reference',
|
||||
value: '12px',
|
||||
type: 'border-radius',
|
||||
path: ['token', 'with', 'reference'],
|
||||
|
||||
// The penpot origin token converted to a js object
|
||||
original: {
|
||||
name: 'token.with.reference',
|
||||
value: '{referenced.token}',
|
||||
type: 'border-radius'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
We also convert `sd-token` value string into a unit that can be used as penpot shape attributes.
|
||||
- Dimensions like '12px' will be converted into numbers
|
||||
- Colors will be validated & converted to hex
|
||||
|
||||
Lastly we check for errors in each token
|
||||
`sd-token` will keep the missing references in the `value` (E.g \"{missing} + {existing}\" -> \"{missing} + 12px\")
|
||||
So we parse out the missing references and add them to `:errors` in the final token."
|
||||
[sd-tokens get-origin-token]
|
||||
(reduce
|
||||
(fn [acc ^js sd-token]
|
||||
(let [origin-token (get-origin-token sd-token)
|
||||
value (.-value sd-token)
|
||||
parsed-token-value (case (:type origin-token)
|
||||
:color (parse-sd-token-color-value value)
|
||||
(parse-sd-token-dimensions-value value))
|
||||
output-token (if (:errors parsed-token-value)
|
||||
(merge origin-token parsed-token-value)
|
||||
(assoc origin-token
|
||||
:resolved-value (:value parsed-token-value)
|
||||
:unit (:unit parsed-token-value)))]
|
||||
(assoc acc (:name output-token) output-token)))
|
||||
{} sd-tokens))
|
||||
|
||||
(defprotocol IStyleDictionary
|
||||
(add-tokens [_ tokens])
|
||||
(enable-debug [_])
|
||||
(get-config [_])
|
||||
(build-dictionary [_]))
|
||||
|
||||
(deftype StyleDictionary [config]
|
||||
IStyleDictionary
|
||||
(add-tokens [_ tokens]
|
||||
(StyleDictionary. (assoc config :tokens tokens)))
|
||||
|
||||
(enable-debug [_]
|
||||
(StyleDictionary. (update config :log merge {:verbosity "verbose"})))
|
||||
|
||||
(get-config [_]
|
||||
config)
|
||||
|
||||
(build-dictionary [_]
|
||||
(-> (sd. (clj->js config))
|
||||
(.buildAllPlatforms "json")
|
||||
(p/then #(.-allTokens ^js %)))))
|
||||
|
||||
(defn resolve-tokens-tree+
|
||||
([tokens-tree get-token]
|
||||
(resolve-tokens-tree+ tokens-tree get-token (StyleDictionary. default-config)))
|
||||
([tokens-tree get-token style-dictionary]
|
||||
(-> style-dictionary
|
||||
(add-tokens tokens-tree)
|
||||
(build-dictionary)
|
||||
(p/then #(process-sd-tokens % get-token)))))
|
||||
|
||||
(defn sd-token-name [^js sd-token]
|
||||
(.. sd-token -original -name))
|
||||
|
||||
(defn sd-token-uuid [^js sd-token]
|
||||
(uuid (.-uuid (.-id ^js sd-token))))
|
||||
|
||||
(defn resolve-tokens+ [tokens]
|
||||
(resolve-tokens-tree+ (ctob/tokens-tree tokens) #(get tokens (sd-token-name %))))
|
||||
|
||||
(defn resolve-tokens-interactive+
|
||||
"Interactive check of resolving tokens.
|
||||
Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
|
||||
|
||||
We have to pass in all tokens from all sets in the entire library to style dictionary
|
||||
so we know if references are missing / to resolve them and possibly show interactive previews (in the tokens form) to the user.
|
||||
|
||||
Since we're using the :name path as the identifier we might be throwing away or overriding tokens in the tree that we pass to StyleDictionary.
|
||||
|
||||
So to get back the original token from the resolved sd-token (see my updates for what an sd-token is) we include a temporary :id for the token that we pass to StyleDictionary,
|
||||
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
|
||||
[tokens]
|
||||
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
|
||||
(resolve-tokens-tree+ tokens-tree #(get ids (sd-token-uuid %)))))
|
||||
|
||||
(defn resolve-tokens-with-errors+ [tokens]
|
||||
(resolve-tokens-tree+
|
||||
(ctob/tokens-tree tokens)
|
||||
#(get tokens (sd-token-name %))
|
||||
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
|
||||
|
||||
;; === Import
|
||||
|
||||
(defn reference-errors
|
||||
"Extracts reference errors from StyleDictionary."
|
||||
[err]
|
||||
(let [[header-1 header-2 & errors] (str/split err "\n")]
|
||||
(when (and
|
||||
(= header-1 "Error: ")
|
||||
(= header-2 "Reference Errors:"))
|
||||
errors)))
|
||||
|
||||
(defn process-json-stream [data-stream]
|
||||
(->> data-stream
|
||||
(rx/map (fn [data]
|
||||
(try
|
||||
(-> (str/replace data "/" "-") ;; TODO Remove when token groups work
|
||||
(t/decode-str))
|
||||
(catch js/Error e
|
||||
(throw (wte/error-ex-info :error.import/json-parse-error data e))))))
|
||||
(rx/map (fn [json-data]
|
||||
(try
|
||||
(ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data)
|
||||
(catch js/Error e
|
||||
(throw (wte/error-ex-info :error.import/invalid-json-data json-data e))))))
|
||||
(rx/mapcat (fn [tokens-lib]
|
||||
(try
|
||||
(-> (ctob/get-all-tokens tokens-lib)
|
||||
(resolve-tokens-with-errors+)
|
||||
(p/then (fn [_] tokens-lib))
|
||||
(p/catch (fn [sd-error]
|
||||
(let [reference-errors (reference-errors sd-error)
|
||||
err (if reference-errors
|
||||
(wte/error-ex-info :error.import/style-dictionary-reference-errors reference-errors sd-error)
|
||||
(wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))]
|
||||
(throw err)))))
|
||||
(catch js/Error e
|
||||
(p/rejected (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e))))))))
|
||||
|
||||
;; === Errors
|
||||
|
||||
(defn humanize-errors [{:keys [errors value] :as _token}]
|
||||
(->> (map (fn [err]
|
||||
(case err
|
||||
:error.style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value)
|
||||
nil))
|
||||
errors)
|
||||
(str/join "\n")))
|
||||
|
||||
;; === Hooks
|
||||
|
||||
(defonce !tokens-cache (atom nil))
|
||||
|
||||
(defonce !theme-tokens-cache (atom nil))
|
||||
|
||||
(defn use-resolved-tokens
|
||||
"The StyleDictionary process function is async, so we can't use resolved values directly.
|
||||
|
||||
This hook will return the unresolved tokens as state until they are processed,
|
||||
then the state will be updated with the resolved tokens."
|
||||
[tokens & {:keys [cache-atom interactive?]
|
||||
:or {cache-atom !tokens-cache}
|
||||
:as config}]
|
||||
(let [tokens-state (mf/use-state (get @cache-atom tokens))]
|
||||
(mf/use-effect
|
||||
(mf/deps tokens config)
|
||||
(fn []
|
||||
(let [cached (get @cache-atom tokens)]
|
||||
(cond
|
||||
(nil? tokens) nil
|
||||
;; The tokens are already processing somewhere
|
||||
(p/promise? cached) (-> cached
|
||||
(p/then #(reset! tokens-state %))
|
||||
#_(p/catch js/console.error))
|
||||
;; Get the cached entry
|
||||
(some? cached) (reset! tokens-state cached)
|
||||
;; No cached entry, start processing
|
||||
:else (let [promise+ (if interactive?
|
||||
(resolve-tokens-interactive+ tokens)
|
||||
(resolve-tokens+ tokens))]
|
||||
(swap! cache-atom assoc tokens promise+)
|
||||
(p/then promise+ (fn [resolved-tokens]
|
||||
(swap! cache-atom assoc tokens resolved-tokens)
|
||||
(reset! tokens-state resolved-tokens))))))))
|
||||
@tokens-state))
|
||||
|
||||
(defn use-resolved-workspace-tokens []
|
||||
(-> (mf/deref refs/workspace-selected-token-set-tokens)
|
||||
(use-resolved-tokens)))
|
||||
|
||||
(defn use-active-theme-sets-tokens []
|
||||
(-> (mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
(use-resolved-tokens {:cache-atom !theme-tokens-cache})))
|
117
frontend/src/app/main/ui/workspace/tokens/theme_select.cljs
Normal file
117
frontend/src/app/main/ui/workspace/tokens/theme_select.cljs
Normal file
|
@ -0,0 +1,117 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.theme-select
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.tokens :as wdt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc themes-list
|
||||
[{:keys [themes active-theme-paths on-close grouped?]}]
|
||||
(when (seq themes)
|
||||
[:ul {:class (stl/css :theme-options)}
|
||||
(for [[_ {:keys [group name] :as theme}] themes
|
||||
:let [theme-id (ctob/theme-path theme)
|
||||
selected? (get active-theme-paths theme-id)
|
||||
select-theme (fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(st/emit! (wdt/toggle-token-theme-active? group name))
|
||||
(on-close))]]
|
||||
[:li {:key theme-id
|
||||
:role "option"
|
||||
:aria-selected selected?
|
||||
:class (stl/css-case
|
||||
:checked-element true
|
||||
:sub-item grouped?
|
||||
:is-selected selected?)
|
||||
:on-click select-theme}
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :label)} name]
|
||||
[:> icon* {:id i/tick
|
||||
:aria-hidden true
|
||||
:class (stl/css-case :check-icon true
|
||||
:check-icon-visible selected?)}]])]))
|
||||
|
||||
(mf/defc theme-options
|
||||
[{:keys [active-theme-paths themes on-close]}]
|
||||
(let []
|
||||
(let [on-edit-click #(modal/show! :tokens/themes {})]
|
||||
[:ul {:class (stl/css :theme-options :custom-select-dropdown)
|
||||
:role "listbox"}
|
||||
(for [[group themes] themes]
|
||||
[:li {:key group
|
||||
:aria-labelledby (dm/str group "-label")
|
||||
:role "group"}
|
||||
(when (seq group)
|
||||
[:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str group "-label")} group])
|
||||
[:& themes-list {:themes themes
|
||||
:active-theme-paths active-theme-paths
|
||||
:on-close on-close
|
||||
:grouped? true}]])
|
||||
[:li {:class (stl/css :separator)
|
||||
:aria-hidden true}]
|
||||
[:li {:class (stl/css-case :checked-element true
|
||||
:checked-element-button true)
|
||||
:role "option"
|
||||
:on-click on-edit-click}
|
||||
[:> text* {:as "span" :typography "body-small"} (tr "workspace.token.edit-themes")]
|
||||
[:> icon* {:id i/arrow-right :aria-hidden true}]]])))
|
||||
|
||||
(mf/defc theme-select
|
||||
[{:keys []}]
|
||||
(let [;; Store
|
||||
active-theme-paths (mf/deref refs/workspace-active-theme-paths-no-hidden)
|
||||
active-themes-count (count active-theme-paths)
|
||||
themes (mf/deref refs/workspace-token-theme-tree-no-hidden)
|
||||
|
||||
;; Data
|
||||
current-label (cond
|
||||
(> active-themes-count 1) (tr "workspace.token.active-themes" active-themes-count)
|
||||
(= active-themes-count 1) (some->> (first active-theme-paths)
|
||||
(ctob/split-token-theme-path)
|
||||
(str/join " / "))
|
||||
:else (tr "workspace.token.no-active-theme"))
|
||||
|
||||
;; State
|
||||
state* (mf/use-state
|
||||
{:id (uuid/next)
|
||||
:is-open? false})
|
||||
state (deref state*)
|
||||
is-open? (:is-open? state)
|
||||
|
||||
;; Dropdown
|
||||
dropdown-element* (mf/use-ref nil)
|
||||
on-close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false))
|
||||
on-open-dropdown (mf/use-fn #(swap! state* assoc :is-open? true))]
|
||||
|
||||
;; TODO: This element should be accessible by keyboard
|
||||
[:div {:on-click on-open-dropdown
|
||||
:aria-expanded is-open?
|
||||
:aria-haspopup "listbox"
|
||||
:tab-index "0"
|
||||
:role "combobox"
|
||||
:class (stl/css :custom-select)}
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :current-label)}
|
||||
current-label]
|
||||
[:> icon* {:id i/arrow-down :class (stl/css :dropdown-button) :aria-hidden true}]
|
||||
[:& dropdown {:show is-open?
|
||||
:on-close on-close-dropdown
|
||||
:ref dropdown-element*}
|
||||
[:& theme-options {:active-theme-paths active-theme-paths
|
||||
:themes themes
|
||||
:on-close on-close-dropdown}]]]))
|
124
frontend/src/app/main/ui/workspace/tokens/theme_select.scss
Normal file
124
frontend/src/app/main/ui/workspace/tokens/theme_select.scss
Normal file
|
@ -0,0 +1,124 @@
|
|||
// 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 INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.custom-select {
|
||||
--custom-select-border-color: var(--menu-background-color);
|
||||
--custom-select-bg-color: var(--menu-background-color);
|
||||
--custom-select-icon-color: var(--color-foreground-secondary);
|
||||
--custom-select-text-color: var(--menu-foreground-color);
|
||||
@extend .new-scrollbar;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
height: $s-32;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: $s-8;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--custom-select-bg-color);
|
||||
border: $s-1 solid var(--custom-select-border-color);
|
||||
color: var(--custom-select-text-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
--custom-select-bg-color: var(--menu-background-color-hover);
|
||||
--custom-select-border-color: var(--menu-background-color);
|
||||
--custom-select-icon-color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
--custom-select-bg-color: var(--menu-background-color-focus);
|
||||
--custom-select-border-color: var(--menu-background-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: block;
|
||||
padding: $s-8;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
--custom-select-bg-color: var(--menu-background-color-disabled);
|
||||
--custom-select-border-color: var(--menu-border-color-disabled);
|
||||
--custom-select-icon-color: var(--menu-foreground-color-disabled);
|
||||
--custom-select-text-color: var(--menu-foreground-color-disabled);
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
@include flexCenter;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.current-icon {
|
||||
@include flexCenter;
|
||||
width: $s-24;
|
||||
padding-right: $s-4;
|
||||
}
|
||||
|
||||
.custom-select-dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0;
|
||||
height: $s-2;
|
||||
border-block-start: $s-1 solid color-mix(in hsl, var(--color-foreground-secondary) 20%, transparent);
|
||||
}
|
||||
|
||||
.custom-select-dropdown[data-direction="up"] {
|
||||
bottom: $s-32;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.sub-item {
|
||||
padding-left: $s-16;
|
||||
}
|
||||
|
||||
.checked-element-button {
|
||||
@extend .dropdown-element-base;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.checked-element {
|
||||
@extend .dropdown-element-base;
|
||||
&.is-selected {
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
&.disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@include flexCenter;
|
||||
color: var(--icon-foreground-primary);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.check-icon-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.current-label {
|
||||
@include textEllipsis;
|
||||
}
|
27
frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs
Normal file
27
frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs
Normal file
|
@ -0,0 +1,27 @@
|
|||
(ns app.main.ui.workspace.tokens.tinycolor
|
||||
"Bindings for tinycolor2 which supports a wide range of css compatible colors.
|
||||
|
||||
This library was chosen as it is already used by StyleDictionary,
|
||||
so there is no extra dependency cost and there was no clojure alternatives with all the necessary features."
|
||||
(:require
|
||||
["tinycolor2" :as tinycolor]))
|
||||
|
||||
(defn tinycolor? [^js x]
|
||||
(and (instance? tinycolor x) (.isValid x)))
|
||||
|
||||
(defn valid-color [color-str]
|
||||
(let [tc (tinycolor color-str)]
|
||||
(when (.isValid tc) tc)))
|
||||
|
||||
(defn ->hex [^js tc]
|
||||
(assert (tinycolor? tc))
|
||||
(.toHex tc))
|
||||
|
||||
(defn color-format [^js tc]
|
||||
(assert (tinycolor? tc))
|
||||
(.getFormat tc))
|
||||
|
||||
(comment
|
||||
(some-> (valid-color "red") ->hex)
|
||||
(some-> (valid-color "red") color-format)
|
||||
nil)
|
142
frontend/src/app/main/ui/workspace/tokens/token.cljs
Normal file
142
frontend/src/app/main/ui/workspace/tokens/token.cljs
Normal file
|
@ -0,0 +1,142 @@
|
|||
(ns app.main.ui.workspace.tokens.token
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def parseable-token-value-regexp
|
||||
"Regexp that can be used to parse a number value out of resolved token value.
|
||||
This regexp also trims whitespace around the value."
|
||||
#"^\s*(-?[0-9]+\.?[0-9]*)(px|%)?\s*$")
|
||||
|
||||
(defn parse-token-value
|
||||
"Parses a resolved value and separates the unit from the value.
|
||||
Returns a map of {:value `number` :unit `string`}."
|
||||
[value]
|
||||
(cond
|
||||
(number? value) {:value value}
|
||||
(string? value) (when-let [[_ value unit] (re-find parseable-token-value-regexp value)]
|
||||
(when-let [parsed-value (d/parse-double value)]
|
||||
{:value parsed-value
|
||||
:unit unit}))))
|
||||
|
||||
(defn token-identifier [{:keys [name] :as _token}]
|
||||
name)
|
||||
|
||||
(defn attributes-map
|
||||
"Creats an attributes map using collection of `attributes` for `id`."
|
||||
[attributes token]
|
||||
(->> (map (fn [attr] [attr (token-identifier token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn remove-attributes-for-token
|
||||
"Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`."
|
||||
[attributes token applied-tokens]
|
||||
(let [attr? (set attributes)]
|
||||
(->> (remove (fn [[k v]]
|
||||
(and (attr? k)
|
||||
(= v (token-identifier token))))
|
||||
applied-tokens)
|
||||
(into {}))))
|
||||
|
||||
(defn token-attribute-applied?
|
||||
"Test if `token` is applied to a `shape` on single `token-attribute`."
|
||||
[token shape token-attribute]
|
||||
(when-let [id (get-in shape [:applied-tokens token-attribute])]
|
||||
(= (token-identifier token) id)))
|
||||
|
||||
(defn token-applied?
|
||||
"Test if `token` is applied to a `shape` with at least one of the one of the given `token-attributes`."
|
||||
[token shape token-attributes]
|
||||
(some #(token-attribute-applied? token shape %) token-attributes))
|
||||
|
||||
(defn shapes-token-applied?
|
||||
"Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`."
|
||||
[token shapes token-attributes]
|
||||
(some #(token-applied? token % token-attributes) shapes))
|
||||
|
||||
(defn shapes-ids-by-applied-attributes [token shapes token-attributes]
|
||||
(reduce (fn [acc shape]
|
||||
(let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %)
|
||||
[% #{(:id shape)}])
|
||||
token-attributes)
|
||||
(filter some?)
|
||||
(into {}))]
|
||||
(merge-with into acc applied-ids-by-attribute)))
|
||||
{} shapes))
|
||||
|
||||
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
|
||||
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
|
||||
|
||||
(defn token-name->path
|
||||
"Splits token-name into a path vector split by `.` characters.
|
||||
|
||||
Will concatenate multiple `.` characters into one."
|
||||
[token-name]
|
||||
(str/split token-name #"\.+"))
|
||||
|
||||
(defn token-name->path-selector
|
||||
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
|
||||
|
||||
`:selector` is the last item of the names path
|
||||
`:path` is everything leading up the the `:selector`."
|
||||
[token-name]
|
||||
(let [path-segments (token-name->path token-name)
|
||||
last-idx (dec (count path-segments))
|
||||
[path [selector]] (split-at last-idx path-segments)]
|
||||
{:path (seq path)
|
||||
:selector selector}))
|
||||
|
||||
(defn token-names-tree-id-map [tokens]
|
||||
(reduce
|
||||
(fn [acc [_ {:keys [name] :as token}]]
|
||||
(when (string? name)
|
||||
(let [temp-id (random-uuid)
|
||||
token (assoc token :temp/id temp-id)]
|
||||
(-> acc
|
||||
(assoc-in (concat [:tree] (token-name->path name)) token)
|
||||
(assoc-in [:ids-map temp-id] token)))))
|
||||
{:tree {}
|
||||
:ids-map {}}
|
||||
tokens))
|
||||
|
||||
(defn token-name-path-exists?
|
||||
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
|
||||
|
||||
It's not allowed to create a token inside a token. E.g.:
|
||||
Creating a token with
|
||||
|
||||
{:name \"foo.bar\"}
|
||||
|
||||
in the tokens tree:
|
||||
|
||||
{\"foo\" {:name \"other\"}}"
|
||||
[token-name token-names-tree]
|
||||
(let [{:keys [path selector]} (token-name->path-selector token-name)
|
||||
path-target (reduce
|
||||
(fn [acc cur]
|
||||
(let [target (get acc cur)]
|
||||
(cond
|
||||
;; Path segment doesn't exist yet
|
||||
(nil? target) (reduced false)
|
||||
;; A token exists at this path
|
||||
(:name target) (reduced true)
|
||||
;; Continue traversing the true
|
||||
:else target)))
|
||||
token-names-tree path)]
|
||||
(cond
|
||||
(boolean? path-target) path-target
|
||||
(get path-target :name) true
|
||||
:else (-> (get path-target selector)
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
(defn color-token? [token]
|
||||
(= (:type token) :color))
|
||||
|
||||
(defn resolved-value-hex [{:keys [resolved-value] :as token}]
|
||||
(when (and resolved-value (color-token? token))
|
||||
(some->> (tinycolor/valid-color resolved-value)
|
||||
(tinycolor/->hex)
|
||||
(str "#"))))
|
56
frontend/src/app/main/ui/workspace/tokens/token_set.cljs
Normal file
56
frontend/src/app/main/ui/workspace/tokens/token_set.cljs
Normal file
|
@ -0,0 +1,56 @@
|
|||
(ns app.main.ui.workspace.tokens.token-set
|
||||
(:require
|
||||
[app.common.types.tokens-lib :as ctob]))
|
||||
|
||||
(defn get-workspace-tokens-lib [state]
|
||||
(get-in state [:workspace-data :tokens-lib]))
|
||||
|
||||
;; Themes ----------------------------------------------------------------------
|
||||
|
||||
(defn get-active-theme-ids [state]
|
||||
(get-in state [:workspace-data :token-active-themes] #{}))
|
||||
|
||||
(defn get-temp-theme-id [state]
|
||||
(get-in state [:workspace-data :token-theme-temporary-id]))
|
||||
|
||||
(defn update-theme-id
|
||||
[state]
|
||||
(let [active-themes (get-active-theme-ids state)
|
||||
temporary-theme-id (get-temp-theme-id state)]
|
||||
(cond
|
||||
(empty? active-themes) temporary-theme-id
|
||||
(= 1 (count active-themes)) (first active-themes)
|
||||
:else temporary-theme-id)))
|
||||
|
||||
(defn get-workspace-token-theme [id state]
|
||||
(get-in state [:workspace-data :token-themes-index id]))
|
||||
|
||||
(defn add-token-set-to-token-theme [token-set-id token-theme]
|
||||
(update token-theme :sets conj token-set-id))
|
||||
|
||||
;; Sets ------------------------------------------------------------------------
|
||||
|
||||
(defn get-active-theme-sets-tokens-names-map [state]
|
||||
(when-let [lib (get-workspace-tokens-lib state)]
|
||||
(ctob/get-active-themes-set-tokens lib)))
|
||||
|
||||
;; === Set selection
|
||||
|
||||
(defn get-selected-token-set-id [state]
|
||||
(or (get-in state [:workspace-local :selected-token-set-id])
|
||||
(some-> (get-workspace-tokens-lib state)
|
||||
(ctob/get-sets)
|
||||
(first)
|
||||
(:name))))
|
||||
|
||||
(defn get-selected-token-set [state]
|
||||
(when-let [id (get-selected-token-set-id state)]
|
||||
(some-> (get-workspace-tokens-lib state)
|
||||
(ctob/get-set id))))
|
||||
|
||||
(defn get-selected-token-set-tokens [state]
|
||||
(some-> (get-selected-token-set state)
|
||||
:tokens))
|
||||
|
||||
(defn assoc-selected-token-set-id [state id]
|
||||
(assoc-in state [:workspace-local :selected-token-set-id] id))
|
88
frontend/src/app/main/ui/workspace/tokens/token_types.cljs
Normal file
88
frontend/src/app/main/ui/workspace/tokens/token_types.cljs
Normal file
|
@ -0,0 +1,88 @@
|
|||
;; 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 INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.token-types
|
||||
(:require
|
||||
[app.common.data :as d :refer [ordered-map]]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.main.ui.workspace.tokens.changes :as wtch]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(def token-types
|
||||
(ordered-map
|
||||
:border-radius
|
||||
{:title "Border Radius"
|
||||
:attributes ctt/border-radius-keys
|
||||
:on-update-shape wtch/update-shape-radius-all
|
||||
:modal {:key :tokens/border-radius
|
||||
:fields [{:label "Border Radius"
|
||||
:key :border-radius}]}}
|
||||
|
||||
:color
|
||||
{:title "Color"
|
||||
:attributes ctt/color-keys
|
||||
:on-update-shape wtch/update-color
|
||||
:modal {:key :tokens/color
|
||||
:fields [{:label "Color" :key :color}]}}
|
||||
|
||||
:stroke-width
|
||||
{:title "Stroke Width"
|
||||
:attributes ctt/stroke-width-keys
|
||||
:on-update-shape wtch/update-stroke-width
|
||||
:modal {:key :tokens/stroke-width
|
||||
:fields [{:label "Stroke Width"
|
||||
:key :stroke-width}]}}
|
||||
|
||||
:sizing
|
||||
{:title "Sizing"
|
||||
:attributes #{:width :height}
|
||||
:all-attributes ctt/sizing-keys
|
||||
:on-update-shape wtch/update-shape-dimensions
|
||||
:modal {:key :tokens/sizing
|
||||
:fields [{:label "Sizing"
|
||||
:key :sizing}]}}
|
||||
:dimensions
|
||||
{:title "Dimensions"
|
||||
:attributes #{:width :height}
|
||||
:all-attributes (set/union
|
||||
ctt/spacing-keys
|
||||
ctt/sizing-keys
|
||||
ctt/border-radius-keys
|
||||
ctt/stroke-width-keys)
|
||||
:on-update-shape wtch/update-shape-dimensions
|
||||
:modal {:key :tokens/dimensions
|
||||
:fields [{:label "Dimensions"
|
||||
:key :dimensions}]}}
|
||||
|
||||
:opacity
|
||||
{:title "Opacity"
|
||||
:attributes ctt/opacity-keys
|
||||
:on-update-shape wtch/update-opacity
|
||||
:modal {:key :tokens/opacity
|
||||
:fields [{:label "Opacity"
|
||||
:key :opacity}]}}
|
||||
|
||||
:rotation
|
||||
{:title "Rotation"
|
||||
:attributes ctt/rotation-keys
|
||||
:on-update-shape wtch/update-rotation
|
||||
:modal {:key :tokens/rotation
|
||||
:fields [{:label "Rotation"
|
||||
:key :rotation}]}}
|
||||
:spacing
|
||||
{:title "Spacing"
|
||||
:attributes #{:column-gap :row-gap}
|
||||
:all-attributes ctt/spacing-keys
|
||||
:on-update-shape wtch/update-layout-spacing
|
||||
:modal {:key :tokens/spacing
|
||||
:fields [{:label "Spacing"
|
||||
:key :spacing}]}}))
|
||||
|
||||
(defn get-token-properties [token]
|
||||
(get token-types (:type token)))
|
||||
|
||||
(defn token-attributes [token-type]
|
||||
(get-in token-types [token-type :attributes]))
|
135
frontend/src/app/main/ui/workspace/tokens/update.cljs
Normal file
135
frontend/src/app/main/ui/workspace/tokens/update.cljs
Normal file
|
@ -0,0 +1,135 @@
|
|||
(ns app.main.ui.workspace.tokens.update
|
||||
(:require
|
||||
[app.common.types.token :as ctt]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.changes :as wtch]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as wtsd]
|
||||
[app.main.ui.workspace.tokens.token-set :as wtts]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.data :as data]
|
||||
[clojure.set :as set]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;; Constants -------------------------------------------------------------------
|
||||
|
||||
(def filter-existing-values? false)
|
||||
|
||||
(def attributes->shape-update
|
||||
{#{:rx :ry} (fn [v ids _] (wtch/update-shape-radius-all v ids))
|
||||
#{:r1 :r2 :r3 :r4} wtch/update-shape-radius-single-corner
|
||||
ctt/color-keys wtch/update-color
|
||||
ctt/stroke-width-keys wtch/update-stroke-width
|
||||
ctt/sizing-keys wtch/update-shape-dimensions
|
||||
ctt/opacity-keys wtch/update-opacity
|
||||
#{:x :y} wtch/update-shape-position
|
||||
#{:p1 :p2 :p3 :p4} (fn [resolved-value shape-ids attrs]
|
||||
(dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat resolved-value))}))
|
||||
#{:column-gap :row-gap} wtch/update-layout-spacing
|
||||
#{:width :height} wtch/update-shape-dimensions
|
||||
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} wtch/update-layout-sizing-limits
|
||||
ctt/rotation-keys wtch/update-rotation})
|
||||
|
||||
(def attribute-actions-map
|
||||
(reduce
|
||||
(fn [acc [ks action]]
|
||||
(into acc (map (fn [k] [k action]) ks)))
|
||||
{} attributes->shape-update))
|
||||
|
||||
;; Helpers ---------------------------------------------------------------------
|
||||
|
||||
(defn deep-merge
|
||||
"Like d/deep-merge but unions set values."
|
||||
([a b]
|
||||
(cond
|
||||
(map? a) (merge-with deep-merge a b)
|
||||
(set? a) (set/union a b)
|
||||
:else b))
|
||||
([a b & rest]
|
||||
(reduce deep-merge a (cons b rest))))
|
||||
|
||||
;; Data flows ------------------------------------------------------------------
|
||||
|
||||
(defn invert-collect-key-vals
|
||||
[xs resolved-tokens shape]
|
||||
(-> (reduce
|
||||
(fn [acc [k v]]
|
||||
(let [resolved-token (get resolved-tokens v)
|
||||
resolved-value (get resolved-token :resolved-value)
|
||||
skip? (or
|
||||
(not (get resolved-tokens v))
|
||||
(and filter-existing-values? (= (get shape k) resolved-value)))]
|
||||
(if skip?
|
||||
acc
|
||||
(update acc resolved-value (fnil conj #{}) k))))
|
||||
{} xs)))
|
||||
|
||||
(defn split-attribute-groups [attrs-values-map]
|
||||
(reduce
|
||||
(fn [acc [attrs v]]
|
||||
(cond
|
||||
(some attrs #{:rx :ry}) (let [[_ a b] (data/diff #{:rx :ry} attrs)]
|
||||
(cond-> (assoc acc b v)
|
||||
;; Exact match in attrs
|
||||
a (assoc a v)))
|
||||
|
||||
(some attrs #{:widht :height}) (let [[_ a b] (data/diff #{:width :height} attrs)]
|
||||
(cond-> (assoc acc b v)
|
||||
;; Exact match in attrs
|
||||
a (assoc a v)))
|
||||
(some attrs ctt/spacing-keys) (let [[_ rst gap] (data/diff #{:row-gap :column-gap} attrs)
|
||||
[_ position padding] (data/diff #{:p1 :p2 :p3 :p4} rst)]
|
||||
(cond-> acc
|
||||
(seq gap) (assoc gap v)
|
||||
(seq position) (assoc position v)
|
||||
(seq padding) (assoc padding v)))
|
||||
attrs (assoc acc attrs v)))
|
||||
{} attrs-values-map))
|
||||
|
||||
(defn shape-ids-by-values
|
||||
[attrs-values-map object-id]
|
||||
(->> (map (fn [[value attrs]] [attrs {value #{object-id}}]) attrs-values-map)
|
||||
(into {})))
|
||||
|
||||
(defn collect-shapes-update-info [resolved-tokens shapes]
|
||||
(reduce
|
||||
(fn [acc [object-id {:keys [applied-tokens] :as shape}]]
|
||||
(if (seq applied-tokens)
|
||||
(let [applied-tokens (-> (invert-collect-key-vals applied-tokens resolved-tokens shape)
|
||||
(shape-ids-by-values object-id)
|
||||
(split-attribute-groups))]
|
||||
(deep-merge acc applied-tokens))
|
||||
acc))
|
||||
{} shapes))
|
||||
|
||||
(defn actionize-shapes-update-info [shapes-update-info]
|
||||
(mapcat (fn [[attrs update-infos]]
|
||||
(let [action (some attribute-actions-map attrs)]
|
||||
(map
|
||||
(fn [[v shape-ids]]
|
||||
(action v shape-ids attrs))
|
||||
update-infos)))
|
||||
shapes-update-info))
|
||||
|
||||
(defn update-tokens [resolved-tokens]
|
||||
(->> @refs/workspace-page-objects
|
||||
(collect-shapes-update-info resolved-tokens)
|
||||
(actionize-shapes-update-info)))
|
||||
|
||||
(defn update-workspace-tokens []
|
||||
(ptk/reify ::update-workspace-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(->>
|
||||
(rx/from
|
||||
(->
|
||||
(wtts/get-active-theme-sets-tokens-names-map state)
|
||||
(wtsd/resolve-tokens+)))
|
||||
(rx/mapcat
|
||||
(fn [sd-tokens]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(rx/concat
|
||||
(rx/of (dwu/start-undo-transaction undo-id))
|
||||
(update-tokens sd-tokens)
|
||||
(rx/of (dwu/commit-undo-transaction undo-id))))))))))
|
|
@ -25,5 +25,7 @@
|
|||
lodash-debounce))
|
||||
|
||||
(defn debounce
|
||||
[f timeout]
|
||||
(ext-debounce f timeout #{:leading false :trailing true}))
|
||||
([f]
|
||||
(debounce f 0))
|
||||
([f timeout]
|
||||
(ext-debounce f timeout #{:leading false :trailing true})))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue