0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Merge pull request #263 from tokens-studio/token-sets-themes

Token sets themes
This commit is contained in:
Florian Schrödl 2024-08-27 17:09:47 +02:00 committed by GitHub
commit 734acd27b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1040 additions and 235 deletions

View file

@ -24,7 +24,9 @@
[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-list :as ctol]
[app.common.types.tokens-theme-list :as ctotl]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
[clojure.set :as set]))
@ -247,6 +249,53 @@
[:type [:= :del-typography]]
[: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 ::sm/uuid]]]]
[:delete-temporary-token-theme
[:map {:title "DeleteTemporaryTokenThemeChange"}
[:type [:= :delete-temporary-token-theme]]
[:id ::sm/uuid]]]
[: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]]
[:id ::sm/uuid]
[:token-theme ::ctot/token-theme]]]
[:del-token-theme
[:map {:title "DelTokenThemeChange"}
[:type [:= :del-token-theme]]
[:id ::sm/uuid]]]
[: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]]
[:id ::sm/uuid]
[:token-set ::ctot/token-set]]]
[:del-token-set
[:map {:title "DelTokenSetChange"}
[:type [:= :del-token-set]]
[:id ::sm/uuid]]]
[:add-token
[:map {:title "AddTokenChange"}
[:type [:= :add-token]]
@ -742,6 +791,42 @@
[data {:keys [id]}]
(ctol/delete-token data id))
(defmethod process-change :add-temporary-token-theme
[data {:keys [token-theme]}]
(ctotl/add-temporary-token-theme data token-theme))
(defmethod process-change :update-active-token-themes
[data {:keys [theme-ids]}]
(ctotl/assoc-active-token-themes data theme-ids))
(defmethod process-change :delete-temporary-token-theme
[data {:keys [id]}]
(ctotl/delete-temporary-token-theme data id))
(defmethod process-change :add-token-theme
[data {:keys [token-theme]}]
(ctotl/add-token-theme data token-theme))
(defmethod process-change :mod-token-theme
[data {:keys [id token-theme]}]
(ctotl/update-token-theme data id merge token-theme))
(defmethod process-change :del-token-theme
[data {:keys [id]}]
(ctotl/delete-token-theme data id))
(defmethod process-change :add-token-set
[data {:keys [token-set]}]
(ctotl/add-token-set data token-set))
(defmethod process-change :mod-token-set
[data {:keys [id token-set]}]
(ctotl/update-token-set data id merge token-set))
(defmethod process-change :del-token-set
[data {:keys [id]}]
(ctotl/delete-token-set data id))
;; === Operations
(defmethod process-operation :set
[on-changed shape op]

View file

@ -328,7 +328,7 @@
(update :redo-changes conj add-change)
(cond->
(and (ctk/in-component-copy? parent) (not ignore-touched))
(update :undo-changes conj restore-touched-change))
(update :undo-changes conj restore-touched-change))
(update :undo-changes conj del-change)
(apply-changes-local)))))
@ -389,7 +389,7 @@
(update :redo-changes conj set-parent-change)
(cond->
(ctk/in-component-copy? parent)
(update :undo-changes conj restore-touched-change))
(update :undo-changes conj restore-touched-change))
(update :undo-changes #(reduce mk-undo-change % shapes))
(apply-changes-local)))))
@ -695,6 +695,68 @@
(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)})
(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 :id (:id token-theme)})
(apply-changes-local)))
(defn update-token-theme
[changes token-theme prev-token-theme]
(-> changes
(update :redo-changes conj {:type :mod-token-theme :id (:id token-theme) :token-theme token-theme})
(update :undo-changes conj {:type :mod-token-theme :id (:id token-theme) :token-theme (or prev-token-theme token-theme)})
(apply-changes-local)))
(defn delete-token-theme
[changes token-theme-id]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-theme (get-in library-data [:token-themes-index token-theme-id])]
(-> changes
(update :redo-changes conj {:type :del-token-theme :id token-theme-id})
(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 :id (:id token-set)})
(apply-changes-local)))
(defn update-token-set
[changes token-set prev-token-set]
(-> changes
(update :redo-changes conj {:type :mod-token-set :id (:id token-set) :token-set token-set})
(update :undo-changes conj {:type :mod-token-set :id (:id token-set) :token-set (or prev-token-set token-set)})
(apply-changes-local)))
(defn delete-token-set
[changes token-set-id]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (get-in library-data [:token-sets-index token-set-id])]
(-> changes
(update :redo-changes conj {:type :del-token-set :id token-set-id})
(update :undo-changes conj {:type :add-token-set :token-set prev-token-set})
(apply-changes-local))))
(defn add-token
[changes token]
(-> changes

View file

@ -27,6 +27,7 @@
[app.common.types.plugins :as ctpg]
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.token-theme :as ctt]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
@ -58,12 +59,26 @@
[:vector {:gen/max 3} ::ctc/recent-color]]
[:typographies {:optional true}
[:map-of {:gen/max 2} ::sm/uuid ::cty/typography]]
[:tokens {:optional true}
[:map-of {:gen/max 100} ::sm/uuid ::cto/token]]
[:media {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::media-object]]
[:plugin-data {:optional true}
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]
[:token-theme-temporary-id {:optional true}
::sm/uuid]
[:token-active-themes {:optional true :default #{}}
[:set ::sm/uuid]]
[:token-themes {:optional true}
[:vector ::sm/uuid]]
[:token-themes-index {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::ctt/token-theme]]
[:token-set-groups {:optional true}
[:vector ::sm/uuid]]
[:token-set-groups-index {:optional true}
[:map-of {:gen/max 10} ::sm/uuid ::ctt/token-set-group]]
[:token-sets-index {:optional true}
[:map-of {:gen/max 10} ::sm/uuid ::ctt/token-set]]
[:tokens {:optional true}
[:map-of {:gen/max 100} ::sm/uuid ::cto/token]]])
(def check-file-data!
(sm/check-fn ::data))

View file

@ -0,0 +1,44 @@
;; 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"}
[:id ::sm/uuid]
[:name :string]
[:group {:optional true} :string]
[:source? {:optional true} :boolean]
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]
[:sets [:set {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(sm/register! ::token-set-group-ref
[:map
[:id ::sm/uuid]
[:type [:= :group]]])
(sm/register! ::token-set-ref
[:map
[:id ::sm/uuid]
[:type [:= :set]]])
(sm/register! ::token-set-group
[:map {:title "TokenSetGroup"}
[:id ::sm/uuid]
[:name :string]
[:items [:vector {:gen/max 10 :gen/min 1}
[:or ::token-set-group-ref ::token-set-ref]]]])
(sm/register! ::token-set
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name :string]
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]
[:tokens [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])

View file

@ -0,0 +1,78 @@
;; 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] :as token-theme}]
(-> file-data
(d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])
(assoc :token-theme-temporary-id id)
(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)
: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))))))

View file

@ -15,8 +15,9 @@
[app.main.data.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.common :refer [workspace-shapes]]
[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]
@ -50,12 +51,6 @@
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
(merge {} shape-leftover token-leftover)))
(defn get-shape-from-state [shape-id state]
(let [current-page-id (get state :current-page-id)
shape (-> (workspace-shapes (:workspace-data state) current-page-id #{shape-id})
(first))]
shape))
(defn token-from-attributes [token attributes]
(->> (map (fn [attr] [attr (wtt/token-identifier token)]) attributes)
(into {})))
@ -86,19 +81,144 @@
(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 (merge
{:id (uuid/next)
:sets #{}
:selected :enabled}
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 ensure-token-theme-changes [changes state {:keys [id new-set?]}]
(let [theme-id (wtts/update-theme-id state)
theme (some-> theme-id (wtts/get-workspace-token-theme state))]
(cond
(not theme-id) (-> changes
(pcb/add-temporary-token-theme
{:id (uuid/next)
:name ""
:sets #{id}}))
new-set? (-> changes
(pcb/update-token-theme
(wtts/add-token-set-to-token-theme id theme)
theme))
:else changes)))
(defn toggle-token-theme [token-theme-id]
(ptk/reify ::toggle-token-theme
ptk/WatchEvent
(watch [it state _]
(let [themes (wtts/get-active-theme-ids state)
new-themes (wtts/toggle-active-theme-id token-theme-id state)
changes (-> (pcb/empty-changes it)
(pcb/update-active-token-themes new-themes themes))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn delete-token-theme [token-theme-id]
(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 token-theme-id))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn create-token-set [token-set]
(let [new-token-set (merge
{:id (uuid/next)
:name "Token Set"
:tokens []}
token-set)]
(ptk/reify ::create-token-set
ptk/WatchEvent
(watch [it state _]
(let [changes (-> (pcb/empty-changes it)
(pcb/add-token-set new-token-set)
(ensure-token-theme-changes state {:id (:id new-token-set)
:new-set? true}))]
(rx/of
(dch/commit-changes changes)))))))
(defn toggle-token-set [token-set-id]
(ptk/reify ::toggle-token-set
ptk/WatchEvent
(watch [it state _]
(let [theme (some-> (wtts/update-theme-id state)
(wtts/get-workspace-token-theme state))
changes (-> (pcb/empty-changes it)
(pcb/update-token-theme
(wtts/toggle-token-set-to-token-theme token-set-id theme)
theme)
(pcb/update-active-token-themes #{(wtts/update-theme-id state)} (wtts/get-active-theme-ids state)))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn delete-token-set [token-set-id]
(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-id))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn update-create-token
[token]
(let [token (update token :id #(or % (uuid/next)))]
(ptk/reify ::add-token
(ptk/reify ::update-create-token
ptk/WatchEvent
(watch [it _ _]
(watch [it state _]
(let [prev-token (get-token-data-from-token-id (:id token))
changes (if prev-token
(-> (pcb/empty-changes it)
(pcb/update-token token prev-token))
(-> (pcb/empty-changes it)
(pcb/add-token token)))]
(rx/of (dch/commit-changes changes)))))))
create-token? (not prev-token)
token-changes (if create-token?
(-> (pcb/empty-changes it)
(pcb/add-token token))
(-> (pcb/empty-changes it)
(pcb/update-token token prev-token)))
token-set (wtts/get-selected-token-set state)
create-set? (not token-set)
new-token-set {:id (uuid/next)
:name "Global"
:tokens [(:id token)]}
selected-token-set-id (if create-set?
(:id new-token-set)
(:id token-set))
set-changes (cond
create-set? (-> token-changes
(pcb/add-token-set new-token-set))
:else (let [updated-token-set (if (contains? token-set (:id token))
token-set
(update token-set :tokens conj (:id token)))]
(-> token-changes
(pcb/update-token-set updated-token-set token-set))))
theme-changes (-> set-changes
(ensure-token-theme-changes state {:new-set? create-set?
:id selected-token-set-id}))]
(rx/of
(set-selected-token-set-id selected-token-set-id)
(dch/commit-changes theme-changes)))))))
(defn delete-token
[id]
@ -119,6 +239,7 @@
(update :name #(str/concat % "-copy")))]
(update-create-token new-token)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TEMP (Move to test)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -14,6 +14,7 @@
[app.common.types.shape.layout :as ctl]
[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
@ -233,11 +234,40 @@
(def workspace-data
(l/derived :workspace-data st/state))
(def workspace-tokens
(l/derived (fn [data]
(get data :tokens {}))
workspace-data
=))
(def workspace-selected-token-set-id
(l/derived
wtts/get-selected-token-set-id
st/state
=))
(def workspace-active-theme-ids
(l/derived wtts/get-active-theme-ids st/state))
(def workspace-active-set-ids
(l/derived wtts/get-active-set-ids st/state))
(def workspace-token-themes
(l/derived wtts/get-workspace-themes-index st/state))
(def workspace-ordered-token-themes
(l/derived wtts/get-workspace-ordered-themes st/state))
(def workspace-token-sets
(l/derived
(fn [data]
(or (wtts/get-workspace-sets data) {}))
st/state
=))
(def workspace-active-theme-sets-tokens
(l/derived wtts/get-active-theme-sets-tokens-names-map st/state =))
(def workspace-selected-token-set-tokens
(l/derived
(fn [data]
(or (wtts/get-selected-token-set-tokens data) {}))
st/state
=))
(def workspace-file-colors
(l/derived (fn [data]

View file

@ -856,7 +856,7 @@
shape (when-not multiple
(first (deref (refs/objects-by-id ids))))
tokens (mf/deref refs/workspace-tokens)
tokens (mf/deref refs/workspace-selected-token-set-tokens)
spacing-tokens (mf/use-memo (mf/deps tokens) #(:spacing (wtc/group-tokens-by-type tokens)))
spacing-column-options (mf/use-memo

View file

@ -101,7 +101,7 @@
selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
selection-parents (mf/deref selection-parents-ref)
tokens (-> (mf/deref refs/workspace-tokens)
tokens (-> (mf/deref refs/workspace-active-theme-sets-tokens)
(sd/use-resolved-tokens))
tokens-by-type (mf/use-memo (mf/deps tokens) #(wtc/group-tokens-by-type tokens))

View file

@ -311,7 +311,7 @@
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
token-id (:token-id mdata)
token (get (mf/deref refs/workspace-tokens) token-id)]
token (get (mf/deref refs/workspace-selected-token-set-tokens) token-id)]
(mf/use-effect
(mf/deps mdata)
(fn []

View file

@ -58,6 +58,6 @@
{:global global}))
(defn download-tokens-as-json []
(let [all-tokens (deref refs/workspace-tokens)
(let [all-tokens (deref refs/workspace-selected-token-set-tokens)
transformed-tokens-json (transform-tokens-into-json-format all-tokens)]
(export-tokens-file transformed-tokens-json)))

View file

@ -99,10 +99,10 @@ Token names should only contain letters and digits separated by . characters.")}
empty-input? (p/rejected nil)
direct-self-reference? (p/rejected :error/token-direct-self-reference)
:else (let [token-id (or (:id token) (random-uuid))
new-tokens (update tokens token-id merge {:id token-id
:value input
:name token-name})]
(-> (sd/resolve-tokens+ new-tokens #_ {:debug? true})
new-tokens (update tokens token-name merge {:id token-id
:value input
:name token-name})]
(-> (sd/resolve-tokens+ new-tokens {:names-map? true})
(p/then
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
@ -139,29 +139,33 @@ Token names should only contain letters and digits separated by . characters.")}
timeout))))]
debounced-resolver-callback))
(defonce form-token-cache-atom (atom nil))
(mf/defc form
{::mf/wrap-props false}
[{:keys [token token-type] :as _args}]
(let [tokens (mf/deref refs/workspace-tokens)
resolved-tokens (sd/use-resolved-tokens tokens)
(let [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 {:names-map? true
:cache-atom form-token-cache-atom})
token-path (mf/use-memo
(mf/deps (:name token))
#(wtt/token-name->path (:name token)))
tokens-tree (mf/use-memo
(mf/deps token-path resolved-tokens)
(fn []
(-> (wtt/token-names-tree resolved-tokens)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))
selected-set-tokens-tree (mf/use-memo
(mf/deps token-path selected-set-tokens)
(fn []
(-> (wtt/token-names-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-callback
(mf/deps tokens-tree)
(mf/deps selected-set-tokens-tree)
(fn [value]
(let [schema (token-name-schema {:token token
:tokens-tree tokens-tree})]
:tokens-tree selected-set-tokens-tree})]
(m/explain schema (finalize-name value)))))
on-update-name-debounced (mf/use-callback
(debounce (fn [e]
@ -187,7 +191,7 @@ Token names should only contain letters and digits separated by . characters.")}
(= token-or-err :error/token-missing-reference) token-or-err
(:resolved-value token-or-err) (:resolved-value token-or-err))]
(reset! token-resolve-result v))))
on-update-value-debounced (use-debonced-resolve-callback name-ref token tokens set-resolve-value)
on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
on-update-value (mf/use-callback
(mf/deps on-update-value-debounced)
(fn [e]

View file

@ -7,95 +7,81 @@
(ns app.main.ui.workspace.tokens.sets
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(def active-sets #{#uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3"
#uuid "d608877b-842a-473b-83ca-b5f8305caf83"})
(def sets-root-order [#uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3"
#uuid "9c5108aa-bdb4-409c-a3c8-c3dfce2f8bf8"
#uuid "0381446e-1f1d-423f-912c-ab577d61b79b"])
(def sets {#uuid "9c5108aa-bdb4-409c-a3c8-c3dfce2f8bf8" {:type :group
:name "Group A"
:children [#uuid "d1754e56-3510-493f-8287-5ef3417d4141"
#uuid "d608877b-842a-473b-83ca-b5f8305caf83"]}
#uuid "d608877b-842a-473b-83ca-b5f8305caf83" {:type :set
:name "Set A / 1"}
#uuid "d1754e56-3510-493f-8287-5ef3417d4141" {:type :group
:name "Group A / B"
:children [#uuid "f608877b-842a-473b-83ca-b5f8305caf83"
#uuid "7cc05389-9391-426e-bc0e-ba5cb8f425eb"]}
#uuid "f608877b-842a-473b-83ca-b5f8305caf83" {:type :set
:name "Set A / B / 1"}
#uuid "7cc05389-9391-426e-bc0e-ba5cb8f425eb" {:type :set
:name "Set A / B / 2"}
#uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3" {:type :set
:name "Set Root 1"}
#uuid "0381446e-1f1d-423f-912c-ab577d61b79b" {:type :set
:name "Set Root 2"}})
(def ^:private chevron-icon
(i/icon-xref :arrow (stl/css :chevron-icon)))
(defn set-selected-set
[set-id]
(dm/assert! (uuid? set-id))
(ptk/reify ::set-selected-set
ptk/UpdateEvent
(update [_ state]
(assoc state :selected-set-id set-id))))
(defn on-toggle-token-set-click [id event]
(dom/stop-propagation event)
(st/emit! (wdt/toggle-token-set id)))
(defn on-select-token-set-click [id event]
(st/emit! (wdt/set-selected-token-set-id id)))
(defn on-delete-token-set-click [id event]
(dom/stop-propagation event)
(st/emit! (wdt/delete-token-set id)))
(mf/defc sets-tree
[{:keys [selected-set-id set-id]}]
(let [set (get sets set-id)]
(when set
(let [{:keys [type name children]} set
visible? (mf/use-state (contains? active-sets set-id))
collapsed? (mf/use-state false)
icon (if (= type :set) i/document i/group)
selected? (mf/use-state (= set-id selected-set-id))
on-click
(mf/use-fn
(mf/deps type set-id)
(fn [event]
(dom/stop-propagation event)
(st/emit! (set-selected-set set-id))))]
[:div {:class (stl/css :set-item-container)
:on-click on-click}
[:div {:class (stl/css-case :set-item-group (= type :group)
:set-item-set (= type :set)
:selected-set (and (= type :set) @selected?))}
(when (= type :group)
[:span {:class (stl/css-case
:collapsabled-icon true
:collapsed @collapsed?)
:on-click #(when (= type :group) (swap! collapsed? not))}
chevron-icon])
[:span {:class (stl/css :icon)} icon]
[:div {:class (stl/css :set-name)} name]
(when (= type :set)
[:span {:class (stl/css :action-btn)
:on-click #(swap! visible? not)}
(if @visible?
i/shown
i/hide)])]
(when (and children (not @collapsed?))
[:div {:class (stl/css :set-children)}
(for [child-id children]
[:& sets-tree {:key child-id :set-id child-id :selected-set-id selected-set-id}])])]))))
[{:keys [token-set token-set-active? token-set-selected?] :as _props}]
(let [{:keys [id name _children]} token-set
selected? (and set? (token-set-selected? id))
visible? (token-set-active? id)
collapsed? (mf/use-state false)
set? true #_(= type :set)
group? false #_(= type :group)]
[:div {:class (stl/css :set-item-container)
:on-click #(on-select-token-set-click id %)}
[:div {:class (stl/css-case :set-item-group group?
:set-item-set set?
:selected-set selected?)}
(when group?
[:span {:class (stl/css-case :collapsabled-icon true
:collapsed @collapsed?)
:on-click #(swap! collapsed? not)}
chevron-icon])
[:span {:class (stl/css :icon)}
(if set? i/document i/group)]
[:div {:class (stl/css :set-name)} name]
[:div {:class (stl/css :delete-set)}
[:button {:on-click #(on-delete-token-set-click id %)}
i/delete]]
(when set?
[:span {:class (stl/css :action-btn)
:on-click #(on-toggle-token-set-click id %)}
(if visible? i/shown i/hide)])]
#_(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)])])]))
(mf/defc sets-list
[{:keys [selected-set-id]}]
[:ul {:class (stl/css :sets-list)}
(for [set-id sets-root-order]
[:& sets-tree {:key set-id
:set-id set-id
:selected-set-id selected-set-id}])])
[{:keys []}]
(let [token-sets (mf/deref refs/workspace-token-sets)
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)
token-set-selected? (mf/use-callback
(mf/deps selected-token-set-id)
(fn [id]
(= id selected-token-set-id)))
active-token-set-ids (mf/deref refs/workspace-active-set-ids)
token-set-active? (mf/use-callback
(mf/deps active-token-set-ids)
(fn [id]
(get active-token-set-ids id)))]
[:ul {:class (stl/css :sets-list)}
(for [[id token-set] token-sets]
[:& sets-tree
{:key id
:token-set token-set
:selected-token-set-id selected-token-set-id
:token-set-selected? token-set-selected?
:token-set-active? token-set-active?}])]))

View file

@ -8,7 +8,7 @@
.sets-list {
width: 100%;
margin-bottom: $s-12;
margin-bottom: 0;
overflow-y: auto;
}
@ -28,6 +28,8 @@
width: 100%;
cursor: pointer;
color: var(--layer-row-foreground-color);
padding-right: $s-2;
.set-name {
@include textEllipsis;
flex-grow: 1;
@ -56,6 +58,10 @@
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);
.delete-set {
visibility: visible;
}
}
}
@ -65,6 +71,27 @@
box-shadow: -100px 0 0 0 var(--layer-row-background-color-selected);
}
.delete-set {
@extend .button-tertiary;
height: $s-28;
width: $s-28;
visibility: hidden;
button {
@include buttonStyle;
@include flexCenter;
width: $s-24;
height: 100%;
svg {
@extend .button-icon-small;
height: $s-12;
width: $s-12;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
}
}
.action-btn {
@extend .button-tertiary;
height: $s-28;

View file

@ -10,12 +10,14 @@
[app.common.data :as d]
[app.main.data.modal :as modal]
[app.main.data.tokens :as dt]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]]
[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.common :refer [labeled-input]]
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.core :as wtc]
[app.main.ui.workspace.tokens.sets :refer [sets-list]]
@ -23,6 +25,7 @@
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom]
[app.util.storage :refer [storage]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]
@ -37,11 +40,19 @@
(def selected-set-id
(l/derived :selected-set-id st/state))
;; Event Functions -------------------------------------------------------------
(defn on-set-add-click [_event]
(when-let [set-name (js/window.prompt "Set name")]
(st/emit! (wdt/create-token-set {:name set-name}))))
;; Components ------------------------------------------------------------------
(mf/defc token-pill
{::mf/wrap-props false}
[{:keys [on-click token highlighted? on-context-menu]}]
[{:keys [on-click token theme-token highlighted? on-context-menu] :as props}]
(let [{:keys [name value resolved-value errors]} token
errors? (seq errors)]
errors? (and (seq errors) (seq (:errors theme-token)))]
[:button {:class (stl/css-case :token-pill true
:token-pill-highlighted highlighted?
:token-pill-invalid errors?)
@ -75,7 +86,7 @@
i/add))
(mf/defc token-component
[{:keys [type tokens selected-shapes token-type-props]}]
[{: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
@ -95,6 +106,7 @@
(fn [event]
(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
@ -115,7 +127,6 @@
[:& 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?}
@ -127,12 +138,14 @@
[:& cmm/asset-section-block {:role :content}
[:div {:class (stl/css :token-pills-wrapper)}
(for [token (sort-by :modified-at tokens)]
[:& token-pill
{:key (:id token)
:token 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)}])]])]]))
(let [theme-token (get active-theme-tokens (wtt/token-identifier token))]
[:& token-pill
{:key (:id 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.
@ -149,32 +162,77 @@
{:empty (sort-by :token-key empty)
:filled (sort-by :token-key filled)}))
(mf/defc tokens-explorer
(mf/defc tokene-theme-create
[_props]
(let [objects (mf/deref refs/workspace-page-objects)
(let [group (mf/use-state "")
name (mf/use-state "")]
[:div {:style {:display "flex"
:flex-direction "column"
:gap "10px"}}
[:& labeled-input {:label "Group name"
:input-props {:value @group
:on-change #(reset! group (dom/event->value %))}}]
[:& labeled-input {:label "Theme name"
:input-props {:value @name
:on-change #(reset! name (dom/event->value %))}}]
[:button {:on-click #(st/emit! (wdt/create-token-theme {:group @group
:name @name}))}
"Create"]]))
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
tokens (-> (mf/deref refs/workspace-tokens)
(sd/use-resolved-tokens))
token-groups (mf/with-memo [tokens]
(sorted-token-groups tokens))]
[:article
[:& token-context-menu]
[: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
:tokens tokens
:token-type-props token-type-props}])]]))
(mf/defc themes-sidebar
[_props]
(let [open? (mf/use-state true)
active-theme-ids (mf/deref refs/workspace-active-theme-ids)
themes (mf/deref refs/workspace-ordered-token-themes)]
[:div {:class (stl/css :sets-sidebar)}
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title "THEMES"
:on-collapsed #(swap! open? not)}]]
(when @open?
[:div
[:style
(str "@scope {"
(str/join "\n"
["ul { list-style-type: circle; margin-left: 20px; }"
".spaced { display: flex; gap: 10px; justify-content: space-between; }"
".spaced-y { display: flex; flex-direction: column; gap: 10px }"
".selected { font-weight: 600; }"
"b { font-weight: 600; }"])
"}")]
[:div.spaced-y
{:style {:padding "10px"}}
[:& tokene-theme-create]
[:div.spaced-y
[:b "Themes"]
[:ul
(for [[group themes] themes]
[:li
{:key (str "token-theme-group" group)}
group
[:ul
(for [{:keys [id name] :as _theme} themes]
[:li {:key (str "tokene-theme-" id)}
[:div.spaced
name
[:div.spaced
[:button
{:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme id)))}
(if (get active-theme-ids id) "✅" "❎")]
[:button {:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/delete-token-theme id)))}
"🗑️"]]]])]])]]]])]))
(mf/defc sets-sidebar
[]
(let [selected-set-id (mf/deref selected-set-id)
open? (mf/use-state true)]
(let [open? (mf/use-state true)]
[:div {:class (stl/css :sets-sidebar)}
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
@ -183,19 +241,76 @@
:title "SETS"
:on-collapsed #(swap! open? not)}]
[:button {:class (stl/css :add-set)
:on-click #(println "Add Set")}
:on-click #(do
(reset! open? true)
(on-set-add-click %))}
i/add]]
(when @open?
[:& sets-list {:selected-set-id selected-set-id}])]))
[:& sets-list])]))
(mf/defc tokens-explorer
[_props]
(let [open? (mf/use-state true)
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))]
[:article
[:& token-context-menu]
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title "TOKENS"
:on-collapsed #(swap! open? not)}]
(when @open?
[: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}])])]))
(defn dev-or-preview-url? [url]
(let [host (-> url js/URL. .-host)
localhost? (= "localhost" (first (str/split host #":")))
pr? (str/ends-with? host "penpot.alpha.tokens.studio")]
(or localhost? pr?)))
(defn location-url-dev-or-preview-url!? []
(dev-or-preview-url? js/window.location.href))
(defn temp-use-themes-flag []
(let [show? (mf/use-state (or
(location-url-dev-or-preview-url!?)
(get @storage ::show-token-themes-sets?)
false))]
(mf/use-effect
(fn []
(letfn [(toggle! []
(swap! storage update ::show-token-themes-sets? not)
(reset! show? (get @storage ::show-token-themes-sets?)))]
(set! js/window.toggleThemes toggle!))))
show?))
(mf/defc tokens-sidebar-tab
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[_props]
(let [show-sets-section? false] ;; temporarily added this variable to see/hide the sets section till we have it working end to end
(let [show-sets-section? (deref (temp-use-themes-flag))]
[:div {:class (stl/css :sidebar-tab-wrapper)}
(when show-sets-section?
[:div {:class (stl/css :sets-section-wrapper)}
[:& themes-sidebar]
[:& sets-sidebar]])
[:div {:class (stl/css :tokens-section-wrapper)}
[:& tokens-explorer]]

View file

@ -2,6 +2,7 @@
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.data :refer [ordered-map]]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.token :as wtt]
[cuerdas.core :as str]
@ -73,13 +74,15 @@
(get errors :style-dictionary/missing-reference)))
(defn resolve-tokens+
[tokens & {:keys [debug?] :as config}]
[tokens & {:keys [names-map? debug?] :as config}]
(p/let [sd-tokens (-> (wtt/token-names-tree tokens)
(resolve-sd-tokens+ config))]
(let [resolved-tokens (reduce
(fn [acc ^js cur]
(let [id (uuid (.-uuid (.-id cur)))
origin-token (get tokens id)
(let [identifier (if names-map?
(.. cur -original -name)
(uuid (.-uuid (.-id cur))))
origin-token (get tokens identifier)
parsed-value (wtt/parse-token-value (.-value cur))
resolved-token (if (not parsed-value)
(assoc origin-token :errors [:style-dictionary/missing-reference])
@ -92,15 +95,12 @@
(js/console.log "Resolved tokens" resolved-tokens))
resolved-tokens)))
(defn resolve-workspace-tokens+
[& {:keys [debug?] :as config}]
(when-let [workspace-tokens @refs/workspace-tokens]
(resolve-tokens+ workspace-tokens)))
;; Hooks -----------------------------------------------------------------------
(defonce !tokens-cache (atom nil))
(defonce !theme-tokens-cache (atom nil))
(defn get-cached-tokens [tokens]
(get @!tokens-cache tokens tokens))
@ -109,11 +109,12 @@
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]
:or {cache-atom !tokens-cache}}]
(let [tokens-state (mf/use-state (get @cache-atom tokens tokens))]
[tokens & {:keys [cache-atom _names-map?]
:or {cache-atom !tokens-cache}
:as config}]
(let [tokens-state (mf/use-state (get @cache-atom tokens))]
(mf/use-effect
(mf/deps tokens)
(mf/deps tokens config)
(fn []
(let [cached (get @cache-atom tokens)]
(cond
@ -122,7 +123,7 @@
;; Get the cached entry
(some? cached) (reset! tokens-state cached)
;; No cached entry, start processing
:else (let [promise+ (resolve-tokens+ tokens)]
:else (let [promise+ (resolve-tokens+ tokens config)]
(swap! cache-atom assoc tokens promise+)
(p/then promise+ (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens)
@ -130,39 +131,11 @@
@tokens-state))
(defn use-resolved-workspace-tokens [& {:as config}]
(-> (mf/deref refs/workspace-tokens)
(-> (mf/deref refs/workspace-selected-token-set-tokens)
(use-resolved-tokens config)))
;; Testing ---------------------------------------------------------------------
(comment
(defonce !output (atom nil))
(-> @refs/workspace-tokens
(resolve-tokens+ {:debug? false})
(.then js/console.log))
(-> (resolve-workspace-tokens+ {:debug? true})
(p/then #(reset! !output %)))
@!output
(->> @refs/workspace-tokens
(resolve-tokens+)
(#(doto % js/console.log)))
(->
(clj->js {"a" {:name "a" :value "5"}
"b" {:name "b" :value "{a} * 2"}})
(#(resolve-sd-tokens+ % {:debug? true})))
(defonce output (atom nil))
(require '[shadow.resource])
(let [example (-> (shadow.resource/inline "./data/example-tokens-set.json")
(js/JSON.parse)
.-core)]
(.then (resolve-sd-tokens+ example {:debug? true})
#(reset! output %)))
nil)
(defn use-active-theme-sets-tokens [& {:as config}]
(-> (mf/deref refs/workspace-active-theme-sets-tokens)
(use-resolved-tokens (merge {:cache-atom !theme-tokens-cache
:names-map? true}
config))))

View file

@ -4,6 +4,14 @@
[clojure.set :as set]
[cuerdas.core :as str]))
(defn get-workspace-tokens
[state]
(get-in state [:workspace-data :tokens] {}))
(defn get-workspace-token
[token-id state]
(get-in state [:workspace-data :tokens token-id]))
(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."

View file

@ -0,0 +1,150 @@
(ns app.main.ui.workspace.tokens.token-set
(:require
[app.common.data :refer [ordered-map]]
[app.main.ui.workspace.tokens.token :as wtt]
[clojure.set :as set]))
;; Themes ----------------------------------------------------------------------
(defn get-theme-group [theme]
(:group theme))
(defn get-workspace-themes [state]
(get-in state [:workspace-data :token-themes] []))
(defn get-workspace-themes-index [state]
(get-in state [:workspace-data :token-themes-index] {}))
(defn get-workspace-token-set-groups [state]
(get-in state [:workspace-data :token-set-groups]))
(defn get-workspace-ordered-themes [state]
(let [themes (get-workspace-themes state)
themes-index (get-workspace-themes-index state)]
(->> (map #(get themes-index %) themes)
(group-by :group))))
(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 get-active-theme-ids-or-fallback [state]
(let [active-theme-ids (get-active-theme-ids state)
temp-theme-id (get-temp-theme-id state)]
(cond
(seq active-theme-ids) active-theme-ids
temp-theme-id #{temp-theme-id})))
(defn get-active-set-ids [state]
(let [active-theme-ids (get-active-theme-ids-or-fallback state)
themes-index (get-workspace-themes-index state)
active-set-ids (reduce
(fn [acc cur]
(if-let [sets (get-in themes-index [cur :sets])]
(set/union acc sets)
acc))
#{} active-theme-ids)]
active-set-ids))
(defn get-ordered-active-set-ids [state]
(let [active-set-ids (get-active-set-ids state)
token-set-groups (get-workspace-token-set-groups state)]
(filter active-set-ids token-set-groups)))
(defn theme-ids-with-group
"Returns set of theme-ids that share the same `:group` property as the theme with `theme-id`.
Will also return matching theme-ids without a `:group` property."
[theme-id state]
(let [themes (get-workspace-themes-index state)
theme-group (get-in themes [theme-id :group])
same-group-theme-ids (->> themes
(eduction
(map val)
(filter #(= (:group %) theme-group))
(map :id))
(into #{}))]
same-group-theme-ids))
(defn toggle-active-theme-id
"Toggle a `theme-id` by checking `:token-active-themes`.
De-activate all theme-ids that have the same group as `theme-id` when activating `theme-id`.
Ensures that the temporary theme id is selected when the resulting set is empty."
[theme-id state]
(let [temp-theme-id-set (some->> (get-temp-theme-id state) (conj #{}))
active-theme-ids (get-active-theme-ids state)
add? (not (get active-theme-ids theme-id))
;; Deactivate themes with the same group when activating a theme
same-group-ids (when add? (theme-ids-with-group theme-id state))
theme-ids-without-same-group (set/difference active-theme-ids
same-group-ids
temp-theme-id-set)
new-themes (if add?
(conj theme-ids-without-same-group theme-id)
(disj theme-ids-without-same-group theme-id))]
(if (empty? new-themes)
(or temp-theme-id-set #{})
new-themes)))
(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))
(defn toggle-token-set-to-token-theme [token-set-id token-theme]
(update token-theme :sets #(if (get % token-set-id)
(disj % token-set-id)
(conj % token-set-id))))
;; Sets ------------------------------------------------------------------------
(defn get-workspace-sets [state]
(get-in state [:workspace-data :token-sets-index]))
(defn get-token-set [set-id state]
(some-> (get-workspace-sets state)
(get set-id)))
(defn get-workspace-token-set-tokens [set-id state]
(-> (get-token-set set-id state)
:tokens))
(defn get-selected-token-set-id [state]
(or (get-in state [:workspace-local :selected-token-set-id])
(get-in state [:workspace-data :token-set-groups 0])))
(defn get-selected-token-set [state]
(when-let [id (get-selected-token-set-id state)]
(get-token-set id state)))
(defn get-selected-token-set-tokens [state]
(when-let [token-set (get-selected-token-set state)]
(let [tokens (or (wtt/get-workspace-tokens state) {})]
(select-keys tokens (:tokens token-set)))))
(defn assoc-selected-token-set-id [state id]
(assoc-in state [:workspace-local :selected-token-set-id] id))
(defn get-active-theme-sets-tokens-names-map [state]
(let [active-set-ids (get-ordered-active-set-ids state)]
(reduce
(fn [names-map-acc set-id]
(let [token-ids (get-workspace-token-set-tokens set-id state)]
(reduce
(fn [acc token-id]
(if-let [token (wtt/get-workspace-token token-id state)]
(assoc acc (wtt/token-identifier token) token)
acc))
names-map-acc token-ids)))
(ordered-map) active-set-ids)))

View file

@ -6,6 +6,7 @@
[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]
@ -120,13 +121,14 @@
ptk/WatchEvent
(watch [_ state _]
(->>
(rx/from (wtsd/resolve-tokens+ (get-in state [:workspace-data :tokens])))
(rx/from
(->
(wtts/get-active-theme-sets-tokens-names-map state)
(wtsd/resolve-tokens+ {:names-map? true})))
(rx/mapcat
(fn [sd-tokens]
(let [undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/concat
(->> (update-tokens sd-tokens)
(rx/concat)))
(update-tokens sd-tokens)
(rx/of (dwu/commit-undo-transaction undo-id))))))))))

View file

@ -4,8 +4,10 @@
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.main.data.tokens :as wdt]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-set :as wtts]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]
@ -18,24 +20,70 @@
(log/set-level! "app.main.data.changes" :error)
(thp/reset-idmap!))})
(defn setup-file
(defn setup-file []
(cthf/sample-file :file-1 :page-label :page-1))
(def border-radius-token
{:value "12"
:name "borderRadius.sm"
:type :border-radius})
(def ^:private reference-border-radius-token
{:value "{borderRadius.sm} * 2"
:name "borderRadius.md"
:type :border-radius})
(defn setup-file-with-tokens
[& {:keys [rect-1 rect-2 rect-3]}]
(-> (cthf/sample-file :file-1 :page-label :page-1)
(-> (setup-file)
(ctho/add-rect :rect-1 rect-1)
(ctho/add-rect :rect-2 rect-2)
(ctho/add-rect :rect-3 rect-3)
(toht/add-token :token-1 {:value "12"
:name "borderRadius.sm"
:type :border-radius})
(toht/add-token :token-2 {:value "{borderRadius.sm} * 2"
:name "borderRadius.md"
:type :border-radius})))
(toht/add-token :token-1 border-radius-token)
(toht/add-token :token-2 reference-border-radius-token)))
(t/deftest test-create-token
(t/testing "creates token in new token set"
(t/async
done
(let [file (setup-file)
store (ths/setup-store file)
events [(wdt/update-create-token border-radius-token)]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [set-id (wtts/get-selected-token-set-id new-state)
token-set (wtts/get-token-set set-id new-state)
set-tokens (wtts/get-active-theme-sets-tokens-names-map new-state)]
(t/testing "selects created workspace set and adds token to it"
(t/is (some? token-set))
(t/is (= 1 (count set-tokens)))
(t/is (= (list border-radius-token) (->> (vals set-tokens)
(map #(dissoc % :id :modified-at)))))))))))))
(t/deftest test-create-multiple-tokens
(t/testing "uses selected tokens set when creating multiple tokens"
(t/async
done
(let [file (setup-file)
store (ths/setup-store file)
events [(wdt/update-create-token border-radius-token)
(wdt/update-create-token reference-border-radius-token)]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [set-tokens (wtts/get-active-theme-sets-tokens-names-map new-state)]
(t/testing "selects created workspace set and adds token to it"
(t/is (= 2 (count set-tokens)))
(t/is (= (list border-radius-token reference-border-radius-token)
(->> (vals set-tokens)
(map #(dissoc % :id :modified-at)))))))))))))
(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)
(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)]
@ -60,7 +108,7 @@
(t/testing "applying a token twice with the same attributes will override the previously applied tokens values"
(t/async
done
(let [file (setup-file)
(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)]
@ -89,7 +137,7 @@
(t/testing "removes old token attributes and applies only single attribute"
(t/async
done
(let [file (setup-file)
(let [file (setup-file-with-tokens)
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [;; Apply `:token-1` to all border radius attributes
@ -123,7 +171,7 @@
(t/testing "applies dimensions token and updates the shapes width and height"
(t/async
done
(let [file (-> (setup-file)
(let [file (-> (setup-file-with-tokens)
(toht/add-token :token-target {:value "100"
:name "dimensions.sm"
:type :dimensions}))
@ -151,7 +199,7 @@
(t/testing "applies sizing token and updates the shapes width and height"
(t/async
done
(let [file (-> (setup-file)
(let [file (-> (setup-file-with-tokens)
(toht/add-token :token-target {:value "100"
:name "sizing.sm"
:type :sizing}))
@ -179,7 +227,7 @@
(t/testing "applies opacity token and updates the shapes opacity"
(t/async
done
(let [file (-> (setup-file)
(let [file (-> (setup-file-with-tokens)
(toht/add-token :opacity-float {:value "0.3"
:name "opacity.float"
:type :opacity})
@ -229,7 +277,7 @@
(t/testing "applies rotation token and updates the shapes rotation"
(t/async
done
(let [file (-> (setup-file)
(let [file (-> (setup-file-with-tokens)
(toht/add-token :token-target {:value "120"
:name "rotation.medium"
:type :rotation}))
@ -253,11 +301,11 @@
(t/testing "applies stroke-width token and updates the shapes with stroke"
(t/async
done
(let [file (-> (setup-file {:rect-1 {:strokes [{:stroke-alignment :inner,
:stroke-style :solid,
:stroke-color "#000000",
:stroke-opacity 1,
:stroke-width 5}]}})
(let [file (-> (setup-file-with-tokens {:rect-1 {:strokes [{:stroke-alignment :inner,
:stroke-style :solid,
:stroke-color "#000000",
:stroke-opacity 1,
:stroke-width 5}]}})
(toht/add-token :token-target {:value "10"
:name "stroke-width.sm"
:type :stroke-width}))
@ -286,7 +334,7 @@
(t/testing "should apply token to all selected items, where no item has the token applied"
(t/async
done
(let [file (setup-file)
(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)
@ -314,7 +362,7 @@
(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)
(let [file (-> (setup-file-with-tokens)
(toht/apply-token-to-shape :rect-1 :token-1 #{:rx :ry})
(toht/apply-token-to-shape :rect-3 :token-2 #{:rx :ry}))
store (ths/setup-store file)
@ -348,7 +396,7 @@
(t/testing "should apply token to all if none of the shapes has it applied"
(t/async
done
(let [file (-> (setup-file)
(let [file (-> (setup-file-with-tokens)
(toht/apply-token-to-shape :rect-1 :token-2 #{:rx :ry})
(toht/apply-token-to-shape :rect-3 :token-2 #{:rx :ry}))
store (ths/setup-store file)

View file

@ -2,7 +2,8 @@
(:require
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[cljs.test :as t :include-macros true]
[promesa.core :as p]))
[promesa.core :as p]
[app.main.ui.workspace.tokens.token :as wtt]))
(def border-radius-token
{:id #uuid "8c868278-7c8d-431b-bbc9-7d8f15c8edb9"
@ -22,7 +23,7 @@
(t/deftest resolve-tokens-test
(t/async
done
(t/testing "resolves tokens using style-dictionary"
(t/testing "resolves tokens using style-dictionary from a ids map"
(-> (sd/resolve-tokens+ tokens)
(p/finally (fn [resolved-tokens]
(let [expected-tokens {"borderRadius.sm"
@ -35,3 +36,22 @@
:resolved-unit "px")}]
(t/is (= expected-tokens resolved-tokens))
(done))))))))
(t/deftest resolve-tokens-names-map-test
(t/async
done
(t/testing "resolves tokens using style-dictionary from a names map"
(-> (vals tokens)
(wtt/token-names-map)
(sd/resolve-tokens+ {:names-map? true})
(p/finally (fn [resolved-tokens]
(let [expected-tokens {"borderRadius.sm"
(assoc border-radius-token
:resolved-value 12
:resolved-unit "px")
"borderRadius.md-with-dashes"
(assoc reference-border-radius-token
:resolved-value 24
:resolved-unit "px")}]
(t/is (= expected-tokens resolved-tokens))
(done))))))))

View file

@ -0,0 +1,37 @@
(ns token-tests.token-set-test
(:require
[app.main.ui.workspace.tokens.token-set :as wtts]
[cljs.test :as t]))
(t/deftest toggle-active-theme-id-test
(t/testing "toggles active theme id"
(let [state {:workspace-data {:token-themes-index {1 {:id 1}}}}]
(t/testing "activates theme with id")
(t/is (= (wtts/toggle-active-theme-id 1 state) #{1})))
(let [state {:workspace-data {:token-active-themes #{1}
:token-themes-index {1 {:id 1}}}}]
(t/testing "missing temp theme returns empty set"
(t/is (= #{} (wtts/toggle-active-theme-id 1 state)))))
(let [state {:workspace-data {:token-theme-temporary-id :temp
:token-active-themes #{1}
:token-themes-index {1 {:id 1}}}}]
(t/testing "empty set returns temp theme"
(t/is (= #{:temp} (wtts/toggle-active-theme-id 1 state)))))
(let [state {:workspace-data {:token-active-themes #{2 3 4}
:token-themes-index {1 {:id 1}
2 {:id 2}
3 {:id 3}
4 {:id 4 :group :different}}}}]
(t/testing "removes same group themes and keeps different group themes"
(t/is (= #{1 4} (wtts/toggle-active-theme-id 1 state)))))
(let [state {:workspace-data {:token-active-themes #{1 2 3 4}}
:token-themes-index {1 {:id 1}
2 {:id 2}
3 {:id 3}
4 {:id 4 :group :different}}}]
(t/testing "removes theme when active"
(t/is (= #{4 3 2} (wtts/toggle-active-theme-id 1 state)))))))