mirror of
https://github.com/penpot/penpot.git
synced 2025-01-21 06:02:32 -05:00
Merge branch 'token-studio-develop' into json-export-changelog
This commit is contained in:
commit
c9a40ee9b3
14 changed files with 476 additions and 206 deletions
|
@ -26,6 +26,10 @@
|
|||
"test:run": "node target/tests.cjs",
|
||||
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
||||
"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:find-unused": "node ./scripts/find-unused-translations.js",
|
||||
"compile": "node ./scripts/compile.js",
|
||||
|
|
|
@ -106,8 +106,8 @@
|
|||
:warnings {:fn-deprecated false}}}
|
||||
|
||||
:lib-penpot
|
||||
{:target :esm
|
||||
:output-dir "resources/public/libs"
|
||||
{:target :esm
|
||||
:output-dir "resources/public/libs"
|
||||
|
||||
:modules
|
||||
{:penpot {:exports {:renderPage app.libs.render/render-page-export
|
||||
|
@ -158,5 +158,17 @@
|
|||
:source-map-detail-level :all
|
||||
: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}}}}}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
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
|
||||
|
||||
### <DATE> - <CHANGE_DESCRIPTION>
|
||||
|
@ -11,9 +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
|
||||
|
||||
- Outline of changes
|
||||
</details>
|
||||
|
||||
## Changes
|
||||
|
||||
json-dtcg-format
|
||||
### 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)
|
||||
|
||||
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-24 - Added Ability to Export Tokens in JSON Format
|
||||
|
||||
Sample JSON Output - https://github.com/user-attachments/files/15957831/tokens.json
|
||||
|
@ -22,6 +44,7 @@ Sample JSON Output - https://github.com/user-attachments/files/15957831/tokens.j
|
|||
|
||||
https://github.com/tokens-studio/tokens-studio-for-penpot/pull/191
|
||||
|
||||
|
||||
### 2024-06-19 - Added CHANGELOG.md
|
||||
|
||||
Added template for changelog
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
(mf/defc labeled-input
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [label input-props auto-complete?]}]
|
||||
[{:keys [label input-props auto-complete? error?]}]
|
||||
(let [input-props (cond-> input-props
|
||||
:always camel-keys
|
||||
;; Disable auto-complete on form fields for proprietary password managers
|
||||
|
@ -42,6 +42,7 @@
|
|||
(not auto-complete?) (assoc "data-1p-ignore" true
|
||||
"data-lpignore" true
|
||||
: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]
|
||||
[:& :input input-props]]))
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.labeled-input-error {
|
||||
border: 1px solid var(--status-color-error-500) !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend .button-primary;
|
||||
}
|
||||
|
|
|
@ -150,8 +150,8 @@
|
|||
(defn transform-tokens-into-json-format [tokens]
|
||||
(let [global (reduce
|
||||
(fn [acc [_ {:keys [name value type]}]]
|
||||
(assoc acc name {:value value
|
||||
:type (str/camel type)}))
|
||||
(assoc acc name {:$value value
|
||||
:$type (str/camel type)}))
|
||||
(sorted-map) tokens)]
|
||||
{:global global}))
|
||||
|
||||
|
|
265
frontend/src/app/main/ui/workspace/tokens/form.cljs
Normal file
265
frontend/src/app/main/ui/workspace/tokens/form.cljs
Normal 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"]]]]))
|
50
frontend/src/app/main/ui/workspace/tokens/form.scss
Normal file
50
frontend/src/app/main/ui/workspace/tokens/form.scss
Normal 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);
|
||||
}
|
|
@ -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"]]]]))
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,85 +5,124 @@
|
|||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.modals
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[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]))
|
||||
|
||||
;; 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/register modal/components
|
||||
::mf/register-as :tokens/boolean}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc border-radius-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/border-radius}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc stroke-width-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/stroke-width}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc box-shadow-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/box-shadow}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc sizing-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/sizing}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc dimensions-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/dimensions}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc numeric-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/numeric}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc opacity-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/opacity}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc other-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/other}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc rotation-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/rotation}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc spacing-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/spacing}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc string-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/string}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
||||
(mf/defc typography-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/typography}
|
||||
[properties]
|
||||
[:& tokens-properties-form properties])
|
||||
[:& modal properties])
|
||||
|
|
16
frontend/src/app/main/ui/workspace/tokens/modals.scss
Normal file
16
frontend/src/app/main/ui/workspace/tokens/modals.scss
Normal 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;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
["style-dictionary$default" :as sd]
|
||||
[app.common.data :as d]
|
||||
[app.main.refs :as refs]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]
|
||||
|
@ -16,12 +17,19 @@
|
|||
(do
|
||||
(sd-transforms/registerTransforms sd)
|
||||
(.registerFormat sd #js {:name "custom/json"
|
||||
:format (fn [res]
|
||||
:format (fn [^js res]
|
||||
(.-tokens (.-dictionary res)))})
|
||||
sd))
|
||||
|
||||
;; 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+
|
||||
"Resolves references and math expressions using StyleDictionary.
|
||||
Returns a promise with the resolved dictionary."
|
||||
|
@ -69,6 +77,11 @@
|
|||
errors)
|
||||
(str/join "\n")))
|
||||
|
||||
(defn missing-reference-error?
|
||||
[errors]
|
||||
(and (set? errors)
|
||||
(get errors :style-dictionary/missing-reference)))
|
||||
|
||||
(defn tokens-name-map [tokens]
|
||||
(->> tokens
|
||||
(map (fn [[_ x]] [(:name x) x]))
|
||||
|
@ -77,7 +90,6 @@
|
|||
(defn resolve-tokens+
|
||||
[tokens & {:keys [debug?] :as config}]
|
||||
(p/let [sd-tokens (-> (tokens-name-map tokens)
|
||||
(clj->js)
|
||||
(resolve-sd-tokens+ config))]
|
||||
(let [resolved-tokens (reduce
|
||||
(fn [acc ^js cur]
|
||||
|
@ -129,25 +141,15 @@
|
|||
(reset! tokens-state resolved-tokens))))))))
|
||||
@tokens-state))
|
||||
|
||||
(defn use-resolved-workspace-tokens
|
||||
([] (use-resolved-tokens nil))
|
||||
([options]
|
||||
(-> (mf/deref refs/workspace-tokens)
|
||||
(use-resolved-tokens options))))
|
||||
(defn use-resolved-workspace-tokens [& {:as config}]
|
||||
(-> (mf/deref refs/workspace-tokens)
|
||||
(use-resolved-tokens config)))
|
||||
|
||||
;; Testing ---------------------------------------------------------------------
|
||||
|
||||
(defn tokens-studio-example []
|
||||
(-> (shadow.resource/inline "./data/example-tokens-set.json")
|
||||
(js/JSON.parse)
|
||||
.-core))
|
||||
|
||||
(comment
|
||||
|
||||
(defonce !output (atom nil))
|
||||
|
||||
@!output
|
||||
|
||||
(-> (resolve-workspace-tokens+ {:debug? true})
|
||||
(p/then #(reset! !output %)))
|
||||
|
||||
|
@ -159,7 +161,9 @@
|
|||
"b" {:name "b" :value "{a} * 2"}})
|
||||
(#(resolve-sd-tokens+ % {:debug? true})))
|
||||
|
||||
(-> (tokens-studio-example)
|
||||
(resolve-sd-tokens+ {:debug? true}))
|
||||
(let [example (-> (shadow.resource/inline "./data/example-tokens-set.json")
|
||||
(js/JSON.parse)
|
||||
.-core)]
|
||||
(resolve-sd-tokens+ example {:debug? true}))
|
||||
|
||||
nil)
|
||||
|
|
20
frontend/test/token_tests/style_dictionary_test.cljs
Normal file
20
frontend/test/token_tests/style_dictionary_test.cljs
Normal 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}"))))
|
Loading…
Add table
Reference in a new issue