Implement token import / export

This commit is contained in:
Florian Schroedl 2024-10-10 13:08:35 +02:00
parent 41dc6083cf
commit c6ed081a0b
16 changed files with 1248 additions and 810 deletions

View file

@ -192,6 +192,24 @@
(dch/commit-changes changes')
(wtu/update-workspace-tokens))))))
(defn import-tokens-lib [lib]
(ptk/reify ::import-tokens-lib
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
update-token-set-change (some-> lib
(ctob/get-sets)
(first)
(:name)
(set-selected-token-set-id))
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-tokens-lib lib))]
(rx/of
(dch/commit-changes changes)
update-token-set-change
(wtu/update-workspace-tokens))))))
(defn delete-token-set [token-set-name]
(ptk/reify ::delete-token-set
ptk/WatchEvent
@ -284,7 +302,7 @@
(update [_ state]
(assoc-in state [:workspace-tokens :open-status token-type] open?))))
;; Token Context Menu Functions -------------------------------------------------
;; === Token Context Menu
(defn show-token-context-menu
[{:keys [position _token-name] :as params}]
@ -300,6 +318,8 @@
(update [_ state]
(assoc-in state [:workspace-local :token-context-menu] nil))))
;; === Token Set Context Menu
(defn show-token-set-context-menu
[{:keys [position _token-set-name] :as params}]
(dm/assert! (gpt/point? position))
@ -313,3 +333,19 @@
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :token-set-context-menu] nil))))
;; === Import Export Context Menu
(defn show-import-export-context-menu
[{:keys [position] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-import-export-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :import-export-context-menu] params))))
(def hide-import-export-set-context-menu
(ptk/reify ::hide-import-export-set-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :import-export-set-context-menu] nil))))

View file

@ -7,11 +7,7 @@
(ns app.main.ui.workspace.tokens.core
(:require
[app.common.data :as d]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.token :as wtt]
[app.util.dom :as dom]
[app.util.webapi :as wapi]
[cuerdas.core :as str]))
[app.main.ui.workspace.tokens.token :as wtt]))
;; Helpers ---------------------------------------------------------------------
@ -36,30 +32,3 @@
(cond-> (assoc token :label name)
(wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true)))
tokens))
;; JSON export functions -------------------------------------------------------
(defn encode-tokens
[data]
(-> data
(clj->js)
(js/JSON.stringify nil 2)))
(defn export-tokens-file [tokens-json]
(let [file-name "tokens.json"
file-content (encode-tokens tokens-json)
blob (wapi/create-blob file-content "application/json")]
(dom/trigger-download file-name blob)))
(defn tokens->dtcg-map [tokens]
(let [global (reduce
(fn [acc [_ {:keys [name value type]}]]
(assoc acc name {"$value" value
"$type" (str/camel type)}))
(d/ordered-map) tokens)]
{:global global}))
(defn download-tokens-as-json []
(let [tokens (deref refs/workspace-active-theme-sets-tokens)
dtcg-format-tokens-map (tokens->dtcg-map tokens)]
(export-tokens-file dtcg-format-tokens-map)))

View file

@ -1,580 +0,0 @@
{
"core": {
"dimension": {
"scale": {
"value": "2",
"type": "dimension"
},
"xs": {
"value": "4",
"type": "dimension"
},
"sm": {
"value": "{dimension.xs} * {dimension.scale}",
"type": "dimension"
},
"md": {
"value": "{dimension.sm} * {dimension.scale}",
"type": "dimension"
},
"lg": {
"value": "{dimension.md} * {dimension.scale}",
"type": "dimension"
},
"xl": {
"value": "{dimension.lg} * {dimension.scale}",
"type": "dimension"
}
},
"spacing": {
"xs": {
"value": "{dimension.xs}",
"type": "spacing"
},
"sm": {
"value": "{dimension.sm}",
"type": "spacing"
},
"md": {
"value": "{dimension.md}",
"type": "spacing"
},
"lg": {
"value": "{dimension.lg}",
"type": "spacing"
},
"xl": {
"value": "{dimension.xl}",
"type": "spacing"
},
"multi-value": {
"value": "{dimension.sm} {dimension.xl}",
"type": "spacing",
"description": "You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens"
}
},
"borderRadius": {
"sm": {
"value": "4",
"type": "borderRadius"
},
"lg": {
"value": "8",
"type": "borderRadius"
},
"xl": {
"value": "16",
"type": "borderRadius"
},
"multi-value": {
"value": "{borderRadius.sm} {borderRadius.lg}",
"type": "borderRadius",
"description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values"
}
},
"colors": {
"black": {
"value": "#000000",
"type": "color"
},
"white": {
"value": "#ffffff",
"type": "color"
},
"gray": {
"100": {
"value": "#f7fafc",
"type": "color"
},
"200": {
"value": "#edf2f7",
"type": "color"
},
"300": {
"value": "#e2e8f0",
"type": "color"
},
"400": {
"value": "#cbd5e0",
"type": "color"
},
"500": {
"value": "#a0aec0",
"type": "color"
},
"600": {
"value": "#718096",
"type": "color"
},
"700": {
"value": "#4a5568",
"type": "color"
},
"800": {
"value": "#2d3748",
"type": "color"
},
"900": {
"value": "#1a202c",
"type": "color"
}
},
"red": {
"100": {
"value": "#fff5f5",
"type": "color"
},
"200": {
"value": "#fed7d7",
"type": "color"
},
"300": {
"value": "#feb2b2",
"type": "color"
},
"400": {
"value": "#fc8181",
"type": "color"
},
"500": {
"value": "#f56565",
"type": "color"
},
"600": {
"value": "#e53e3e",
"type": "color"
},
"700": {
"value": "#c53030",
"type": "color"
},
"800": {
"value": "#9b2c2c",
"type": "color"
},
"900": {
"value": "#742a2a",
"type": "color"
}
},
"orange": {
"100": {
"value": "#fffaf0",
"type": "color"
},
"200": {
"value": "#feebc8",
"type": "color"
},
"300": {
"value": "#fbd38d",
"type": "color"
},
"400": {
"value": "#f6ad55",
"type": "color"
},
"500": {
"value": "#ed8936",
"type": "color"
},
"600": {
"value": "#dd6b20",
"type": "color"
},
"700": {
"value": "#c05621",
"type": "color"
},
"800": {
"value": "#9c4221",
"type": "color"
},
"900": {
"value": "#7b341e",
"type": "color"
}
},
"yellow": {
"100": {
"value": "#fffff0",
"type": "color"
},
"200": {
"value": "#fefcbf",
"type": "color"
},
"300": {
"value": "#faf089",
"type": "color"
},
"400": {
"value": "#f6e05e",
"type": "color"
},
"500": {
"value": "#ecc94b",
"type": "color"
},
"600": {
"value": "#d69e2e",
"type": "color"
},
"700": {
"value": "#b7791f",
"type": "color"
},
"800": {
"value": "#975a16",
"type": "color"
},
"900": {
"value": "#744210",
"type": "color"
}
},
"green": {
"100": {
"value": "#f0fff4",
"type": "color"
},
"200": {
"value": "#c6f6d5",
"type": "color"
},
"300": {
"value": "#9ae6b4",
"type": "color"
},
"400": {
"value": "#68d391",
"type": "color"
},
"500": {
"value": "#48bb78",
"type": "color"
},
"600": {
"value": "#38a169",
"type": "color"
},
"700": {
"value": "#2f855a",
"type": "color"
},
"800": {
"value": "#276749",
"type": "color"
},
"900": {
"value": "#22543d",
"type": "color"
}
},
"teal": {
"100": {
"value": "#e6fffa",
"type": "color"
},
"200": {
"value": "#b2f5ea",
"type": "color"
},
"300": {
"value": "#81e6d9",
"type": "color"
},
"400": {
"value": "#4fd1c5",
"type": "color"
},
"500": {
"value": "#38b2ac",
"type": "color"
},
"600": {
"value": "#319795",
"type": "color"
},
"700": {
"value": "#2c7a7b",
"type": "color"
},
"800": {
"value": "#285e61",
"type": "color"
},
"900": {
"value": "#234e52",
"type": "color"
}
},
"blue": {
"100": {
"value": "#ebf8ff",
"type": "color"
},
"200": {
"value": "#bee3f8",
"type": "color"
},
"300": {
"value": "#90cdf4",
"type": "color"
},
"400": {
"value": "#63b3ed",
"type": "color"
},
"500": {
"value": "#4299e1",
"type": "color"
},
"600": {
"value": "#3182ce",
"type": "color"
},
"700": {
"value": "#2b6cb0",
"type": "color"
},
"800": {
"value": "#2c5282",
"type": "color"
},
"900": {
"value": "#2a4365",
"type": "color"
}
},
"indigo": {
"100": {
"value": "#ebf4ff",
"type": "color"
},
"200": {
"value": "#c3dafe",
"type": "color"
},
"300": {
"value": "#a3bffa",
"type": "color"
},
"400": {
"value": "#7f9cf5",
"type": "color"
},
"500": {
"value": "#667eea",
"type": "color"
},
"600": {
"value": "#5a67d8",
"type": "color"
},
"700": {
"value": "#4c51bf",
"type": "color"
},
"800": {
"value": "#434190",
"type": "color"
},
"900": {
"value": "#3c366b",
"type": "color"
}
},
"purple": {
"100": {
"value": "#faf5ff",
"type": "color"
},
"200": {
"value": "#e9d8fd",
"type": "color"
},
"300": {
"value": "#d6bcfa",
"type": "color"
},
"400": {
"value": "#b794f4",
"type": "color"
},
"500": {
"value": "#9f7aea",
"type": "color"
},
"600": {
"value": "#805ad5",
"type": "color"
},
"700": {
"value": "#6b46c1",
"type": "color"
},
"800": {
"value": "#553c9a",
"type": "color"
},
"900": {
"value": "#44337a",
"type": "color"
}
},
"pink": {
"100": {
"value": "#fff5f7",
"type": "color"
},
"200": {
"value": "#fed7e2",
"type": "color"
},
"300": {
"value": "#fbb6ce",
"type": "color"
},
"400": {
"value": "#f687b3",
"type": "color"
},
"500": {
"value": "#ed64a6",
"type": "color"
},
"600": {
"value": "#d53f8c",
"type": "color"
},
"700": {
"value": "#b83280",
"type": "color"
},
"800": {
"value": "#97266d",
"type": "color"
},
"900": {
"value": "#702459",
"type": "color"
}
}
},
"opacity": {
"low": {
"value": "10%",
"type": "opacity"
},
"md": {
"value": "50%",
"type": "opacity"
},
"high": {
"value": "90%",
"type": "opacity"
}
},
"fontFamilies": {
"heading": {
"value": "Inter",
"type": "fontFamilies"
},
"body": {
"value": "Roboto",
"type": "fontFamilies"
}
},
"lineHeights": {
"heading": {
"value": "110%",
"type": "lineHeights"
},
"body": {
"value": "140%",
"type": "lineHeights"
}
},
"letterSpacing": {
"default": {
"value": "0",
"type": "letterSpacing"
},
"increased": {
"value": "150%",
"type": "letterSpacing"
},
"decreased": {
"value": "-5%",
"type": "letterSpacing"
}
},
"paragraphSpacing": {
"h1": {
"value": "32",
"type": "paragraphSpacing"
},
"h2": {
"value": "26",
"type": "paragraphSpacing"
}
},
"fontWeights": {
"headingRegular": {
"value": "Regular",
"type": "fontWeights"
},
"headingBold": {
"value": "Bold",
"type": "fontWeights"
},
"bodyRegular": {
"value": "Regular",
"type": "fontWeights"
},
"bodyBold": {
"value": "Bold",
"type": "fontWeights"
}
},
"fontSizes": {
"h1": {
"value": "{fontSizes.h2} * 1.25",
"type": "fontSizes"
},
"h2": {
"value": "{fontSizes.h3} * 1.25",
"type": "fontSizes"
},
"h3": {
"value": "{fontSizes.h4} * 1.25",
"type": "fontSizes"
},
"h4": {
"value": "{fontSizes.h5} * 1.25",
"type": "fontSizes"
},
"h5": {
"value": "{fontSizes.h6} * 1.25",
"type": "fontSizes"
},
"h6": {
"value": "{fontSizes.body} * 1",
"type": "fontSizes"
},
"body": {
"value": "16",
"type": "fontSizes"
},
"sm": {
"value": "{fontSizes.body} * 0.85",
"type": "fontSizes"
},
"xs": {
"value": "{fontSizes.body} * 0.65",
"type": "fontSizes"
}
}
}
}

View file

@ -205,7 +205,7 @@ Token names should only contain letters and digits separated by . characters.")}
selected-set-tokens-tree (mf/use-memo
(mf/deps token-path selected-set-tokens)
(fn []
(-> (wtt/token-names-tree selected-set-tokens)
(-> (ctob/tokens-tree selected-set-tokens)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))

View file

@ -8,12 +8,16 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.transit :as t]
[app.common.types.tokens-lib :as ctob]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.tokens :as dt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :refer [color-bullet]]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu
dropdown-menu-item*]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.hooks :as h]
[app.main.ui.hooks.resize :refer [use-resize-hook]]
@ -21,7 +25,6 @@
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.core :as wtc]
[app.main.ui.workspace.tokens.sets :refer [sets-list]]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.main.ui.workspace.tokens.sets-context-menu :refer [sets-context-menu]]
@ -30,7 +33,8 @@
[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]]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]
@ -140,7 +144,7 @@
(when open?
[:& cmm/asset-section-block {:role :content}
[:div {:class (stl/css :token-pills-wrapper)}
(for [token (sort-by :modified-at tokens)]
(for [token (sort-by :name tokens)]
(let [theme-token (get active-theme-tokens (wtt/token-identifier token))]
[:& token-pill
{:key (:name token)
@ -173,10 +177,10 @@
(modal/show! :tokens/themes {}))}
(if create? "Create" "Edit")])
(mf/defc themes-sidebar
(mf/defc themes-header
[_props]
(let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden)]
[:div {:class (stl/css :theme-sidebar)}
[:div {:class (stl/css :themes-wrapper)}
[:span {:class (stl/css :themes-header)} "Themes"]
[:div {:class (stl/css :theme-select-wrapper)}
[:& theme-select]
@ -191,13 +195,14 @@
(on-create))}
i/add]))
(mf/defc sets-sidebar
(mf/defc themes-sets-tab
[]
(let [open? (mf/use-state true)
on-open (mf/use-fn #(reset! open? true))]
[:& sets-context/provider {}
[:& sets-context-menu]
[:div {:class (stl/css :sets-sidebar)}
[:& themes-header]
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
@ -209,10 +214,9 @@
[:& h/sortable-container {}
[:& sets-list]])]]))
(mf/defc tokens-explorer
(mf/defc tokens-tab
[_props]
(let [open? (mf/use-state true)
objects (mf/deref refs/workspace-page-objects)
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
@ -222,70 +226,125 @@
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}])])]))
[:& title-bar {:all-clickable true
:title "TOKENS"}]
[:div.assets-bar
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
(:empty token-groups))]
[:& token-component {:key token-key
:type token-key
:selected-shapes selected-shapes
:active-theme-tokens active-theme-tokens
:tokens tokens
:token-type-props token-type-props}])]]))
(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?)))
(mf/defc json-import-button []
(let []
[:div
(defn location-url-dev-or-preview-url!? []
(dev-or-preview-url? js/window.location.href))
[:button {:class (stl/css :download-json-button)
:on-click #(.click (js/document.getElementById "file-input"))}
download-icon
"Import JSON"]]))
(defn temp-use-themes-flag []
(let [show? (mf/use-state (or
(location-url-dev-or-preview-url!?)
(get @storage ::show-token-themes-sets?)
true))]
(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 import-export-button
{::mf/wrap-props false}
[{:keys []}]
(let [show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
open-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* true)))
close-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)))
input-ref (mf/use-ref)
on-import
(fn [event]
(let [file (-> event .-target .-files (aget 0))]
(->> (wapi/read-file-as-text file)
(rx/map (fn [data]
(try
(t/decode-str data)
(catch js/Error e
(throw (ex-info "Json parse error"
{:user-error "Import Error: Could not parse json"
:type :json-parse-error
:data data
:exception e}))))))
(rx/map (fn [json-data]
(try
(ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data)
(catch js/Error e
(throw (ex-info "invalid token data"
{:user-error "Import Error: Invalid token data in json."
:type :invalid-token-data
:data json-data
:exception e}))))))
(rx/subs! (fn [lib]
(st/emit! (dt/import-tokens-lib lib)))
(fn [err]
(let [{:keys [user-error]} (ex-data err)]
(st/emit! (msg/show {:content user-error
:notification-type :toast
:type :warning
:timeout 3000}))))))
(set! (.-value (mf/ref-val input-ref)) "")))
on-export (fn []
(let [tokens-blob (some-> (deref refs/tokens-lib)
(ctob/encode-dtcg)
(clj->js)
(js/JSON.stringify nil 2)
(wapi/create-blob "application/json"))]
(dom/trigger-download "tokens.json" tokens-blob)))]
[:div {:class (stl/css :import-export-button-wrapper)}
[:input {:type "file"
:ref input-ref
:style {:display "none"}
:id "file-input"
:accept ".json"
:on-change on-import}]
[:button {:class (stl/css :import-export-button)
:on-click open-menu}
download-icon
"Tokens"]
[:& dropdown-menu {:show show-menu?
:on-close close-menu
:list-class (stl/css :import-export-menu)}
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click #(.click (mf/ref-val input-ref))}
"Import"]
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-export}
"Export"]]]))
(mf/defc tokens-sidebar-tab
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[_props]
(let [show-sets-section? (deref (temp-use-themes-flag))
{on-pointer-down-pages :on-pointer-down
(let [{on-pointer-down-pages :on-pointer-down
on-lost-pointer-capture-pages :on-lost-pointer-capture
on-pointer-move-pages :on-pointer-move
size-pages-opened :size}
(use-resize-hook :sitemap 200 38 400 :y false nil)]
[:div {:class (stl/css :sidebar-tab-wrapper)}
(when show-sets-section?
[:div {:class (stl/css :sets-section-wrapper)
:style {:height (str size-pages-opened "px")}}
[:& themes-sidebar]
[:& sets-sidebar]])
[:div {:class (stl/css :tokens-section-wrapper)}
(when show-sets-section?
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}])
[:& tokens-explorer]]
[:button {:class (stl/css :download-json-button)
:on-click wtc/download-tokens-as-json}
download-icon
"Export JSON"]]))
(use-resize-hook :tokens 200 38 400 :y false nil)]
[:div {:class (stl/css :sidebar-wrapper)}
[:article {:class (stl/css :sets-section-wrapper)
:style {"--resize-height" (str size-pages-opened "px")}}
[:& themes-sets-tab]]
[:article {:class (stl/css :tokens-section-wrapper)}
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}]
[:& tokens-tab]
[:& import-export-button]]]))

View file

@ -7,25 +7,37 @@
@import "refactor/common-refactor.scss";
@import "./common.scss";
.sidebar-tab-wrapper {
display: flex;
flex-direction: column;
height: 100%;
.sidebar-wrapper {
display: grid;
grid-template-rows: auto auto 1fr;
// Overflow on the bottom section can't be done without hardcoded values for the height
// This has to be changed from the wrapping sidebar styles
height: calc(100vh - #{$s-84});
overflow: hidden;
}
.sets-section-wrapper {
position: relative;
display: flex;
flex: 1;
height: var(--resize-height);
flex-direction: column;
margin-bottom: $s-8;
overflow-y: auto;
scrollbar-gutter: stable;
}
.tokens-section-wrapper {
height: 100%;
padding-left: $s-12;
overflow-y: auto;
scrollbar-gutter: stable;
}
.sets-sidebar {
position: relative;
}
.theme-sidebar {
.themes-wrapper {
padding: $s-12;
padding-bottom: 0;
}
@ -52,18 +64,6 @@
}
}
.tokens-section-wrapper {
flex: 1;
padding-top: $s-12;
padding-left: $s-12;
overflow-y: auto;
}
// TODO Remove once sets are available to public
.sets-section-wrapper + .tokens-section-wrapper {
padding-top: 0;
}
.token-pills-wrapper {
display: flex;
gap: $s-4;
@ -103,11 +103,14 @@
translate: 0px -1px;
}
.download-json-button {
@extend .button-secondary;
.import-export-button-wrapper {
position: absolute;
bottom: $s-12;
right: $s-12;
}
.import-export-button {
@extend .button-secondary;
display: flex;
align-items: center;
padding: $s-6 $s-8;
@ -122,6 +125,38 @@
}
}
.import-export-menu {
@extend .menu-dropdown;
top: -#{$s-6};
right: 0;
translate: 0 -100%;
width: $s-192;
margin: 0;
}
.import-export-menu-item {
@extend .menu-item-base;
cursor: pointer;
.open-arrow {
@include flexCenter;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
&:hover {
color: var(--menu-foreground-color-hover);
.open-arrow {
svg {
stroke: var(--menu-foreground-color-hover);
}
}
.shortcut-key {
color: var(--menu-shortcut-foreground-color-hover);
}
}
}
.theme-select-wrapper {
display: grid;
grid-template-columns: 1fr 0.28fr;

View file

@ -1,7 +1,6 @@
(ns app.main.ui.workspace.tokens.token
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[clojure.set :as set]
[cuerdas.core :as str]))
@ -22,13 +21,6 @@
{:value parsed-value
:unit unit}))))
(defn find-token-references
"Finds token reference values in `value-string` and returns a set with all contained namespaces."
[value-string]
(some->> (re-seq #"\{([^}]*)\}" value-string)
(map second)
(into #{})))
(defn token-identifier [{:keys [name] :as _token}]
name)
@ -96,14 +88,6 @@
{:path (seq path)
:selector selector}))
(defn token-names-map
"Convert tokens into a map with their `:name` as the key.
E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}"
[tokens]
(->> (map (fn [{:keys [name] :as token}] [name token]) tokens)
(into {})))
(defn token-names-tree-id-map [tokens]
(reduce
(fn [acc [_ {:keys [name] :as token}]]
@ -117,16 +101,6 @@
:ids-map {}}
tokens))
(defn token-names-tree
"Convert tokens into a nested tree with their `:name` as the path."
[tokens]
(reduce
(fn [acc [_ {:keys [name] :as token}]]
(when (string? name)
(let [path (token-name->path name)]
(assoc-in acc path token))))
{} tokens))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.

View file

@ -6,6 +6,7 @@
[app.common.test-helpers.shapes :as cths]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.token :as wtt]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]
@ -133,34 +134,6 @@
(t/testing "while :r4 was kept with borderRadius.sm"
(t/is (= (:r4 (:applied-tokens rect-1')) (:name token-sm)))))))))))
(t/deftest test-apply-dimensions
(t/testing "applies dimensions token and updates the shapes width and height"
(t/async
done
(let [file (-> (setup-file-with-tokens)
(toht/add-token :token-target {:value "100"
:name "dimensions.sm"
:type :dimensions}))
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:width :height}
:token (toht/get-token file :token-target)
:on-update-shape wtch/update-shape-dimensions})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-target' (toht/get-token file' :token-target)
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:width (:applied-tokens rect-1')) (wtt/token-identifier token-target')))
(t/is (= (:height (:applied-tokens rect-1')) (wtt/token-identifier token-target'))))
(t/testing "shapes width and height got updated"
(t/is (= (:width rect-1') 100))
(t/is (= (:height rect-1') 100))))))))))
(t/deftest test-apply-dimensions
(t/testing "applies dimensions token and updates the shapes width and height"
(t/async

View file

@ -39,22 +39,3 @@
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
(done))))))))
(t/deftest 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
:unit "px")
"borderRadius.md-with-dashes"
(assoc reference-border-radius-token
:resolved-value 24
:unit "px")}]
(t/is (= expected-tokens resolved-tokens))
(done))))))))

View file

@ -91,26 +91,6 @@
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz...."))))
(t/deftest tokens-name-map-test
(t/testing "creates a a names map from tokens"
(t/is (= {"border-radius.sm" {:name "border-radius.sm", :value "10"}
"border-radius.md" {:name "border-radius.md", :value "20"}}
(wtt/token-names-map [{:name "border-radius.sm" :value "10"}
{:name "border-radius.md" :value "20"}])))))
(t/deftest tokens-name-tree-test
(t/is (= {"foo"
{"bar"
{"baz" {:name "foo.bar.baz", :value "a"},
"bam" {:name "foo.bar.bam", :value "b"}}},
"baz" {"bar" {"foo" {:name "baz.bar.foo", :value "{foo.bar.baz}"}}}}
(wtt/token-names-tree {:a {:name "foo.bar.baz"
:value "a"}
:b {:name "foo.bar.bam"
:value "b"}
:c {:name "baz.bar.foo"
:value "{foo.bar.baz}"}}))))
(t/deftest token-name-path-exists?-test
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))