From e5c5640413a05a68378f076a827e67fbea2dbb79 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 25 Apr 2024 19:00:00 +0200 Subject: [PATCH] Start tokens studio plugin base --- common/src/app/common/files/changes.cljc | 32 +++++ .../src/app/common/files/changes_builder.cljc | 24 ++++ common/src/app/common/types/file.cljc | 3 + common/src/app/common/types/shape.cljc | 4 +- common/src/app/common/types/shape/attrs.cljc | 4 + common/src/app/common/types/token.cljc | 80 +++++++++++++ common/src/app/common/types/tokens_list.cljc | 45 +++++++ frontend/src/app/main/data/tokens.cljs | 110 +++++++++++++++++ .../src/app/main/data/workspace/layout.cljs | 6 +- frontend/src/app/main/ui/workspace.cljs | 1 + .../src/app/main/ui/workspace/sidebar.cljs | 8 +- .../app/main/ui/workspace/tokens/common.cljs | 41 +++++++ .../app/main/ui/workspace/tokens/common.scss | 32 +++++ .../app/main/ui/workspace/tokens/core.cljs | 112 ++++++++++++++++++ .../app/main/ui/workspace/tokens/modal.cljs | 96 +++++++++++++++ .../app/main/ui/workspace/tokens/modal.scss | 66 +++++++++++ .../app/main/ui/workspace/tokens/modals.cljs | 83 +++++++++++++ .../app/main/ui/workspace/tokens/sidebar.cljs | 106 +++++++++++++++++ .../app/main/ui/workspace/tokens/sidebar.scss | 30 +++++ 19 files changed, 880 insertions(+), 3 deletions(-) create mode 100644 common/src/app/common/types/token.cljc create mode 100644 common/src/app/common/types/tokens_list.cljc create mode 100644 frontend/src/app/main/data/tokens.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/common.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/common.scss create mode 100644 frontend/src/app/main/ui/workspace/tokens/core.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/modal.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/modal.scss create mode 100644 frontend/src/app/main/ui/workspace/tokens/modals.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/sidebar.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/sidebar.scss diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 978cc8edf..26fdac592 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -23,6 +23,8 @@ [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] + [app.common.types.token :as cto] + [app.common.types.tokens-list :as ctol] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as ctt] [clojure.set :as set])) @@ -232,6 +234,22 @@ [:del-typography [:map {:title "DelTypogrphyChange"} [:type [:= :del-typography]] + [:id ::sm/uuid]]] + + [:add-token + [:map {:title "AddTokenChange"} + [:type [:= :add-token]] + [:token ::cto/token]]] + + [:mod-token + [:map {:title "ModTokenChange"} + [:type [:= :mod-token]] + [:id ::sm/uuid] + [:token ::cto/token]]] + + [:del-token + [:map {:title "DelTokenChange"} + [:type [:= :del-token]] [:id ::sm/uuid]]]]]) (sm/define! ::changes @@ -669,6 +687,20 @@ [data {:keys [id]}] (ctyl/delete-typography data id)) +;; -- Tokens + +(defmethod process-change :add-token + [data {:keys [token]}] + (ctol/add-token data token)) + +(defmethod process-change :mod-token + [data {:keys [id token]}] + (ctol/update-token data id merge token)) + +(defmethod process-change :del-token + [data {:keys [id]}] + (ctol/delete-token data id)) + ;; === Operations (defmethod process-operation :set diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 7d693c370..83afd2b92 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -664,6 +664,30 @@ (update :undo-changes conj {:type :add-typography :typography prev-typography}) (apply-changes-local)))) +(defn add-token + [changes token] + (-> changes + (update :redo-changes conj {:type :add-token :token token}) + (update :undo-changes conj {:type :del-token :id (:id token)}) + (apply-changes-local))) + +(defn update-token + [changes token] + (let [token-id (:id token)] + (-> changes + (update :redo-changes conj {:type :mod-token :id token-id :token token}) + (apply-changes-local)))) + +(defn delete-token + [changes token-id] + (assert-library! changes) + (let [library-data (::library-data (meta changes)) + prev-token (get-in library-data [:tokens token-id])] + (-> changes + (update :redo-changes conj {:type :del-token :id token-id}) + (update :undo-changes conj {:type :add-token :token prev-token}) + (apply-changes-local)))) + (defn add-component ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page] (add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil)) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 0c5fbf572..7500845c4 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -24,6 +24,7 @@ [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] + [app.common.types.token :as cto] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as cty] [app.common.uuid :as uuid] @@ -55,6 +56,8 @@ [: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]]]) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 2b610ca45..d46214c6e 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -28,6 +28,7 @@ [app.common.types.shape.path :as ctsp] [app.common.types.shape.shadow :as ctss] [app.common.types.shape.text :as ctsx] + [app.common.types.token :as cto] [app.common.uuid :as uuid] [clojure.set :as set])) @@ -180,7 +181,8 @@ [:vector {:gen/max 1} ::ctss/shadow]] [:blur {:optional true} ::ctsb/blur] [:grow-type {:optional true} - [::sm/one-of #{:auto-width :auto-height :fixed}]]]) + [::sm/one-of #{:auto-width :auto-height :fixed}]] + [:applied-tokens {:optional true} ::cto/applied-tokens]]) (sm/define! ::group-attrs [:map {:title "GroupAttrs"} diff --git a/common/src/app/common/types/shape/attrs.cljc b/common/src/app/common/types/shape/attrs.cljc index 84fc30f81..75509094e 100644 --- a/common/src/app/common/types/shape/attrs.cljc +++ b/common/src/app/common/types/shape/attrs.cljc @@ -23,6 +23,8 @@ :show-content :hide-in-viewer + :applied-tokens + :opacity :blend-mode :blocked @@ -95,6 +97,8 @@ :parent-id :frame-id + :applied-tokens + :opacity :blend-mode :blocked diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc new file mode 100644 index 000000000..e29a54cc7 --- /dev/null +++ b/common/src/app/common/types/token.cljc @@ -0,0 +1,80 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.types.token + (:require + [app.common.schema :as sm] + [app.common.schema.registry :as sr])) + +(defn merge-schemas [& schema-keys] + (let [schemas (map #(get @sr/registry %) schema-keys)] + (reduce sm/merge schemas))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMA +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def token-types + #{:boolean + :border-radius + :box-shadow + :dimension + :numeric + :opacity + :other + :rotation + :sizing + :spacing + :string + :typography}) + +(sm/def! ::token + [:map {:title "Token"} + [:id ::sm/uuid] + [:name :string] + [:type [::sm/one-of token-types]] + [:value :any] + [:description {:optional true} :string] + [:modified-at {:optional true} ::sm/inst]]) + +(sm/def! ::border-radius + [:map + [:rx {:optional true} ::sm/uuid] + [:ry {:optional true} ::sm/uuid] + [:r1 {:optional true} ::sm/uuid] + [:r2 {:optional true} ::sm/uuid] + [:r3 {:optional true} ::sm/uuid] + [:r4 {:optional true} ::sm/uuid]]) + +(sm/def! ::dimensions + [:map + [:width {:optional true} ::sm/uuid] + [:height {:optional true} ::sm/uuid] + [:min-height {:optional true} ::sm/uuid] + [:max-height {:optional true} ::sm/uuid] + [:min-width {:optional true} ::sm/uuid] + [:max-width {:optional true} ::sm/uuid]]) + +(sm/def! ::spacing + [:map + [:spacing-column {:optional true} ::sm/uuid] + [:spacing-row {:optional true} ::sm/uuid] + [:padding-p1 {:optional true} ::sm/uuid] + [:padding-p2 {:optional true} ::sm/uuid] + [:padding-p3 {:optional true} ::sm/uuid] + [:padding-p4 {:optional true} ::sm/uuid] + [:padding-all {:optional true} ::sm/uuid] + [:position-x {:optional true} ::sm/uuid] + [:position-y {:optional true} ::sm/uuid]]) + +(sm/def! ::tokens + [:map {:title "Applied Tokens"}]) + +(sm/def! ::applied-tokens + (merge-schemas ::tokens + ::border-radius + ::dimensions + ::spacing)) diff --git a/common/src/app/common/types/tokens_list.cljc b/common/src/app/common/types/tokens_list.cljc new file mode 100644 index 000000000..58479f72e --- /dev/null +++ b/common/src/app/common/types/tokens_list.cljc @@ -0,0 +1,45 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.types.tokens-list + (:require + [app.common.data :as d] + [app.common.time :as dt])) + +(defn tokens-seq + "Returns a sequence of all tokens within the file data." + [file-data] + (vals (:tokens file-data))) + +(defn- touch + "Updates the `modified-at` timestamp of a token." + [token] + (assoc token :modified-at (dt/now))) + +(defn add-token + "Adds a new token to the file data, setting its `modified-at` timestamp." + [file-data token] + (update file-data :tokens assoc (:id token) (touch token))) + +(defn get-token + "Retrieves a token by its ID from the file data." + [file-data token-id] + (get-in file-data [:tokens token-id])) + +(defn set-token + "Sets or updates a token in the file data, updating its `modified-at` timestamp." + [file-data token] + (d/assoc-in-when file-data [:tokens (:id token)] (touch token))) + +(defn update-token + "Applies a function to update a token in the file data, then touches it." + [file-data token-id f & args] + (d/update-in-when file-data [:tokens token-id] #(-> (apply f % args) (touch)))) + +(defn delete-token + "Removes a token from the file data by its ID." + [file-data token-id] + (update file-data :tokens dissoc token-id)) diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs new file mode 100644 index 000000000..e3e16b2d3 --- /dev/null +++ b/frontend/src/app/main/data/tokens.cljs @@ -0,0 +1,110 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.tokens + (:require + [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.ui.workspace.tokens.common :refer [workspace-shapes]] + [beicon.v2.core :as rx] + [clojure.data :as data] + [potok.v2.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; TODO HYMA: Copied over from workspace.cljs +(defn update-shape + [id attrs] + (dm/assert! + "expected valid parameters" + (and (cts/check-shape-attrs! attrs) + (uuid? id))) + + (ptk/reify ::update-shape + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes [id] #(merge % attrs)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TOKENS Actions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn toggle-or-apply-token + "Remove any shape attributes from token if they exists. + Othewise apply token attributes." + [shape token] + (let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)] + (merge {} shape-leftover token-leftover))) + +(defn 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-id attributes] + (->> (map (fn [attr] [attr token-id]) attributes) + (into {}))) + +(defn update-token-from-attributes + [{:keys [token-id shape-id attributes]}] + (ptk/reify ::update-token-from-attributes + ptk/WatchEvent + (watch [_ state _] + (let [shape (get-shape-from-state shape-id state) + token (token-from-attributes token-id attributes) + next-applied-tokens (toggle-or-apply-token shape token)] + (rx/of (update-shape shape-id {:applied-tokens next-applied-tokens})))))) + +(defn add-token + [token] + (let [token (update token :id #(or % (uuid/next)))] + (ptk/reify ::add-token + ptk/WatchEvent + (watch [it _ _] + (let [changes (-> (pcb/empty-changes it) + (pcb/add-token token))] + (rx/of (dch/commit-changes changes))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TEMP (Move to test) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(comment + (def shape-1 {:r3 3}) + + (def token-1 {:rx 1 + :ry 1}) + + + (def shape-after-token-1-is-applied {:rx 1 + :ry 1 + :r3 3}) + + (def token-2 {:r3 1}) + + + (def shape-after-token-2-is-applied {:rx 1 + :ry 1 + :r3 1}) + + (def token-3 {:r3 1}) + + (def shape-after-token-3-is-applied {:rx 1 + :ry 1}) + + (= (toggle-or-apply-token shape-1 token-1) + shape-after-token-1-is-applied) + (= (toggle-or-apply-token shape-after-token-1-is-applied token-2) + shape-after-token-2-is-applied) + (= (toggle-or-apply-token shape-after-token-2-is-applied token-3) + shape-after-token-3-is-applied) + nil) diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs index 157de23f7..c4a6b5985 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -43,7 +43,11 @@ :layers {:del #{:document-history :assets} - :add #{:sitemap :layers}}}) + :add #{:sitemap :layers}} + + :tokens + {:del #{:sitemap :layers :document-history :assets} + :add #{:tokens}}}) (def valid-options-mode #{:design :prototype :inspect}) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 8363e438f..8b4ee10c1 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -29,6 +29,7 @@ [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] + [app.main.ui.workspace.tokens.modals] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.debug :as dbg] [app.util.dom :as dom] diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index d64842075..edf7605bf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -25,6 +25,7 @@ [app.main.ui.workspace.sidebar.options :refer [options-toolbox]] [app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]] [app.main.ui.workspace.sidebar.sitemap :refer [sitemap]] + [app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab]] [app.util.debug :as dbg] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) @@ -42,6 +43,7 @@ toggle-pages (mf/use-callback #(reset! show-pages? not)) section (cond (or mode-inspect? (contains? layout :layers)) :layers + (contains? layout :tokens) :tokens (contains? layout :assets) :assets) shortcuts? (contains? layout :shortcuts) @@ -115,7 +117,11 @@ (when-not ^boolean mode-inspect? [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} - [:& assets-toolbox {:size (- size 58)}]])]])]])) + [:& assets-toolbox {:size (- size 58)}]]) + + [:& tab-element {:id :tokens + :title "Tokens"} + [:& tokens-sidebar-tab]]]])]])) ;; --- Right Sidebar (Component) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs new file mode 100644 index 000000000..0ecd7c505 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -0,0 +1,41 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.common + (:require-macros [app.main.style :as stl]) + (:require + [rumext.v2 :as mf])) + +;; Helpers --------------------------------------------------------------------- + +(defn workspace-shapes [workspace page-id shape-ids] + (-> (get-in workspace [:pages-index page-id :objects]) + (keep shape-ids))) + +(defn vec-remove + "remove elem in coll" + [pos coll] + (into (subvec coll 0 pos) (subvec coll (inc pos)))) + +;; Components ------------------------------------------------------------------ + +(mf/defc input + {::mf/wrap-props false} + [{:keys [type placeholder] + :or {type "text"}}] + [:input {:type type + :class (stl/css :input) + :placeholder placeholder}]) + +(mf/defc labeled-input + {::mf/wrap-props false} + [{:keys [input-ref label default-value on-change auto-focus?]}] + [:label {:class (stl/css :labeled-input)} + [:span {:class (stl/css :label)} label] + [:input {:ref input-ref + :default-value default-value + :autoFocus auto-focus? + :on-change on-change}]]) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.scss b/frontend/src/app/main/ui/workspace/tokens/common.scss new file mode 100644 index 000000000..30d611be7 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/common.scss @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.input { + @extend .input-element; +} + +.labeled-input { + @extend .input-element; + .label { + width: auto; + text-wrap: nowrap; + } +} + +.button { + @extend .button-primary; +} + +.action-button { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs new file mode 100644 index 000000000..62a582f2d --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -0,0 +1,112 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.core + (:require + [app.common.data :as d :refer [ordered-map]] + [app.common.types.shape.radius :as ctsr] + [app.main.data.tokens :as dt] + [app.main.data.workspace.changes :as dch])) + +;; Helpers --------------------------------------------------------------------- + +(defn token-applied? + "Test if `token` is applied to a `shape` with the given `token-attributes`." + [token shape token-attributes] + (let [{:keys [id]} token + applied-tokens (get shape :applied-tokens {})] + (some (fn [attr] + (= (get applied-tokens attr) id)) + token-attributes))) + +(defn tokens-applied? + "Test if `token` is applied to to any of `shapes` with the given `token-attributes`." + [token shapes token-attributes] + (some #(token-applied? token % token-attributes) shapes)) + +;; Update functions ------------------------------------------------------------ + +(defn update-shape-radius [value shape-ids] + (let [parsed-value (d/parse-integer value)] + (dch/update-shapes shape-ids + (fn [shape] + (when (ctsr/has-radius? shape) + (ctsr/set-radius-1 shape parsed-value))) + {:reg-objects? true + :attrs [:rx :ry :r1 :r2 :r3 :r4]}))) + +;; Token types ----------------------------------------------------------------- + +(def token-types + (ordered-map + [:boolean {:title "Boolean" + :modal {:key :tokens/boolean + :fields [{:label "Boolean"}]}}] + [:border-radius {:title "Border Radius" + :attributes #{:rx :ry :r1 :r2 :r3 :r4} + :on-apply dt/update-token-from-attributes + :modal {:key :tokens/border-radius + :fields [{:label "Border Radius" + :key :border-radius}]} + :on-update-shape update-shape-radius}] + [:box-shadow + {:title "Box Shadow" + :modal {:key :tokens/box-shadow + :fields [{:label "Box shadows" + :key :box-shadow + :type :box-shadow}]}}] + [:sizing + {:title "Sizing" + :modal {:key :tokens/sizing + :fields [{:label "Sizing" + :key :sizing}]}}] + [:dimension + {:title "Dimension" + :modal {:key :tokens/dimensions + :fields [{:label "Dimensions" + :key :dimensions}]}}] + [:numeric + {:title "Numeric" + :modal {:key :tokens/numeric + :fields [{:label "Numeric" + :key :numeric}]}}] + [:opacity + {:title "Opacity" + :modal {:key :tokens/opacity + :fields [{:label "Opacity" + :key :opacity}]}}] + [:other + {:title "Other" + :modal {:key :tokens/other + :fields [{:label "Other" + :key :other}]}}] + [:rotation + {:title "Rotation" + :modal {:key :tokens/rotation + :fields [{:label "Rotation" + :key :rotation}]}}] + [:spacing + {:title "Spacing" + :modal {:key :tokens/spacing + :fields [{:label "Spacing" + :key :spacing}]}}] + [:string + {:title "String" + :modal {:key :tokens/string + :fields [{:label "String" + :key :string}]}}] + [:typography + {:title "Typography" + :modal {:key :tokens/typography + :fields [{:label "Font" :key :font-family} + {:label "Weight" :key :weight} + {:label "Font Size" :key :font-size} + {:label "Line Height" :key :line-height} + {:label "Letter Spacing" :key :letter-spacing} + {:label "Paragraph Spacing" :key :paragraph-spacing} + {:label "Paragraph Indent" :key :paragraph-indent} + {:label "Text Decoration" :key :text-decoration} + {:label "Text Case" :key :text-case}]}}])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modal.cljs b/frontend/src/app/main/ui/workspace/tokens/modal.cljs new file mode 100644 index 000000000..f2f614762 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modal.cljs @@ -0,0 +1,96 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.modal + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.data.tokens :as dt] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.workspace.tokens.common :as tokens.common] + [app.util.dom :as dom] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(defn calculate-position + "Calculates the style properties for the given coordinates and position" + [{vh :height} position x y] + (let [;; picker height in pixels + h 510 + ;; Checks for overflow outside the viewport height + overflow-fix (max 0 (+ y (- 50) h (- vh))) + + x-pos 325] + (cond + (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} + (= position :left) {:left (str (- x x-pos) "px") + :top (str (- y 50 overflow-fix) "px")} + :else {:left (str (+ x 80) "px") + :top (str (- y 70 overflow-fix) "px")}))) + +(def viewport + (l/derived :vport refs/workspace-local)) + +(defn fields->map [fields] + (->> (map (fn [{:keys [key] :as field}] + [key (:value field)]) fields) + (into {}))) + +(mf/defc tokens-properties-form + {::mf/wrap-props false} + [{:keys [token-type x y position fields]}] + (let [vport (mf/deref viewport) + style (calculate-position vport position x y) + + name (mf/use-var nil) + on-update-name #(reset! name (dom/get-target-val %)) + name-ref (mf/use-ref) + + description (mf/use-var nil) + on-update-description #(reset! description (dom/get-target-val %)) + + state (mf/use-state fields) + on-update-state-field (fn [idx e] + (->> (dom/get-target-val e) + (assoc-in @state [idx :value]) + (reset! state))) + + on-submit (fn [e] + (dom/prevent-default e) + (let [token-value (-> (fields->map @state) + (first) + (val)) + token (cond-> {:name @name + :type token-type + :value token-value} + @description (assoc :description @description))] + (st/emit! (dt/add-token token))))] + + (mf/use-effect + (fn [] + (dom/focus! (mf/ref-val name-ref)))) + + [:form + {:class (stl/css :shadow) + :style (clj->js style) + :on-submit on-submit} + [:div {:class (stl/css :token-rows)} + [:& tokens.common/labeled-input {:label "Name" + :on-change on-update-name + :input-ref name-ref}] + (for [[idx {:keys [type label]}] (d/enumerate @state)] + [:* {:key (str "form-field-" idx)} + (case type + :box-shadow [:p "TODO BOX SHADOW"] + [:& tokens.common/labeled-input {:label label + :on-change #(on-update-state-field idx %)}])]) + [:& tokens.common/labeled-input {:label "Description" + :on-change #(on-update-description %)}] + [:div {:class (stl/css :button-row)} + [:button {:class (stl/css :button) + :type "submit"} + "Save"]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modal.scss b/frontend/src/app/main/ui/workspace/tokens/modal.scss new file mode 100644 index 000000000..d40a861dd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modal.scss @@ -0,0 +1,66 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; +@import "./common.scss"; + +.button-row { + display: flex; + flex-direction: column; + margin-top: $s-16; +} + +.token-rows { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.shadow { + @extend .modal-container-base; + @include menuShadow; + position: absolute; + z-index: 11; + overflow-y: auto; + overflow-x: hidden; + + &-select-wrapper { + display: flex; + grid-gap: $s-4; + } + + &-properties { + display: flex; + flex-direction: column; + grid-gap: $s-4; + } + + .inputs-grid { + display: grid; + grid-template-areas: + "x blur blur spread spread" + "y color color color color"; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(2, 1fr); + grid-gap: $s-4; + + label:nth-child(1) { + grid-area: x; + } + label:nth-child(2) { + grid-area: y; + } + label:nth-child(3) { + grid-area: blur; + } + label:nth-child(4) { + grid-area: spread; + } + label:nth-child(5) { + grid-area: color; + } + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs new file mode 100644 index 000000000..83c1ef5c2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -0,0 +1,83 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.modals + (:require + [app.main.data.modal :as modal] + [app.main.ui.workspace.tokens.modal :refer [tokens-properties-form]] + [rumext.v2 :as mf])) + +(mf/defc boolean-modal + {::mf/register modal/components + ::mf/register-as :tokens/boolean} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc border-radius-modal + {::mf/register modal/components + ::mf/register-as :tokens/border-radius} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc box-shadow-modal + {::mf/register modal/components + ::mf/register-as :tokens/box-shadow} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc sizing-modal + {::mf/register modal/components + ::mf/register-as :tokens/sizing} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc dimensions-modal + {::mf/register modal/components + ::mf/register-as :tokens/dimensions} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc numeric-modal + {::mf/register modal/components + ::mf/register-as :tokens/numeric} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc opacity-modal + {::mf/register modal/components + ::mf/register-as :tokens/opacity} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc other-modal + {::mf/register modal/components + ::mf/register-as :tokens/other} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc rotation-modal + {::mf/register modal/components + ::mf/register-as :tokens/rotation} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc spacing-modal + {::mf/register modal/components + ::mf/register-as :tokens/spacing} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc string-modal + {::mf/register modal/components + ::mf/register-as :tokens/string} + [properties] + [:& tokens-properties-form properties]) + +(mf/defc typography-modal + {::mf/register modal/components + ::mf/register-as :tokens/typography} + [properties] + [:& tokens-properties-form properties]) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs new file mode 100644 index 000000000..1d051f65e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -0,0 +1,106 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.sidebar + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.tokens.common :refer [workspace-shapes]] + [app.main.ui.workspace.tokens.core :refer [token-types tokens-applied?]] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc token-pill + {::mf/wrap-props false} + [{:keys [on-click token highlighted?]}] + (let [{:keys [name value]} token] + [:div {:class (stl/css-case :token-pill true + :token-pill-highlighted highlighted?) + :title (str "Token value: " value) + :on-click on-click} + name])) + +(defn- on-apply-token [token attributes selected-shapes on-apply on-update-shape event] + (let [shapes-to-apply-token (filter #(not (tokens-applied? token % attributes)) selected-shapes) + shapes-to-apply-token-ids (map #(:id %) shapes-to-apply-token)] + (dom/stop-propagation event) + (doseq [shape selected-shapes] + (st/emit! (on-apply {:token-id (:id token) + :shape-id (:id shape) + :attributes attributes})) + (st/emit! (on-update-shape (:value token) shapes-to-apply-token-ids))))) + +(mf/defc token-component + [{:keys [type file tokens selected-shapes token-type-props]}] + (let [open? (mf/use-state false) + {:keys [modal attributes title on-apply on-update-shape]} token-type-props + on-toggle-open-click (mf/use-fn + (mf/deps open? tokens) + #(when (seq tokens) + (swap! open? not))) + on-popover-open-click (mf/use-fn + (fn [event] + (let [{:keys [key fields]} modal] + (dom/stop-propagation event) + (modal/show! key {:x (.-clientX ^js event) + :y (.-clientY ^js event) + :position :right + :fields fields + :token-type type})))) + tokens-count (count tokens)] + [:div {:on-click on-toggle-open-click} + [:& cmm/asset-section {:file-id (:id file) + :title title + :assets-count tokens-count + :open? @open?} + [:& cmm/asset-section-block {:role :title-button} + [:button {:class (stl/css :action-button) + :on-click on-popover-open-click} + i/add]] + (when open? + [:& cmm/asset-section-block {:role :content} + [:div {:class (stl/css :token-pills-wrapper)} + (for [token tokens] + [:& token-pill {:key (:id token) + :token token + :highlighted? (tokens-applied? token selected-shapes attributes) + :on-click #(on-apply-token token attributes selected-shapes on-apply on-update-shape %1)}])]])]])) + +(mf/defc tokens-explorer + [_props] + (let [file (mf/deref refs/workspace-file) + current-page-id (:current-page-id @st/state) + workspace-data (mf/deref refs/workspace-data) + tokens (get workspace-data :tokens) + tokens-by-group (->> (vals tokens) + (group-by :type)) + selected-shape-ids (mf/deref refs/selected-shapes) + selected-shapes (workspace-shapes workspace-data current-page-id selected-shape-ids)] + (js/console.log "tokens" tokens) + [:article + [:& search-bar {:placeholder "Filter" + :on-change js/console.log}] + [:div.assets-bar + (for [[token-key token-type-props] token-types + :let [tokens (or (get tokens-by-group token-key) [])]] + [:& token-component {:key token-key + :type token-key + :file file + :selected-shapes selected-shapes + :tokens tokens + :token-type-props token-type-props}])]])) + +(mf/defc tokens-sidebar-tab + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [_props] + [:div {:class (stl/css :sidebar-tab-wrapper)} + [:& tokens-explorer]]) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss new file mode 100644 index 000000000..75cd515bc --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -0,0 +1,30 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +@import "./common.scss"; + +.sidebar-tab-wrapper { + padding: $s-12; +} + +.token-pills-wrapper { + display: flex; + gap: $s-4; +} + +.token-pill { + @extend .button-secondary; + padding: $s-4 $s-8; + border-radius: $br-6; + font-size: $fs-14; + + &.token-pill-highlighted { + color: var(--button-primary-foreground-color-rest); + background: var(--button-primary-background-color-rest); + } +}