diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 9505175cb..d8c05619a 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -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] diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 35f4a32b7..ed779cb5c 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -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 diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index fefef7b75..593ec5d2d 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -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)) diff --git a/common/src/app/common/types/token_theme.cljc b/common/src/app/common/types/token_theme.cljc new file mode 100644 index 000000000..d76f1e277 --- /dev/null +++ b/common/src/app/common/types/token_theme.cljc @@ -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]]]) diff --git a/common/src/app/common/types/tokens_theme_list.cljc b/common/src/app/common/types/tokens_theme_list.cljc new file mode 100644 index 000000000..f9c3857ff --- /dev/null +++ b/common/src/app/common/types/tokens_theme_list.cljc @@ -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)))))) diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index a27c7f30c..d40734f70 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -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) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 65efa9f93..b1177e8c2 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 294185fe6..ef32a4e67 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index e786b1141..2f9dd5a3a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index 57e6311d1..56c392f3c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -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 [] diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs index a68d7e046..489d3f041 100644 --- a/frontend/src/app/main/ui/workspace/tokens/core.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -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))) diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index 5e3351940..c02783ac0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 01a2c212e..852844bf5 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -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?}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index 5c32bf199..a2049236e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -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; diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 7729f41df..b9e6c6a87 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -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]] diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index 3ce334a02..62b068a32 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -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)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index 9c0efd356..aba9ae9a0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -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." diff --git a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs new file mode 100644 index 000000000..6a6a737c8 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs @@ -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))) diff --git a/frontend/src/app/main/ui/workspace/tokens/update.cljs b/frontend/src/app/main/ui/workspace/tokens/update.cljs index 1543d8d06..2f64aa65b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/update.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/update.cljs @@ -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)))))))))) diff --git a/frontend/test/token_tests/logic/token_actions_test.cljs b/frontend/test/token_tests/logic/token_actions_test.cljs index e0d3fac38..0fb1ea5ec 100644 --- a/frontend/test/token_tests/logic/token_actions_test.cljs +++ b/frontend/test/token_tests/logic/token_actions_test.cljs @@ -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) diff --git a/frontend/test/token_tests/style_dictionary_test.cljs b/frontend/test/token_tests/style_dictionary_test.cljs index cc96abe1b..638fa7e3c 100644 --- a/frontend/test/token_tests/style_dictionary_test.cljs +++ b/frontend/test/token_tests/style_dictionary_test.cljs @@ -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)))))))) diff --git a/frontend/test/token_tests/token_set_test.cljs b/frontend/test/token_tests/token_set_test.cljs new file mode 100644 index 000000000..d199e221c --- /dev/null +++ b/frontend/test/token_tests/token_set_test.cljs @@ -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)))))))