mirror of
https://github.com/penpot/penpot.git
synced 2025-03-17 10:11:22 -05:00
Merge pull request #5242 from penpot/niwinz-design-tokens
🎉 Merge tokens-studio
This commit is contained in:
commit
46a6aff4da
84 changed files with 10622 additions and 174 deletions
|
@ -27,12 +27,6 @@ jobs:
|
|||
yarn run fmt:clj:check
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "JS tests"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn run test
|
||||
|
||||
- run:
|
||||
name: "JVM tests"
|
||||
working_directory: "./common"
|
||||
|
|
|
@ -57,6 +57,58 @@
|
|||
#?(:cljs (instance? lkm/LinkedMap o)
|
||||
:clj (instance? LinkedMap o)))
|
||||
|
||||
(defn oassoc
|
||||
[o & kvs]
|
||||
(apply assoc (or o (ordered-map)) kvs))
|
||||
|
||||
(defn oassoc-in
|
||||
[o [k & ks] v]
|
||||
(if ks
|
||||
(oassoc o k (oassoc-in (get o k) ks v))
|
||||
(oassoc o k v)))
|
||||
|
||||
(defn oupdate-in
|
||||
[m ks f & args]
|
||||
(let [up (fn up [m ks f args]
|
||||
(let [[k & ks] ks]
|
||||
(if ks
|
||||
(oassoc m k (up (get m k) ks f args))
|
||||
(oassoc m k (apply f (get m k) args)))))]
|
||||
(up m ks f args)))
|
||||
|
||||
(declare index-of)
|
||||
|
||||
(defn oassoc-before
|
||||
"Assoc a k v pair, in the order position just before the other key"
|
||||
[o before-k k v]
|
||||
(if-let [index (index-of (keys o) before-k)]
|
||||
(-> (ordered-map)
|
||||
(into (take index o))
|
||||
(assoc k v)
|
||||
(into (drop index o)))
|
||||
(oassoc o k v)))
|
||||
|
||||
(defn oassoc-in-before
|
||||
[o [before-k & before-ks] [k & ks] v]
|
||||
(if-let [index (index-of (keys o) before-k)]
|
||||
(let [new-v (if ks
|
||||
(oassoc-in-before (get o k) before-ks ks v)
|
||||
v)
|
||||
current-index (index-of (keys o) k)
|
||||
new-index (if (and current-index (< current-index index))
|
||||
(dec index)
|
||||
index)]
|
||||
(if (= k before-k)
|
||||
(-> (ordered-map)
|
||||
(into (take new-index o))
|
||||
(assoc k new-v)
|
||||
(into (drop (inc new-index) o)))
|
||||
(-> (ordered-map)
|
||||
(into (take new-index (dissoc o k)))
|
||||
(assoc k new-v)
|
||||
(into (drop new-index (dissoc o k))))))
|
||||
(oassoc-in o (cons k ks) v)))
|
||||
|
||||
(defn vec2
|
||||
"Creates a optimized vector compatible type of length 2 backed
|
||||
internally with MapEntry impl because it has faster access method
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"styles/v2"
|
||||
"layout/grid"
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"
|
||||
"text-editor/v2"})
|
||||
|
||||
;; A set of features enabled by default
|
||||
|
@ -84,6 +85,7 @@
|
|||
"layout/grid"
|
||||
"fdata/shape-data-type"
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"
|
||||
"text-editor/v2"}
|
||||
(into frontend-only-features)))
|
||||
|
||||
|
@ -104,6 +106,7 @@
|
|||
:feature-fdata-objects-map "fdata/objects-map"
|
||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||
:feature-plugins "plugins/runtime"
|
||||
:feature-design-tokens "design-tokens/v1"
|
||||
:feature-text-editor-v2 "text-editor/v2"
|
||||
nil))
|
||||
|
||||
|
@ -312,5 +315,3 @@
|
|||
:feature (first not-supported)
|
||||
:hint (str/ffmt "paste features '%' not enabled on the application"
|
||||
(str/join "," not-supported))))))
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.token-theme :as ctot]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -366,7 +369,87 @@
|
|||
[:del-typography
|
||||
[:map {:title "DelTypogrphyChange"}
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]]])
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:add-temporary-token-theme
|
||||
[:map {:title "AddTemporaryTokenThemeChange"}
|
||||
[:type [:= :add-temporary-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[:theme-ids [:set :string]]]]
|
||||
|
||||
[:delete-temporary-token-theme
|
||||
[:map {:title "DeleteTemporaryTokenThemeChange"}
|
||||
[:type [:= :delete-temporary-token-theme]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-theme
|
||||
[:map {:title "AddTokenThemeChange"}
|
||||
[:type [:= :add-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:mod-token-theme
|
||||
[:map {:title "ModTokenThemeChange"}
|
||||
[:type [:= :mod-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:del-token-theme
|
||||
[:map {:title "DelTokenThemeChange"}
|
||||
[:type [:= :del-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-set
|
||||
[:map {:title "AddTokenSetChange"}
|
||||
[:type [:= :add-token-set]]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:mod-token-set
|
||||
[:map {:title "ModTokenSetChange"}
|
||||
[:type [:= :mod-token-set]]
|
||||
[:name :string]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:move-token-set-before
|
||||
[:map {:title "MoveTokenSetBefore"}
|
||||
[:type [:= :move-token-set-before]]
|
||||
[:set-name :string]
|
||||
[:before-set-name [:maybe :string]]]]
|
||||
|
||||
[:del-token-set
|
||||
[:map {:title "DelTokenSetChange"}
|
||||
[:type [:= :del-token-set]]
|
||||
[:name :string]]]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib :any]]]
|
||||
|
||||
[:add-token
|
||||
[:map {:title "AddTokenChange"}
|
||||
[:type [:= :add-token]]
|
||||
[:set-name :string]
|
||||
[:token ::cto/token]]]
|
||||
|
||||
[:mod-token
|
||||
[:map {:title "ModTokenChange"}
|
||||
[:type [:= :mod-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]
|
||||
[:token ::cto/token]]]
|
||||
|
||||
[:del-token
|
||||
[:map {:title "DelTokenChange"}
|
||||
[:type [:= :del-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
|
@ -889,6 +972,103 @@
|
|||
[data {:keys [id]}]
|
||||
(ctyl/delete-typography data id))
|
||||
|
||||
;; -- Tokens
|
||||
|
||||
(defmethod process-change :set-tokens-lib
|
||||
[data {:keys [tokens-lib]}]
|
||||
(assoc data :tokens-lib tokens-lib))
|
||||
|
||||
(defmethod process-change :add-token
|
||||
[data {:keys [set-name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-token-in-set set-name (ctob/make-token token)))))
|
||||
|
||||
(defmethod process-change :mod-token
|
||||
[data {:keys [set-name name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-token-in-set
|
||||
set-name
|
||||
name
|
||||
(fn [old-token]
|
||||
(ctob/make-token (merge old-token token)))))))
|
||||
|
||||
(defmethod process-change :del-token
|
||||
[data {:keys [set-name name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-token-from-set
|
||||
set-name
|
||||
name))))
|
||||
|
||||
(defmethod process-change :add-temporary-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme token-theme)))))
|
||||
|
||||
(defmethod process-change :update-active-token-themes
|
||||
[data {:keys [theme-ids]}]
|
||||
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
|
||||
(ctob/set-active-themes theme-ids))))
|
||||
|
||||
(defmethod process-change :delete-temporary-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (-> token-theme
|
||||
(ctob/make-token-theme))))))
|
||||
|
||||
(defmethod process-change :mod-token-theme
|
||||
[data {:keys [name group token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-theme group name
|
||||
(fn [prev-theme]
|
||||
(merge prev-theme token-theme))))))
|
||||
|
||||
(defmethod process-change :del-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-set
|
||||
[data {:keys [token-set]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set token-set)))))
|
||||
|
||||
(defmethod process-change :mod-token-set
|
||||
[data {:keys [name token-set]}]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
(let [path-changed? (not= name (:name token-set))
|
||||
lib' (-> lib
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-set name (fn [prev-set]
|
||||
(merge prev-set (dissoc token-set :tokens)))))]
|
||||
(cond-> lib'
|
||||
path-changed? (ctob/update-set-name name (:name token-set)))))))
|
||||
|
||||
(defmethod process-change :move-token-set-before
|
||||
[data {:keys [set-name before-set-name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-before set-name before-set-name))))
|
||||
|
||||
(defmethod process-change :del-token-set
|
||||
[data {:keys [name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-set name))))
|
||||
|
||||
;; === Operations
|
||||
|
||||
(def ^:private decode-shape
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
[app.common.types.component :as ctk]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;; Auxiliary functions to help create a set of changes (undo + redo)
|
||||
|
@ -760,6 +761,116 @@
|
|||
(update :undo-changes conj {:type :add-typography :typography prev-typography})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-temporary-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-temporary-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-active-token-themes
|
||||
[changes token-active-theme-ids prev-token-active-theme-ids]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :update-active-token-themes :theme-ids token-active-theme-ids})
|
||||
(update :undo-changes conj {:type :update-active-token-themes :theme-ids prev-token-active-theme-ids})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn add-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :del-token-theme :group (:group token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token-theme
|
||||
[changes token-theme prev-token-theme]
|
||||
(let [name (or (:name prev-token-theme)
|
||||
(:name token-theme))
|
||||
group (or (:group prev-token-theme)
|
||||
(:group token-theme))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-theme :group group :name name :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :mod-token-theme :group group :name name :token-theme (or prev-token-theme token-theme)})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn delete-token-theme
|
||||
[changes group name]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-theme group name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-theme :group group :name name})
|
||||
(update :undo-changes conj {:type :add-token-theme :token-theme prev-token-theme})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token-set
|
||||
[changes token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-set :token-set token-set})
|
||||
(update :undo-changes conj {:type :del-token-set :name (:name token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token-set
|
||||
[changes token-set prev-token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-set :name (:name prev-token-set) :token-set token-set})
|
||||
(update :undo-changes conj {:type :mod-token-set :name (:name token-set) :token-set (or prev-token-set token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token-set
|
||||
[changes token-set-name]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set token-set-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-set :name token-set-name})
|
||||
(update :undo-changes conj {:type :add-token-set :token-set prev-token-theme})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn move-token-set-before
|
||||
[changes set-name before-set-name prev-before-set-name]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name before-set-name})
|
||||
(update :undo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name prev-before-set-name})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn set-tokens-lib
|
||||
[changes tokens-lib]
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-tokens-lib (get library-data :tokens-lib)]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
|
||||
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token
|
||||
[changes set-name token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token :set-name set-name :token token})
|
||||
(update :undo-changes conj {:type :del-token :set-name set-name :name (:name token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token
|
||||
[changes set-name token prev-token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token :set-name set-name :name (:name prev-token) :token token})
|
||||
(update :undo-changes conj {:type :mod-token :set-name set-name :name (:name token) :token (or prev-token token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token
|
||||
[changes set-name token-name]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set set-name)
|
||||
(ctob/get-token token-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token :set-name set-name :name token-name})
|
||||
(update :undo-changes conj {:type :add-token :set-name set-name :token prev-token})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-component
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil))
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
java.time.OffsetDateTime
|
||||
java.util.List
|
||||
linked.map.LinkedMap
|
||||
linked.set.LinkedSet
|
||||
org.fressian.Reader
|
||||
org.fressian.StreamingWriter
|
||||
org.fressian.Writer
|
||||
|
@ -275,7 +276,12 @@
|
|||
{:name "clj/seq"
|
||||
:class clojure.lang.ISeq
|
||||
:wfn write-list-like
|
||||
:rfn (comp sequence read-object!)})
|
||||
:rfn (comp sequence read-object!)}
|
||||
|
||||
{:name "linked/set"
|
||||
:class LinkedSet
|
||||
:wfn write-list-like
|
||||
:rfn (comp #(into (d/ordered-set) %) read-object!)})
|
||||
|
||||
;; --- PUBLIC API
|
||||
|
||||
|
|
|
@ -27,6 +27,11 @@
|
|||
#?(:clj (Instant/now)
|
||||
:cljs (.local ^js DateTime)))
|
||||
|
||||
#?(:clj
|
||||
(defn is-after?
|
||||
[one other]
|
||||
(.isAfter one other)))
|
||||
|
||||
(defn instant?
|
||||
[o]
|
||||
#?(:clj (instance? Instant o)
|
||||
|
@ -51,6 +56,8 @@
|
|||
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||
:cljs (.toISO ^js v)))
|
||||
|
||||
;; To check for valid date time we can just use the core inst? function
|
||||
|
||||
#?(:cljs
|
||||
(extend-protocol IComparable
|
||||
DateTime
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.tokens-lib :as ctl]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as cty]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -69,7 +70,8 @@
|
|||
[:colors {:optional true} schema:colors]
|
||||
[:components {:optional true} schema:components]
|
||||
[:typographies {:optional true} schema:typographies]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]
|
||||
[:tokens-lib {:optional true} ::ctl/tokens-lib]])
|
||||
|
||||
(def schema:file
|
||||
"A schema for validate a file data structure; data is optional
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
[app.common.types.shape.path :as ctsp]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
|
@ -189,6 +190,7 @@
|
|||
[:blur {:optional true} ::ctsb/blur]
|
||||
[:grow-type {:optional true}
|
||||
[::sm/one-of grow-types]]
|
||||
[:applied-tokens {:optional true} ::cto/applied-tokens]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(def schema:group-attrs
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
:show-content
|
||||
:hide-in-viewer
|
||||
|
||||
:applied-tokens
|
||||
|
||||
:opacity
|
||||
:blend-mode
|
||||
:blocked
|
||||
|
@ -95,6 +97,8 @@
|
|||
:parent-id
|
||||
:frame-id
|
||||
|
||||
:applied-tokens
|
||||
|
||||
:opacity
|
||||
:blend-mode
|
||||
:blocked
|
||||
|
|
151
common/src/app/common/types/token.cljc
Normal file
151
common/src/app/common/types/token.cljc
Normal file
|
@ -0,0 +1,151 @@
|
|||
;; 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.common.types.token
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.registry :as sr]
|
||||
[clojure.set :as set]
|
||||
[malli.util :as mu]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn merge-schemas
|
||||
"Merge registered schemas."
|
||||
[& schema-keys]
|
||||
(let [schemas (map #(get @sr/registry %) schema-keys)]
|
||||
(reduce sm/merge schemas)))
|
||||
|
||||
(defn schema-keys
|
||||
"Converts registed map schema into set of keys."
|
||||
[registered-schema]
|
||||
(->> (get @sr/registry registered-schema)
|
||||
(sm/schema)
|
||||
(mu/keys)
|
||||
(into #{})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def token-type->dtcg-token-type
|
||||
{:boolean "boolean"
|
||||
:border-radius "borderRadius"
|
||||
:color "color"
|
||||
:dimensions "dimension"
|
||||
:numeric "numeric"
|
||||
:opacity "opacity"
|
||||
:other "other"
|
||||
:rotation "rotation"
|
||||
:sizing "sizing"
|
||||
:spacing "spacing"
|
||||
:string "string"
|
||||
:stroke-width "strokeWidth"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
(set/map-invert token-type->dtcg-token-type))
|
||||
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(defn valid-token-type?
|
||||
[t]
|
||||
(token-types t))
|
||||
|
||||
(def token-name-ref :string)
|
||||
|
||||
(defn valid-token-name-ref?
|
||||
[n]
|
||||
(string? n))
|
||||
|
||||
;; TODO Move this to tokens-lib
|
||||
(sm/register! ::token
|
||||
[:map {:title "Token"}
|
||||
[:name token-name-ref]
|
||||
[:type [::sm/one-of token-types]]
|
||||
[:value :any]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::sm/inst]])
|
||||
|
||||
(sm/register! ::color
|
||||
[:map
|
||||
[:color {:optional true} token-name-ref]])
|
||||
|
||||
(def color-keys (schema-keys ::color))
|
||||
|
||||
(sm/register! ::border-radius
|
||||
[:map
|
||||
[:rx {:optional true} token-name-ref]
|
||||
[:ry {:optional true} token-name-ref]
|
||||
[:r1 {:optional true} token-name-ref]
|
||||
[:r2 {:optional true} token-name-ref]
|
||||
[:r3 {:optional true} token-name-ref]
|
||||
[:r4 {:optional true} token-name-ref]])
|
||||
|
||||
(def border-radius-keys (schema-keys ::border-radius))
|
||||
|
||||
(sm/register! ::stroke-width
|
||||
[:map
|
||||
[:stroke-width {:optional true} token-name-ref]])
|
||||
|
||||
(def stroke-width-keys (schema-keys ::stroke-width))
|
||||
|
||||
(sm/register! ::sizing
|
||||
[:map
|
||||
[:width {:optional true} token-name-ref]
|
||||
[:height {:optional true} token-name-ref]
|
||||
[:layout-item-min-w {:optional true} token-name-ref]
|
||||
[:layout-item-max-w {:optional true} token-name-ref]
|
||||
[:layout-item-min-h {:optional true} token-name-ref]
|
||||
[:layout-item-max-h {:optional true} token-name-ref]])
|
||||
|
||||
(def sizing-keys (schema-keys ::sizing))
|
||||
|
||||
(sm/register! ::opacity
|
||||
[:map
|
||||
[:opacity {:optional true} token-name-ref]])
|
||||
|
||||
(def opacity-keys (schema-keys ::opacity))
|
||||
|
||||
(sm/register! ::spacing
|
||||
[:map
|
||||
[:row-gap {:optional true} token-name-ref]
|
||||
[:column-gap {:optional true} token-name-ref]
|
||||
[:p1 {:optional true} token-name-ref]
|
||||
[:p2 {:optional true} token-name-ref]
|
||||
[:p3 {:optional true} token-name-ref]
|
||||
[:p4 {:optional true} token-name-ref]
|
||||
[:x {:optional true} token-name-ref]
|
||||
[:y {:optional true} token-name-ref]])
|
||||
|
||||
(def spacing-keys (schema-keys ::spacing))
|
||||
|
||||
(sm/register! ::dimensions
|
||||
(merge-schemas ::sizing
|
||||
::spacing
|
||||
::stroke-width
|
||||
::border-radius))
|
||||
|
||||
(def dimensions-keys (schema-keys ::dimensions))
|
||||
|
||||
(sm/register! ::rotation
|
||||
[:map
|
||||
[:rotation {:optional true} token-name-ref]])
|
||||
|
||||
(def rotation-keys (schema-keys ::rotation))
|
||||
|
||||
(sm/register! ::tokens
|
||||
[:map {:title "Applied Tokens"}])
|
||||
|
||||
(sm/register! ::applied-tokens
|
||||
(merge-schemas ::tokens
|
||||
::border-radius
|
||||
::sizing
|
||||
::spacing
|
||||
::rotation
|
||||
::dimensions))
|
25
common/src/app/common/types/token_theme.cljc
Normal file
25
common/src/app/common/types/token_theme.cljc
Normal file
|
@ -0,0 +1,25 @@
|
|||
;; 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.common.types.token-theme
|
||||
(:require
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(sm/register! ::token-theme
|
||||
[:map {:title "TokenTheme"}
|
||||
[:name :string]
|
||||
[:group :string]
|
||||
[:description [:maybe :string]]
|
||||
[:is-source :boolean]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:sets :any]])
|
||||
|
||||
(sm/register! ::token-set
|
||||
[:map {:title "TokenSet"}
|
||||
[:name :string]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:tokens :any]])
|
947
common/src/app/common/types/tokens_lib.cljc
Normal file
947
common/src/app/common/types/tokens_lib.cljc
Normal file
|
@ -0,0 +1,947 @@
|
|||
;; 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.common.types.tokens-lib
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as dt]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.token :as cto]
|
||||
[clojure.set :as set]
|
||||
[clojure.walk :as walk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; === Groups handling
|
||||
|
||||
(def schema:groupable-item
|
||||
[:map {:title "Groupable item"}
|
||||
[:name :string]])
|
||||
|
||||
(def valid-groupable-item?
|
||||
(sm/validator schema:groupable-item))
|
||||
|
||||
(defn split-path
|
||||
"Decompose a string in the form 'one.two.three' into a vector of strings, removing spaces."
|
||||
[path separator]
|
||||
(let [xf (comp (map str/trim)
|
||||
(remove str/empty?))]
|
||||
(->> (str/split path separator)
|
||||
(into [] xf))))
|
||||
|
||||
(defn join-path
|
||||
"Regenerate a path as a string, from a vector."
|
||||
[path separator]
|
||||
(str/join separator path))
|
||||
|
||||
(defn group-item
|
||||
"Add a group to the item name, in the form group.name."
|
||||
[item group-name separator]
|
||||
(dm/assert!
|
||||
"expected groupable item"
|
||||
(valid-groupable-item? item))
|
||||
(update item :name #(str group-name separator %)))
|
||||
|
||||
(defn ungroup-item
|
||||
"Remove the first group from the item name."
|
||||
[item separator]
|
||||
(dm/assert!
|
||||
"expected groupable item"
|
||||
(valid-groupable-item? item))
|
||||
(update item :name #(-> %
|
||||
(split-path separator)
|
||||
(rest)
|
||||
(join-path separator))))
|
||||
|
||||
(defn get-path
|
||||
"Get the groups part of the name as a vector. E.g. group.subgroup.name -> ['group' 'subrgoup']"
|
||||
[item separator]
|
||||
(dm/assert!
|
||||
"expected groupable item"
|
||||
(valid-groupable-item? item))
|
||||
(split-path (:name item) separator))
|
||||
|
||||
(defn get-groups-str
|
||||
"Get the groups part of the name. E.g. group.subgroup.name -> group.subrgoup"
|
||||
[item separator]
|
||||
(-> (get-path item separator)
|
||||
(butlast)
|
||||
(join-path separator)))
|
||||
|
||||
(defn get-final-name
|
||||
"Get the final part of the name. E.g. group.subgroup.name -> name"
|
||||
[item separator]
|
||||
(dm/assert!
|
||||
"expected groupable item"
|
||||
(valid-groupable-item? item))
|
||||
(-> (:name item)
|
||||
(split-path separator)
|
||||
(last)))
|
||||
|
||||
(defn group?
|
||||
"Check if a node of the grouping tree is a group or a final item."
|
||||
[item]
|
||||
(d/ordered-map? item))
|
||||
|
||||
(defn get-children
|
||||
"Get all children of a group of a grouping tree. Each child is
|
||||
a tuple [name item], where item "
|
||||
[group]
|
||||
(dm/assert!
|
||||
"expected group node"
|
||||
(group? group))
|
||||
(seq group))
|
||||
|
||||
;; === Token
|
||||
|
||||
(def token-separator ".")
|
||||
|
||||
(defn get-token-path [path]
|
||||
(get-path path token-separator))
|
||||
|
||||
(defn split-token-path [path]
|
||||
(split-path path token-separator))
|
||||
|
||||
(defrecord Token [name type value description modified-at])
|
||||
|
||||
(def schema:token
|
||||
[:and
|
||||
[:map {:title "Token"}
|
||||
[:name cto/token-name-ref]
|
||||
[:type [::sm/one-of cto/token-types]]
|
||||
[:value :any]
|
||||
[:description [:maybe :string]]
|
||||
[:modified-at ::sm/inst]]
|
||||
[:fn (partial instance? Token)]])
|
||||
|
||||
(sm/register! ::token schema:token)
|
||||
|
||||
(def valid-token?
|
||||
(sm/validator schema:token))
|
||||
|
||||
(def check-token!
|
||||
(sm/check-fn ::token))
|
||||
|
||||
(defn make-token
|
||||
[& {:keys [] :as params}]
|
||||
(let [params (-> params
|
||||
(dissoc :id) ;; we will remove this when old data structures are removed
|
||||
(update :modified-at #(or % (dt/now))))
|
||||
token (map->Token params)]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid token"
|
||||
(check-token! token))
|
||||
|
||||
token))
|
||||
|
||||
(defn find-token-value-references
|
||||
"Returns set of token references found in `token-value`.
|
||||
|
||||
Used for checking if a token has a reference in the value.
|
||||
Token references are strings delimited by curly braces.
|
||||
E.g.: {foo.bar.baz} -> foo.bar.baz"
|
||||
[token-value]
|
||||
(some->> (re-seq #"\{([^}]*)\}" token-value)
|
||||
(map second)
|
||||
(into #{})))
|
||||
|
||||
(defn token-value-self-reference?
|
||||
"Check if the token is self referencing with its `token-name` in `token-value`.
|
||||
Simple 1 level check, doesn't account for circular self refernces across multiple tokens."
|
||||
[token-name token-value]
|
||||
(let [token-references (find-token-value-references token-value)
|
||||
self-reference? (get token-references token-name)]
|
||||
self-reference?))
|
||||
|
||||
(defn group-by-type [tokens]
|
||||
(let [tokens' (if (or (map? tokens)
|
||||
(d/ordered-map? tokens))
|
||||
(vals tokens)
|
||||
tokens)]
|
||||
(group-by :type tokens')))
|
||||
|
||||
(defn filter-by-type [token-type tokens]
|
||||
(let [token-type? #(= token-type (:type %))]
|
||||
(cond
|
||||
(d/ordered-map? tokens) (into (d/ordered-map) (filter (comp token-type? val) tokens))
|
||||
(map? tokens) (into {} (filter (comp token-type? val) tokens))
|
||||
:else (filter token-type? tokens))))
|
||||
|
||||
;; === Token Set
|
||||
|
||||
(def set-separator "/")
|
||||
|
||||
(defn get-token-set-path [path]
|
||||
(get-path path set-separator))
|
||||
|
||||
(defn get-token-set-group-str [path]
|
||||
(get-groups-str path set-separator))
|
||||
|
||||
(defn split-token-set-path [path]
|
||||
(split-path path set-separator))
|
||||
|
||||
(defn tokens-tree
|
||||
"Convert tokens into a nested tree with their `:name` as the path.
|
||||
Optionally use `update-token-fn` option to transform the token."
|
||||
[tokens & {:keys [update-token-fn]
|
||||
:or {update-token-fn identity}}]
|
||||
(reduce
|
||||
(fn [acc [_ token]]
|
||||
(let [path (split-token-path (:name token))]
|
||||
(assoc-in acc path (update-token-fn token))))
|
||||
{} tokens))
|
||||
|
||||
(defn backtrace-tokens-tree
|
||||
"Convert tokens into a nested tree with their `:name` as the path.
|
||||
Generates a uuid per token to backtrace a token from an external source (StyleDictionary).
|
||||
The backtrace can't be the name as the name might not exist when the user is creating a token."
|
||||
[tokens]
|
||||
(reduce
|
||||
(fn [acc [_ token]]
|
||||
(let [temp-id (random-uuid)
|
||||
token (assoc token :temp/id temp-id)
|
||||
path (split-token-path (:name token))]
|
||||
(-> acc
|
||||
(assoc-in (concat [:tokens-tree] path) token)
|
||||
(assoc-in [:ids temp-id] token))))
|
||||
{:tokens-tree {} :ids {}} tokens))
|
||||
|
||||
(defprotocol ITokenSet
|
||||
(add-token [_ token] "add a token at the end of the list")
|
||||
(update-token [_ token-name f] "update a token in the list")
|
||||
(delete-token [_ token-name] "delete a token from the list")
|
||||
(get-token [_ token-name] "return token by token-name")
|
||||
(get-tokens [_] "return an ordered sequence of all tokens in the set")
|
||||
(get-tokens-tree [_] "returns a tree of tokens split & nested by their name path")
|
||||
(get-dtcg-tokens-tree [_] "returns tokens tree formated to the dtcg spec"))
|
||||
|
||||
(defrecord TokenSet [name description modified-at tokens]
|
||||
ITokenSet
|
||||
(add-token [_ token]
|
||||
(dm/assert! "expected valid token" (check-token! token))
|
||||
(TokenSet. name
|
||||
description
|
||||
(dt/now)
|
||||
(assoc tokens (:name token) token)))
|
||||
|
||||
(update-token [this token-name f]
|
||||
(if-let [token (get tokens token-name)]
|
||||
(let [token' (-> (make-token (f token))
|
||||
(assoc :modified-at (dt/now)))]
|
||||
(check-token! token')
|
||||
(TokenSet. name
|
||||
description
|
||||
(dt/now)
|
||||
(if (= (:name token) (:name token'))
|
||||
(assoc tokens (:name token') token')
|
||||
(-> tokens
|
||||
(d/oassoc-before (:name token) (:name token') token')
|
||||
(dissoc (:name token))))))
|
||||
this))
|
||||
|
||||
(delete-token [_ token-name]
|
||||
(TokenSet. name
|
||||
description
|
||||
(dt/now)
|
||||
(dissoc tokens token-name)))
|
||||
|
||||
(get-token [_ token-name]
|
||||
(get tokens token-name))
|
||||
|
||||
(get-tokens [_]
|
||||
(vals tokens))
|
||||
|
||||
(get-tokens-tree [_]
|
||||
(tokens-tree tokens))
|
||||
|
||||
(get-dtcg-tokens-tree [_]
|
||||
(tokens-tree tokens :update-token-fn (fn [token]
|
||||
(cond-> {"$value" (:value token)
|
||||
"$type" (cto/token-type->dtcg-token-type (:type token))}
|
||||
(:description token) (assoc "$description" (:description token)))))))
|
||||
|
||||
(def schema:token-set
|
||||
[:and [:map {:title "TokenSet"}
|
||||
[:name :string]
|
||||
[:description [:maybe :string]]
|
||||
[:modified-at ::sm/inst]
|
||||
[:tokens [:and [:map-of {:gen/max 5} :string ::token]
|
||||
[:fn d/ordered-map?]]]]
|
||||
[:fn (partial instance? TokenSet)]])
|
||||
|
||||
(sm/register! ::token-set schema:token-set)
|
||||
|
||||
(def valid-token-set?
|
||||
(sm/validator schema:token-set))
|
||||
|
||||
(def check-token-set!
|
||||
(sm/check-fn ::token-set))
|
||||
|
||||
(defn make-token-set
|
||||
[& {:keys [] :as params}]
|
||||
(let [params (-> params
|
||||
(dissoc :id)
|
||||
(update :modified-at #(or % (dt/now)))
|
||||
(update :tokens #(into (d/ordered-map) %)))
|
||||
token-set (map->TokenSet params)]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid token set"
|
||||
(check-token-set! token-set))
|
||||
|
||||
token-set))
|
||||
|
||||
;; === TokenSetGroup
|
||||
|
||||
(defrecord TokenSetGroup [attr1 attr2])
|
||||
|
||||
;; TODO schema, validators, etc.
|
||||
|
||||
(defn make-token-set-group
|
||||
[]
|
||||
(TokenSetGroup. "one" "two"))
|
||||
|
||||
;; === TokenSets (collection)
|
||||
|
||||
(defprotocol ITokenSets
|
||||
(add-set [_ token-set] "add a set to the library, at the end")
|
||||
(update-set [_ set-name f] "modify a set in the ilbrary")
|
||||
(delete-set [_ set-name] "delete a set in the library")
|
||||
(move-set-before [_ set-name before-set-name] "move a set with `set-name` before a set with `before-set-name` in the library.
|
||||
When `before-set-name` is nil, move set to bottom")
|
||||
(set-count [_] "get the total number if sets in the library")
|
||||
(get-set-tree [_] "get a nested tree of all sets in the library")
|
||||
(get-sets [_] "get an ordered sequence of all sets in the library")
|
||||
(get-ordered-set-names [_] "get an ordered sequence of all sets names in the library")
|
||||
(get-set [_ set-name] "get one set looking for name")
|
||||
(get-neighbor-set-name [_ set-name index-offset] "get neighboring set name offset by `index-offset`")
|
||||
(get-set-group [_ set-group-path] "get the attributes of a set group"))
|
||||
|
||||
(def schema:token-set-node
|
||||
[:schema {:registry {::node [:or ::token-set
|
||||
[:and
|
||||
[:map-of {:gen/max 5} :string [:ref ::node]]
|
||||
[:fn d/ordered-map?]]]}}
|
||||
[:ref ::node]])
|
||||
|
||||
(sm/register! ::token-set-node schema:token-set-node)
|
||||
|
||||
(def schema:token-sets
|
||||
[:and
|
||||
[:map-of {:title "TokenSets"}
|
||||
:string ::token-set-node]
|
||||
[:fn d/ordered-map?]])
|
||||
|
||||
(sm/register! ::token-sets schema:token-sets)
|
||||
|
||||
(def valid-token-sets?
|
||||
(sm/validator schema:token-sets))
|
||||
|
||||
(def check-token-sets!
|
||||
(sm/check-fn ::token-sets))
|
||||
|
||||
;; === TokenTheme
|
||||
|
||||
(def theme-separator "/")
|
||||
|
||||
(defn token-theme-path [group name]
|
||||
(join-path [group name] theme-separator))
|
||||
|
||||
(defn split-token-theme-path [path]
|
||||
(split-path path theme-separator))
|
||||
|
||||
(def hidden-token-theme-group
|
||||
"")
|
||||
|
||||
(def hidden-token-theme-name
|
||||
"__PENPOT__HIDDEN__TOKEN__THEME__")
|
||||
|
||||
(def hidden-token-theme-path
|
||||
(token-theme-path hidden-token-theme-group hidden-token-theme-name))
|
||||
|
||||
|
||||
(defprotocol ITokenTheme
|
||||
(set-sets [_ set-names] "set the active token sets")
|
||||
(disable-set [_ set-name] "disable set in theme")
|
||||
(toggle-set [_ set-name] "toggle a set enabled / disabled in the theme")
|
||||
(theme-path [_] "get `token-theme-path` from theme")
|
||||
(theme-matches-group-name [_ group name] "if a theme matches the given group & name")
|
||||
(hidden-temporary-theme? [_] "if a theme is the (from the user ui) hidden temporary theme"))
|
||||
|
||||
(defrecord TokenTheme [name group description is-source modified-at sets]
|
||||
ITokenTheme
|
||||
(set-sets [_ set-names]
|
||||
(TokenTheme. name
|
||||
group
|
||||
description
|
||||
is-source
|
||||
(dt/now)
|
||||
set-names))
|
||||
|
||||
(disable-set [this set-name]
|
||||
(set-sets this (disj sets set-name)))
|
||||
|
||||
(toggle-set [this set-name]
|
||||
(set-sets this (if (sets set-name)
|
||||
(disj sets set-name)
|
||||
(conj sets set-name))))
|
||||
|
||||
(theme-path [_]
|
||||
(token-theme-path group name))
|
||||
|
||||
(theme-matches-group-name [this group name]
|
||||
(and (= (:group this) group)
|
||||
(= (:name this) name)))
|
||||
|
||||
(hidden-temporary-theme? [this]
|
||||
(theme-matches-group-name this hidden-token-theme-group hidden-token-theme-name)))
|
||||
|
||||
(def schema:token-theme
|
||||
[:and [:map {:title "TokenTheme"}
|
||||
[:name :string]
|
||||
[:group :string]
|
||||
[:description [:maybe :string]]
|
||||
[:is-source :boolean]
|
||||
[:modified-at ::sm/inst]
|
||||
[:sets [:set {:gen/max 5} :string]]]
|
||||
[:fn (partial instance? TokenTheme)]])
|
||||
|
||||
(sm/register! ::token-theme schema:token-theme)
|
||||
|
||||
(def valid-token-theme?
|
||||
(sm/validator schema:token-theme))
|
||||
|
||||
(def check-token-theme!
|
||||
(sm/check-fn ::token-theme))
|
||||
|
||||
(def top-level-theme-group-name
|
||||
"Top level theme groups have an empty string as the theme group."
|
||||
"")
|
||||
|
||||
(defn top-level-theme-group? [group]
|
||||
(= group top-level-theme-group-name))
|
||||
|
||||
(defn make-token-theme
|
||||
[& {:keys [] :as params}]
|
||||
(let [params (-> params
|
||||
(dissoc :id)
|
||||
(update :group #(or % top-level-theme-group-name))
|
||||
(update :is-source #(or % false))
|
||||
(update :modified-at #(or % (dt/now)))
|
||||
(update :sets #(into #{} %)))
|
||||
token-theme (map->TokenTheme params)]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid token theme"
|
||||
(check-token-theme! token-theme))
|
||||
|
||||
token-theme))
|
||||
|
||||
(defn make-hidden-token-theme
|
||||
[& {:keys [] :as params}]
|
||||
(make-token-theme (assoc params
|
||||
:group hidden-token-theme-group
|
||||
:name hidden-token-theme-name)))
|
||||
|
||||
;; === TokenThemes (collection)
|
||||
|
||||
(defprotocol ITokenThemes
|
||||
(add-theme [_ token-theme] "add a theme to the library, at the end")
|
||||
(update-theme [_ group name f] "modify a theme in the ilbrary")
|
||||
(delete-theme [_ group name] "delete a theme in the library")
|
||||
(theme-count [_] "get the total number if themes in the library")
|
||||
(get-theme-tree [_] "get a nested tree of all themes in the library")
|
||||
(get-themes [_] "get an ordered sequence of all themes in the library")
|
||||
(get-theme [_ group name] "get one theme looking for name")
|
||||
(get-theme-groups [_] "get a sequence of group names by order")
|
||||
(get-active-theme-paths [_] "get the active theme paths")
|
||||
(get-active-themes [_] "get an ordered sequence of active themes in the library")
|
||||
(set-active-themes [_ active-themes] "set active themes in library")
|
||||
(theme-active? [_ group name] "predicate if token theme is active")
|
||||
(activate-theme [_ group name] "adds theme from the active-themes")
|
||||
(deactivate-theme [_ group name] "removes theme from the active-themes")
|
||||
(toggle-theme-active? [_ group name] "toggles theme in the active-themes"))
|
||||
|
||||
(def schema:token-themes
|
||||
[:and
|
||||
[:map-of {:title "TokenThemes"}
|
||||
:string [:and [:map-of :string ::token-theme]
|
||||
[:fn d/ordered-map?]]]
|
||||
[:fn d/ordered-map?]])
|
||||
|
||||
(sm/register! ::token-themes schema:token-themes)
|
||||
|
||||
(def valid-token-themes?
|
||||
(sm/validator schema:token-themes))
|
||||
|
||||
(def check-token-themes!
|
||||
(sm/check-fn ::token-themes))
|
||||
|
||||
(def schema:active-token-themes
|
||||
[:set string?])
|
||||
|
||||
(def valid-active-token-themes?
|
||||
(sm/validator schema:active-token-themes))
|
||||
|
||||
;; === Import / Export from DTCG format
|
||||
|
||||
(defn flatten-nested-tokens-json
|
||||
"Recursively flatten the dtcg token structure, joining keys with '.'."
|
||||
[tokens token-path]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(let [child-path (if (empty? token-path)
|
||||
(name k)
|
||||
(str token-path "." k))]
|
||||
(if (and (map? v)
|
||||
(not (contains? v "$type")))
|
||||
(merge acc (flatten-nested-tokens-json v child-path))
|
||||
(let [token-type (cto/dtcg-token-type->token-type (get v "$type"))]
|
||||
(if token-type
|
||||
(assoc acc child-path (make-token
|
||||
:name child-path
|
||||
:type token-type
|
||||
:value (get v "$value")
|
||||
:description (get v "$description")))
|
||||
;; Discard unknown tokens
|
||||
acc)))))
|
||||
{}
|
||||
tokens))
|
||||
|
||||
;; === Tokens Lib
|
||||
|
||||
(defprotocol ITokensLib
|
||||
"A library of tokens, sets and themes."
|
||||
(add-token-in-set [_ set-name token] "add token to a set")
|
||||
(update-token-in-set [_ set-name token-name f] "update a token in a set")
|
||||
(delete-token-from-set [_ set-name token-name] "delete a token from a set")
|
||||
(toggle-set-in-theme [_ group-name theme-name set-name] "toggle a set used / not used in a theme")
|
||||
(get-active-themes-set-names [_] "set of set names that are active in the the active themes")
|
||||
(get-active-themes-set-tokens [_] "set of set names that are active in the the active themes")
|
||||
(update-set-name [_ old-set-name new-set-name] "updates set name in themes")
|
||||
(encode-dtcg [_] "Encodes library to a dtcg compatible json string")
|
||||
(decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library")
|
||||
(get-all-tokens [_] "all tokens in the lib")
|
||||
(validate [_]))
|
||||
|
||||
(deftype TokensLib [sets set-groups themes active-themes]
|
||||
;; NOTE: This is only for debug purposes, pending to properly
|
||||
;; implement the toString and alternative printing.
|
||||
#?@(:clj [clojure.lang.IDeref
|
||||
(deref [_] {:sets sets
|
||||
:set-groups set-groups
|
||||
:themes themes
|
||||
:active-themes active-themes})]
|
||||
:cljs [cljs.core/IDeref
|
||||
(-deref [_] {:sets sets
|
||||
:set-groups set-groups
|
||||
:themes themes
|
||||
:active-themes active-themes})])
|
||||
|
||||
#?@(:cljs [cljs.core/IEncodeJS
|
||||
(-clj->js [_] (js-obj "sets" (clj->js sets)
|
||||
"set-groups" (clj->js set-groups)
|
||||
"themes" (clj->js themes)
|
||||
"active-themes" (clj->js active-themes)))])
|
||||
|
||||
ITokenSets
|
||||
(add-set [_ token-set]
|
||||
(dm/assert! "expected valid token set" (check-token-set! token-set))
|
||||
(let [path (get-token-set-path token-set)
|
||||
groups-str (get-token-set-group-str token-set)]
|
||||
(TokensLib. (d/oassoc-in sets path token-set)
|
||||
(cond-> set-groups
|
||||
(not (str/empty? groups-str))
|
||||
(assoc groups-str (make-token-set-group)))
|
||||
themes
|
||||
active-themes)))
|
||||
|
||||
(update-set [this set-name f]
|
||||
(let [path (split-token-set-path set-name)
|
||||
set (get-in sets path)]
|
||||
(if set
|
||||
(let [set' (-> (make-token-set (f set))
|
||||
(assoc :modified-at (dt/now)))
|
||||
path' (get-path set' "/")]
|
||||
(check-token-set! set')
|
||||
(TokensLib. (if (= (:name set) (:name set'))
|
||||
(d/oassoc-in sets path set')
|
||||
(-> sets
|
||||
(d/oassoc-in-before path path' set')
|
||||
(d/dissoc-in path)))
|
||||
set-groups ;; TODO update set-groups as needed
|
||||
themes
|
||||
active-themes))
|
||||
this)))
|
||||
|
||||
(delete-set [_ set-name]
|
||||
(let [path (split-token-set-path set-name)]
|
||||
(TokensLib. (d/dissoc-in sets path)
|
||||
set-groups ;; TODO remove set-group if needed
|
||||
(walk/postwalk
|
||||
(fn [form]
|
||||
(if (instance? TokenTheme form)
|
||||
(disable-set form set-name)
|
||||
form))
|
||||
themes)
|
||||
active-themes)))
|
||||
|
||||
;; TODO Handle groups and nesting
|
||||
(move-set-before [this set-name before-set-name]
|
||||
(let [source-path (split-token-set-path set-name)
|
||||
token-set (-> (get-set this set-name)
|
||||
(assoc :modified-at (dt/now)))
|
||||
target-path (split-token-set-path before-set-name)]
|
||||
(if before-set-name
|
||||
(TokensLib. (d/oassoc-in-before sets target-path source-path token-set)
|
||||
set-groups ;; TODO remove set-group if needed
|
||||
themes
|
||||
active-themes)
|
||||
(TokensLib. (-> sets
|
||||
(d/dissoc-in source-path)
|
||||
(d/oassoc-in source-path token-set))
|
||||
set-groups ;; TODO remove set-group if needed
|
||||
themes
|
||||
active-themes))))
|
||||
|
||||
(get-set-tree [_]
|
||||
sets)
|
||||
|
||||
(get-sets [_]
|
||||
(->> (tree-seq d/ordered-map? vals sets)
|
||||
(filter (partial instance? TokenSet))))
|
||||
|
||||
(get-ordered-set-names [this]
|
||||
(map :name (get-sets this)))
|
||||
|
||||
(set-count [this]
|
||||
(count (get-sets this)))
|
||||
|
||||
(get-set [_ set-name]
|
||||
(let [path (split-path set-name "/")]
|
||||
(get-in sets path)))
|
||||
|
||||
(get-neighbor-set-name [this set-name index-offset]
|
||||
(let [sets (get-ordered-set-names this)
|
||||
index (d/index-of sets set-name)
|
||||
neighbor-set-name (when index
|
||||
(nth sets (+ index-offset index) nil))]
|
||||
neighbor-set-name))
|
||||
|
||||
(get-set-group [_ set-group-path]
|
||||
(get set-groups set-group-path))
|
||||
|
||||
ITokenThemes
|
||||
(add-theme [_ token-theme]
|
||||
(dm/assert! "expected valid token theme" (check-token-theme! token-theme))
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
(update themes (:group token-theme) d/oassoc (:name token-theme) token-theme)
|
||||
active-themes))
|
||||
|
||||
(update-theme [this group name f]
|
||||
(let [theme (dm/get-in themes [group name])]
|
||||
(if theme
|
||||
(let [theme' (-> (make-token-theme (f theme))
|
||||
(assoc :modified-at (dt/now)))
|
||||
group' (:group theme')
|
||||
name' (:name theme')
|
||||
same-group? (= group group')
|
||||
same-name? (= name name')
|
||||
same-path? (and same-group? same-name?)]
|
||||
(check-token-theme! theme')
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
(if same-path?
|
||||
(update themes group' assoc name' theme')
|
||||
(-> themes
|
||||
(d/oassoc-in-before [group name] [group' name'] theme')
|
||||
(d/dissoc-in [group name])))
|
||||
(if same-path?
|
||||
active-themes
|
||||
(disj active-themes (token-theme-path group name)))))
|
||||
this)))
|
||||
|
||||
(delete-theme [_ group name]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
(d/dissoc-in themes [group name])
|
||||
(disj active-themes (token-theme-path group name))))
|
||||
|
||||
(get-theme-tree [_]
|
||||
themes)
|
||||
|
||||
(get-theme-groups [_]
|
||||
(into [] (comp
|
||||
(map key)
|
||||
(remove top-level-theme-group?))
|
||||
themes))
|
||||
|
||||
(get-themes [_]
|
||||
(->> (tree-seq d/ordered-map? vals themes)
|
||||
(filter (partial instance? TokenTheme))))
|
||||
|
||||
(theme-count [this]
|
||||
(count (get-themes this)))
|
||||
|
||||
(get-theme [_ group name]
|
||||
(dm/get-in themes [group name]))
|
||||
|
||||
(set-active-themes [_ active-themes]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
themes
|
||||
active-themes))
|
||||
|
||||
(activate-theme [this group name]
|
||||
(if-let [theme (get-theme this group name)]
|
||||
(let [group-themes (->> (get themes group)
|
||||
(map (comp theme-path val))
|
||||
(into #{}))
|
||||
active-themes' (-> (set/difference active-themes group-themes)
|
||||
(conj (theme-path theme)))]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
themes
|
||||
active-themes'))
|
||||
this))
|
||||
|
||||
(deactivate-theme [_ group name]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
themes
|
||||
(disj active-themes (token-theme-path group name))))
|
||||
|
||||
(theme-active? [_ group name]
|
||||
(contains? active-themes (token-theme-path group name)))
|
||||
|
||||
(toggle-theme-active? [this group name]
|
||||
(if (theme-active? this group name)
|
||||
(deactivate-theme this group name)
|
||||
(activate-theme this group name)))
|
||||
|
||||
(get-active-theme-paths [_]
|
||||
active-themes)
|
||||
|
||||
(get-active-themes [this]
|
||||
(into
|
||||
(list)
|
||||
(comp
|
||||
(filter (partial instance? TokenTheme))
|
||||
(filter #(theme-active? this (:group %) (:name %))))
|
||||
(tree-seq d/ordered-map? vals themes)))
|
||||
|
||||
ITokensLib
|
||||
(add-token-in-set [this set-name token]
|
||||
(dm/assert! "expected valid token instance" (check-token! token))
|
||||
(if (contains? sets set-name)
|
||||
(TokensLib. (update sets set-name add-token token)
|
||||
set-groups
|
||||
themes
|
||||
active-themes)
|
||||
this))
|
||||
|
||||
(update-token-in-set [this set-name token-name f]
|
||||
(if (contains? sets set-name)
|
||||
(TokensLib. (update sets set-name
|
||||
#(update-token % token-name f))
|
||||
set-groups
|
||||
themes
|
||||
active-themes)
|
||||
this))
|
||||
|
||||
(delete-token-from-set [this set-name token-name]
|
||||
(if (contains? sets set-name)
|
||||
(TokensLib. (update sets set-name
|
||||
#(delete-token % token-name))
|
||||
set-groups
|
||||
themes
|
||||
active-themes)
|
||||
this))
|
||||
|
||||
(toggle-set-in-theme [this theme-group theme-name set-name]
|
||||
(if-let [_theme (get-in themes theme-group theme-name)]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
(d/oupdate-in themes [theme-group theme-name]
|
||||
#(toggle-set % set-name))
|
||||
active-themes)
|
||||
this))
|
||||
|
||||
(get-active-themes-set-names [this]
|
||||
(into #{}
|
||||
(mapcat :sets)
|
||||
(get-active-themes this)))
|
||||
|
||||
(get-active-themes-set-tokens [this]
|
||||
(let [sets-order (get-ordered-set-names this)
|
||||
active-themes (get-active-themes this)
|
||||
order-theme-set (fn [theme]
|
||||
(filter #(contains? (set (:sets theme)) %) sets-order))]
|
||||
(reduce
|
||||
(fn [tokens theme]
|
||||
(reduce
|
||||
(fn [tokens' cur]
|
||||
(merge tokens' (:tokens (get-set this cur))))
|
||||
tokens (order-theme-set theme)))
|
||||
(d/ordered-map) active-themes)))
|
||||
|
||||
;; TODO Move to `update-set`
|
||||
(update-set-name [_ old-set-name new-set-name]
|
||||
(TokensLib. sets
|
||||
set-groups
|
||||
(walk/postwalk
|
||||
(fn [form]
|
||||
(if (instance? TokenTheme form)
|
||||
(-> form
|
||||
(update :sets disj old-set-name)
|
||||
(update :sets conj new-set-name))
|
||||
form))
|
||||
themes)
|
||||
active-themes))
|
||||
|
||||
(encode-dtcg [_]
|
||||
(into {} (map (fn [[k v]]
|
||||
[k (get-dtcg-tokens-tree v)])
|
||||
sets)))
|
||||
|
||||
(decode-dtcg-json [_ parsed-json]
|
||||
(let [token-sets (into (d/ordered-map)
|
||||
(map (fn [[set-name tokens]]
|
||||
[set-name (make-token-set
|
||||
:name set-name
|
||||
:tokens (flatten-nested-tokens-json tokens ""))]))
|
||||
(-> parsed-json
|
||||
;; tokens-studio/plugin will add these meta properties, remove them for now
|
||||
(dissoc "$themes" "$metadata")))]
|
||||
(TokensLib. token-sets
|
||||
set-groups
|
||||
themes
|
||||
active-themes)))
|
||||
|
||||
(get-all-tokens [this]
|
||||
(reduce
|
||||
(fn [tokens' set]
|
||||
(into tokens' (map (fn [x] [(:name x) x]) (get-tokens set))))
|
||||
{} (get-sets this)))
|
||||
|
||||
(validate [_]
|
||||
(and (valid-token-sets? sets) ;; TODO: validate set-groups
|
||||
(valid-token-themes? themes)
|
||||
(valid-active-token-themes? active-themes))))
|
||||
|
||||
(defn valid-tokens-lib?
|
||||
[o]
|
||||
(and (instance? TokensLib o)
|
||||
(validate o)))
|
||||
|
||||
(defn check-tokens-lib!
|
||||
[lib]
|
||||
(dm/assert!
|
||||
"expected valid tokens lib"
|
||||
(valid-tokens-lib? lib)))
|
||||
|
||||
(defn make-tokens-lib
|
||||
"Create an empty or prepopulated tokens library."
|
||||
([]
|
||||
;; NOTE: is possible that ordered map is not the most apropriate
|
||||
;; data structure and maybe we need a specific that allows us an
|
||||
;; easy way to reorder it, or just store inside Tokens data
|
||||
;; structure the data and the order separately as we already do
|
||||
;; with pages and pages-index.
|
||||
(make-tokens-lib :sets (d/ordered-map)
|
||||
:set-groups {}
|
||||
:themes (d/ordered-map)
|
||||
:active-themes #{}))
|
||||
|
||||
([& {:keys [sets set-groups themes active-themes]}]
|
||||
(let [tokens-lib (TokensLib. sets set-groups themes (or active-themes #{}))]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid tokens lib"
|
||||
(valid-tokens-lib? tokens-lib))
|
||||
|
||||
tokens-lib)))
|
||||
|
||||
(defn ensure-tokens-lib
|
||||
[tokens-lib]
|
||||
(or tokens-lib (make-tokens-lib)))
|
||||
|
||||
(def type:tokens-lib
|
||||
{:type ::tokens-lib
|
||||
:pred valid-tokens-lib?})
|
||||
|
||||
(sm/register! ::tokens-lib type:tokens-lib)
|
||||
|
||||
;; === Serialization handlers for RPC API (transit) and database (fressian)
|
||||
|
||||
(t/add-handlers!
|
||||
{:id "penpot/tokens-lib"
|
||||
:class TokensLib
|
||||
:wfn deref
|
||||
:rfn #(make-tokens-lib %)}
|
||||
|
||||
{:id "penpot/token-set"
|
||||
:class TokenSet
|
||||
:wfn #(into {} %)
|
||||
:rfn #(make-token-set %)}
|
||||
|
||||
{:id "penpot/token-theme"
|
||||
:class TokenTheme
|
||||
:wfn #(into {} %)
|
||||
:rfn #(make-token-theme %)}
|
||||
|
||||
{:id "penpot/token"
|
||||
:class Token
|
||||
:wfn #(into {} %)
|
||||
:rfn #(make-token %)})
|
||||
|
||||
#?(:clj
|
||||
(fres/add-handlers!
|
||||
{:name "penpot/token/v1"
|
||||
:class Token
|
||||
:wfn (fn [n w o]
|
||||
(fres/write-tag! w n 1)
|
||||
(fres/write-object! w (into {} o)))
|
||||
:rfn (fn [r]
|
||||
(let [obj (fres/read-object! r)]
|
||||
(map->Token obj)))}
|
||||
|
||||
{:name "penpot/token-set/v1"
|
||||
:class TokenSet
|
||||
:wfn (fn [n w o]
|
||||
(fres/write-tag! w n 1)
|
||||
(fres/write-object! w (into {} o)))
|
||||
:rfn (fn [r]
|
||||
(let [obj (fres/read-object! r)]
|
||||
(map->TokenSet obj)))}
|
||||
|
||||
{:name "penpot/token-theme/v1"
|
||||
:class TokenTheme
|
||||
:wfn (fn [n w o]
|
||||
(fres/write-tag! w n 1)
|
||||
(fres/write-object! w (into {} o)))
|
||||
:rfn (fn [r]
|
||||
(let [obj (fres/read-object! r)]
|
||||
(map->TokenTheme obj)))}
|
||||
|
||||
{:name "penpot/tokens-lib/v1"
|
||||
:class TokensLib
|
||||
:wfn (fn [n w o]
|
||||
(fres/write-tag! w n 3)
|
||||
(fres/write-object! w (.-sets o))
|
||||
(fres/write-object! w (.-set-groups o))
|
||||
(fres/write-object! w (.-themes o))
|
||||
(fres/write-object! w (.-active-themes o)))
|
||||
:rfn (fn [r]
|
||||
(let [sets (fres/read-object! r)
|
||||
set-groups (fres/read-object! r)
|
||||
themes (fres/read-object! r)
|
||||
active-themes (fres/read-object! r)]
|
||||
(->TokensLib sets set-groups themes active-themes)))}))
|
49
common/src/app/common/types/tokens_list.cljc
Normal file
49
common/src/app/common/types/tokens_list.cljc
Normal file
|
@ -0,0 +1,49 @@
|
|||
;; 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.common.types.tokens-list
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.time :as dt]))
|
||||
|
||||
(defn tokens-seq
|
||||
"Returns a sequence of all tokens within the file data."
|
||||
[file-data]
|
||||
(vals (:tokens file-data)))
|
||||
|
||||
(defn- touch
|
||||
"Updates the `modified-at` timestamp of a token."
|
||||
[token]
|
||||
(assoc token :modified-at (dt/now)))
|
||||
|
||||
(defn add-token
|
||||
"Adds a new token to the file data, setting its `modified-at` timestamp."
|
||||
[file-data token-set-id token]
|
||||
(-> file-data
|
||||
(update :tokens assoc (:id token) (touch token))
|
||||
(d/update-in-when [:token-sets-index token-set-id] #(->
|
||||
(update % :tokens conj (:id token))
|
||||
(touch)))))
|
||||
|
||||
(defn get-token
|
||||
"Retrieves a token by its ID from the file data."
|
||||
[file-data token-id]
|
||||
(get-in file-data [:tokens token-id]))
|
||||
|
||||
(defn set-token
|
||||
"Sets or updates a token in the file data, updating its `modified-at` timestamp."
|
||||
[file-data token]
|
||||
(d/assoc-in-when file-data [:tokens (:id token)] (touch token)))
|
||||
|
||||
(defn update-token
|
||||
"Applies a function to update a token in the file data, then touches it."
|
||||
[file-data token-id f & args]
|
||||
(d/update-in-when file-data [:tokens token-id] #(-> (apply f % args) (touch))))
|
||||
|
||||
(defn delete-token
|
||||
"Removes a token from the file data by its ID."
|
||||
[file-data token-id]
|
||||
(update file-data :tokens dissoc token-id))
|
79
common/src/app/common/types/tokens_theme_list.cljc
Normal file
79
common/src/app/common/types/tokens_theme_list.cljc
Normal file
|
@ -0,0 +1,79 @@
|
|||
;; 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.common.types.tokens-theme-list
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.time :as dt]))
|
||||
|
||||
(defn- touch
|
||||
"Updates the `modified-at` timestamp of a token set."
|
||||
[token-set]
|
||||
(assoc token-set :modified-at (dt/now)))
|
||||
|
||||
(defn assoc-active-token-themes
|
||||
[file-data theme-ids]
|
||||
(assoc file-data :token-active-themes theme-ids))
|
||||
|
||||
(defn add-temporary-token-theme
|
||||
[file-data {:keys [id name] :as token-theme}]
|
||||
(-> file-data
|
||||
(d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])
|
||||
(assoc :token-theme-temporary-id id)
|
||||
(assoc :token-theme-temporary-name name)
|
||||
(update :token-themes-index assoc id token-theme)))
|
||||
|
||||
(defn delete-temporary-token-theme
|
||||
[file-data token-theme-id]
|
||||
(cond-> file-data
|
||||
(= (:token-theme-temporary-id file-data) token-theme-id) (dissoc :token-theme-temporary-id :token-theme-temporary-name)
|
||||
:always (d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])))
|
||||
|
||||
(defn add-token-theme
|
||||
[file-data {:keys [index id] :as token-theme}]
|
||||
(-> file-data
|
||||
(update :token-themes
|
||||
(fn [token-themes]
|
||||
(let [exists? (some (partial = id) token-themes)]
|
||||
(cond
|
||||
exists? token-themes
|
||||
(nil? index) (conj (or token-themes []) id)
|
||||
:else (d/insert-at-index token-themes index [id])))))
|
||||
(update :token-themes-index assoc id token-theme)))
|
||||
|
||||
(defn update-token-theme
|
||||
[file-data token-theme-id f & args]
|
||||
(d/update-in-when file-data [:token-themes-index token-theme-id] #(-> (apply f % args) (touch))))
|
||||
|
||||
(defn delete-token-theme
|
||||
[file-data theme-id]
|
||||
(-> file-data
|
||||
(update :token-themes (fn [ids] (d/removev #(= % theme-id) ids)))
|
||||
(update :token-themes-index dissoc theme-id)
|
||||
(update :token-active-themes disj theme-id)))
|
||||
|
||||
(defn add-token-set
|
||||
[file-data {:keys [index id] :as token-set}]
|
||||
(-> file-data
|
||||
(update :token-set-groups
|
||||
(fn [token-set-groups]
|
||||
(let [exists? (some (partial = id) token-set-groups)]
|
||||
(cond
|
||||
exists? token-set-groups
|
||||
(nil? index) (conj (or token-set-groups []) id)
|
||||
:else (d/insert-at-index token-set-groups index [id])))))
|
||||
(update :token-sets-index assoc id token-set)))
|
||||
|
||||
(defn update-token-set
|
||||
[file-data token-set-id f & args]
|
||||
(d/update-in-when file-data [:token-sets-index token-set-id] #(-> (apply f % args) (touch))))
|
||||
|
||||
(defn delete-token-set
|
||||
[file-data token-set-id]
|
||||
(-> file-data
|
||||
(update :token-set-groups (fn [xs] (into [] (remove #(= (:id %) token-set-id) xs))))
|
||||
(update :token-sets-index dissoc token-set-id)
|
||||
(update :token-themes-index (fn [xs] (update-vals xs #(update % :sets disj token-set-id))))))
|
|
@ -0,0 +1,803 @@
|
|||
{
|
||||
"core": {
|
||||
"dimension": {
|
||||
"scale": {
|
||||
"$value": "2",
|
||||
"$type": "dimension"
|
||||
},
|
||||
"xs": {
|
||||
"$value": "4",
|
||||
"$type": "dimension"
|
||||
},
|
||||
"sm": {
|
||||
"$value": "{dimension.xs} * {dimension.scale}",
|
||||
"$type": "dimension"
|
||||
},
|
||||
"md": {
|
||||
"$value": "{dimension.sm} * {dimension.scale}",
|
||||
"$type": "dimension"
|
||||
},
|
||||
"lg": {
|
||||
"$value": "{dimension.md} * {dimension.scale}",
|
||||
"$type": "dimension"
|
||||
},
|
||||
"xl": {
|
||||
"$value": "{dimension.lg} * {dimension.scale}",
|
||||
"$type": "dimension"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"xs": {
|
||||
"$value": "{dimension.xs}",
|
||||
"$type": "spacing"
|
||||
},
|
||||
"sm": {
|
||||
"$value": "{dimension.sm}",
|
||||
"$type": "spacing"
|
||||
},
|
||||
"md": {
|
||||
"$value": "{dimension.md}",
|
||||
"$type": "spacing"
|
||||
},
|
||||
"lg": {
|
||||
"$value": "{dimension.lg}",
|
||||
"$type": "spacing"
|
||||
},
|
||||
"xl": {
|
||||
"$value": "{dimension.xl}",
|
||||
"$type": "spacing"
|
||||
},
|
||||
"multi-value": {
|
||||
"$value": "{dimension.sm} {dimension.xl}",
|
||||
"$type": "spacing",
|
||||
"$description": "You can have multiple values in a single spacing token"
|
||||
}
|
||||
},
|
||||
"borderRadius": {
|
||||
"sm": {
|
||||
"$value": "4",
|
||||
"$type": "borderRadius"
|
||||
},
|
||||
"lg": {
|
||||
"$value": "8",
|
||||
"$type": "borderRadius"
|
||||
},
|
||||
"xl": {
|
||||
"$value": "16",
|
||||
"$type": "borderRadius"
|
||||
},
|
||||
"multi-value": {
|
||||
"$value": "{borderRadius.sm} {borderRadius.lg}",
|
||||
"$type": "borderRadius",
|
||||
"$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": {
|
||||
"$value": "#000000",
|
||||
"$type": "color"
|
||||
},
|
||||
"white": {
|
||||
"$value": "#ffffff",
|
||||
"$type": "color"
|
||||
},
|
||||
"gray": {
|
||||
"100": {
|
||||
"$value": "#f7fafc",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#edf2f7",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#e2e8f0",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#cbd5e0",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#a0aec0",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#718096",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#4a5568",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#2d3748",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#1a202c",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"red": {
|
||||
"100": {
|
||||
"$value": "#fff5f5",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#fed7d7",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#feb2b2",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#fc8181",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#f56565",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#e53e3e",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#c53030",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#9b2c2c",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#742a2a",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"orange": {
|
||||
"100": {
|
||||
"$value": "#fffaf0",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#feebc8",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#fbd38d",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#f6ad55",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#ed8936",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#dd6b20",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#c05621",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#9c4221",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#7b341e",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"yellow": {
|
||||
"100": {
|
||||
"$value": "#fffff0",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#fefcbf",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#faf089",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#f6e05e",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#ecc94b",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#d69e2e",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#b7791f",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#975a16",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#744210",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"green": {
|
||||
"100": {
|
||||
"$value": "#f0fff4",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#c6f6d5",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#9ae6b4",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#68d391",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#48bb78",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#38a169",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#2f855a",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#276749",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#22543d",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"teal": {
|
||||
"100": {
|
||||
"$value": "#e6fffa",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#b2f5ea",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#81e6d9",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#4fd1c5",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#38b2ac",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#319795",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#2c7a7b",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#285e61",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#234e52",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"blue": {
|
||||
"100": {
|
||||
"$value": "#ebf8ff",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#bee3f8",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#90cdf4",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#63b3ed",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#4299e1",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#3182ce",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#2b6cb0",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#2c5282",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#2a4365",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"indigo": {
|
||||
"100": {
|
||||
"$value": "#ebf4ff",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#c3dafe",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#a3bffa",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#7f9cf5",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#667eea",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#5a67d8",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#4c51bf",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#434190",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#3c366b",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"purple": {
|
||||
"100": {
|
||||
"$value": "#faf5ff",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#e9d8fd",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#d6bcfa",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#b794f4",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#9f7aea",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#805ad5",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#6b46c1",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#553c9a",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#44337a",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"pink": {
|
||||
"100": {
|
||||
"$value": "#fff5f7",
|
||||
"$type": "color"
|
||||
},
|
||||
"200": {
|
||||
"$value": "#fed7e2",
|
||||
"$type": "color"
|
||||
},
|
||||
"300": {
|
||||
"$value": "#fbb6ce",
|
||||
"$type": "color"
|
||||
},
|
||||
"400": {
|
||||
"$value": "#f687b3",
|
||||
"$type": "color"
|
||||
},
|
||||
"500": {
|
||||
"$value": "#ed64a6",
|
||||
"$type": "color"
|
||||
},
|
||||
"600": {
|
||||
"$value": "#d53f8c",
|
||||
"$type": "color"
|
||||
},
|
||||
"700": {
|
||||
"$value": "#b83280",
|
||||
"$type": "color"
|
||||
},
|
||||
"800": {
|
||||
"$value": "#97266d",
|
||||
"$type": "color"
|
||||
},
|
||||
"900": {
|
||||
"$value": "#702459",
|
||||
"$type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"low": {
|
||||
"$value": "10%",
|
||||
"$type": "opacity"
|
||||
},
|
||||
"md": {
|
||||
"$value": "50%",
|
||||
"$type": "opacity"
|
||||
},
|
||||
"high": {
|
||||
"$value": "90%",
|
||||
"$type": "opacity"
|
||||
}
|
||||
},
|
||||
"fontFamilies": {
|
||||
"heading": {
|
||||
"$value": "Inter",
|
||||
"$type": "fontFamilies"
|
||||
},
|
||||
"body": {
|
||||
"$value": "Roboto",
|
||||
"$type": "fontFamilies"
|
||||
}
|
||||
},
|
||||
"lineHeights": {
|
||||
"heading": {
|
||||
"$value": "110%",
|
||||
"$type": "lineHeights"
|
||||
},
|
||||
"body": {
|
||||
"$value": "140%",
|
||||
"$type": "lineHeights"
|
||||
}
|
||||
},
|
||||
"letterSpacing": {
|
||||
"default": {
|
||||
"$value": "0",
|
||||
"$type": "letterSpacing"
|
||||
},
|
||||
"increased": {
|
||||
"$value": "150%",
|
||||
"$type": "letterSpacing"
|
||||
},
|
||||
"decreased": {
|
||||
"$value": "-5%",
|
||||
"$type": "letterSpacing"
|
||||
}
|
||||
},
|
||||
"paragraphSpacing": {
|
||||
"h1": {
|
||||
"$value": "32",
|
||||
"$type": "paragraphSpacing"
|
||||
},
|
||||
"h2": {
|
||||
"$value": "26",
|
||||
"$type": "paragraphSpacing"
|
||||
}
|
||||
},
|
||||
"fontWeights": {
|
||||
"headingRegular": {
|
||||
"$value": "Regular",
|
||||
"$type": "fontWeights"
|
||||
},
|
||||
"headingBold": {
|
||||
"$value": "Bold",
|
||||
"$type": "fontWeights"
|
||||
},
|
||||
"bodyRegular": {
|
||||
"$value": "Regular",
|
||||
"$type": "fontWeights"
|
||||
},
|
||||
"bodyBold": {
|
||||
"$value": "Bold",
|
||||
"$type": "fontWeights"
|
||||
}
|
||||
},
|
||||
"fontSizes": {
|
||||
"h1": {
|
||||
"$value": "{fontSizes.h2} * 1.25",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"h2": {
|
||||
"$value": "{fontSizes.h3} * 1.25",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"h3": {
|
||||
"$value": "{fontSizes.h4} * 1.25",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"h4": {
|
||||
"$value": "{fontSizes.h5} * 1.25",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"h5": {
|
||||
"$value": "{fontSizes.h6} * 1.25",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"h6": {
|
||||
"$value": "{fontSizes.body} * 1",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"body": {
|
||||
"$value": "16",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"sm": {
|
||||
"$value": "{fontSizes.body} * 0.85",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"xs": {
|
||||
"$value": "{fontSizes.body} * 0.65",
|
||||
"$type": "fontSizes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"fg": {
|
||||
"default": {
|
||||
"$value": "{colors.black}",
|
||||
"$type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"$value": "{colors.gray.700}",
|
||||
"$type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"$value": "{colors.gray.500}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"default": {
|
||||
"$value": "{colors.white}",
|
||||
"$type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"$value": "{colors.gray.100}",
|
||||
"$type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"$value": "{colors.gray.200}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"accent": {
|
||||
"default": {
|
||||
"$value": "{colors.indigo.400}",
|
||||
"$type": "color"
|
||||
},
|
||||
"onAccent": {
|
||||
"$value": "{colors.white}",
|
||||
"$type": "color"
|
||||
},
|
||||
"bg": {
|
||||
"$value": "{colors.indigo.200}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"shadows": {
|
||||
"default": {
|
||||
"$value": "{colors.gray.900}",
|
||||
"$type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"fg": {
|
||||
"default": {
|
||||
"$value": "{colors.white}",
|
||||
"$type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"$value": "{colors.gray.300}",
|
||||
"$type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"$value": "{colors.gray.500}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"default": {
|
||||
"$value": "{colors.gray.900}",
|
||||
"$type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"$value": "{colors.gray.700}",
|
||||
"$type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"$value": "{colors.gray.600}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"accent": {
|
||||
"default": {
|
||||
"$value": "{colors.indigo.600}",
|
||||
"$type": "color"
|
||||
},
|
||||
"onAccent": {
|
||||
"$value": "{colors.white}",
|
||||
"$type": "color"
|
||||
},
|
||||
"bg": {
|
||||
"$value": "{colors.indigo.800}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"shadows": {
|
||||
"default": {
|
||||
"$value": "rgba({colors.black}, 0.3)",
|
||||
"$type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"button": {
|
||||
"primary": {
|
||||
"background": {
|
||||
"$value": "{accent.default}",
|
||||
"$type": "color"
|
||||
},
|
||||
"text": {
|
||||
"$value": "{accent.onAccent}",
|
||||
"$type": "color"
|
||||
}
|
||||
},
|
||||
"borderRadius": {
|
||||
"$value": "{borderRadius.lg}",
|
||||
"$type": "borderRadius"
|
||||
},
|
||||
"borderWidth": {
|
||||
"$value": "{dimension.sm}",
|
||||
"$type": "borderWidth"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"borderRadius": {
|
||||
"$value": "{borderRadius.lg}",
|
||||
"$type": "borderRadius"
|
||||
},
|
||||
"background": {
|
||||
"$value": "{bg.default}",
|
||||
"$type": "color"
|
||||
},
|
||||
"padding": {
|
||||
"$value": "{dimension.md}",
|
||||
"$type": "dimension"
|
||||
}
|
||||
},
|
||||
"boxShadow": {
|
||||
"default": {
|
||||
"$value": [
|
||||
{
|
||||
"x": 5,
|
||||
"y": 5,
|
||||
"spread": 3,
|
||||
"color": "rgba({shadows.default}, 0.15)",
|
||||
"blur": 5,
|
||||
"$type": "dropShadow"
|
||||
},
|
||||
{
|
||||
"x": 4,
|
||||
"y": 4,
|
||||
"spread": 6,
|
||||
"color": "#00000033",
|
||||
"blur": 5,
|
||||
"$type": "innerShadow"
|
||||
}
|
||||
],
|
||||
"$type": "boxShadow"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"H1": {
|
||||
"Bold": {
|
||||
"$value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingBold}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h1}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h1}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"$type": "typography"
|
||||
},
|
||||
"Regular": {
|
||||
"$value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h1}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h1}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"$type": "typography"
|
||||
}
|
||||
},
|
||||
"H2": {
|
||||
"Bold": {
|
||||
"$value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingBold}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h2}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"$type": "typography"
|
||||
},
|
||||
"Regular": {
|
||||
"$value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h2}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"$type": "typography"
|
||||
}
|
||||
},
|
||||
"Body": {
|
||||
"$value": {
|
||||
"fontFamily": "{fontFamilies.body}",
|
||||
"fontWeight": "{fontWeights.bodyRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.body}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}"
|
||||
},
|
||||
"$type": "typography"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$themes": [],
|
||||
"$metadata": {
|
||||
"tokenSetOrder": ["core", "light", "dark", "theme"]
|
||||
}
|
||||
}
|
1142
common/test/common_tests/types/tokens_lib_test.cljc
Normal file
1142
common/test/common_tests/types/tokens_lib_test.cljc
Normal file
File diff suppressed because it is too large
Load diff
|
@ -32,9 +32,9 @@
|
|||
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test-esm",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "yarn run build:test && node target/tests/test.js",
|
||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test-esm\" \"nodemon -w target/tests/test.js --exec 'sleep 2 && node target/tests/test.js'\"",
|
||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -w target/tests/test.js --exec 'sleep 2 && node target/tests/test.js'\"",
|
||||
"test:e2e": "playwright test --project default",
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
|
@ -54,6 +54,7 @@
|
|||
"@storybook/react-vite": "^8.3.6",
|
||||
"@types/node": "^22.7.7",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bun": "^1.1.25",
|
||||
"concurrently": "^9.0.1",
|
||||
"esbuild": "^0.24.0",
|
||||
"express": "^4.21.1",
|
||||
|
@ -100,12 +101,14 @@
|
|||
"@penpot/mousetrap": "file:./vendor/mousetrap",
|
||||
"@penpot/svgo": "penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b",
|
||||
"@penpot/text-editor": "penpot/penpot-text-editor#449e3322f3fa40b1318c9154afbbc7932a3cb766",
|
||||
"@tokens-studio/sd-transforms": "^0.16.1",
|
||||
"compression": "^1.7.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"js-beautify": "^1.15.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"luxon": "^3.5.0",
|
||||
"opentype.js": "^1.3.4",
|
||||
"postcss-modules": "^6.0.0",
|
||||
|
@ -117,7 +120,9 @@
|
|||
"rxjs": "8.0.0-alpha.14",
|
||||
"sax": "^1.4.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"style-dictionary": "^4.1.4",
|
||||
"tdigest": "^0.1.2",
|
||||
"tinycolor2": "npm:^1.6.0",
|
||||
"ua-parser-js": "2.0.0-rc.1",
|
||||
"xregexp": "^5.1.1"
|
||||
}
|
||||
|
|
3
frontend/resources/images/icons/arrow-down.svg
Normal file
3
frontend/resources/images/icons/arrow-down.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m4 6 4 4 4-4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 144 B |
3
frontend/resources/images/icons/arrow-left.svg
Normal file
3
frontend/resources/images/icons/arrow-left.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 12 6 8l4-4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 145 B |
3
frontend/resources/images/icons/arrow-right.svg
Normal file
3
frontend/resources/images/icons/arrow-right.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 12 4-4-4-4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 144 B |
3
frontend/resources/images/icons/arrow-up.svg
Normal file
3
frontend/resources/images/icons/arrow-up.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m4 10 4-4 4 4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 140 B |
|
@ -8,8 +8,7 @@
|
|||
{:target :browser
|
||||
:output-dir "resources/public/js/"
|
||||
:asset-path "/js"
|
||||
:devtools {:browser-inject :main
|
||||
:watch-dir "resources/public"
|
||||
:devtools {:watch-dir "resources/public"
|
||||
:reload-strategy :full}
|
||||
:build-options {:manifest-name "manifest.json"}
|
||||
:module-loader true
|
||||
|
@ -136,38 +135,37 @@
|
|||
:output-wrapper false
|
||||
:warnings {:fn-deprecated false}}}
|
||||
|
||||
:test-esm
|
||||
{:target :esm
|
||||
:output-dir "target/tests"
|
||||
:runtime :custom
|
||||
:test
|
||||
{:target :esm
|
||||
:output-dir "target/tests"
|
||||
:runtime :custom
|
||||
:js-options {:js-provider :import}
|
||||
|
||||
:modules
|
||||
{:test {:init-fn frontend-tests.runner/init}}}
|
||||
|
||||
;; :compiler-options
|
||||
;; {:output-feature-set :es2020
|
||||
;; :warnings {:fn-deprecated false}}}
|
||||
:modules
|
||||
{:test {:init-fn frontend-tests.runner/init
|
||||
:prepend-js "globalThis.navigator = {userAgent: \"\"}"}}}
|
||||
|
||||
:lib-penpot
|
||||
{:target :esm
|
||||
:output-dir "resources/public/libs"
|
||||
{:target :esm
|
||||
:output-dir "resources/public/libs"
|
||||
|
||||
:modules
|
||||
{:penpot {:exports {:renderPage app.libs.render/render-page-export
|
||||
:createFile app.libs.file-builder/create-file-export}}}
|
||||
:modules
|
||||
{:penpot {:exports {:renderPage app.libs.render/render-page-export
|
||||
:createFile app.libs.file-builder/create-file-export}}}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
:output-wrapper false
|
||||
:warnings {:fn-deprecated false}}
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
:output-wrapper false
|
||||
:warnings {:fn-deprecated false}}
|
||||
|
||||
:release
|
||||
{:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:source-map true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off
|
||||
:source-map-detail-level :all}}}
|
||||
:release
|
||||
{:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:source-map true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off
|
||||
:source-map-detail-level :all}}}
|
||||
|
||||
:bench
|
||||
{:target :node-script
|
||||
|
@ -184,24 +182,5 @@
|
|||
{:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off}}}
|
||||
:anon-fn-naming-policy :off}}}}}
|
||||
|
||||
:test
|
||||
{:target :node-test
|
||||
:output-to "target/tests.cjs"
|
||||
:output-dir "target/test/"
|
||||
:ns-regexp "^frontend-tests.*-test$"
|
||||
:autorun true
|
||||
|
||||
:js-options
|
||||
{:entry-keys ["module" "browser" "main"]}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
:output-wrapper false
|
||||
:source-map true
|
||||
:source-map-include-sources-content true
|
||||
:source-map-detail-level :all
|
||||
:warnings {:fn-deprecated false}}}
|
||||
|
||||
}}
|
||||
|
|
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})
|
||||
|
|
|
@ -110,7 +110,9 @@
|
|||
(when *assert*
|
||||
(->> (rx/from cfeat/no-migration-features)
|
||||
;; text editor v2 isn't enabled by default even in devenv
|
||||
(rx/filter #(not (or (contains? cfeat/backend-only-features %) (= "text-editor/v2" %))))
|
||||
(rx/filter #(not (or (contains? cfeat/backend-only-features %)
|
||||
(= "text-editor/v2" %)
|
||||
(= "design-tokens/v1" %))))
|
||||
(rx/observe-on :async)
|
||||
(rx/map enable-feature))))
|
||||
|
||||
|
|
|
@ -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,17 +101,31 @@
|
|||
assets-tab
|
||||
(mf/html [:& assets-toolbox {:size (- size 58)}])
|
||||
|
||||
tokens-tab
|
||||
(when design-tokens?
|
||||
(mf/html [:& tokens-sidebar-tab]))
|
||||
|
||||
tabs
|
||||
(if ^boolean mode-inspect?
|
||||
#js [#js {:label (tr "workspace.sidebar.layers")
|
||||
:id "layers"
|
||||
:content layers-tab}]
|
||||
#js [#js {:label (tr "workspace.sidebar.layers")
|
||||
:id "layers"
|
||||
:content layers-tab}
|
||||
#js {:label (tr "workspace.toolbar.assets")
|
||||
:id "assets"
|
||||
:content assets-tab}])]
|
||||
(if ^boolean design-tokens?
|
||||
#js [#js {:label (tr "workspace.sidebar.layers")
|
||||
:id "layers"
|
||||
:content layers-tab}
|
||||
#js {:label (tr "workspace.toolbar.assets")
|
||||
:id "assets"
|
||||
:content assets-tab}
|
||||
#js {:label "Tokens"
|
||||
:id "tokens"
|
||||
:content tokens-tab}]
|
||||
#js [#js {:label (tr "workspace.sidebar.layers")
|
||||
:id "layers"
|
||||
:content layers-tab}
|
||||
#js {:label (tr "workspace.toolbar.assets")
|
||||
:id "assets"
|
||||
:content assets-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]
|
||||
|
@ -22,8 +24,13 @@
|
|||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.context :as muc]
|
||||
[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]]
|
||||
|
@ -84,6 +91,8 @@
|
|||
(reduce #(union %1 %2) (map #(get type->options %) all-types))
|
||||
(get type->options type))
|
||||
|
||||
design-tokens? (mf/use-ctx muc/design-tokens)
|
||||
|
||||
ids-with-children (or ids-with-children ids)
|
||||
|
||||
old-shapes (if (= type :multiple)
|
||||
|
@ -96,6 +105,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 +246,21 @@
|
|||
(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)]
|
||||
(if-not design-tokens?
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(udw/update-dimensions ids attr (or token-value value)))
|
||||
(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 +306,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 +331,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 +460,50 @@
|
|||
: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)}]]
|
||||
(if-not design-tokens?
|
||||
[:> 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)}]]
|
||||
(if-not design-tokens?
|
||||
[:> 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 +560,24 @@
|
|||
[:div {:class (stl/css :radius-1)
|
||||
:title (tr "workspace.options.radius")}
|
||||
[:span {:class (stl/css :icon)} i/corner-radius]
|
||||
[:> numeric-input*
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:ref radius-input-ref
|
||||
:min 0
|
||||
:on-change on-radius-1-change
|
||||
:class (stl/css :numeric-input)
|
||||
:value (:rx values)}]]
|
||||
(if-not design-tokens?
|
||||
[:> numeric-input*
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:ref radius-input-ref
|
||||
:min 0
|
||||
:on-change on-radius-1-change
|
||||
:class (stl/css :numeric-input)
|
||||
:value (:rx values)}]
|
||||
[:& editable-select
|
||||
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
|
||||
:class (stl/css :token-select)
|
||||
:on-change on-radius-1-change
|
||||
: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})))
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
[frontend-tests.logic.frame-guides-test]
|
||||
[frontend-tests.logic.groups-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
[frontend-tests.tokens.logic.token-actions-test]
|
||||
[frontend-tests.tokens.style-dictionary-test]
|
||||
[frontend-tests.tokens.token-form-test]
|
||||
[frontend-tests.tokens.token-test]
|
||||
[frontend-tests.util-range-tree-test]
|
||||
[frontend-tests.util-simple-math-test]
|
||||
[frontend-tests.util-snap-data-test]))
|
||||
|
@ -19,16 +23,20 @@
|
|||
(.exit js/process 0)
|
||||
(.exit js/process 1)))
|
||||
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(t/run-tests 'frontend-tests.helpers-shapes-test
|
||||
'frontend-tests.logic.comp-remove-swap-slots-test
|
||||
'frontend-tests.logic.copying-and-duplicating-test
|
||||
'frontend-tests.logic.frame-guides-test
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.util-range-tree-test
|
||||
'frontend-tests.util-snap-data-test
|
||||
'frontend-tests.util-simple-math-test
|
||||
'frontend-tests.basic-shapes-test))
|
||||
(t/run-tests
|
||||
'frontend-tests.helpers-shapes-test
|
||||
'frontend-tests.logic.comp-remove-swap-slots-test
|
||||
'frontend-tests.logic.copying-and-duplicating-test
|
||||
'frontend-tests.logic.frame-guides-test
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.util-range-tree-test
|
||||
'frontend-tests.util-snap-data-test
|
||||
'frontend-tests.util-simple-math-test
|
||||
'frontend-tests.basic-shapes-test
|
||||
;; 'frontend-tests.tokens.logic.token-actions-test
|
||||
;; 'frontend-tests.tokens.style-dictionary-test
|
||||
'frontend-tests.tokens.token-test
|
||||
'frontend-tests.tokens.token-form-test))
|
||||
|
|
74
frontend/test/frontend_tests/tokens/helpers/state.cljs
Normal file
74
frontend/test/frontend_tests/tokens/helpers/state.cljs
Normal file
|
@ -0,0 +1,74 @@
|
|||
(ns frontend-tests.tokens.helpers.state
|
||||
(:require
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn end
|
||||
"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."
|
||||
[]
|
||||
(ptk/reify ::end
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/empty))))
|
||||
|
||||
(defn end+
|
||||
[]
|
||||
(ptk/reify ::end+
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(->> (rx/from (-> (get-in state [:workspace-data :tokens-lib])
|
||||
(ctob/get-active-themes-set-tokens)
|
||||
(sd/resolve-tokens+)))
|
||||
(rx/mapcat #(rx/of (end)))))))
|
||||
|
||||
(defn stop-on
|
||||
"Helper function to be used with async version of run-store.
|
||||
|
||||
Will stop the execution after event with `event-type` has completed."
|
||||
[event-type]
|
||||
(fn [stream]
|
||||
(->> stream
|
||||
#_(rx/tap #(prn (ptk/type %)))
|
||||
(rx/filter #(ptk/type? event-type %)))))
|
||||
|
||||
(def stop-on-send-update-indices
|
||||
"Stops on `send-update-indices` function being called, which should be the last function of an event chain."
|
||||
(stop-on ::end))
|
||||
|
||||
;; Support for async events in tests
|
||||
;; https://chat.kaleidos.net/penpot-partners/pl/tz1yoes3w3fr9qanxqpuhoz3ch
|
||||
(defn run-store
|
||||
"Async version of `frontend-tests.helpers.state/run-store`."
|
||||
([store done events completed-cb]
|
||||
(run-store store done events completed-cb nil))
|
||||
([store done events completed-cb stopper]
|
||||
(let [stream (ptk/input-stream store)
|
||||
stopper-s (if (fn? stopper)
|
||||
(stopper stream)
|
||||
(rx/filter #(= :the/end %) stream))]
|
||||
(->> stream
|
||||
(rx/take-until stopper-s)
|
||||
(rx/last)
|
||||
(rx/tap (fn [_]
|
||||
(completed-cb @store)))
|
||||
(rx/subs! (fn [_] (done))
|
||||
(fn [cause]
|
||||
(js/console.log "[error]:" cause))
|
||||
(fn [_]
|
||||
#_(js/console.log "[complete]"))))
|
||||
(doseq [event (concat events [(end+)])]
|
||||
(ptk/emit! store event))
|
||||
(ptk/emit! store :the/end))))
|
||||
|
||||
(defn run-store-async
|
||||
"Helper version of `run-store` that automatically stops on the `send-update-indices` event"
|
||||
([store done events completed-cb]
|
||||
(run-store store done events completed-cb stop-on-send-update-indices))
|
||||
([store done events completed-cb stop-on]
|
||||
(run-store store done events completed-cb stop-on)))
|
26
frontend/test/frontend_tests/tokens/helpers/tokens.cljs
Normal file
26
frontend/test/frontend_tests/tokens/helpers/tokens.cljs
Normal file
|
@ -0,0 +1,26 @@
|
|||
(ns frontend-tests.tokens.helpers.tokens
|
||||
(:require
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]))
|
||||
|
||||
(defn add-token [state label params]
|
||||
(let [id (thi/new-id! label)
|
||||
token (assoc params :id id)]
|
||||
(update-in state [:data :tokens] assoc id token)))
|
||||
|
||||
(defn get-token [file name]
|
||||
(some-> (get-in file [:data :tokens-lib])
|
||||
(ctob/get-active-themes-set-tokens)
|
||||
(get name)))
|
||||
|
||||
(defn apply-token-to-shape [file shape-label token-label attributes]
|
||||
(let [first-page-id (get-in file [:data :pages 0])
|
||||
shape-id (thi/id shape-label)
|
||||
token (get-token file token-label)
|
||||
applied-attributes (wtt/attributes-map attributes token)]
|
||||
(update-in file [:data
|
||||
:pages-index first-page-id
|
||||
:objects shape-id
|
||||
:applied-tokens]
|
||||
merge applied-attributes)))
|
|
@ -0,0 +1,407 @@
|
|||
(ns frontend-tests.tokens.logic.token-actions-test
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.common.test-helpers.compositions :as ctho]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.ui.workspace.tokens.changes :as wtch]
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.tokens.helpers.state :as tohs]
|
||||
[frontend-tests.tokens.helpers.tokens :as toht]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before (fn []
|
||||
;; Ignore rxjs async errors
|
||||
(log/set-level! "app.main.data.changes" :error)
|
||||
(thp/reset-idmap!))})
|
||||
|
||||
(defn setup-file []
|
||||
(cthf/sample-file :file-1 :page-label :page-1))
|
||||
|
||||
(def border-radius-token
|
||||
{:name "borderRadius.sm"
|
||||
:value "12"
|
||||
:type :border-radius})
|
||||
|
||||
(def reference-border-radius-token
|
||||
{:name "borderRadius.md"
|
||||
:value "{borderRadius.sm} * 2"
|
||||
:type :border-radius})
|
||||
|
||||
(defn setup-file-with-tokens
|
||||
[& {:keys [rect-1 rect-2 rect-3]}]
|
||||
(-> (setup-file)
|
||||
(ctho/add-rect :rect-1 rect-1)
|
||||
(ctho/add-rect :rect-2 rect-2)
|
||||
(ctho/add-rect :rect-3 rect-3)
|
||||
(assoc-in [:data :tokens-lib]
|
||||
(-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"}))
|
||||
(ctob/set-active-themes #{"/Theme A"})
|
||||
(ctob/add-set (ctob/make-token-set :name "Set A"))
|
||||
(ctob/add-token-in-set "Set A" (ctob/make-token border-radius-token))
|
||||
(ctob/add-token-in-set "Set A" (ctob/make-token reference-border-radius-token))))))
|
||||
|
||||
(t/deftest test-apply-token
|
||||
(t/testing "applies token to shape and updates shape attributes to resolved value"
|
||||
(t/async
|
||||
done
|
||||
(let [file (setup-file-with-tokens)
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:rx :ry}
|
||||
:token (toht/get-token file "borderRadius.md")
|
||||
:on-update-shape wtch/update-shape-radius-all})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token (toht/get-token file' "borderRadius.md")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/testing "shape `:applied-tokens` got updated"
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:rx (:applied-tokens rect-1')) (:name token)))
|
||||
(t/is (= (:ry (:applied-tokens rect-1')) (:name token))))
|
||||
(t/testing "shape radius got update to the resolved token value."
|
||||
(t/is (= (:rx rect-1') 24))
|
||||
(t/is (= (:ry rect-1') 24))))))))))
|
||||
|
||||
(t/deftest test-apply-multiple-tokens
|
||||
(t/testing "applying a token twice with the same attributes will override the previously applied tokens values"
|
||||
(t/async
|
||||
done
|
||||
(let [file (setup-file-with-tokens)
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:rx :ry}
|
||||
:token (toht/get-token file "borderRadius.sm")
|
||||
:on-update-shape wtch/update-shape-radius-all})
|
||||
(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:rx :ry}
|
||||
:token (toht/get-token file "borderRadius.md")
|
||||
:on-update-shape wtch/update-shape-radius-all})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token (toht/get-token file' "borderRadius.md")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/testing "shape `:applied-tokens` got updated"
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:rx (:applied-tokens rect-1')) (:name token)))
|
||||
(t/is (= (:ry (:applied-tokens rect-1')) (:name token))))
|
||||
(t/testing "shape radius got update to the resolved token value."
|
||||
(t/is (= (:rx rect-1') 24))
|
||||
(t/is (= (:ry rect-1') 24))))))))))
|
||||
|
||||
(t/deftest test-apply-token-overwrite
|
||||
(t/testing "removes old token attributes and applies only single attribute"
|
||||
(t/async
|
||||
done
|
||||
(let [file (setup-file-with-tokens)
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [;; Apply "borderRadius.sm" to all border radius attributes
|
||||
(wtch/apply-token {:attributes #{:rx :ry :r1 :r2 :r3 :r4}
|
||||
:token (toht/get-token file "borderRadius.sm")
|
||||
:shape-ids [(:id rect-1)]
|
||||
:on-update-shape wtch/update-shape-radius-all})
|
||||
;; Apply single `:r1` attribute to same shape
|
||||
;; while removing other attributes from the border-radius set
|
||||
;; but keep `:r4` for testing purposes
|
||||
(wtch/apply-token {:attributes #{:r1}
|
||||
:attributes-to-remove #{:rx :ry :r1 :r2 :r3}
|
||||
:token (toht/get-token file "borderRadius.md")
|
||||
:shape-ids [(:id rect-1)]
|
||||
:on-update-shape wtch/update-shape-radius-all})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-sm (toht/get-token file' "borderRadius.sm")
|
||||
token-md (toht/get-token file' "borderRadius.md")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/testing "other border-radius attributes got removed"
|
||||
(t/is (nil? (:rx (:applied-tokens rect-1')))))
|
||||
(t/testing "r1 got applied with borderRadius.md"
|
||||
(t/is (= (:r1 (:applied-tokens rect-1')) (:name token-md))))
|
||||
(t/testing "while :r4 was kept with borderRadius.sm"
|
||||
(t/is (= (:r4 (:applied-tokens rect-1')) (:name token-sm)))))))))))
|
||||
|
||||
(t/deftest test-apply-dimensions
|
||||
(t/testing "applies dimensions token and updates the shapes width and height"
|
||||
(t/async
|
||||
done
|
||||
(let [dimensions-token {:name "dimensions.sm"
|
||||
:value "100"
|
||||
:type :dimensions}
|
||||
file (-> (setup-file-with-tokens)
|
||||
(update-in [:data :tokens-lib]
|
||||
#(ctob/add-token-in-set % "Set A" (ctob/make-token dimensions-token))))
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:width :height}
|
||||
:token (toht/get-token file "dimensions.sm")
|
||||
:on-update-shape wtch/update-shape-dimensions})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-target' (toht/get-token file' "dimensions.sm")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/testing "shape `:applied-tokens` got updated"
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:width (:applied-tokens rect-1')) (:name token-target')))
|
||||
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
|
||||
(t/testing "shapes width and height got updated"
|
||||
(t/is (= (:width rect-1') 100))
|
||||
(t/is (= (:height rect-1') 100))))))))))
|
||||
|
||||
(t/deftest test-apply-sizing
|
||||
(t/testing "applies sizing token and updates the shapes width and height"
|
||||
(t/async
|
||||
done
|
||||
(let [sizing-token {:name "sizing.sm"
|
||||
:value "100"
|
||||
:type :sizing}
|
||||
file (-> (setup-file-with-tokens)
|
||||
(update-in [:data :tokens-lib]
|
||||
#(ctob/add-token-in-set % "Set A" (ctob/make-token sizing-token))))
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:width :height}
|
||||
:token (toht/get-token file "sizing.sm")
|
||||
:on-update-shape wtch/update-shape-dimensions})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-target' (toht/get-token file' "sizing.sm")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/testing "shape `:applied-tokens` got updated"
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:width (:applied-tokens rect-1')) (:name token-target')))
|
||||
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
|
||||
(t/testing "shapes width and height got updated"
|
||||
(t/is (= (:width rect-1') 100))
|
||||
(t/is (= (:height rect-1') 100))))))))))
|
||||
|
||||
(t/deftest test-apply-opacity
|
||||
(t/testing "applies opacity token and updates the shapes opacity"
|
||||
(t/async
|
||||
done
|
||||
(let [opacity-float {:name "opacity.float"
|
||||
:value "0.3"
|
||||
:type :opacity}
|
||||
opacity-percent {:name "opacity.percent"
|
||||
:value "40%"
|
||||
:type :opacity}
|
||||
opacity-invalid {:name "opacity.invalid"
|
||||
:value "100"
|
||||
:type :opacity}
|
||||
file (-> (setup-file-with-tokens)
|
||||
(update-in [:data :tokens-lib]
|
||||
#(-> %
|
||||
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-float))
|
||||
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-percent))
|
||||
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-invalid)))))
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
rect-2 (cths/get-shape file :rect-2)
|
||||
rect-3 (cths/get-shape file :rect-3)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:opacity}
|
||||
:token (toht/get-token file "opacity.float")
|
||||
:on-update-shape wtch/update-opacity})
|
||||
(wtch/apply-token {:shape-ids [(:id rect-2)]
|
||||
:attributes #{:opacity}
|
||||
:token (toht/get-token file "opacity.percent")
|
||||
:on-update-shape wtch/update-opacity})
|
||||
(wtch/apply-token {:shape-ids [(:id rect-3)]
|
||||
:attributes #{:opacity}
|
||||
:token (toht/get-token file "opacity.invalid")
|
||||
:on-update-shape wtch/update-opacity})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
rect-1' (cths/get-shape file' :rect-1)
|
||||
rect-2' (cths/get-shape file' :rect-2)
|
||||
rect-3' (cths/get-shape file' :rect-3)
|
||||
token-opacity-float (toht/get-token file' "opacity.float")
|
||||
token-opacity-percent (toht/get-token file' "opacity.percent")
|
||||
token-opacity-invalid (toht/get-token file' "opacity.invalid")]
|
||||
(t/testing "float value got translated to float and applied to opacity"
|
||||
(t/is (= (:opacity (:applied-tokens rect-1')) (:name token-opacity-float)))
|
||||
(t/is (= (:opacity rect-1') 0.3)))
|
||||
(t/testing "percentage value got translated to float and applied to opacity"
|
||||
(t/is (= (:opacity (:applied-tokens rect-2')) (:name token-opacity-percent)))
|
||||
(t/is (= (:opacity rect-2') 0.4)))
|
||||
(t/testing "invalid opacity value got applied but did not change shape"
|
||||
(t/is (= (:opacity (:applied-tokens rect-3')) (:name token-opacity-invalid)))
|
||||
(t/is (nil? (:opacity rect-3')))))))))))
|
||||
|
||||
(t/deftest test-apply-rotation
|
||||
(t/testing "applies rotation token and updates the shapes rotation"
|
||||
(t/async
|
||||
done
|
||||
(let [rotation-token {:name "rotation.medium"
|
||||
:value "120"
|
||||
:type :rotation}
|
||||
file (-> (setup-file-with-tokens)
|
||||
(update-in [:data :tokens-lib]
|
||||
#(ctob/add-token-in-set % "Set A" (ctob/make-token rotation-token))))
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:rotation}
|
||||
:token (toht/get-token file "rotation.medium")
|
||||
:on-update-shape wtch/update-rotation})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-target' (toht/get-token file' "rotation.medium")
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:rotation (:applied-tokens rect-1')) (:name token-target')))
|
||||
(t/is (= (:rotation rect-1') 120)))))))))
|
||||
|
||||
(t/deftest test-apply-stroke-width
|
||||
(t/testing "applies stroke-width token and updates the shapes with stroke"
|
||||
(t/async
|
||||
done
|
||||
(let [stroke-width-token {:name "stroke-width.sm"
|
||||
:value "10"
|
||||
:type :stroke-width}
|
||||
file (-> (setup-file-with-tokens {:rect-1 {:strokes [{:stroke-alignment :inner,
|
||||
:stroke-style :solid,
|
||||
:stroke-color "#000000",
|
||||
:stroke-opacity 1,
|
||||
:stroke-width 5}]}})
|
||||
(update-in [:data :tokens-lib]
|
||||
#(ctob/add-token-in-set % "Set A" (ctob/make-token stroke-width-token))))
|
||||
store (ths/setup-store file)
|
||||
rect-with-stroke (cths/get-shape file :rect-1)
|
||||
rect-without-stroke (cths/get-shape file :rect-2)
|
||||
events [(wtch/apply-token {:shape-ids [(:id rect-with-stroke) (:id rect-without-stroke)]
|
||||
:attributes #{:stroke-width}
|
||||
:token (toht/get-token file "stroke-width.sm")
|
||||
:on-update-shape wtch/update-stroke-width})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-target' (toht/get-token file' "stroke-width.sm")
|
||||
rect-with-stroke' (cths/get-shape file' :rect-1)
|
||||
rect-without-stroke' (cths/get-shape file' :rect-2)]
|
||||
(t/testing "token got applied to rect with stroke and shape stroke got updated"
|
||||
(t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (:name token-target')))
|
||||
(t/is (= (get-in rect-with-stroke' [:strokes 0 :stroke-width]) 10)))
|
||||
(t/testing "token got applied to rect without stroke but shape didnt get updated"
|
||||
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
|
||||
(t/is (empty? (:strokes rect-without-stroke')))))))))))
|
||||
|
||||
(t/deftest test-toggle-token-none
|
||||
(t/testing "should apply token to all selected items, where no item has the token applied"
|
||||
(t/async
|
||||
done
|
||||
(let [file (setup-file-with-tokens)
|
||||
store (ths/setup-store file)
|
||||
rect-1 (cths/get-shape file :rect-1)
|
||||
rect-2 (cths/get-shape file :rect-2)
|
||||
events [(wtch/toggle-token {:shapes [rect-1 rect-2]
|
||||
:token-type-props {:attributes #{:rx :ry}
|
||||
:on-update-shape wtch/update-shape-radius-all}
|
||||
:token (toht/get-token file "borderRadius.md")})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
token-2' (toht/get-token file' "borderRadius.md")
|
||||
rect-1' (cths/get-shape file' :rect-1)
|
||||
rect-2' (cths/get-shape file' :rect-2)]
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (some? (:applied-tokens rect-2')))
|
||||
(t/is (= (:rx (:applied-tokens rect-1')) (:name token-2')))
|
||||
(t/is (= (:rx (:applied-tokens rect-2')) (:name token-2')))
|
||||
(t/is (= (:ry (:applied-tokens rect-1')) (:name token-2')))
|
||||
(t/is (= (:ry (:applied-tokens rect-2')) (:name token-2')))
|
||||
(t/is (= (:rx rect-1') 24))
|
||||
(t/is (= (:rx rect-2') 24)))))))))
|
||||
|
||||
(t/deftest test-toggle-token-mixed
|
||||
(t/testing "should unapply given token if one of the selected items has the token applied while keeping other tokens with some attributes"
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (setup-file-with-tokens)
|
||||
(toht/apply-token-to-shape :rect-1 "borderRadius.sm" #{:rx :ry})
|
||||
(toht/apply-token-to-shape :rect-3 "borderRadius.md" #{:rx :ry}))
|
||||
store (ths/setup-store file)
|
||||
|
||||
rect-with-token (cths/get-shape file :rect-1)
|
||||
rect-without-token (cths/get-shape file :rect-2)
|
||||
rect-with-other-token (cths/get-shape file :rect-3)
|
||||
|
||||
events [(wtch/toggle-token {:shapes [rect-with-token rect-without-token rect-with-other-token]
|
||||
:token (toht/get-token file "borderRadius.sm")
|
||||
:token-type-props {:attributes #{:rx :ry}}})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
rect-with-token' (cths/get-shape file' :rect-1)
|
||||
rect-without-token' (cths/get-shape file' :rect-2)
|
||||
rect-with-other-token' (cths/get-shape file' :rect-3)]
|
||||
|
||||
(t/testing "rect-with-token got the token removed"
|
||||
(t/is (nil? (:rx (:applied-tokens rect-with-token'))))
|
||||
(t/is (nil? (:ry (:applied-tokens rect-with-token')))))
|
||||
|
||||
(t/testing "rect-without-token didn't get updated"
|
||||
(t/is (= (:applied-tokens rect-without-token') (:applied-tokens rect-without-token))))
|
||||
|
||||
(t/testing "rect-with-other-token didn't get updated"
|
||||
(t/is (= (:applied-tokens rect-with-other-token') (:applied-tokens rect-with-other-token)))))))))))
|
||||
|
||||
(t/deftest test-toggle-token-apply-to-all
|
||||
(t/testing "should apply token to all if none of the shapes has it applied"
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (setup-file-with-tokens)
|
||||
(toht/apply-token-to-shape :rect-1 "borderRadius.md" #{:rx :ry})
|
||||
(toht/apply-token-to-shape :rect-3 "borderRadius.md" #{:rx :ry}))
|
||||
store (ths/setup-store file)
|
||||
|
||||
rect-with-other-token-1 (cths/get-shape file :rect-1)
|
||||
rect-without-token (cths/get-shape file :rect-2)
|
||||
rect-with-other-token-2 (cths/get-shape file :rect-3)
|
||||
|
||||
events [(wtch/toggle-token {:shapes [rect-with-other-token-1 rect-without-token rect-with-other-token-2]
|
||||
:token (toht/get-token file "borderRadius.sm")
|
||||
:token-type-props {:attributes #{:rx :ry}}})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [file' (ths/get-file-from-store new-state)
|
||||
target-token (toht/get-token file' "borderRadius.sm")
|
||||
rect-with-other-token-1' (cths/get-shape file' :rect-1)
|
||||
rect-without-token' (cths/get-shape file' :rect-2)
|
||||
rect-with-other-token-2' (cths/get-shape file' :rect-3)]
|
||||
|
||||
(t/testing "token got applied to all shapes"
|
||||
(t/is (= (:rx (:applied-tokens rect-with-other-token-1')) (:name target-token)))
|
||||
(t/is (= (:rx (:applied-tokens rect-without-token')) (:name target-token)))
|
||||
(t/is (= (:rx (:applied-tokens rect-with-other-token-2')) (:name target-token)))
|
||||
|
||||
(t/is (= (:ry (:applied-tokens rect-with-other-token-1')) (:name target-token)))
|
||||
(t/is (= (:ry (:applied-tokens rect-without-token')) (:name target-token)))
|
||||
(t/is (= (:ry (:applied-tokens rect-with-other-token-2')) (:name target-token)))))))))))
|
115
frontend/test/frontend_tests/tokens/style_dictionary_test.cljs
Normal file
115
frontend/test/frontend_tests/tokens/style_dictionary_test.cljs
Normal file
|
@ -0,0 +1,115 @@
|
|||
(ns frontend-tests.tokens.style-dictionary-test
|
||||
(:require
|
||||
[app.common.transit :as tr]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(t/deftest resolve-tokens-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "resolves tokens using style-dictionary from a ids map"
|
||||
(let [tokens (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "core"))
|
||||
(ctob/add-token-in-set "core" (ctob/make-token {:name "borderRadius.sm"
|
||||
:value "12px"
|
||||
:type :border-radius}))
|
||||
(ctob/add-token-in-set "core" (ctob/make-token {:value "{borderRadius.sm} * 2"
|
||||
:name "borderRadius.md-with-dashes"
|
||||
:type :border-radius}))
|
||||
(ctob/get-all-tokens))]
|
||||
(-> (sd/resolve-tokens+ tokens)
|
||||
(p/finally
|
||||
(fn [resolved-tokens]
|
||||
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
|
||||
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
|
||||
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
|
||||
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
|
||||
(done))))))))
|
||||
|
||||
(t/deftest process-json-stream-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "processes empty json string"
|
||||
(let [json (-> {"core" {"color" {"$value" "red"
|
||||
"$type" "color"}}}
|
||||
(tr/encode-str {:type :json-verbose}))]
|
||||
(->> (rx/of json)
|
||||
(sd/process-json-stream)
|
||||
(rx/subs! (fn [tokens-lib]
|
||||
(t/is (instance? ctob/TokensLib tokens-lib))
|
||||
(t/is (= "red" (-> (ctob/get-set tokens-lib "core")
|
||||
(ctob/get-token "color")
|
||||
(:value))))
|
||||
(done))))))))
|
||||
|
||||
(t/deftest reference-errros-test
|
||||
(t/testing "Extracts reference errors from StyleDictionary errors"
|
||||
;; Using unicode for the white-space after "Error: " as some editors might remove it and its more visible
|
||||
(t/is (=
|
||||
["Some token references (2) could not be found."
|
||||
""
|
||||
"foo.value tries to reference missing, which is not defined."
|
||||
"color.value tries to reference missing, which is not defined."]
|
||||
(sd/reference-errors "Error:\u0020
|
||||
Reference Errors:
|
||||
Some token references (2) could not be found.
|
||||
|
||||
foo.value tries to reference missing, which is not defined.
|
||||
color.value tries to reference missing, which is not defined.")))
|
||||
(t/is (nil? (sd/reference-errors nil)))
|
||||
(t/is (nil? (sd/reference-errors "none")))))
|
||||
|
||||
(t/deftest process-empty-json-stream-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "processes empty json string"
|
||||
(->> (rx/of "{}")
|
||||
(sd/process-json-stream)
|
||||
(rx/subs! (fn [tokens-lib]
|
||||
(t/is (instance? ctob/TokensLib tokens-lib))
|
||||
(done)))))))
|
||||
|
||||
(t/deftest process-invalid-json-stream-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "fails on invalid json"
|
||||
(->> (rx/of "{,}")
|
||||
(sd/process-json-stream)
|
||||
(rx/subs!
|
||||
(fn []
|
||||
(throw (js/Error. "Should be an error")))
|
||||
(fn [err]
|
||||
(t/is (= :error.import/json-parse-error (:error/code (ex-data err))))
|
||||
(done)))))))
|
||||
|
||||
(t/deftest process-non-token-json-stream-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "fails on non-token json"
|
||||
(->> (rx/of "{\"foo\": \"bar\"}")
|
||||
(sd/process-json-stream)
|
||||
(rx/subs!
|
||||
(fn []
|
||||
(throw (js/Error. "Should be an error")))
|
||||
(fn [err]
|
||||
(t/is (= :error.import/invalid-json-data (:error/code (ex-data err))))
|
||||
(done)))))))
|
||||
|
||||
(t/deftest process-missing-references-json-test
|
||||
(t/async
|
||||
done
|
||||
(t/testing "fails on missing references in tokens"
|
||||
(let [json (-> {"core" {"color" {"$value" "{missing}"
|
||||
"$type" "color"}}}
|
||||
(tr/encode-str {:type :json-verbose}))]
|
||||
(->> (rx/of json)
|
||||
(sd/process-json-stream)
|
||||
(rx/subs!
|
||||
(fn []
|
||||
(throw (js/Error. "Should be an error")))
|
||||
(fn [err]
|
||||
(t/is (= :error.import/style-dictionary-reference-errors (:error/code (ex-data err))))
|
||||
(done))))))))
|
26
frontend/test/frontend_tests/tokens/token_form_test.cljs
Normal file
26
frontend/test/frontend_tests/tokens/token_form_test.cljs
Normal file
|
@ -0,0 +1,26 @@
|
|||
;; 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 frontend-tests.tokens.token-form-test
|
||||
(:require
|
||||
[app.main.ui.workspace.tokens.form :as wtf]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[malli.core :as m]))
|
||||
|
||||
(t/deftest test-valid-token-name-schema
|
||||
;; Allow regular namespace token names
|
||||
(t/is (some? (m/validate wtf/valid-token-name-schema "Foo")))
|
||||
(t/is (some? (m/validate wtf/valid-token-name-schema "foo")))
|
||||
(t/is (some? (m/validate wtf/valid-token-name-schema "FOO")))
|
||||
(t/is (some? (m/validate wtf/valid-token-name-schema "Foo.Bar.Baz")))
|
||||
;; Allow trailing tokens
|
||||
(t/is (nil? (m/validate wtf/valid-token-name-schema "Foo.Bar.Baz....")))
|
||||
;; Disallow multiple separator dots
|
||||
(t/is (nil? (m/validate wtf/valid-token-name-schema "Foo..Bar.Baz")))
|
||||
;; Disallow any special characters
|
||||
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey Foo.Bar")))
|
||||
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey😈Foo.Bar")))
|
||||
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey%Foo.Bar"))))
|
100
frontend/test/frontend_tests/tokens/token_test.cljs
Normal file
100
frontend/test/frontend_tests/tokens/token_test.cljs
Normal file
|
@ -0,0 +1,100 @@
|
|||
;; 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 frontend-tests.tokens.token-test
|
||||
(:require
|
||||
[app.main.ui.workspace.tokens.token :as wtt]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest test-parse-token-value
|
||||
(t/testing "parses double from a token value"
|
||||
(t/is (= {:value 100.1 :unit nil} (wtt/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9 :unit nil} (wtt/parse-token-value "-9"))))
|
||||
(t/testing "trims white-space"
|
||||
(t/is (= {:value -1.3 :unit nil} (wtt/parse-token-value " -1.3 "))))
|
||||
(t/testing "parses unit: px"
|
||||
(t/is (= {:value 70.3 :unit "px"} (wtt/parse-token-value " 70.3px "))))
|
||||
(t/testing "parses unit: %"
|
||||
(t/is (= {:value -10 :unit "%"} (wtt/parse-token-value "-10%"))))
|
||||
(t/testing "parses unit: px")
|
||||
(t/testing "returns nil for any invalid characters"
|
||||
(t/is (nil? (wtt/parse-token-value " -1.3a "))))
|
||||
(t/testing "doesnt accept invalid double"
|
||||
(t/is (nil? (wtt/parse-token-value ".3")))))
|
||||
|
||||
(t/deftest remove-attributes-for-token-id
|
||||
(t/testing "removes attributes matching the `token`, keeps other attributes"
|
||||
(t/is (= {:ry "b"}
|
||||
(wtt/remove-attributes-for-token #{:rx :ry} {:name "a"} {:rx "a" :ry "b"})))))
|
||||
|
||||
(t/deftest token-applied-test
|
||||
(t/testing "matches passed token with `:token-attributes`"
|
||||
(t/is (true? (wtt/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match empty token"
|
||||
(t/is (nil? (wtt/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "does't match passed token `:id`"
|
||||
(t/is (nil? (wtt/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match passed `:token-attributes`"
|
||||
(t/is (nil? (wtt/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
|
||||
(t/deftest shapes-ids-by-applied-attributes
|
||||
(t/testing "Returns set of matched attributes that fit the applied token"
|
||||
(let [attributes #{:x :y :z}
|
||||
shape-applied-x {:id "shape-applied-x"
|
||||
:applied-tokens {:x "1"}}
|
||||
shape-applied-y {:id "shape-applied-y"
|
||||
:applied-tokens {:y "1"}}
|
||||
shape-applied-x-y {:id "shape-applied-x-y"
|
||||
:applied-tokens {:x "1" :y "1"}}
|
||||
shape-applied-none {:id "shape-applied-none"
|
||||
:applied-tokens {}}
|
||||
shape-applied-all {:id "shape-applied-all"
|
||||
:applied-tokens {:x "1" :y "1" :z "1"}}
|
||||
shape-ids (fn [& xs] (into #{} (map :id xs)))
|
||||
shapes [shape-applied-x
|
||||
shape-applied-y
|
||||
shape-applied-x-y
|
||||
shape-applied-all
|
||||
shape-applied-none]
|
||||
expected (wtt/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
(t/is (= (:x expected) (shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
(t/is (= (:y expected) (shape-ids shape-applied-y
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
(t/is (= (:z expected) (shape-ids shape-applied-all)))
|
||||
(t/is (true? (wtt/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
|
||||
(t/is (false? (wtt/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
|
||||
(shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all))))
|
||||
|
||||
(t/deftest tokens-applied-test
|
||||
(t/testing "is true when single shape matches the token and attributes"
|
||||
(t/is (true? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x}))))
|
||||
(t/testing "is false when no shape matches the token or attributes"
|
||||
(t/is (nil? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x})))
|
||||
(t/is (nil? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "a"}}]
|
||||
#{:y})))))
|
||||
|
||||
(t/deftest name->path-test
|
||||
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo.bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz...."))))
|
||||
|
||||
(t/deftest token-name-path-exists?-test
|
||||
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
|
||||
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (wtt/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (wtt/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (wtt/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (wtt/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
|
@ -1613,6 +1613,10 @@ msgstr "Canva"
|
|||
msgid "labels.close"
|
||||
msgstr "Close"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "labels.collapse"
|
||||
msgstr "Collapse"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:126
|
||||
msgid "labels.comments"
|
||||
msgstr "Comments"
|
||||
|
@ -2081,6 +2085,14 @@ msgstr "Team Leader"
|
|||
msgid "labels.team-member"
|
||||
msgstr "Team member"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "labels.themes"
|
||||
msgstr "Themes"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "labels.sets"
|
||||
msgstr "Sets"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:992, src/app/main/ui/workspace/main_menu.cljs:118
|
||||
msgid "labels.tutorials"
|
||||
msgstr "Tutorials"
|
||||
|
@ -6359,6 +6371,114 @@ msgstr "Update"
|
|||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Click to close the path"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.create-token"
|
||||
msgstr "Create new %s token"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.edit-token"
|
||||
msgstr "Edit token"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.resolved-value"
|
||||
msgstr "Resolved value: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.original-value"
|
||||
msgstr "Original value: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.no-themes"
|
||||
msgstr "There are no themes."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.create-one"
|
||||
msgstr "Create one."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.add set"
|
||||
msgstr "Add set"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.save-theme"
|
||||
msgstr "Save theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.create-theme-title"
|
||||
msgstr "Create theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.edit-theme-title"
|
||||
msgstr "Edit theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.delete-theme-title"
|
||||
msgstr "Delete theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.no-themes-currently"
|
||||
msgstr "You currently have no themes."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.create-new-theme"
|
||||
msgstr "Create your first theme now."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.new-theme"
|
||||
msgstr "New theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.themes"
|
||||
msgstr "Themes"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.theme-name"
|
||||
msgstr "Theme %s"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.no-sets"
|
||||
msgstr "No sets"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.num-sets"
|
||||
msgstr "%s sets"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.back-to-themes"
|
||||
msgstr "Back to theme list"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.edit-themes"
|
||||
msgstr "Edit themes"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.no-active-theme"
|
||||
msgstr "No theme active"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.active-themes"
|
||||
msgstr "%s active themes"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.grouping-set-alert"
|
||||
msgstr "Token Set grouping is not supported yet."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.select-set"
|
||||
msgstr "Select set."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.set-selection-theme"
|
||||
msgstr "Define what token sets should be used as part of this theme option:"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.no-sets-yet"
|
||||
msgstr "There are no sets yet."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.no-sets-create"
|
||||
msgstr "There are no sets defined yet. Create one first."
|
||||
|
||||
msgid "workspace.versions.button.save"
|
||||
msgstr "Save version"
|
||||
|
||||
|
|
|
@ -1613,6 +1613,10 @@ msgstr "Canva"
|
|||
msgid "labels.close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "labels.collapse"
|
||||
msgstr "Colapsar"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:126
|
||||
msgid "labels.comments"
|
||||
msgstr "Comentarios"
|
||||
|
@ -2073,6 +2077,14 @@ msgstr "Líder de equipo"
|
|||
msgid "labels.team-member"
|
||||
msgstr "Miembro de equipo"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "labels.themes"
|
||||
msgstr "Temas"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "labels.sets"
|
||||
msgstr "Sets"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:992, src/app/main/ui/workspace/main_menu.cljs:118
|
||||
msgid "labels.tutorials"
|
||||
msgstr "Tutoriales"
|
||||
|
@ -6341,6 +6353,118 @@ msgstr "Pulsar para cerrar la ruta"
|
|||
msgid "errors.maximum-invitations-by-request-reached"
|
||||
msgstr "Se ha alcanzado el número máximo (%s) de correos electrónicos que se pueden invitar en una sola solicitud"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.create-token"
|
||||
msgstr "Crear un token de %s"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.edit-token"
|
||||
msgstr "Editar token"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/form.cljs
|
||||
msgid "workspace.token.resolved-value"
|
||||
msgstr "Valor resuelto: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.original-value"
|
||||
msgstr "Valor original: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.no-themes"
|
||||
msgstr "No hay temas."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.create-one"
|
||||
msgstr "Crear uno."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs
|
||||
msgid "workspace.token.add set"
|
||||
msgstr "Añadir set"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.save-theme"
|
||||
msgstr "Guardar tema"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.create-theme-title"
|
||||
msgstr "Crear tema"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.edit-theme-title"
|
||||
msgstr "Editar tema"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.delete-theme-title"
|
||||
msgstr "Borrar theme"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.no-themes-currently"
|
||||
msgstr "Actualmente no existen temas."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.create-new-theme"
|
||||
msgstr "Crea un nuevo tema ahora."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.new-theme"
|
||||
msgstr "Nuevo tema"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.themes"
|
||||
msgstr "Temas"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.theme-name"
|
||||
msgstr "Tema %s"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.no-sets"
|
||||
msgstr "No hay sets"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.num-sets"
|
||||
msgstr "%s sets"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
|
||||
msgid "workspace.token.back-to-themes"
|
||||
msgstr "Volver al listado de temas"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.edit-themes"
|
||||
msgstr "Editar temas"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.no-active-theme"
|
||||
msgstr "No hay temas activos"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/theme_select.cljs
|
||||
msgid "workspace.token.active-themes"
|
||||
msgstr "%s temas activos"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.grouping-set-alert"
|
||||
msgstr "La agrupación de sets aun no está soportada."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.select-set"
|
||||
msgstr "Selecciona set"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.set-selection-theme"
|
||||
msgstr "Define que sets de tokens deberian formar parte de este tema:"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.no-sets"
|
||||
msgstr "Aun no hay sets."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.create-one"
|
||||
msgstr "Crea uno."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
msgid "workspace.token.no-sets-create"
|
||||
msgstr "Aun no hay sets definidos. Crea uno primero"
|
||||
|
||||
msgid "workspace.versions.button.save"
|
||||
msgstr "Guardar versión"
|
||||
|
||||
|
|
|
@ -209,6 +209,46 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundled-es-modules/deepmerge@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@bundled-es-modules/deepmerge@npm:4.3.1"
|
||||
dependencies:
|
||||
deepmerge: "npm:^4.3.1"
|
||||
checksum: 10c0/50493fb741d588aa358edc5e844cbf31493cb64aca0a5ca0d33d73f61eb9eb853f7038074429343afbe199e614a6be8400abfd31909f9e5f14a53a4cff39b894
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundled-es-modules/glob@npm:^10.4.2":
|
||||
version: 10.4.2
|
||||
resolution: "@bundled-es-modules/glob@npm:10.4.2"
|
||||
dependencies:
|
||||
buffer: "npm:^6.0.3"
|
||||
events: "npm:^3.3.0"
|
||||
glob: "npm:^10.4.2"
|
||||
patch-package: "npm:^8.0.0"
|
||||
path: "npm:^0.12.7"
|
||||
stream: "npm:^0.0.3"
|
||||
string_decoder: "npm:^1.3.0"
|
||||
url: "npm:^0.11.3"
|
||||
checksum: 10c0/0c61907efb170750c69c7a6953d613bcbffdefca5ced668c0579baf46e28232793fb6e2ac3b736dd937f750572ef5a17483c417060df43e4be30dc4c8567aaba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundled-es-modules/memfs@npm:^4.9.4":
|
||||
version: 4.9.4
|
||||
resolution: "@bundled-es-modules/memfs@npm:4.9.4"
|
||||
dependencies:
|
||||
assert: "npm:^2.0.0"
|
||||
buffer: "npm:^6.0.3"
|
||||
events: "npm:^3.3.0"
|
||||
memfs: "npm:^4.9.3"
|
||||
path: "npm:^0.12.7"
|
||||
stream: "npm:^0.0.3"
|
||||
util: "npm:^0.12.5"
|
||||
checksum: 10c0/e3548c14379183fb74aa9a94407c1cdb8587320216fb557c0af7277d2dccf23f10a2edf8726e99f878758730c0c8d71524f77e19b26660a067b01d9afa07c891
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "@colors/colors@npm:1.6.0"
|
||||
|
@ -653,6 +693,38 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jsonjoy.com/base64@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "@jsonjoy.com/base64@npm:1.1.2"
|
||||
peerDependencies:
|
||||
tslib: 2
|
||||
checksum: 10c0/88717945f66dc89bf58ce75624c99fe6a5c9a0c8614e26d03e406447b28abff80c69fb37dabe5aafef1862cf315071ae66e5c85f6018b437d95f8d13d235e6eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jsonjoy.com/json-pack@npm:^1.0.3":
|
||||
version: 1.1.0
|
||||
resolution: "@jsonjoy.com/json-pack@npm:1.1.0"
|
||||
dependencies:
|
||||
"@jsonjoy.com/base64": "npm:^1.1.1"
|
||||
"@jsonjoy.com/util": "npm:^1.1.2"
|
||||
hyperdyperid: "npm:^1.2.0"
|
||||
thingies: "npm:^1.20.0"
|
||||
peerDependencies:
|
||||
tslib: 2
|
||||
checksum: 10c0/cdf5cb567a7f2e703d4966a3e3a5f7f7b54ee40a2102aa0ede5c79bcf2060c8465d82f39de8583db4cf1d8415bec8e57dfb1156ef663567b846cdea45813d9d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jsonjoy.com/util@npm:^1.1.2, @jsonjoy.com/util@npm:^1.3.0":
|
||||
version: 1.5.0
|
||||
resolution: "@jsonjoy.com/util@npm:1.5.0"
|
||||
peerDependencies:
|
||||
tslib: 2
|
||||
checksum: 10c0/0065ae12c4108d8aede01a479c8d2b5a39bce99e9a449d235befc753f57e8385d9c1115720529f26597840b7398d512898155423d9859fd638319fb0c827365d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdx-js/react@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "@mdx-js/react@npm:3.1.0"
|
||||
|
@ -694,6 +766,62 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-darwin-aarch64@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-darwin-aarch64@npm:1.1.34"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-darwin-x64-baseline@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-darwin-x64-baseline@npm:1.1.34"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-darwin-x64@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-darwin-x64@npm:1.1.34"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-linux-aarch64@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-linux-aarch64@npm:1.1.34"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-linux-x64-baseline@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-linux-x64-baseline@npm:1.1.34"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-linux-x64@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-linux-x64@npm:1.1.34"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-windows-x64-baseline@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-windows-x64-baseline@npm:1.1.34"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oven/bun-windows-x64@npm:1.1.34":
|
||||
version: 1.1.34
|
||||
resolution: "@oven/bun-windows-x64@npm:1.1.34"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@parcel/watcher-android-arm64@npm:2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "@parcel/watcher-android-arm64@npm:2.5.0"
|
||||
|
@ -1508,6 +1636,30 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tokens-studio/sd-transforms@npm:^0.16.1":
|
||||
version: 0.16.1
|
||||
resolution: "@tokens-studio/sd-transforms@npm:0.16.1"
|
||||
dependencies:
|
||||
"@tokens-studio/types": "npm:^0.4.0"
|
||||
color2k: "npm:^2.0.1"
|
||||
colorjs.io: "npm:^0.4.3"
|
||||
deepmerge: "npm:^4.3.1"
|
||||
expr-eval-fork: "npm:^2.0.2"
|
||||
is-mergeable-object: "npm:^1.1.1"
|
||||
postcss-calc-ast-parser: "npm:^0.1.4"
|
||||
peerDependencies:
|
||||
style-dictionary: ^4.0.0-prerelease.27
|
||||
checksum: 10c0/496a22026ffa25e3f6d8438a1fb39d67383fa55c89de9ac6759e2dce10a16268f5009e4809d03ceab38597fc02025a90eb1d32083b98a9353feded83831549c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tokens-studio/types@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "@tokens-studio/types@npm:0.4.0"
|
||||
checksum: 10c0/0641385653c94704f63dc5e10699c49bdbb1e1d8cba54af31bf50c3be85056123109bb2fe5091b1ccebaa9eba4c4afce3148a3b850919ed67bc81e3294ae839c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@trysound/sax@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@trysound/sax@npm:0.2.0"
|
||||
|
@ -1712,6 +1864,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@yarnpkg/lockfile@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@yarnpkg/lockfile@npm:1.1.0"
|
||||
checksum: 10c0/0bfa50a3d756623d1f3409bc23f225a1d069424dbc77c6fd2f14fb377390cd57ec703dc70286e081c564be9051ead9ba85d81d66a3e68eeb6eb506d4e0c0fbda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@zip.js/zip.js@npm:^2.7.44":
|
||||
version: 2.7.53
|
||||
resolution: "@zip.js/zip.js@npm:2.7.53"
|
||||
checksum: 10c0/883527bf09ce7c312117536c79d5f07e736d87de802a6c19e39ba2e18027499dcb9359df94dfde13c9bcf6118a20b4f26a40f9892ee82d7cac3124d6986b15c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abbrev@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "abbrev@npm:2.0.0"
|
||||
|
@ -1938,6 +2104,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assert@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "assert@npm:2.1.0"
|
||||
dependencies:
|
||||
call-bind: "npm:^1.0.2"
|
||||
is-nan: "npm:^1.3.2"
|
||||
object-is: "npm:^1.1.5"
|
||||
object.assign: "npm:^4.1.4"
|
||||
util: "npm:^0.12.5"
|
||||
checksum: 10c0/7271a5da883c256a1fa690677bf1dd9d6aa882139f2bed1cd15da4f9e7459683e1da8e32a203d6cc6767e5e0f730c77a9532a87b896b4b0af0dd535f668775f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assertion-error@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "assertion-error@npm:2.0.1"
|
||||
|
@ -1975,6 +2154,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"at-least-node@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "at-least-node@npm:1.0.0"
|
||||
checksum: 10c0/4c058baf6df1bc5a1697cf182e2029c58cd99975288a13f9e70068ef5d6f4e1f1fd7c4d2c3c4912eae44797d1725be9700995736deca441b39f3e66d8dee97ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"atob@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "atob@npm:2.1.2"
|
||||
|
@ -2289,6 +2475,43 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bun@npm:^1.1.25":
|
||||
version: 1.1.34
|
||||
resolution: "bun@npm:1.1.34"
|
||||
dependencies:
|
||||
"@oven/bun-darwin-aarch64": "npm:1.1.34"
|
||||
"@oven/bun-darwin-x64": "npm:1.1.34"
|
||||
"@oven/bun-darwin-x64-baseline": "npm:1.1.34"
|
||||
"@oven/bun-linux-aarch64": "npm:1.1.34"
|
||||
"@oven/bun-linux-x64": "npm:1.1.34"
|
||||
"@oven/bun-linux-x64-baseline": "npm:1.1.34"
|
||||
"@oven/bun-windows-x64": "npm:1.1.34"
|
||||
"@oven/bun-windows-x64-baseline": "npm:1.1.34"
|
||||
dependenciesMeta:
|
||||
"@oven/bun-darwin-aarch64":
|
||||
optional: true
|
||||
"@oven/bun-darwin-x64":
|
||||
optional: true
|
||||
"@oven/bun-darwin-x64-baseline":
|
||||
optional: true
|
||||
"@oven/bun-linux-aarch64":
|
||||
optional: true
|
||||
"@oven/bun-linux-x64":
|
||||
optional: true
|
||||
"@oven/bun-linux-x64-baseline":
|
||||
optional: true
|
||||
"@oven/bun-windows-x64":
|
||||
optional: true
|
||||
"@oven/bun-windows-x64-baseline":
|
||||
optional: true
|
||||
bin:
|
||||
bun: bin/bun.exe
|
||||
bunx: bin/bun.exe
|
||||
checksum: 10c0/d7a69a3e6a7545d7c76edaf86633f23f791641732fb0f5a6378f1503d267d03a3353afcc01e735acb6981b12acc83827d73bca701f8e3f62183bb00ad7e22e9d
|
||||
conditions: (os=darwin | os=linux | os=win32) & (cpu=arm64 | cpu=x64)
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bytes@npm:3.1.2, bytes@npm:^3.0.0":
|
||||
version: 3.1.2
|
||||
resolution: "bytes@npm:3.1.2"
|
||||
|
@ -2323,7 +2546,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7":
|
||||
"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "call-bind@npm:1.0.7"
|
||||
dependencies:
|
||||
|
@ -2377,6 +2600,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^5.3.0":
|
||||
version: 5.3.0
|
||||
resolution: "chalk@npm:5.3.0"
|
||||
checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"change-case@npm:^5.3.0":
|
||||
version: 5.4.4
|
||||
resolution: "change-case@npm:5.4.4"
|
||||
checksum: 10c0/2a9c2b9c9ad6ab2491105aaf506db1a9acaf543a18967798dcce20926c6a173aa63266cb6189f3086e3c14bf7ae1f8ea4f96ecc466fcd582310efa00372f3734
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"check-error@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "check-error@npm:2.1.1"
|
||||
|
@ -2419,6 +2656,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ci-info@npm:^3.7.0":
|
||||
version: 3.9.0
|
||||
resolution: "ci-info@npm:3.9.0"
|
||||
checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3":
|
||||
version: 1.0.4
|
||||
resolution: "cipher-base@npm:1.0.4"
|
||||
|
@ -2546,6 +2790,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color2k@npm:^2.0.1":
|
||||
version: 2.0.3
|
||||
resolution: "color2k@npm:2.0.3"
|
||||
checksum: 10c0/e7c13d212c9d1abb1690e378bbc0a6fb1751e4b02e9a73ba3b2ade9d54da673834597d342791d577d1ce400ec486c7f92c5098f9fa85cd113bcfde57420a2bb9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color@npm:^3.1.3":
|
||||
version: 3.2.1
|
||||
resolution: "color@npm:3.2.1"
|
||||
|
@ -2556,6 +2807,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"colorjs.io@npm:^0.4.3":
|
||||
version: 0.4.5
|
||||
resolution: "colorjs.io@npm:0.4.5"
|
||||
checksum: 10c0/4cc58d18223426bcb8caa558e7554002b62bf87bd20db06596abf5efe5ea65416266402db86b504ac5fa2c38360913dbb8e6ef7c4fa19a992fd1818d5710ef6f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"colorjs.io@npm:^0.5.0":
|
||||
version: 0.5.2
|
||||
resolution: "colorjs.io@npm:0.5.2"
|
||||
|
@ -2596,6 +2854,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^8.3.0":
|
||||
version: 8.3.0
|
||||
resolution: "commander@npm:8.3.0"
|
||||
checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"component-emitter@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "component-emitter@npm:2.0.0"
|
||||
checksum: 10c0/65dfaf787ea49eb48f0ffec766bda7ec67e8dbeb3b406f08724dcae842e0aa274731fcccb9280b77d2b41693061731a9080b60d276020246a146544cd9900b83
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compressible@npm:~2.0.18":
|
||||
version: 2.0.18
|
||||
resolution: "compressible@npm:2.0.18"
|
||||
|
@ -2795,7 +3067,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-spawn@npm:^7.0.0":
|
||||
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3":
|
||||
version: 7.0.3
|
||||
resolution: "cross-spawn@npm:7.0.3"
|
||||
dependencies:
|
||||
|
@ -3079,6 +3351,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deepmerge@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "deepmerge@npm:4.3.1"
|
||||
checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "define-data-property@npm:1.1.4"
|
||||
|
@ -3097,7 +3376,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"define-properties@npm:^1.2.0, define-properties@npm:^1.2.1":
|
||||
"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "define-properties@npm:1.2.1"
|
||||
dependencies:
|
||||
|
@ -3883,6 +4162,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expr-eval-fork@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "expr-eval-fork@npm:2.0.2"
|
||||
checksum: 10c0/ab5143fe65017d8811c155be55abd700321b8a32117635c35ce1309488f3263a251788f27f2e4a77425f58f7a64f99fd46d652c35a8c1668b22b4a8861702b75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"express@npm:^4.21.1":
|
||||
version: 4.21.1
|
||||
resolution: "express@npm:4.21.1"
|
||||
|
@ -4025,6 +4311,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-yarn-workspace-root@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "find-yarn-workspace-root@npm:2.0.0"
|
||||
dependencies:
|
||||
micromatch: "npm:^4.0.2"
|
||||
checksum: 10c0/b0d3843013fbdaf4e57140e0165889d09fa61745c9e85da2af86e54974f4cc9f1967e40f0d8fc36a79d53091f0829c651d06607d552582e53976f3cd8f4e5689
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fn.name@npm:1.x.x":
|
||||
version: 1.1.0
|
||||
resolution: "fn.name@npm:1.1.0"
|
||||
|
@ -4108,8 +4403,10 @@ __metadata:
|
|||
"@storybook/blocks": "npm:^8.3.6"
|
||||
"@storybook/react": "npm:^8.3.6"
|
||||
"@storybook/react-vite": "npm:^8.3.6"
|
||||
"@tokens-studio/sd-transforms": "npm:^0.16.1"
|
||||
"@types/node": "npm:^22.7.7"
|
||||
autoprefixer: "npm:^10.4.20"
|
||||
bun: "npm:^1.1.25"
|
||||
compression: "npm:^1.7.4"
|
||||
concurrently: "npm:^9.0.1"
|
||||
date-fns: "npm:^4.1.0"
|
||||
|
@ -4131,6 +4428,7 @@ __metadata:
|
|||
jsdom: "npm:^25.0.1"
|
||||
jszip: "npm:^3.10.1"
|
||||
lodash: "npm:^4.17.21"
|
||||
lodash.debounce: "npm:^4.0.8"
|
||||
luxon: "npm:^3.5.0"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:^14.1.3"
|
||||
|
@ -4159,8 +4457,10 @@ __metadata:
|
|||
shadow-cljs: "npm:2.28.18"
|
||||
source-map-support: "npm:^0.5.21"
|
||||
storybook: "npm:^8.3.6"
|
||||
style-dictionary: "npm:^4.1.4"
|
||||
svg-sprite: "npm:^2.0.4"
|
||||
tdigest: "npm:^0.1.2"
|
||||
tinycolor2: "npm:^1.6.0"
|
||||
typescript: "npm:^5.6.3"
|
||||
ua-parser-js: "npm:2.0.0-rc.1"
|
||||
vite: "npm:^5.4.9"
|
||||
|
@ -4172,6 +4472,18 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"fs-extra@npm:^9.0.0":
|
||||
version: 9.1.0
|
||||
resolution: "fs-extra@npm:9.1.0"
|
||||
dependencies:
|
||||
at-least-node: "npm:^1.0.0"
|
||||
graceful-fs: "npm:^4.2.0"
|
||||
jsonfile: "npm:^6.0.1"
|
||||
universalify: "npm:^2.0.0"
|
||||
checksum: 10c0/9b808bd884beff5cb940773018179a6b94a966381d005479f00adda6b44e5e3d4abf765135773d849cc27efe68c349e4a7b86acd7d3306d5932c14f3a4b17a92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-minipass@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "fs-minipass@npm:2.1.0"
|
||||
|
@ -4347,7 +4659,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.3":
|
||||
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.3, glob@npm:^10.4.2":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
dependencies:
|
||||
|
@ -4419,7 +4731,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.6":
|
||||
"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
|
||||
version: 4.2.11
|
||||
resolution: "graceful-fs@npm:4.2.11"
|
||||
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
|
||||
|
@ -4702,6 +5014,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hyperdyperid@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "hyperdyperid@npm:1.2.0"
|
||||
checksum: 10c0/885ba3177c7181d315a856ee9c0005ff8eb5dcb1ce9e9d61be70987895d934d84686c37c981cceeb53216d4c9c15c1cc25f1804e84cc6a74a16993c5d7fd0893
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
|
@ -4990,6 +5309,23 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-mergeable-object@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "is-mergeable-object@npm:1.1.1"
|
||||
checksum: 10c0/ed895a17686eb88d28040e0281c507639e5a07e63ac51f033c34091c2d8679ca86775ecfe80d5f0636bc2b7c530acd731527e5a2e9c32a88f8847286451720f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-nan@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "is-nan@npm:1.3.2"
|
||||
dependencies:
|
||||
call-bind: "npm:^1.0.0"
|
||||
define-properties: "npm:^1.1.3"
|
||||
checksum: 10c0/8bfb286f85763f9c2e28ea32e9127702fe980ffd15fa5d63ade3be7786559e6e21355d3625dd364c769c033c5aedf0a2ed3d4025d336abf1b9241e3d9eddc5b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-negative-zero@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "is-negative-zero@npm:2.0.3"
|
||||
|
@ -5013,6 +5349,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-plain-obj@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "is-plain-obj@npm:4.1.0"
|
||||
checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-plain-object@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "is-plain-object@npm:2.0.4"
|
||||
|
@ -5098,7 +5441,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-wsl@npm:^2.2.0":
|
||||
"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "is-wsl@npm:2.2.0"
|
||||
dependencies:
|
||||
|
@ -5270,6 +5613,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-stable-stringify@npm:^1.0.2":
|
||||
version: 1.1.1
|
||||
resolution: "json-stable-stringify@npm:1.1.1"
|
||||
dependencies:
|
||||
call-bind: "npm:^1.0.5"
|
||||
isarray: "npm:^2.0.5"
|
||||
jsonify: "npm:^0.0.1"
|
||||
object-keys: "npm:^1.1.1"
|
||||
checksum: 10c0/3801e3eeccbd030afb970f54bea690a079cfea7d9ed206a1b17ca9367f4b7772c764bf77a48f03e56b50e5f7ee7d11c52339fe20d8d7ccead003e4ca69e4cfde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json5@npm:^2.2.2, json5@npm:^2.2.3":
|
||||
version: 2.2.3
|
||||
resolution: "json5@npm:2.2.3"
|
||||
|
@ -5279,6 +5634,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsonfile@npm:^6.0.1":
|
||||
version: 6.1.0
|
||||
resolution: "jsonfile@npm:6.1.0"
|
||||
dependencies:
|
||||
graceful-fs: "npm:^4.1.6"
|
||||
universalify: "npm:^2.0.0"
|
||||
dependenciesMeta:
|
||||
graceful-fs:
|
||||
optional: true
|
||||
checksum: 10c0/4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsonify@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "jsonify@npm:0.0.1"
|
||||
checksum: 10c0/7f5499cdd59a0967ed35bda48b7cec43d850bbc8fb955cdd3a1717bb0efadbe300724d5646de765bb7a99fc1c3ab06eb80d93503c6faaf99b4ff50a3326692f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jszip@npm:^3.10.1":
|
||||
version: 3.10.1
|
||||
resolution: "jszip@npm:3.10.1"
|
||||
|
@ -5291,6 +5666,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"klaw-sync@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "klaw-sync@npm:6.0.0"
|
||||
dependencies:
|
||||
graceful-fs: "npm:^4.1.11"
|
||||
checksum: 10c0/00d8e4c48d0d699b743b3b028e807295ea0b225caf6179f51029e19783a93ad8bb9bccde617d169659fbe99559d73fb35f796214de031d0023c26b906cecd70a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kuler@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "kuler@npm:2.0.0"
|
||||
|
@ -5356,6 +5740,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.debounce@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "lodash.debounce@npm:4.0.8"
|
||||
checksum: 10c0/762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.escape@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "lodash.escape@npm:4.0.1"
|
||||
|
@ -5548,6 +5939,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"memfs@npm:^4.9.3":
|
||||
version: 4.14.0
|
||||
resolution: "memfs@npm:4.14.0"
|
||||
dependencies:
|
||||
"@jsonjoy.com/json-pack": "npm:^1.0.3"
|
||||
"@jsonjoy.com/util": "npm:^1.3.0"
|
||||
tree-dump: "npm:^1.0.1"
|
||||
tslib: "npm:^2.0.0"
|
||||
checksum: 10c0/d1de2e4b3c269f5b5f27b63f60bb8ea9ae5800843776e0bed4548f2957dcd55237ac5eab3a5ffe0d561a6be53e42c055a7bc79efc1613563b14e14c287ef3b0a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"memoizee@npm:0.4.X":
|
||||
version: 0.4.17
|
||||
resolution: "memoizee@npm:0.4.17"
|
||||
|
@ -5594,7 +5997,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromatch@npm:^4.0.5":
|
||||
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.5":
|
||||
version: 4.0.8
|
||||
resolution: "micromatch@npm:4.0.8"
|
||||
dependencies:
|
||||
|
@ -6072,6 +6475,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"object-is@npm:^1.1.5":
|
||||
version: 1.1.6
|
||||
resolution: "object-is@npm:1.1.6"
|
||||
dependencies:
|
||||
call-bind: "npm:^1.0.7"
|
||||
define-properties: "npm:^1.2.1"
|
||||
checksum: 10c0/506af444c4dce7f8e31f34fc549e2fb8152d6b9c4a30c6e62852badd7f520b579c679af433e7a072f9d78eb7808d230dc12e1cf58da9154dfbf8813099ea0fe0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"object-keys@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "object-keys@npm:1.1.1"
|
||||
|
@ -6125,6 +6538,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"open@npm:^7.4.2":
|
||||
version: 7.4.2
|
||||
resolution: "open@npm:7.4.2"
|
||||
dependencies:
|
||||
is-docker: "npm:^2.0.0"
|
||||
is-wsl: "npm:^2.1.1"
|
||||
checksum: 10c0/77573a6a68f7364f3a19a4c80492712720746b63680ee304555112605ead196afe91052bd3c3d165efdf4e9d04d255e87de0d0a77acec11ef47fd5261251813f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"open@npm:^8.0.4":
|
||||
version: 8.4.2
|
||||
resolution: "open@npm:8.4.2"
|
||||
|
@ -6155,6 +6578,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"os-tmpdir@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "os-tmpdir@npm:1.0.2"
|
||||
checksum: 10c0/f438450224f8e2687605a8dd318f0db694b6293c5d835ae509a69e97c8de38b6994645337e5577f5001115470414638978cc49da1cdcc25106dad8738dc69990
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-limit@npm:^3.0.2":
|
||||
version: 3.1.0
|
||||
resolution: "p-limit@npm:3.1.0"
|
||||
|
@ -6252,6 +6682,31 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"patch-package@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "patch-package@npm:8.0.0"
|
||||
dependencies:
|
||||
"@yarnpkg/lockfile": "npm:^1.1.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
ci-info: "npm:^3.7.0"
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
find-yarn-workspace-root: "npm:^2.0.0"
|
||||
fs-extra: "npm:^9.0.0"
|
||||
json-stable-stringify: "npm:^1.0.2"
|
||||
klaw-sync: "npm:^6.0.0"
|
||||
minimist: "npm:^1.2.6"
|
||||
open: "npm:^7.4.2"
|
||||
rimraf: "npm:^2.6.3"
|
||||
semver: "npm:^7.5.3"
|
||||
slash: "npm:^2.0.0"
|
||||
tmp: "npm:^0.0.33"
|
||||
yaml: "npm:^2.2.2"
|
||||
bin:
|
||||
patch-package: index.js
|
||||
checksum: 10c0/690eab0537e953a3fd7d32bb23f0e82f97cd448f8244c3227ed55933611a126f9476397325c06ad2c11d881a19b427a02bd1881bee78d89f1731373fc4fe0fee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-browserify@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "path-browserify@npm:0.0.1"
|
||||
|
@ -6330,6 +6785,23 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-unified@npm:^0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "path-unified@npm:0.1.0"
|
||||
checksum: 10c0/26c314221bcc0ca3ce59b67f50dffb6f37214d294fd9dfeb0219e9f12b93d8852c8525d32be9387011d902d361669a43e22ec419d522055794790222665b2de9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path@npm:^0.12.7":
|
||||
version: 0.12.7
|
||||
resolution: "path@npm:0.12.7"
|
||||
dependencies:
|
||||
process: "npm:^0.11.1"
|
||||
util: "npm:^0.10.3"
|
||||
checksum: 10c0/f795ce5438a988a590c7b6dfd450ec9baa1c391a8be4c2dea48baa6e0f5b199e56cd83b8c9ebf3991b81bea58236d2c32bdafe2c17a2e70c3a2e4c69891ade59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pathe@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "pathe@npm:1.1.2"
|
||||
|
@ -6462,6 +6934,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-calc-ast-parser@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "postcss-calc-ast-parser@npm:0.1.4"
|
||||
dependencies:
|
||||
postcss-value-parser: "npm:^3.3.1"
|
||||
checksum: 10c0/6ab488da4024aefe749baff2ee2cd41d1a7b84611291a6fd5d220262255c86f37687b3541696cab3e4edb1b7601634719877184ee426048ad82ed15185a5f64f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-clean@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "postcss-clean@npm:1.2.2"
|
||||
|
@ -6565,6 +7046,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-value-parser@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "postcss-value-parser@npm:3.3.1"
|
||||
checksum: 10c0/23eed98d8eeadb1f9ef1db4a2757da0f1d8e7c1dac2a38d6b35d971aab9eb3c6d8a967d0e9f435558834ffcd966afbbe875a56bcc5bcdd09e663008c106b3e47
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "postcss-value-parser@npm:4.2.0"
|
||||
|
@ -6641,7 +7129,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"process@npm:^0.11.10":
|
||||
"process@npm:^0.11.1, process@npm:^0.11.10":
|
||||
version: 0.11.10
|
||||
resolution: "process@npm:0.11.10"
|
||||
checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3
|
||||
|
@ -7063,6 +7551,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^2.6.3":
|
||||
version: 2.7.1
|
||||
resolution: "rimraf@npm:2.7.1"
|
||||
dependencies:
|
||||
glob: "npm:^7.1.3"
|
||||
bin:
|
||||
rimraf: ./bin.js
|
||||
checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "rimraf@npm:3.0.2"
|
||||
|
@ -7717,6 +8216,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slash@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "slash@npm:2.0.0"
|
||||
checksum: 10c0/f83dbd3cb62c41bb8fcbbc6bf5473f3234b97fa1d008f571710a9d3757a28c7169e1811cad1554ccb1cc531460b3d221c9a7b37f549398d9a30707f0a5af9193
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"smart-buffer@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "smart-buffer@npm:4.2.0"
|
||||
|
@ -7930,6 +8436,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream@npm:^0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "stream@npm:0.0.3"
|
||||
dependencies:
|
||||
component-emitter: "npm:^2.0.0"
|
||||
checksum: 10c0/5d262408583f3d5fed8077b33ad670320d85c6b7c0fb3ab73a9a632fbad0ee36f3c66e6feb5264cb39dbee3a619174fa886b5f69f98217666d0844f6a2f6510b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-hash@npm:^1.1.1":
|
||||
version: 1.1.3
|
||||
resolution: "string-hash@npm:1.1.3"
|
||||
|
@ -8078,6 +8593,28 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"style-dictionary@npm:^4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "style-dictionary@npm:4.1.4"
|
||||
dependencies:
|
||||
"@bundled-es-modules/deepmerge": "npm:^4.3.1"
|
||||
"@bundled-es-modules/glob": "npm:^10.4.2"
|
||||
"@bundled-es-modules/memfs": "npm:^4.9.4"
|
||||
"@zip.js/zip.js": "npm:^2.7.44"
|
||||
chalk: "npm:^5.3.0"
|
||||
change-case: "npm:^5.3.0"
|
||||
commander: "npm:^8.3.0"
|
||||
is-plain-obj: "npm:^4.1.0"
|
||||
json5: "npm:^2.2.2"
|
||||
patch-package: "npm:^8.0.0"
|
||||
path-unified: "npm:^0.1.0"
|
||||
tinycolor2: "npm:^1.6.0"
|
||||
bin:
|
||||
style-dictionary: bin/style-dictionary.js
|
||||
checksum: 10c0/b88e2f94615bc851e2e797e685863911dbb875d312bf1571a3be6b6a9dde7e0b324d83495f153446eceefe93ec119c80e2ca032a600818dcecc72174d285e429
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "supports-color@npm:5.5.0"
|
||||
|
@ -8193,6 +8730,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thingies@npm:^1.20.0":
|
||||
version: 1.21.0
|
||||
resolution: "thingies@npm:1.21.0"
|
||||
peerDependencies:
|
||||
tslib: ^2
|
||||
checksum: 10c0/7570ee855aecb73185a672ecf3eb1c287a6512bf5476449388433b2d4debcf78100bc8bfd439b0edd38d2bc3bfb8341de5ce85b8557dec66d0f27b962c9a8bc1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through2@npm:^2.0.0, through2@npm:^2.0.3":
|
||||
version: 2.0.5
|
||||
resolution: "through2@npm:2.0.5"
|
||||
|
@ -8269,6 +8815,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinycolor2@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "tinycolor2@npm:1.6.0"
|
||||
checksum: 10c0/9aa79a36ba2c2a87cb221453465cabacd04b9e35f9694373e846fdc78b1c768110f81e581ea41440106c0f24d9a023891d0887e8075885e790ac40eb0e74a5c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "tinyexec@npm:0.3.1"
|
||||
|
@ -8315,6 +8868,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tmp@npm:^0.0.33":
|
||||
version: 0.0.33
|
||||
resolution: "tmp@npm:0.0.33"
|
||||
dependencies:
|
||||
os-tmpdir: "npm:~1.0.2"
|
||||
checksum: 10c0/69863947b8c29cabad43fe0ce65cec5bb4b481d15d4b4b21e036b060b3edbf3bc7a5541de1bacb437bb3f7c4538f669752627fdf9b4aaf034cebd172ba373408
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-arraybuffer@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "to-arraybuffer@npm:1.0.1"
|
||||
|
@ -8372,6 +8934,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-dump@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "tree-dump@npm:1.0.2"
|
||||
peerDependencies:
|
||||
tslib: 2
|
||||
checksum: 10c0/d1d180764e9c691b28332dbd74226c6b6af361dfb1e134bb11e60e17cb11c215894adee50ffc578da5dcf546006693947be8b6665eb1269b56e2f534926f1c1f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-kill@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "tree-kill@npm:1.2.2"
|
||||
|
@ -8406,7 +8977,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.0.1, tslib@npm:^2.1.0":
|
||||
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0":
|
||||
version: 2.8.1
|
||||
resolution: "tslib@npm:2.8.1"
|
||||
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
||||
|
@ -8580,6 +9151,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "universalify@npm:2.0.1"
|
||||
checksum: 10c0/73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "unpipe@npm:1.0.0"
|
||||
|
@ -8616,7 +9194,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"url@npm:^0.11.0":
|
||||
"url@npm:^0.11.0, url@npm:^0.11.3":
|
||||
version: 0.11.4
|
||||
resolution: "url@npm:0.11.4"
|
||||
dependencies:
|
||||
|
@ -8633,7 +9211,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"util@npm:^0.10.4":
|
||||
"util@npm:^0.10.3, util@npm:^0.10.4":
|
||||
version: 0.10.4
|
||||
resolution: "util@npm:0.10.4"
|
||||
dependencies:
|
||||
|
@ -9154,7 +9732,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.4.2":
|
||||
"yaml@npm:^2.2.2, yaml@npm:^2.4.2":
|
||||
version: 2.6.0
|
||||
resolution: "yaml@npm:2.6.0"
|
||||
bin:
|
||||
|
|
Loading…
Add table
Reference in a new issue