Merge branch 'token-studio-develop' into json-dtcg-format

This commit is contained in:
Akshay Gupta 2024-06-26 17:47:54 +05:30 committed by GitHub
commit bf1c9e2c18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 468 additions and 204 deletions

View file

@ -26,6 +26,10 @@
"test:run": "node target/tests.cjs", "test:run": "node target/tests.cjs",
"test:watch": "clojure -M:dev:shadow-cljs watch test", "test:watch": "clojure -M:dev:shadow-cljs watch test",
"test": "yarn run test:compile && yarn run test:run", "test": "yarn run test:compile && yarn run test:run",
"token-test:compile": "clojure -M:dev:shadow-cljs compile test-esm --config-merge '{:autorun false}'",
"token-test:run": "bun target/tests-esm.cjs",
"token-test:watch": "clojure -M:dev:shadow-cljs watch test-esm",
"token-test": "yarn run token-test:compile && yarn run token-test:run",
"translations:validate": "node ./scripts/validate-translations.js", "translations:validate": "node ./scripts/validate-translations.js",
"translations:find-unused": "node ./scripts/find-unused-translations.js", "translations:find-unused": "node ./scripts/find-unused-translations.js",
"compile": "node ./scripts/compile.js", "compile": "node ./scripts/compile.js",

View file

@ -158,5 +158,17 @@
:source-map-detail-level :all :source-map-detail-level :all
:warnings {:fn-deprecated false}}} :warnings {:fn-deprecated false}}}
}} :test-esm
{:target :node-test
:output-to "target/tests-esm.cjs"
:output-dir "target/test-esm"
:ns-regexp "^token-tests.*-test$"
:autorun true
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:source-map true
:source-map-include-sources-content true
:source-map-detail-level :all
:warnings {:fn-deprecated false}}}}}

View file

@ -2,6 +2,9 @@
Add changes that are meaningful to the user here after each PR so they can be updated in feature base. Add changes that are meaningful to the user here after each PR so they can be updated in feature base.
<details>
<summary>Template</summary>
## Template ## Template
### <DATE> - <CHANGE_DESCRIPTION> ### <DATE> - <CHANGE_DESCRIPTION>
@ -11,14 +14,28 @@ Add changes that are meaningful to the user here after each PR so they can be up
If possible add video here from PR as well If possible add video here from PR as well
- Outline of changes - Outline of changes
</details>
## Changes ## Changes
json-dtcg-format
### 2024-06-26 - Make Tokens JSON Export DTCG compatible ### 2024-06-26 - Make Tokens JSON Export DTCG compatible
![Screenshot of sample JSON Export in DTCG format](https://private-user-images.githubusercontent.com/9948167/343043570-b4bb39f7-ec53-409a-a053-b284d60848d9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTk0MDMyMzcsIm5iZiI6MTcxOTQwMjkzNywicGF0aCI6Ii85OTQ4MTY3LzM0MzA0MzU3MC1iNGJiMzlmNy1lYzUzLTQwOWEtYTA1My1iMjg0ZDYwODQ4ZDkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYyNiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MjZUMTE1NTM3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MWEzZTU5OWQ0M2JkZWE5MTA5MDc4MTY1OTkyZWE5MmE5YzBlYmQ2NTcwMmEwZTdmMjViNGU5YTFjNWIxYjU5ZCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.qWJxRa_Y7LZ6EDJg5yPdOUIQkURFmZwMNec_BbdH9Co) ![Screenshot of sample JSON Export in DTCG format](https://private-user-images.githubusercontent.com/9948167/343043570-b4bb39f7-ec53-409a-a053-b284d60848d9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTk0MDMyMzcsIm5iZiI6MTcxOTQwMjkzNywicGF0aCI6Ii85OTQ4MTY3LzM0MzA0MzU3MC1iNGJiMzlmNy1lYzUzLTQwOWEtYTA1My1iMjg0ZDYwODQ4ZDkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYyNiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MjZUMTE1NTM3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MWEzZTU5OWQ0M2JkZWE5MTA5MDc4MTY1OTkyZWE5MmE5YzBlYmQ2NTcwMmEwZTdmMjViNGU5YTFjNWIxYjU5ZCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.qWJxRa_Y7LZ6EDJg5yPdOUIQkURFmZwMNec_BbdH9Co)
https://github.com/tokens-studio/tokens-studio-for-penpot/issues/197 https://github.com/tokens-studio/tokens-studio-for-penpot/issues/197
### 2024-06-25 - Token Insert/Edit Validation + Value Preview
[Video](https://private-user-images.githubusercontent.com/1898374/342781533-06054a7e-3efb-4f48-a063-8b03f4b8fe5c.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTkzMjgwNzYsIm5iZiI6MTcxOTMyNzc3NiwicGF0aCI6Ii8xODk4Mzc0LzM0Mjc4MTUzMy0wNjA1NGE3ZS0zZWZiLTRmNDgtYTA2My04YjAzZjRiOGZlNWMubXA0P1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYyNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MjVUMTUwMjU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZDliZmUwMzU1MWY3NWQ2NWZkYzA0ODYxYzYzMTYzMjMyOGZjZGMzZDNhMWJmZGI4ZmM3NmU2NzNjYjY2MTdmMCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.44rKA1h3Cvw-vDWevnx7xVUeuZ1ezV4pqEtekVXgVds)
https://github.com/tokens-studio/tokens-studio-for-penpot/pull/194
Adds validation to the token create/edit field
- Name duplication is not allowed and takes a min/max length
- Value has to be a resolvable value
- Description has max value
### 2024-06-19 - Added CHANGELOG.md ### 2024-06-19 - Added CHANGELOG.md
Added template for changelog Added template for changelog

View file

@ -34,7 +34,7 @@
(mf/defc labeled-input (mf/defc labeled-input
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [label input-props auto-complete?]}] [{:keys [label input-props auto-complete? error?]}]
(let [input-props (cond-> input-props (let [input-props (cond-> input-props
:always camel-keys :always camel-keys
;; Disable auto-complete on form fields for proprietary password managers ;; Disable auto-complete on form fields for proprietary password managers
@ -42,6 +42,7 @@
(not auto-complete?) (assoc "data-1p-ignore" true (not auto-complete?) (assoc "data-1p-ignore" true
"data-lpignore" true "data-lpignore" true
:auto-complete "off"))] :auto-complete "off"))]
[:label {:class (stl/css :labeled-input)} [:label {:class (stl/css-case :labeled-input true
:labeled-input-error error?)}
[:span {:class (stl/css :label)} label] [:span {:class (stl/css :label)} label]
[:& :input input-props]])) [:& :input input-props]]))

View file

@ -18,6 +18,10 @@
} }
} }
.labeled-input-error {
border: 1px solid var(--status-color-error-500) !important;
}
.button { .button {
@extend .button-primary; @extend .button-primary;
} }

View file

@ -0,0 +1,265 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.tokens.form
(:require-macros [app.main.style :as stl])
(:require
["lodash.debounce" :as debounce]
[app.main.data.modal :as modal]
[app.main.data.tokens :as dt]
[app.main.store :as st]
[app.main.ui.workspace.tokens.common :as tokens.common]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.util.dom :as dom]
[cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[promesa.core :as p]
[rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(defn token-name-schema
"Generate a dynamic schema validation to check if a token name already exists.
`existing-token-names` should be a set of strings."
[existing-token-names]
(let [non-existing-token-schema
(m/-simple-schema
{:type :token/name-exists
:pred #(not (get existing-token-names %))
:type-properties {:error/fn #(str (:value %) " is an already existing token name")
:existing-token-names existing-token-names}})]
(m/schema
[:and
[:string {:min 1 :max 255}]
non-existing-token-schema])))
(def token-description-schema
(m/schema
[:string {:max 2048}]))
;; Helpers ---------------------------------------------------------------------
(defn finalize-name [name]
(str/trim name))
(defn valid-name? [name]
(seq (finalize-name (str name))))
(defn finalize-value [value]
(-> (str value)
(str/trim)))
(defn valid-value? [value]
(seq (finalize-value value)))
(defn schema-validation->promise [validated]
(if (:errors validated)
(p/rejected validated)
(p/resolved validated)))
;; Component -------------------------------------------------------------------
(defn validate-token-value+
"Validates token value by resolving the value `input` using `StyleDictionary`.
Returns a promise of either resolved tokens or rejects with an error state."
[{:keys [input name-value token tokens]}]
(let [empty-input? (empty? (str/trim input))
;; Check if the given value contains a reference that is the current token-name
;; When creating a new token we dont have a token name yet,
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names.
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)
token-references (sd/find-token-references input)
direct-self-reference? (get token-references token-name)]
(cond
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)
(p/then
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-id)]
(cond
resolved-value (p/resolved resolved-token)
(sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference)
:else (p/rejected :error/unknown-error))))))))))
(defn use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`.
This function is debounced as the resolving might be an expensive calculation.
Uses a custom debouncing logic, as the resolve function is async."
[name-ref token tokens callback & {:keys [timeout] :or {timeout 160}}]
(let [timeout-id-ref (mf/use-ref nil)
debounced-resolver-callback
(mf/use-callback
(mf/deps token callback tokens)
(fn [event]
(let [input (dom/get-target-val event)
timeout-id (js/Symbol)
;; Dont execute callback when the timout-id-ref is outdated because this function got called again
timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)]
(mf/set-ref-val! timeout-id-ref timeout-id)
(js/setTimeout
(fn []
(when (not (timeout-outdated-cb?))
(-> (validate-token-value+ {:input input
:name-value @name-ref
:token token
:tokens tokens})
(p/finally (fn [x err]
(when-not (timeout-outdated-cb?)
(callback (or err x))))))))
timeout))))]
debounced-resolver-callback))
(mf/defc form
{::mf/wrap-props false}
[{:keys [token token-type] :as _args}]
(let [tokens (sd/use-resolved-workspace-tokens)
existing-token-names (mf/use-memo
(mf/deps tokens)
(fn []
(-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens))
;; Remove the currently editing token name,
;; as we don't want it to show when checking for duplicate names.
(disj (:name token)))))
;; Name
name-ref (mf/use-var (:name token))
name-errors (mf/use-state nil)
validate-name (mf/use-callback
(mf/deps existing-token-names)
(fn [value]
(let [schema (token-name-schema existing-token-names)]
(m/explain schema (finalize-name value)))))
on-update-name-debounced (mf/use-callback
(debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-name value)]
(reset! name-errors errors)))))
on-update-name (mf/use-callback
(mf/deps on-update-name-debounced)
(fn [e]
(reset! name-ref (dom/get-target-val e))
(on-update-name-debounced e)))
valid-name-field? (and
(not @name-errors)
(valid-name? @name-ref))
;; Value
value-ref (mf/use-var (:value token))
token-resolve-result (mf/use-state (get-in tokens [(:id token) :resolved-value]))
set-resolve-value (mf/use-callback
(fn [token-or-err]
(let [v (cond
(= token-or-err :error/token-direct-self-reference) token-or-err
(= 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 (mf/use-callback
(mf/deps on-update-value-debounced)
(fn [e]
(reset! value-ref (dom/get-target-val e))
(on-update-value-debounced e)))
value-error? (when (keyword? @token-resolve-result)
(= (namespace @token-resolve-result) "error"))
valid-value-field? (and
(not value-error?)
(valid-value? @token-resolve-result))
;; Description
description-ref (mf/use-var (:description token))
description-errors (mf/use-state nil)
validate-descripion (mf/use-callback #(m/explain token-description-schema %))
on-update-description-debounced (mf/use-callback
(debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-descripion value)]
(reset! description-errors errors)))))
on-update-description (mf/use-callback
(mf/deps on-update-description-debounced)
(fn [e]
(reset! description-ref (dom/get-target-val e))
(on-update-description-debounced e)))
valid-description-field? (not @description-errors)
;; Form
disabled? (or (not valid-name-field?)
(not valid-value-field?)
(not valid-description-field?))
on-submit (mf/use-callback
(mf/deps validate-name validate-descripion token tokens)
(fn [e]
(dom/prevent-default e)
;; We have to re-validate the current form values before submitting
;; because the validation is asynchronous/debounced
;; and the user might have edited a valid form to make it invalid,
;; and press enter before the next validations could return.
(let [final-name (finalize-name @name-ref)
valid-name?+ (-> (validate-name final-name) schema-validation->promise)
final-value (finalize-value @value-ref)
final-description @description-ref
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)]
(-> (p/all [valid-name?+
valid-description?+
(validate-token-value+ {:input final-value
:name-value final-name
:token token
:tokens tokens})])
(p/finally (fn [result err]
;; The result should be a vector of all resolved validations
;; We do not handle the error case as it will be handled by the components validations
(when (and (seq result) (not err))
(let [token (cond-> {:name final-name
:type (or (:type token) token-type)
:value final-value}
final-description (assoc :description final-description)
(:id token) (assoc :id (:id token)))]
(st/emit! (dt/add-token token))
(modal/hide!)))))))))]
[:form
{:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:div
[:& tokens.common/labeled-input {:label "Name"
:error? @name-errors
:input-props {:default-value @name-ref
:auto-focus true
:on-blur on-update-name
:on-change on-update-name}}]
(when @name-errors
[:p {:class (stl/css :error)}
(me/humanize @name-errors)])]
[:& tokens.common/labeled-input {:label "Value"
:input-props {:default-value @value-ref
:on-blur on-update-value
:on-change on-update-value}}]
[:div {:class (stl/css-case :resolved-value true
:resolved-value-placeholder (nil? @token-resolve-result)
:resolved-value-error value-error?)}
(case @token-resolve-result
:error/token-direct-self-reference "Token has self reference"
:error/token-missing-reference "Token has missing reference"
:error/unknown-error ""
nil "Enter token value"
[:p @token-resolve-result])]
[:div
[:& tokens.common/labeled-input {:label "Description"
:input-props {:default-value @description-ref
:on-change on-update-description}}]
(when @description-errors
[:p {:class (stl/css :error)}
(me/humanize @description-errors)])]
[:div {:class (stl/css :button-row)}
[:button {:class (stl/css :button)
:type "submit"
:disabled disabled?}
"Save"]]]]))

View file

@ -0,0 +1,50 @@
// 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;
}
.error {
@include bodySmallTypography;
margin-top: $s-6;
margin-bottom: 0;
color: var(--status-color-error-500);
}
.resolved-value {
@include bodySmallTypography;
padding: $s-4 $s-6;
font-weight: medium;
height: $s-24;
color: var(--color-foreground-primary);
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
p {
font-size: $fs-12;
margin: 0;
}
}
.resolved-value-placeholder {
color: var(--color-foreground-secondary);
}
.resolved-value-error {
color: var(--status-color-error-500);
}

View file

@ -1,102 +0,0 @@
;; 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.modal :as modal]
[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 token]}]
(let [vport (mf/deref viewport)
style (calculate-position vport position x y)
name (mf/use-var (or (:name token) ""))
on-update-name #(reset! name (dom/get-target-val %))
token-value (mf/use-var (or (:value token) ""))
description (mf/use-var (or (:description token) ""))
on-update-description #(reset! description (dom/get-target-val %))
initial-fields (mapv (fn [field]
(assoc field :value (or (:value token) "")))
fields)
state (mf/use-state initial-fields)
on-update-state-field (fn [idx e]
(let [value (dom/get-target-val e)]
(swap! state assoc-in [idx :value] value)))
on-submit (fn [e]
(dom/prevent-default e)
(let [token-value (-> (fields->map @state)
(first)
(val))
token (cond-> {:name @name
:type (or (:type token) token-type)
:value token-value}
@description (assoc :description @description)
(:id token) (assoc :id (:id token)))]
(st/emit! (dt/add-token token))
(modal/hide!)))]
[: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"
:input-props {:default-value @name
:auto-focus true
:on-change on-update-name}}]
(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
:input-props {:default-value @token-value
:on-change #(on-update-state-field idx %)}}])])
[:& tokens.common/labeled-input {:label "Description"
:input-props {:default-value @description
:on-change #(on-update-description %)}}]
[:div {:class (stl/css :button-row)}
[:button {:class (stl/css :button)
:type "submit"}
"Save"]]]]))

View file

@ -1,66 +0,0 @@
// 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;
}
}
}

View file

@ -5,85 +5,124 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.tokens.modals (ns app.main.ui.workspace.tokens.modals
(:require-macros [app.main.style :as stl])
(:require (:require
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.ui.workspace.tokens.modal :refer [tokens-properties-form]] [app.main.refs :as refs]
[app.main.ui.workspace.tokens.form :refer [form]]
[okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Component -------------------------------------------------------------------
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
(let [;; picker height in pixels
h 510
;; Checks for overflow outside the viewport height
overflow-fix (max 0 (+ y (- 50) h (- vh)))
x-pos 325]
(cond
(or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"}
(= position :left) {:left (str (- x x-pos) "px")
:top (str (- y 50 overflow-fix) "px")}
:else {:left (str (+ x 80) "px")
:top (str (- y 70 overflow-fix) "px")})))
(defn use-viewport-position-style [x y position]
(let [vport (-> (l/derived :vport refs/workspace-local)
(mf/deref))]
(-> (calculate-position vport position x y)
(clj->js))))
(mf/defc modal
{::mf/wrap-props false}
[{:keys [x y position token token-type] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position)]
[:div
{:class (stl/css :shadow)
:style wrapper-style}
[:& form {:token token
:token-type token-type}]]))
;; Modals ----------------------------------------------------------------------
(mf/defc boolean-modal (mf/defc boolean-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/boolean} ::mf/register-as :tokens/boolean}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc border-radius-modal (mf/defc border-radius-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/border-radius} ::mf/register-as :tokens/border-radius}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc stroke-width-modal (mf/defc stroke-width-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/stroke-width} ::mf/register-as :tokens/stroke-width}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc box-shadow-modal (mf/defc box-shadow-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/box-shadow} ::mf/register-as :tokens/box-shadow}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc sizing-modal (mf/defc sizing-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/sizing} ::mf/register-as :tokens/sizing}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc dimensions-modal (mf/defc dimensions-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/dimensions} ::mf/register-as :tokens/dimensions}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc numeric-modal (mf/defc numeric-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/numeric} ::mf/register-as :tokens/numeric}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc opacity-modal (mf/defc opacity-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/opacity} ::mf/register-as :tokens/opacity}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc other-modal (mf/defc other-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/other} ::mf/register-as :tokens/other}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc rotation-modal (mf/defc rotation-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/rotation} ::mf/register-as :tokens/rotation}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc spacing-modal (mf/defc spacing-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/spacing} ::mf/register-as :tokens/spacing}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc string-modal (mf/defc string-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/string} ::mf/register-as :tokens/string}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])
(mf/defc typography-modal (mf/defc typography-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/typography} ::mf/register-as :tokens/typography}
[properties] [properties]
[:& tokens-properties-form properties]) [:& modal properties])

View file

@ -0,0 +1,16 @@
// 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";
.shadow {
@extend .modal-container-base;
@include menuShadow;
position: absolute;
z-index: 11;
overflow-y: auto;
overflow-x: hidden;
}

View file

@ -4,6 +4,7 @@
["style-dictionary$default" :as sd] ["style-dictionary$default" :as sd]
[app.common.data :as d] [app.common.data :as d]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.util.dom :as dom]
[cuerdas.core :as str] [cuerdas.core :as str]
[promesa.core :as p] [promesa.core :as p]
[rumext.v2 :as mf] [rumext.v2 :as mf]
@ -16,12 +17,19 @@
(do (do
(sd-transforms/registerTransforms sd) (sd-transforms/registerTransforms sd)
(.registerFormat sd #js {:name "custom/json" (.registerFormat sd #js {:name "custom/json"
:format (fn [res] :format (fn [^js res]
(.-tokens (.-dictionary res)))}) (.-tokens (.-dictionary res)))})
sd)) sd))
;; Functions ------------------------------------------------------------------- ;; Functions -------------------------------------------------------------------
(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 tokens->style-dictionary+ (defn tokens->style-dictionary+
"Resolves references and math expressions using StyleDictionary. "Resolves references and math expressions using StyleDictionary.
Returns a promise with the resolved dictionary." Returns a promise with the resolved dictionary."
@ -69,6 +77,11 @@
errors) errors)
(str/join "\n"))) (str/join "\n")))
(defn missing-reference-error?
[errors]
(and (set? errors)
(get errors :style-dictionary/missing-reference)))
(defn tokens-name-map [tokens] (defn tokens-name-map [tokens]
(->> tokens (->> tokens
(map (fn [[_ x]] [(:name x) x])) (map (fn [[_ x]] [(:name x) x]))
@ -77,7 +90,6 @@
(defn resolve-tokens+ (defn resolve-tokens+
[tokens & {:keys [debug?] :as config}] [tokens & {:keys [debug?] :as config}]
(p/let [sd-tokens (-> (tokens-name-map tokens) (p/let [sd-tokens (-> (tokens-name-map tokens)
(clj->js)
(resolve-sd-tokens+ config))] (resolve-sd-tokens+ config))]
(let [resolved-tokens (reduce (let [resolved-tokens (reduce
(fn [acc ^js cur] (fn [acc ^js cur]
@ -129,25 +141,15 @@
(reset! tokens-state resolved-tokens)))))))) (reset! tokens-state resolved-tokens))))))))
@tokens-state)) @tokens-state))
(defn use-resolved-workspace-tokens (defn use-resolved-workspace-tokens [& {:as config}]
([] (use-resolved-tokens nil))
([options]
(-> (mf/deref refs/workspace-tokens) (-> (mf/deref refs/workspace-tokens)
(use-resolved-tokens options)))) (use-resolved-tokens config)))
;; Testing --------------------------------------------------------------------- ;; Testing ---------------------------------------------------------------------
(defn tokens-studio-example []
(-> (shadow.resource/inline "./data/example-tokens-set.json")
(js/JSON.parse)
.-core))
(comment (comment
(defonce !output (atom nil)) (defonce !output (atom nil))
@!output
(-> (resolve-workspace-tokens+ {:debug? true}) (-> (resolve-workspace-tokens+ {:debug? true})
(p/then #(reset! !output %))) (p/then #(reset! !output %)))
@ -159,7 +161,9 @@
"b" {:name "b" :value "{a} * 2"}}) "b" {:name "b" :value "{a} * 2"}})
(#(resolve-sd-tokens+ % {:debug? true}))) (#(resolve-sd-tokens+ % {:debug? true})))
(-> (tokens-studio-example) (let [example (-> (shadow.resource/inline "./data/example-tokens-set.json")
(resolve-sd-tokens+ {:debug? true})) (js/JSON.parse)
.-core)]
(resolve-sd-tokens+ example {:debug? true}))
nil) nil)

View file

@ -0,0 +1,20 @@
;; 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 token-tests.style-dictionary-test
(:require
[app.main.ui.workspace.tokens.style-dictionary :as wtsd]
[cljs.test :as t :include-macros true]))
(t/deftest test-find-token-references
;; Return references
(t/is (= #{"foo" "bar"} (wtsd/find-token-references "{foo} + {bar}")))
;; Ignore non reference text
(t/is (= #{"foo.bar.baz"} (wtsd/find-token-references "{foo.bar.baz} + something")))
;; No references found
(t/is (nil? (wtsd/find-token-references "1 + 2")))
;; Edge-case: Ignore unmatched closing parens
(t/is (= #{"foo" "bar"} (wtsd/find-token-references "{foo}} + {bar}"))))