mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
68 Commits
eva-replac
...
tokens-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b786b8e07a | ||
|
|
e8ff89836e | ||
|
|
8a8f360c7f | ||
|
|
e35fc85c3d | ||
|
|
a614207f7e | ||
|
|
1798461d21 | ||
|
|
6ce3249c6d | ||
|
|
dde0fddd6f | ||
|
|
4637aced8c | ||
|
|
9dfe5b0865 | ||
|
|
33bcc9544a | ||
|
|
babd481b7f | ||
|
|
a9733c792d | ||
|
|
b0351be724 | ||
|
|
d04fdb5fbd | ||
|
|
81e0e4f222 | ||
|
|
b8392b3731 | ||
|
|
77dba477ca | ||
|
|
b6598d1f07 | ||
|
|
f13b3c8737 | ||
|
|
a0f8559ffc | ||
|
|
bf1dc21c75 | ||
|
|
46c20a993f | ||
|
|
0e0106f69a | ||
|
|
19bb69cc60 | ||
|
|
504eb70988 | ||
|
|
75a2331edf | ||
|
|
c2b4c9907d | ||
|
|
bd5bbcae26 | ||
|
|
84273508ad | ||
|
|
9245ba6bc2 | ||
|
|
4be046406d | ||
|
|
84c747cd31 | ||
|
|
0036a9a0cd | ||
|
|
2105c3a68c | ||
|
|
38efa88460 | ||
|
|
6e254c2cf4 | ||
|
|
416980f063 | ||
|
|
f76710296c | ||
|
|
6251fa6b22 | ||
|
|
aedd8cc11e | ||
|
|
d1379c55f6 | ||
|
|
b125c7b5a3 | ||
|
|
496d37795b | ||
|
|
2f0853f5cc | ||
|
|
648e660bcf | ||
|
|
9f6899007a | ||
|
|
bee2f70bfa | ||
|
|
00f8eac8fa | ||
|
|
df7caacb45 | ||
|
|
641df77834 | ||
|
|
49bbdfb257 | ||
|
|
94af978be8 | ||
|
|
feababe2a8 | ||
|
|
5ef06685fc | ||
|
|
57fcec5afc | ||
|
|
58f82da61e | ||
|
|
a28c5b61ca | ||
|
|
53aad7bc15 | ||
|
|
9123d199b7 | ||
|
|
57297741f5 | ||
|
|
eeaf28bb25 | ||
|
|
d63d692d34 | ||
|
|
6b8091bb90 | ||
|
|
fe72d0af82 | ||
|
|
bba02473d5 | ||
|
|
95b7784a42 | ||
|
|
4690f740b9 |
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: _NITRATE MODULE
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
1
.github/workflows/build-tag.yml
vendored
1
.github/workflows/build-tag.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
|
||||
notify:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
|
||||
steps:
|
||||
|
||||
10
CHANGES.md
10
CHANGES.md
@@ -8,12 +8,17 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
|
||||
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
|
||||
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
|
||||
|
||||
|
||||
## 2.12.0 (Unreleased)
|
||||
@@ -77,6 +82,7 @@ example. It's still usable as before, we just removed the example.
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
|
||||
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@@ -104,6 +110,10 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
|
||||
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
|
||||
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
|
||||
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
|
||||
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
|
||||
@@ -106,17 +106,17 @@
|
||||
(let [content-part (MimeBodyPart.)
|
||||
alternative-mpart (MimeMultipart. "alternative")]
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(when-let [content (get body "text/html")]
|
||||
(let [html-part (MimeBodyPart.)]
|
||||
(.setContent html-part ^String content
|
||||
(str "text/html; charset=" charset))
|
||||
(.addBodyPart alternative-mpart html-part)))
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(.setContent content-part alternative-mpart)
|
||||
(.addBodyPart mixed-mpart content-part))
|
||||
|
||||
|
||||
@@ -79,18 +79,6 @@
|
||||
(remove #(contains? reserved-props (key %))))
|
||||
props))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context {:external-session-id (::rpc/external-session-id params)
|
||||
:external-event-origin (::rpc/external-event-origin params)
|
||||
:triggered-by (::rpc/handler-name params)}]
|
||||
{::type "action"
|
||||
::profile-id (::rpc/profile-id params)
|
||||
::ip-addr (::rpc/ip-addr params)
|
||||
::context (d/without-nils context)}))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
@@ -99,13 +87,24 @@
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-external-event-origin
|
||||
(defn- get-client-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
(str/prune origin 200))))
|
||||
|
||||
(defn get-client-user-agent
|
||||
[request]
|
||||
(when-let [user-agent (yreq/get-header request "user-agent")]
|
||||
(str/prune user-agent 500)))
|
||||
|
||||
(defn- get-client-version
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-frontend-version")]
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
(str/prune origin 100))))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -134,6 +133,33 @@
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
token-id (::actoken/id request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:access-token-id (some-> token-id str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
@@ -148,18 +174,10 @@
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id
|
||||
(get-external-session-id request))
|
||||
(assoc :external-event-origin
|
||||
(get-external-event-origin request))
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
context (merge (::context resultm)
|
||||
(prepare-context-from-request request))
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
@@ -378,7 +379,7 @@
|
||||
[:type [:= :set-token]]
|
||||
[:set-id ::sm/uuid]
|
||||
[:token-id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-attrs]]]]
|
||||
[:attrs [:maybe cto/schema:token-attrs]]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
|
||||
@@ -8,9 +8,112 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.i18n :refer [tr]]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HIGH LEVEL SCHEMAS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Token
|
||||
|
||||
(defn make-token-name-schema
|
||||
"Dynamically generates a schema to check a token name, adding translated error messages
|
||||
and two additional validations:
|
||||
- Min and max length.
|
||||
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
|
||||
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
|
||||
[tokens-tree]
|
||||
[:and
|
||||
(-> cto/schema:token-name
|
||||
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (ctob/token-name-path-exists? % tokens-tree))]])
|
||||
|
||||
(def schema:token-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-schema
|
||||
[tokens-tree]
|
||||
(sm/merge
|
||||
cto/schema:token-attrs
|
||||
[:map
|
||||
[:name (make-token-name-schema tokens-tree)]
|
||||
[:description {:optional true} schema:token-description]]))
|
||||
|
||||
;; Token set
|
||||
|
||||
(defn make-token-set-name-schema
|
||||
"Generates a dynamic schema to check a token set name:
|
||||
- Validate name length.
|
||||
- Checks if other token set with a path derived from the name already exists in the tokens lib."
|
||||
[tokens-lib set-id]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
|
||||
(fn [name]
|
||||
(let [set (ctob/get-set-by-name tokens-lib name)]
|
||||
(or (nil? set) (= (ctob/get-id set) set-id))))]])
|
||||
|
||||
(def schema:token-set-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-set-schema
|
||||
[tokens-lib set-id]
|
||||
(sm/merge
|
||||
ctob/schema:token-set-attrs
|
||||
[:map
|
||||
[:name [:and (make-token-set-name-schema tokens-lib set-id)
|
||||
[:fn #(ctob/normalized-set-name? %)]]]
|
||||
[:description {:optional true} schema:token-set-description]]))
|
||||
|
||||
;; Token theme
|
||||
|
||||
(defn make-token-theme-group-schema
|
||||
"Generates a dynamic schema to check a token theme group:
|
||||
- Validate group length.
|
||||
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
|
||||
[tokens-lib name theme-id]
|
||||
[:and
|
||||
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
|
||||
(fn [group]
|
||||
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
|
||||
(or (nil? theme) (= (:id theme) theme-id))))]])
|
||||
|
||||
(defn make-token-theme-name-schema
|
||||
"Generates a dynamic schema to check a token theme name:
|
||||
- Validate name length.
|
||||
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
|
||||
[tokens-lib group theme-id]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
|
||||
(fn [name]
|
||||
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
|
||||
(or (nil? theme) (= (:id theme) theme-id))))]])
|
||||
|
||||
(def schema:token-theme-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-theme-schema
|
||||
[tokens-lib group name theme-id]
|
||||
(sm/merge
|
||||
ctob/schema:token-theme-attrs
|
||||
[:map
|
||||
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
|
||||
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
|
||||
[:description {:optional true} schema:token-theme-description]]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def parseable-token-value-regexp
|
||||
"Regexp that can be used to parse a number value out of resolved token value.
|
||||
This regexp also trims whitespace around the value."
|
||||
@@ -80,56 +183,6 @@
|
||||
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
|
||||
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
|
||||
|
||||
(defn token-name->path
|
||||
"Splits token-name into a path vector split by `.` characters.
|
||||
|
||||
Will concatenate multiple `.` characters into one."
|
||||
[token-name]
|
||||
(str/split token-name #"\.+"))
|
||||
|
||||
(defn token-name->path-selector
|
||||
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
|
||||
|
||||
`:selector` is the last item of the names path
|
||||
`:path` is everything leading up the the `:selector`."
|
||||
[token-name]
|
||||
(let [path-segments (token-name->path token-name)
|
||||
last-idx (dec (count path-segments))
|
||||
[path [selector]] (split-at last-idx path-segments)]
|
||||
{:path (seq path)
|
||||
:selector selector}))
|
||||
|
||||
(defn token-name-path-exists?
|
||||
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
|
||||
|
||||
It's not allowed to create a token inside a token. E.g.:
|
||||
Creating a token with
|
||||
|
||||
{:name \"foo.bar\"}
|
||||
|
||||
in the tokens tree:
|
||||
|
||||
{\"foo\" {:name \"other\"}}"
|
||||
[token-name token-names-tree]
|
||||
(let [{:keys [path selector]} (token-name->path-selector token-name)
|
||||
path-target (reduce
|
||||
(fn [acc cur]
|
||||
(let [target (get acc cur)]
|
||||
(cond
|
||||
;; Path segment doesn't exist yet
|
||||
(nil? target) (reduced false)
|
||||
;; A token exists at this path
|
||||
(:name target) (reduced true)
|
||||
;; Continue traversing the true
|
||||
:else target)))
|
||||
token-names-tree path)]
|
||||
(cond
|
||||
(boolean? path-target) path-target
|
||||
(get path-target :name) true
|
||||
:else (-> (get path-target selector)
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
(defn color-token? [token]
|
||||
(= (:type token) :color))
|
||||
|
||||
|
||||
15
common/src/app/common/i18n.cljc
Normal file
15
common/src/app/common/i18n.cljc
Normal file
@@ -0,0 +1,15 @@
|
||||
;; 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.i18n
|
||||
"Dummy i18n functions, to be used by code in common that needs translations.")
|
||||
|
||||
(defn tr
|
||||
"This function will be monkeypatched at runtime with the real function in frontend i18n.
|
||||
Here it just returns the key passed as argument. This way the result can be used in
|
||||
unit tests or backend code for logs or error messages."
|
||||
[key & _args]
|
||||
key)
|
||||
@@ -12,8 +12,11 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
@@ -25,6 +28,7 @@
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
@@ -1874,6 +1878,44 @@
|
||||
roperations'
|
||||
uoperations')))))))
|
||||
|
||||
(defn- set-path-new-values
|
||||
[current-shape prev-shape transform]
|
||||
(let [new-content (segment/transform-content
|
||||
(:content current-shape)
|
||||
(gmt/transform-in (gpt/point 0 0) transform))
|
||||
new-points (-> (segment/content->selrect new-content)
|
||||
(grc/rect->points))
|
||||
points-center (gco/points->center new-points)
|
||||
new-selrect (gsh/calculate-selrect new-points points-center)
|
||||
shape (assoc current-shape
|
||||
:content new-content
|
||||
:points new-points
|
||||
:selrect new-selrect)
|
||||
|
||||
prev-center (segment/content-center (:content prev-shape))
|
||||
delta (gpt/subtract points-center (first new-points))
|
||||
new-pos (gpt/subtract prev-center delta)]
|
||||
(gsh/absolute-move shape new-pos)))
|
||||
|
||||
(defn- switch-path-change-value
|
||||
[prev-shape ;; The shape before the switch
|
||||
current-shape ;; The shape after the switch (a clean copy)
|
||||
ref-shape ;; The referenced shape on the main component
|
||||
;; before the switch
|
||||
attr]
|
||||
(let [old-width (-> ref-shape :selrect :width)
|
||||
new-width (-> prev-shape :selrect :width)
|
||||
|
||||
old-height (-> ref-shape :selrect :height)
|
||||
new-height (-> prev-shape :selrect :height)
|
||||
|
||||
transform (-> (gpt/point (/ new-width old-width)
|
||||
(/ new-height old-height))
|
||||
(gmt/scale-matrix))
|
||||
|
||||
shape (set-path-new-values current-shape prev-shape transform)]
|
||||
(get shape attr)))
|
||||
|
||||
|
||||
(defn- switch-text-change-value
|
||||
[prev-content ;; The :content of the text before the switch
|
||||
@@ -2025,6 +2067,10 @@
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
path-change?
|
||||
(and (= :path (:type current-shape))
|
||||
(contains? #{:points :selrect :content} attr))
|
||||
|
||||
;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
;; so it's calculated again
|
||||
@@ -2053,6 +2099,12 @@
|
||||
(:content origin-ref-shape)
|
||||
touched)
|
||||
|
||||
path-change?
|
||||
(switch-path-change-value previous-shape
|
||||
current-shape
|
||||
origin-ref-shape
|
||||
attr)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
@@ -2439,11 +2491,13 @@
|
||||
(ctk/get-swap-slot))
|
||||
(constantly false))
|
||||
|
||||
;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the
|
||||
;; mask property as part of the sanitization check on generate-delete-shapes, passing "ignore-mask" to prevent this
|
||||
[all-parents changes]
|
||||
(-> changes
|
||||
(cls/generate-delete-shapes
|
||||
file page objects (d/ordered-set (:id shape))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true}))
|
||||
[new-shape changes]
|
||||
(-> changes
|
||||
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
(cto/shape-attr->token-attrs attr changed-sub-attr))]
|
||||
|
||||
(if (some #(contains? tokens %) token-attrs)
|
||||
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
|
||||
(pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
|
||||
changes)))
|
||||
|
||||
check-shape
|
||||
@@ -123,8 +123,10 @@
|
||||
;; ignore-children-fn is used to ignore some descendants
|
||||
;; on the deletion process. It should receive a shape and
|
||||
;; return a boolean
|
||||
ignore-children-fn]
|
||||
:or {ignore-children-fn (constantly false)}}]
|
||||
ignore-children-fn
|
||||
ignore-mask]
|
||||
:or {ignore-children-fn (constantly false)
|
||||
ignore-mask false}}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
data (pcb/get-library-data changes)
|
||||
page-id (pcb/get-page-id changes)
|
||||
@@ -162,18 +164,20 @@
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
(when-not ignore-mask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
[])
|
||||
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
[& items]
|
||||
(apply mu/merge (map schema items)))
|
||||
|
||||
(defn assoc-key
|
||||
"Add a key & value to a schema"
|
||||
[s k v]
|
||||
(mu/assoc (schema s) k v))
|
||||
|
||||
(defn dissoc-key
|
||||
"Remove a key from a schema"
|
||||
[s k]
|
||||
(mu/dissoc (schema s) k))
|
||||
|
||||
(defn ref?
|
||||
[s]
|
||||
(m/-ref-schema? s))
|
||||
@@ -270,6 +280,13 @@
|
||||
(let [explain (fn [] (me/with-error-messages explain))]
|
||||
((mdp/prettifier variant message explain default-options)))))
|
||||
|
||||
(defn validation-errors
|
||||
"Checks a value against a schema. If valid, returns nil. If not, returns a list
|
||||
of english error messages."
|
||||
[value schema]
|
||||
(let [explainer (explainer schema)]
|
||||
(-> value explainer simplify not-empty)))
|
||||
|
||||
(defmacro ignoring
|
||||
[expr]
|
||||
(if (:ns &env)
|
||||
@@ -284,7 +301,20 @@
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [s (schema s)
|
||||
(let [s #?(:clj
|
||||
(schema s)
|
||||
:cljs
|
||||
(try
|
||||
(schema s)
|
||||
(catch :default cause
|
||||
(let [data (ex-data cause)]
|
||||
(if (= :malli.core/invalid-schema (:type data))
|
||||
(throw (ex-info
|
||||
(str "Invalid schema\n"
|
||||
(pp/pprint-str (:data data)))
|
||||
{}))
|
||||
(throw cause))))))
|
||||
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
@@ -304,7 +334,7 @@
|
||||
|
||||
(defn coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (decoder schema json-transformer)
|
||||
(let [decode-fn (lazy-decoder schema json-transformer)
|
||||
check-fn (check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[clojure.data :as data]
|
||||
[app.common.time :as ct]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[malli.util :as mu]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;; GENERAL HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- schema-keys
|
||||
@@ -45,32 +45,33 @@
|
||||
[token-name token-value]
|
||||
(let [token-references (find-token-value-references token-value)
|
||||
self-reference? (get token-references token-name)]
|
||||
self-reference?))
|
||||
(boolean self-reference?)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(defn composite-token-reference?
|
||||
"Predicate if a composite token is a reference value - a string pointing to another token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
(def token-type->dtcg-token-type
|
||||
{:boolean "boolean"
|
||||
:border-radius "borderRadius"
|
||||
:shadow "shadow"
|
||||
:color "color"
|
||||
:dimensions "dimension"
|
||||
:font-family "fontFamilies"
|
||||
:font-size "fontSizes"
|
||||
:font-weight "fontWeights"
|
||||
:letter-spacing "letterSpacing"
|
||||
:number "number"
|
||||
:opacity "opacity"
|
||||
:other "other"
|
||||
:rotation "rotation"
|
||||
:shadow "shadow"
|
||||
:sizing "sizing"
|
||||
:spacing "spacing"
|
||||
:string "string"
|
||||
:stroke-width "borderWidth"
|
||||
:text-case "textCase"
|
||||
:text-decoration "textDecoration"
|
||||
:font-weight "fontWeights"
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
@@ -82,14 +83,13 @@
|
||||
"boxShadow" :shadow)))
|
||||
|
||||
(def composite-token-type->dtcg-token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"When converting the type of one element inside a composite token, an additional type
|
||||
:line-height is available, that is not allowed for a standalone token."
|
||||
(assoc token-type->dtcg-token-type
|
||||
:line-height "lineHeights"))
|
||||
|
||||
(def composite-dtcg-token-type->token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"Same as above, in the opposite direction."
|
||||
(assoc dtcg-token-type->token-type
|
||||
"lineHeights" :line-height
|
||||
"lineHeight" :line-height))
|
||||
@@ -97,93 +97,95 @@
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA: Token
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:token-name
|
||||
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
||||
not start with $ or end with a dot. The $ character does not have any special meaning,
|
||||
but dots separate token groups (e.g. color.primary.background)."
|
||||
[:re {:title "TokenName"
|
||||
:gen/gen sg/text}
|
||||
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:fill {:optional true} token-name-ref]
|
||||
[:stroke-color {:optional true} token-name-ref]])
|
||||
(def schema:token-type
|
||||
[::sm/one-of {:decode/json (fn [type]
|
||||
(if (string? type)
|
||||
(dtcg-token-type->token-type type)
|
||||
type))}
|
||||
|
||||
(def color-keys (schema-keys schema:color))
|
||||
token-types])
|
||||
|
||||
(def schema:token-attrs
|
||||
[:map {:title "Token"}
|
||||
[:id ::sm/uuid]
|
||||
[:name schema:token-name]
|
||||
[:type schema:token-type]
|
||||
[:value ::sm/any]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA: Token application to shape
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; All the following schemas define the `:applied-tokens` attribute of a shape.
|
||||
;; This attribute is a map <token-attribute> -> <token-name>.
|
||||
;; Token attributes approximately match shape attributes, but not always.
|
||||
;; For each schema there is a `*keys` set including all the possible token attributes
|
||||
;; to which a token of the corresponding type can be applied.
|
||||
;; Some token types can be applied to some attributes only if the shape has a
|
||||
;; particular condition (i.e. has a layout itself or is a layout item).
|
||||
|
||||
(def ^:private schema:border-radius
|
||||
[:map {:title "BorderRadiusTokenAttrs"}
|
||||
[:r1 {:optional true} token-name-ref]
|
||||
[:r2 {:optional true} token-name-ref]
|
||||
[:r3 {:optional true} token-name-ref]
|
||||
[:r4 {:optional true} token-name-ref]])
|
||||
[:r1 {:optional true} schema:token-name]
|
||||
[:r2 {:optional true} schema:token-name]
|
||||
[:r3 {:optional true} schema:token-name]
|
||||
[:r4 {:optional true} schema:token-name]])
|
||||
|
||||
(def border-radius-keys (schema-keys schema:border-radius))
|
||||
|
||||
(def ^:private schema:shadow
|
||||
[:map {:title "ShadowTokenAttrs"}
|
||||
[:shadow {:optional true} token-name-ref]])
|
||||
|
||||
(def shadow-keys (schema-keys schema:shadow))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:stroke-width {:optional true} token-name-ref]])
|
||||
[:fill {:optional true} schema:token-name]
|
||||
[:stroke-color {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
(def color-keys (schema-keys schema:color))
|
||||
|
||||
(def ^:private schema:sizing-base
|
||||
[:map {:title "SizingBaseTokenAttrs"}
|
||||
[:width {:optional true} token-name-ref]
|
||||
[:height {:optional true} token-name-ref]])
|
||||
[:width {:optional true} schema:token-name]
|
||||
[:height {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:sizing-layout-item
|
||||
[:map {:title "SizingLayoutItemTokenAttrs"}
|
||||
[:layout-item-min-w {:optional true} token-name-ref]
|
||||
[:layout-item-max-w {:optional true} token-name-ref]
|
||||
[:layout-item-min-h {:optional true} token-name-ref]
|
||||
[:layout-item-max-h {:optional true} token-name-ref]])
|
||||
[:layout-item-min-w {:optional true} schema:token-name]
|
||||
[:layout-item-max-w {:optional true} schema:token-name]
|
||||
[:layout-item-min-h {:optional true} schema:token-name]
|
||||
[:layout-item-max-h {:optional true} schema:token-name]])
|
||||
|
||||
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
|
||||
|
||||
(def ^:private schema:sizing
|
||||
(-> (reduce mu/union [schema:sizing-base
|
||||
schema:sizing-layout-item])
|
||||
(mu/update-properties assoc :title "SizingTokenAttrs")))
|
||||
|
||||
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
|
||||
|
||||
(def sizing-keys (schema-keys schema:sizing))
|
||||
|
||||
(def ^:private schema:opacity
|
||||
[:map {:title "OpacityTokenAttrs"}
|
||||
[:opacity {:optional true} token-name-ref]])
|
||||
|
||||
(def opacity-keys (schema-keys schema:opacity))
|
||||
|
||||
(def ^:private schema:spacing-gap
|
||||
[:map {:title "SpacingGapTokenAttrs"}
|
||||
[:row-gap {:optional true} token-name-ref]
|
||||
[:column-gap {:optional true} token-name-ref]])
|
||||
[:row-gap {:optional true} schema:token-name]
|
||||
[:column-gap {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:spacing-padding
|
||||
[:map {:title "SpacingPaddingTokenAttrs"}
|
||||
[:p1 {:optional true} token-name-ref]
|
||||
[:p2 {:optional true} token-name-ref]
|
||||
[:p3 {:optional true} token-name-ref]
|
||||
[:p4 {:optional true} token-name-ref]])
|
||||
|
||||
(def ^:private schema:spacing-margin
|
||||
[:map {:title "SpacingMarginTokenAttrs"}
|
||||
[:m1 {:optional true} token-name-ref]
|
||||
[:m2 {:optional true} token-name-ref]
|
||||
[:m3 {:optional true} token-name-ref]
|
||||
[:m4 {:optional true} token-name-ref]])
|
||||
|
||||
(def ^:private schema:spacing
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
schema:spacing-padding
|
||||
schema:spacing-margin])
|
||||
(mu/update-properties assoc :title "SpacingTokenAttrs")))
|
||||
|
||||
(def spacing-margin-keys (schema-keys schema:spacing-margin))
|
||||
|
||||
(def spacing-keys (schema-keys schema:spacing))
|
||||
[:p1 {:optional true} schema:token-name]
|
||||
[:p2 {:optional true} schema:token-name]
|
||||
[:p3 {:optional true} schema:token-name]
|
||||
[:p4 {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:spacing-gap-padding
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
@@ -192,6 +194,29 @@
|
||||
|
||||
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
|
||||
|
||||
(def ^:private schema:spacing-margin
|
||||
[:map {:title "SpacingMarginTokenAttrs"}
|
||||
[:m1 {:optional true} schema:token-name]
|
||||
[:m2 {:optional true} schema:token-name]
|
||||
[:m3 {:optional true} schema:token-name]
|
||||
[:m4 {:optional true} schema:token-name]])
|
||||
|
||||
(def spacing-margin-keys (schema-keys schema:spacing-margin))
|
||||
|
||||
(def ^:private schema:spacing
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
schema:spacing-padding
|
||||
schema:spacing-margin])
|
||||
(mu/update-properties assoc :title "SpacingTokenAttrs")))
|
||||
|
||||
(def spacing-keys (schema-keys schema:spacing))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
[:map
|
||||
[:stroke-width {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
|
||||
(def ^:private schema:dimensions
|
||||
(-> (reduce mu/union [schema:sizing
|
||||
schema:spacing
|
||||
@@ -201,91 +226,109 @@
|
||||
|
||||
(def dimensions-keys (schema-keys schema:dimensions))
|
||||
|
||||
(def ^:private schema:axis
|
||||
[:map
|
||||
[:x {:optional true} token-name-ref]
|
||||
[:y {:optional true} token-name-ref]])
|
||||
|
||||
(def axis-keys (schema-keys schema:axis))
|
||||
|
||||
(def ^:private schema:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[:rotation {:optional true} token-name-ref]])
|
||||
|
||||
(def rotation-keys (schema-keys schema:rotation))
|
||||
|
||||
(def ^:private schema:font-size
|
||||
[:map {:title "FontSizeTokenAttrs"}
|
||||
[:font-size {:optional true} token-name-ref]])
|
||||
|
||||
(def font-size-keys (schema-keys schema:font-size))
|
||||
|
||||
(def ^:private schema:letter-spacing
|
||||
[:map {:title "LetterSpacingTokenAttrs"}
|
||||
[:letter-spacing {:optional true} token-name-ref]])
|
||||
|
||||
(def letter-spacing-keys (schema-keys schema:letter-spacing))
|
||||
|
||||
(def ^:private schema:font-family
|
||||
[:map
|
||||
[:font-family {:optional true} token-name-ref]])
|
||||
[:font-family {:optional true} schema:token-name]])
|
||||
|
||||
(def font-family-keys (schema-keys schema:font-family))
|
||||
|
||||
(def ^:private schema:text-case
|
||||
[:map
|
||||
[:text-case {:optional true} token-name-ref]])
|
||||
(def ^:private schema:font-size
|
||||
[:map {:title "FontSizeTokenAttrs"}
|
||||
[:font-size {:optional true} schema:token-name]])
|
||||
|
||||
(def text-case-keys (schema-keys schema:text-case))
|
||||
(def font-size-keys (schema-keys schema:font-size))
|
||||
|
||||
(def ^:private schema:font-weight
|
||||
[:map
|
||||
[:font-weight {:optional true} token-name-ref]])
|
||||
[:font-weight {:optional true} schema:token-name]])
|
||||
|
||||
(def font-weight-keys (schema-keys schema:font-weight))
|
||||
|
||||
(def ^:private schema:typography
|
||||
[:map
|
||||
[:typography {:optional true} token-name-ref]])
|
||||
(def ^:private schema:letter-spacing
|
||||
[:map {:title "LetterSpacingTokenAttrs"}
|
||||
[:letter-spacing {:optional true} schema:token-name]])
|
||||
|
||||
(def typography-token-keys (schema-keys schema:typography))
|
||||
(def letter-spacing-keys (schema-keys schema:letter-spacing))
|
||||
|
||||
(def ^:private schema:text-decoration
|
||||
[:map
|
||||
[:text-decoration {:optional true} token-name-ref]])
|
||||
(def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
|
||||
[:map {:title "LineHeightTokenAttrs"}
|
||||
[:line-height {:optional true} schema:token-name]])
|
||||
|
||||
(def text-decoration-keys (schema-keys schema:text-decoration))
|
||||
(def line-height-keys (schema-keys schema:line-height))
|
||||
|
||||
(def typography-keys (set/union font-size-keys
|
||||
letter-spacing-keys
|
||||
font-family-keys
|
||||
font-weight-keys
|
||||
text-case-keys
|
||||
text-decoration-keys
|
||||
font-weight-keys
|
||||
typography-token-keys
|
||||
#{:line-height}))
|
||||
(def ^:private schema:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[:rotation {:optional true} schema:token-name]])
|
||||
|
||||
(def rotation-keys (schema-keys schema:rotation))
|
||||
|
||||
(def ^:private schema:number
|
||||
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
|
||||
(-> (reduce mu/union [schema:line-height
|
||||
schema:rotation])
|
||||
(mu/update-properties assoc :title "NumberTokenAttrs")))
|
||||
|
||||
(def number-keys (schema-keys schema:number))
|
||||
|
||||
(def all-keys (set/union color-keys
|
||||
(def ^:private schema:opacity
|
||||
[:map {:title "OpacityTokenAttrs"}
|
||||
[:opacity {:optional true} schema:token-name]])
|
||||
|
||||
(def opacity-keys (schema-keys schema:opacity))
|
||||
|
||||
(def ^:private schema:shadow
|
||||
[:map {:title "ShadowTokenAttrs"}
|
||||
[:shadow {:optional true} schema:token-name]])
|
||||
|
||||
(def shadow-keys (schema-keys schema:shadow))
|
||||
|
||||
(def ^:private schema:text-case
|
||||
[:map
|
||||
[:text-case {:optional true} schema:token-name]])
|
||||
|
||||
(def text-case-keys (schema-keys schema:text-case))
|
||||
|
||||
(def ^:private schema:text-decoration
|
||||
[:map
|
||||
[:text-decoration {:optional true} schema:token-name]])
|
||||
|
||||
(def text-decoration-keys (schema-keys schema:text-decoration))
|
||||
|
||||
(def ^:private schema:typography
|
||||
[:map
|
||||
[:typography {:optional true} schema:token-name]])
|
||||
|
||||
(def typography-token-keys (schema-keys schema:typography))
|
||||
|
||||
(def typography-keys (set/union font-family-keys
|
||||
font-size-keys
|
||||
font-weight-keys
|
||||
font-weight-keys
|
||||
letter-spacing-keys
|
||||
line-height-keys
|
||||
text-case-keys
|
||||
text-decoration-keys
|
||||
typography-token-keys))
|
||||
|
||||
(def ^:private schema:axis
|
||||
[:map
|
||||
[:x {:optional true} schema:token-name]
|
||||
[:y {:optional true} schema:token-name]])
|
||||
|
||||
(def axis-keys (schema-keys schema:axis))
|
||||
|
||||
(def all-keys (set/union axis-keys
|
||||
border-radius-keys
|
||||
shadow-keys
|
||||
stroke-width-keys
|
||||
sizing-keys
|
||||
opacity-keys
|
||||
spacing-keys
|
||||
color-keys
|
||||
dimensions-keys
|
||||
axis-keys
|
||||
number-keys
|
||||
opacity-keys
|
||||
rotation-keys
|
||||
shadow-keys
|
||||
sizing-keys
|
||||
spacing-keys
|
||||
stroke-width-keys
|
||||
typography-keys
|
||||
typography-token-keys
|
||||
number-keys))
|
||||
typography-token-keys))
|
||||
|
||||
(def ^:private schema:tokens
|
||||
[:map {:title "GenericTokenAttrs"}])
|
||||
@@ -306,11 +349,28 @@
|
||||
schema:text-decoration
|
||||
schema:dimensions])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS for token attributes by token type
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn token-attr?
|
||||
[attr]
|
||||
(contains? all-keys attr))
|
||||
|
||||
(defn token-attr->shape-attr
|
||||
"Returns the actual shape attribute affected when a token have been applied
|
||||
to a given `token-attr`."
|
||||
[token-attr]
|
||||
(case token-attr
|
||||
:fill :fills
|
||||
:stroke-color :strokes
|
||||
:stroke-width :strokes
|
||||
token-attr))
|
||||
|
||||
(defn shape-attr->token-attrs
|
||||
"Returns the token-attr affected when a given attribute in a shape is changed.
|
||||
The sub-attr is for attributes that may have multiple values, like strokes
|
||||
(may be width or color) and layout padding & margin (may have 4 edges)."
|
||||
([shape-attr] (shape-attr->token-attrs shape-attr nil))
|
||||
([shape-attr changed-sub-attr]
|
||||
(cond
|
||||
@@ -352,21 +412,13 @@
|
||||
(number-keys shape-attr) #{shape-attr}
|
||||
(axis-keys shape-attr) #{shape-attr})))
|
||||
|
||||
(defn token-attr->shape-attr
|
||||
[token-attr]
|
||||
(case token-attr
|
||||
:fill :fills
|
||||
:stroke-color :strokes
|
||||
:stroke-width :strokes
|
||||
token-attr))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN SHAPE ATTRIBUTES
|
||||
;; HELPERS for token attributes by shape type
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def position-attributes #{:x :y})
|
||||
(def ^:private position-attributes #{:x :y})
|
||||
|
||||
(def generic-attributes
|
||||
(def ^:private generic-attributes
|
||||
(set/union color-keys
|
||||
stroke-width-keys
|
||||
rotation-keys
|
||||
@@ -375,20 +427,22 @@
|
||||
shadow-keys
|
||||
position-attributes))
|
||||
|
||||
(def rect-attributes
|
||||
(def ^:private rect-attributes
|
||||
(set/union generic-attributes
|
||||
border-radius-keys))
|
||||
|
||||
(def frame-with-layout-attributes
|
||||
(def ^:private frame-with-layout-attributes
|
||||
(set/union rect-attributes
|
||||
spacing-gap-padding-keys))
|
||||
|
||||
(def text-attributes
|
||||
(def ^:private text-attributes
|
||||
(set/union generic-attributes
|
||||
typography-keys
|
||||
number-keys))
|
||||
|
||||
(defn shape-type->attributes
|
||||
"Returns what token attributes may be applied to a shape depending on its type
|
||||
and if it is a frame with a layout."
|
||||
[type is-layout]
|
||||
(case type
|
||||
:bool generic-attributes
|
||||
@@ -404,12 +458,14 @@
|
||||
nil))
|
||||
|
||||
(defn appliable-attrs-for-shape
|
||||
"Returns intersection of shape `attributes` for `shape-type`."
|
||||
"Returns which ones of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes shape-type is-layout]
|
||||
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
|
||||
|
||||
(defn any-appliable-attr-for-shape?
|
||||
"Checks if `token-type` supports given shape `attributes`."
|
||||
"Returns if any of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes token-type is-layout]
|
||||
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
|
||||
|
||||
@@ -420,42 +476,6 @@
|
||||
typography-keys
|
||||
#{:fill}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS IN SHAPES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(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- token-from-attributes [token attributes]
|
||||
(->> (map (fn [attr] [attr (:name token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
|
||||
(let [token (token-from-attributes token attributes)]
|
||||
(toggle-or-apply-token shape token)))
|
||||
|
||||
(defn apply-token-to-shape
|
||||
[{:keys [shape token attributes] :as _props}]
|
||||
(let [applied-tokens (apply-token-to-attributes {:shape shape
|
||||
:token token
|
||||
:attributes attributes})]
|
||||
(update shape :applied-tokens #(merge % applied-tokens))))
|
||||
|
||||
(defn unapply-token-id [shape attributes]
|
||||
(update shape :applied-tokens d/without-keys attributes))
|
||||
|
||||
(defn unapply-layout-item-tokens
|
||||
"Unapplies all layout item related tokens from shape."
|
||||
[shape]
|
||||
(let [layout-item-attrs (set/union sizing-layout-item-keys
|
||||
spacing-margin-keys)]
|
||||
(unapply-token-id shape layout-item-attrs)))
|
||||
|
||||
(def tokens-by-input
|
||||
"A map from input name to applicable token for that input."
|
||||
{:width #{:sizing :dimensions}
|
||||
@@ -481,7 +501,48 @@
|
||||
:stroke-color #{:color}})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TYPOGRAPHY
|
||||
;; HELPERS for tokens application
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; TODO it seems that this function is redundant, maybe?
|
||||
;; (defn- toggle-or-apply-token
|
||||
;; "Remove any shape attributes from token if they exists.
|
||||
;; Othewise apply token attributes."
|
||||
;; [shape token]
|
||||
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
|
||||
;; (merge {} only-in-shape only-in-token)))
|
||||
|
||||
(defn- generate-attr-map [token attributes]
|
||||
(->> (map (fn [attr] [attr (:name token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn apply-token-to-shape
|
||||
"Applies the token to the given attributes in the shape."
|
||||
[{:keys [shape token attributes] :as _props}]
|
||||
(let [map-to-apply (generate-attr-map token attributes)]
|
||||
(update shape :applied-tokens #(merge % map-to-apply))))
|
||||
|
||||
;; (defn apply-token-to-shape
|
||||
;; [{:keys [shape token attributes] :as _props}]
|
||||
;; (let [map-to-apply (generate-attr-map token attributes)
|
||||
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
|
||||
;; (update shape :applied-tokens #(merge % applied-tokens))))
|
||||
|
||||
(defn unapply-tokens-from-shape
|
||||
"Removes any token applied to the given attributes in the shape."
|
||||
[shape attributes]
|
||||
(update shape :applied-tokens d/without-keys attributes))
|
||||
|
||||
(defn unapply-layout-item-tokens
|
||||
"Unapplies all layout item related tokens from shape."
|
||||
[shape]
|
||||
(let [layout-item-attrs (set/union sizing-layout-item-keys
|
||||
spacing-margin-keys)]
|
||||
(unapply-tokens-from-shape shape layout-item-attrs)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS for typography tokens
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn split-font-family
|
||||
@@ -544,17 +605,3 @@
|
||||
(when (font-weight-values weight)
|
||||
(cond-> {:weight weight}
|
||||
italic? (assoc :style "italic")))))
|
||||
|
||||
(defn typography-composite-token-reference?
|
||||
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SHADOW
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn shadow-composite-token-reference?
|
||||
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
@@ -114,25 +114,19 @@
|
||||
[o]
|
||||
(instance? Token o))
|
||||
|
||||
(def schema:token-attrs
|
||||
[:map {:title "Token"}
|
||||
[:id ::sm/uuid]
|
||||
[:name cto/token-name-ref]
|
||||
[:type [::sm/one-of cto/token-types]]
|
||||
[:value ::sm/any]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
(declare make-token)
|
||||
|
||||
(def schema:token
|
||||
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
|
||||
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
|
||||
(sg/fmap #(make-token %)))}
|
||||
(sm/required-keys schema:token-attrs)
|
||||
(sm/required-keys cto/schema:token-attrs)
|
||||
[:fn token?]])
|
||||
|
||||
(def ^:private check-token-attrs
|
||||
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
|
||||
(sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
|
||||
|
||||
(def decode-token-attrs
|
||||
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
|
||||
|
||||
(def check-token
|
||||
(sm/check-fn schema:token :hint "expected valid token"))
|
||||
@@ -317,10 +311,13 @@
|
||||
[o]
|
||||
(instance? TokenSetLegacy o))
|
||||
|
||||
(declare make-token-set)
|
||||
(declare normalized-set-name?)
|
||||
|
||||
(def schema:token-set-attrs
|
||||
[:map {:title "TokenSet"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:name [:and :string [:fn #(normalized-set-name? %)]]]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:tokens {:optional true
|
||||
@@ -342,8 +339,6 @@
|
||||
:string schema:token]
|
||||
[:fn d/ordered-map?]]]])
|
||||
|
||||
(declare make-token-set)
|
||||
|
||||
(def schema:token-set
|
||||
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
|
||||
(sg/fmap #(make-token-set %)))}
|
||||
@@ -404,12 +399,25 @@
|
||||
(split-set-name name))
|
||||
(cpn/join-path :separator set-separator :with-spaces? false))))
|
||||
|
||||
(defn normalized-set-name?
|
||||
"Check if a set name is normalized (no extra spaces)."
|
||||
[name]
|
||||
(= name (normalize-set-name name)))
|
||||
|
||||
(defn replace-last-path-name
|
||||
"Replaces the last element in a `path` vector with `name`."
|
||||
[path name]
|
||||
(-> (into [] (drop-last path))
|
||||
(conj name)))
|
||||
|
||||
(defn make-child-name
|
||||
"Generate the name of a set child of `parent-set` adding the name `name`."
|
||||
[parent-set name]
|
||||
(if-let [parent-path (get-set-path parent-set)]
|
||||
(->> (concat parent-path (split-set-name name))
|
||||
(join-set-path))
|
||||
(normalize-set-name name)))
|
||||
|
||||
;; The following functions will be removed after refactoring the internal structure of TokensLib,
|
||||
;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
|
||||
|
||||
@@ -1365,10 +1373,13 @@ Will return a value that matches this schema:
|
||||
(def ^:private check-tokens-lib-map
|
||||
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
|
||||
|
||||
(defn tokens-lib?
|
||||
[o]
|
||||
(instance? TokensLib o))
|
||||
|
||||
(defn valid-tokens-lib?
|
||||
[o]
|
||||
(and (instance? TokensLib o)
|
||||
(valid? o)))
|
||||
(and (tokens-lib? o) (valid? o)))
|
||||
|
||||
(defn- ensure-hidden-theme
|
||||
"A helper that is responsible to ensure that the hidden theme always
|
||||
@@ -1430,6 +1441,50 @@ Will return a value that matches this schema:
|
||||
(rename copy-name)
|
||||
(reid (uuid/next))))))
|
||||
|
||||
(defn- token-name->path-selector
|
||||
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
|
||||
|
||||
`:selector` is the last item of the names path
|
||||
`:path` is everything leading up the the `:selector`."
|
||||
[token-name]
|
||||
(let [path-segments (get-token-path {:name token-name})
|
||||
last-idx (dec (count path-segments))
|
||||
[path [selector]] (split-at last-idx path-segments)]
|
||||
{:path (seq path)
|
||||
:selector selector}))
|
||||
|
||||
(defn token-name-path-exists?
|
||||
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
|
||||
|
||||
It's not allowed to create a token inside a token. E.g.:
|
||||
Creating a token with
|
||||
|
||||
{:name \"foo.bar\"}
|
||||
|
||||
in the tokens tree:
|
||||
|
||||
{\"foo\" {:name \"other\"}}"
|
||||
[token-name tokens-tree]
|
||||
(let [{:keys [path selector]} (token-name->path-selector token-name)
|
||||
path-target (reduce
|
||||
(fn [acc cur]
|
||||
(let [target (get acc cur)]
|
||||
(cond
|
||||
;; Path segment doesn't exist yet
|
||||
(nil? target) (reduced false)
|
||||
;; A token exists at this path
|
||||
(:name target) (reduced true)
|
||||
;; Continue traversing the true
|
||||
:else target)))
|
||||
tokens-tree
|
||||
path)]
|
||||
(cond
|
||||
(boolean? path-target) path-target
|
||||
(get path-target :name) true
|
||||
:else (-> (get path-target selector)
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
;; === Import / Export from JSON format
|
||||
|
||||
;; Supported formats:
|
||||
@@ -1575,10 +1630,10 @@ Will return a value that matches this schema:
|
||||
(if (map? shadow)
|
||||
(let [legacy-shadow-type (get "type" shadow)]
|
||||
(-> shadow
|
||||
(set/rename-keys {"x" :offsetX
|
||||
"offsetX" :offsetX
|
||||
"y" :offsetY
|
||||
"offsetY" :offsetY
|
||||
(set/rename-keys {"x" :offset-x
|
||||
"offsetX" :offset-x
|
||||
"y" :offset-y
|
||||
"offsetY" :offset-y
|
||||
"blur" :blur
|
||||
"spread" :spread
|
||||
"color" :color
|
||||
@@ -1589,7 +1644,7 @@ Will return a value that matches this schema:
|
||||
(= "false" %) false
|
||||
(= legacy-shadow-type "innerShadow") true
|
||||
:else false))
|
||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
||||
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||
shadow))]
|
||||
(cond
|
||||
;; Reference value - keep as string
|
||||
@@ -1860,8 +1915,8 @@ Will return a value that matches this schema:
|
||||
(mapv (fn [shadow]
|
||||
(if (map? shadow)
|
||||
(-> shadow
|
||||
(set/rename-keys {:offsetX "offsetX"
|
||||
:offsetY "offsetY"
|
||||
(set/rename-keys {:offset-x "offsetX"
|
||||
:offset-y "offsetY"
|
||||
:blur "blur"
|
||||
:spread "spread"
|
||||
:color "color"
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
(defn parse
|
||||
[data]
|
||||
(cond
|
||||
(str/starts-with? data "%")
|
||||
(or (str/starts-with? data "%")
|
||||
(= data "develop"))
|
||||
{:full "develop"
|
||||
:branch "develop"
|
||||
:base "0.0.0"
|
||||
|
||||
@@ -6,34 +6,34 @@
|
||||
|
||||
(ns common-tests.files.tokens-test
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-parse-token-value
|
||||
(t/testing "parses double from a token value"
|
||||
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
|
||||
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
|
||||
(t/testing "trims white-space"
|
||||
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
|
||||
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
|
||||
(t/testing "parses unit: px"
|
||||
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
|
||||
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
|
||||
(t/testing "parses unit: %"
|
||||
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
|
||||
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
|
||||
(t/testing "parses unit: px")
|
||||
(t/testing "returns nil for any invalid characters"
|
||||
(t/is (nil? (cft/parse-token-value " -1.3a "))))
|
||||
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
|
||||
(t/testing "doesnt accept invalid double"
|
||||
(t/is (nil? (cft/parse-token-value ".3")))))
|
||||
(t/is (nil? (cfo/parse-token-value ".3")))))
|
||||
|
||||
(t/deftest token-applied-test
|
||||
(t/testing "matches passed token with `:token-attributes`"
|
||||
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match empty token"
|
||||
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "does't match passed token `:id`"
|
||||
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match passed `:token-attributes`"
|
||||
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
|
||||
(t/deftest shapes-ids-by-applied-attributes
|
||||
(t/testing "Returns set of matched attributes that fit the applied token"
|
||||
@@ -54,7 +54,7 @@
|
||||
shape-applied-x-y
|
||||
shape-applied-all
|
||||
shape-applied-none]
|
||||
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
(t/is (= (:x expected) (shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
@@ -62,34 +62,21 @@
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
(t/is (= (:z expected) (shape-ids shape-applied-all)))
|
||||
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
|
||||
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
|
||||
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
|
||||
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
|
||||
(shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all))))
|
||||
|
||||
(t/deftest tokens-applied-test
|
||||
(t/testing "is true when single shape matches the token and attributes"
|
||||
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x}))))
|
||||
(t/testing "is false when no shape matches the token or attributes"
|
||||
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x})))
|
||||
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "a"}}]
|
||||
#{:y})))))
|
||||
|
||||
(t/deftest name->path-test
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
|
||||
|
||||
(t/deftest token-name-path-exists?-test
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
||||
|
||||
@@ -255,28 +255,28 @@
|
||||
(cls/generate-update-shapes [(:id frame1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:r1 :r2 :r3 :r4])
|
||||
(cto/unapply-token-id [:rotation])
|
||||
(cto/unapply-token-id [:opacity])
|
||||
(cto/unapply-token-id [:stroke-width])
|
||||
(cto/unapply-token-id [:stroke-color])
|
||||
(cto/unapply-token-id [:fill])
|
||||
(cto/unapply-token-id [:width :height])))
|
||||
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
|
||||
(cto/unapply-tokens-from-shape [:rotation])
|
||||
(cto/unapply-tokens-from-shape [:opacity])
|
||||
(cto/unapply-tokens-from-shape [:stroke-width])
|
||||
(cto/unapply-tokens-from-shape [:stroke-color])
|
||||
(cto/unapply-tokens-from-shape [:fill])
|
||||
(cto/unapply-tokens-from-shape [:width :height])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id text1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:font-size])
|
||||
(cto/unapply-token-id [:letter-spacing])
|
||||
(cto/unapply-token-id [:font-family])))
|
||||
(cto/unapply-tokens-from-shape [:font-size])
|
||||
(cto/unapply-tokens-from-shape [:letter-spacing])
|
||||
(cto/unapply-tokens-from-shape [:font-family])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id circle1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
|
||||
(cto/unapply-token-id [:m1 :m2 :m3 :m4])))
|
||||
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
|
||||
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
|
||||
(:objects page)
|
||||
{}))
|
||||
|
||||
|
||||
@@ -8,20 +8,19 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-valid-token-name-schema
|
||||
;; Allow regular namespace token names
|
||||
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "foo")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
|
||||
;; Disallow trailing tokens
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
|
||||
;; Disallow multiple separator dots
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
|
||||
;; Disallow any special characters
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))
|
||||
|
||||
@@ -678,35 +678,35 @@
|
||||
|
||||
(t/deftest list-active-themes-tokens-bug-taiga-10617
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode / Dark"
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode/Dark"
|
||||
:tokens {"red"
|
||||
(ctob/make-token :name "red"
|
||||
:type :color
|
||||
:value "#700000")}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode / Light"
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode/Light"
|
||||
:tokens {"red"
|
||||
(ctob/make-token :name "red"
|
||||
:type :color
|
||||
:value "#ff0000")}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Device / Desktop"
|
||||
(ctob/add-set (ctob/make-token-set :name "Device/Desktop"
|
||||
:tokens {"border1"
|
||||
(ctob/make-token :name "border1"
|
||||
:type :border-radius
|
||||
:value 30)}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Device / Mobile"
|
||||
(ctob/add-set (ctob/make-token-set :name "Device/Mobile"
|
||||
:tokens {"border1"
|
||||
(ctob/make-token :name "border1"
|
||||
:type :border-radius
|
||||
:value 50)}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "App"
|
||||
:name "Mobile"
|
||||
:sets #{"Mode / Dark" "Device / Mobile"}))
|
||||
:sets #{"Mode/Dark" "Device/Mobile"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "App"
|
||||
:name "Web"
|
||||
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"}))
|
||||
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "Brand"
|
||||
:name "Brand A"
|
||||
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"}))
|
||||
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "Brand"
|
||||
:name "Brand B"
|
||||
:sets #{}))
|
||||
@@ -1897,15 +1897,15 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "multiple shadow token"
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with reference"
|
||||
@@ -1918,7 +1918,7 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with description"
|
||||
@@ -1937,14 +1937,14 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.single"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:description "A single shadow"})
|
||||
"shadow.multiple"
|
||||
(ctob/make-token
|
||||
{:name "shadow.multiple"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
{:name "shadow.ref"
|
||||
@@ -1991,7 +1991,7 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.test"
|
||||
:type :shadow
|
||||
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:description "Round trip test"})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
@@ -2013,3 +2013,11 @@
|
||||
(t/is (some? imported-ref))
|
||||
(t/is (= (:type original-ref) (:type imported-ref)))
|
||||
(t/is (= (:value imported-ref) (:value original-ref))))))))
|
||||
|
||||
(t/deftest token-name-path-exists?-test
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
||||
|
||||
@@ -395,6 +395,8 @@ COPY files/tmux.conf /root/.tmux.conf
|
||||
COPY files/sudoers /etc/sudoers
|
||||
|
||||
COPY files/Caddyfile /home/
|
||||
COPY files/selfsigned.crt /home/
|
||||
COPY files/selfsigned.key /home/
|
||||
COPY files/start-tmux.sh /home/start-tmux.sh
|
||||
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
|
||||
COPY files/entrypoint.sh /home/entrypoint.sh
|
||||
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
- 3447:3447
|
||||
- 3448:3448
|
||||
- 3449:3449
|
||||
- 3449:3449/udp
|
||||
- 3450:3450
|
||||
- 6006:6006
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
localhost:3449 {
|
||||
tls internal
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
nginx;
|
||||
caddy run -c /home/Caddyfile;
|
||||
nginx
|
||||
caddy start -c /home/Caddyfile
|
||||
tail -f /dev/null;
|
||||
|
||||
@@ -38,11 +38,11 @@ http {
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 3;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
@@ -223,16 +223,15 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store";
|
||||
# This header is what we need to use on prod
|
||||
# add_header Cache-Control "public, must-revalidate, max-age=0";
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
22
docker/devenv/files/selfsigned.crt
Normal file
22
docker/devenv/files/selfsigned.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuzCCAqOgAwIBAgIUa3THJQSn1+ErK65g1jDL0tjUkBYwDQYJKoZIhvcNAQEL
|
||||
BQAwXzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
|
||||
bDEOMAwGA1UECgwFTG9jYWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MB4XDTI1MTIwMjA4MjUyM1oXDTI2MTIwMjA4MjUyM1owXzELMAkGA1UEBhMC
|
||||
VVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2NhbDEOMAwGA1UECgwFTG9j
|
||||
YWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVIlfpIPE+QyL/q7IQOilEA7wEOZ6wbsh2Fr
|
||||
59H1gSLFvgoCxI6RVUkQ/MFRnw/r1ZbAqRpc2xAl5a9Ml14q20Zlj6dAHsWX6O2J
|
||||
EwNsD18dQmX3BncnjV3yCZM2iQcMFKuXG4KQNdIQNNvdIgtlrHYp0ohS9s3XC7cj
|
||||
KxNrm/pW9EAXfn9AYDd/qER090L2E4ipP9m/5l3MjinNc4l2kpH9rLOgb79H0RLt
|
||||
PK3/KP8ErZhAvzdmDBAdM5Z5K37b+TfB/kSVNUKL6qyw5CCjlShERLhBNprlnRfz
|
||||
tHNIQ1RHq3qJJN19ZnJrLqICuQ5ztvj7hBDiOSV0LnmyKgXr6wIDAQABo28wbTAd
|
||||
BgNVHQ4EFgQUPL8WGf6z/wB8TimJBx1zybsIeikwHwYDVR0jBBgwFoAUPL8WGf6z
|
||||
/wB8TimJBx1zybsIeikwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
|
||||
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBACMMVyR3kbNxnzuUc2lahKH4
|
||||
cPXVWOsvCvnDtjzm41XmKjUJTbtjn3p5d/ZmLbZ4zzIQULfWXO3XG/HevkvVo0g6
|
||||
6pJXTXc6C6ZhFG0rIYMcPPzmGmalDV5n+lUaCVx5XbFFxvRQ7893auwhRATdwGs+
|
||||
xiMyYbE2w9otKqyDItmJZJ5nW6vmXJ42YHxlXF18u9U88xqtOSMd5xZahbsmw7Gg
|
||||
A4/o4TPoAX5QfA306sL443WaczsF7bmsTf9qcYa/3xxQkP5Seyqx8ePWpS22qysE
|
||||
jG6XPpymxb6sb2mVaFBAzhEMb/eBvE9nRAopxmB7uV4TbqC51K/U3uo6jFX4Jbw=
|
||||
-----END CERTIFICATE-----
|
||||
28
docker/devenv/files/selfsigned.key
Normal file
28
docker/devenv/files/selfsigned.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUiV+kg8T5DIv
|
||||
+rshA6KUQDvAQ5nrBuyHYWvn0fWBIsW+CgLEjpFVSRD8wVGfD+vVlsCpGlzbECXl
|
||||
r0yXXirbRmWPp0AexZfo7YkTA2wPXx1CZfcGdyeNXfIJkzaJBwwUq5cbgpA10hA0
|
||||
290iC2WsdinSiFL2zdcLtyMrE2ub+lb0QBd+f0BgN3+oRHT3QvYTiKk/2b/mXcyO
|
||||
Kc1ziXaSkf2ss6Bvv0fREu08rf8o/wStmEC/N2YMEB0zlnkrftv5N8H+RJU1Qovq
|
||||
rLDkIKOVKEREuEE2muWdF/O0c0hDVEereokk3X1mcmsuogK5DnO2+PuEEOI5JXQu
|
||||
ebIqBevrAgMBAAECggEABqtE+LNn8nW9v98jcc2IBjc2g4D5yVJaZYWxqGVJJ7T6
|
||||
Lfhw7Qf4AoZAHM9en9FMM7Ahw7hO2SboynoLJHyHGOp1FNQqiJptFNdBkjKr0rqI
|
||||
4pk0HK+3zLQO/4gz50gne0vP3qZtlorV5Jpf8e/Et3jWm9XOQcTB2e6AKL4k827B
|
||||
dv4Tld+/7PoZVXjahfrUWuIZr5mzyF1eUkD8sPOpdr3HJxSueqsOMjbG8XMRqCQ+
|
||||
5eCWWSW5yPQlMr7M7cXM+a0k73Xn1sKl7fP3/9byji25zxGUaMu5RA1kw0Oqseid
|
||||
RXuRxGphGZgnx1aFxDAPg3FtmGch7/Cc6WfqboOL0QKBgQD4GZO1gGaE8cg4lvuo
|
||||
ZUX2YJu6UJuNOmuhfvG3ui4WO9PHy3btc2q+3kutSuBcyIjhi+qbXasBcX/QOOJF
|
||||
udyTZc5PopNkJojS4JdXAZCiu5sKI3lp4DIt9qNISlXGgrJgdxGUO+DzarBctXdn
|
||||
BSwXFw5hcjJjl7wsPGQl1tBTQwKBgQDPuz5MEM5ZeUe9CT5sQDq/ld0u4aL5AHmx
|
||||
aaA2gzDgd9l2R5wHX6wLzjoVWXOmeqaYzJopt2JN4iXrtbjWkyePgZeZMyWoyJ/v
|
||||
clW9bi8HM9f9EpPr7czSj9sLUnsjd9cuTD+JuXK//jRGbRpw7r7nWtLHImjj6d2v
|
||||
APZRq0v2OQKBgBcESG/OObSbubeGSlKVEqiIzem7ELNJeDLDVCl3XE8zvbILbj0Z
|
||||
OA39EYhCKg5xjEFgeaNwTS0VGoZ2wIc3dv81sq4wpvvjl035CBFKU+DFBt0p7Vml
|
||||
MwKQnxVV0B9agLHyWe8mnvf2LeZr72ffUvfRa8QelA4pRYvVDnV0OF+BAoGAW6rM
|
||||
+tQPuvwB5DFIEozlX9XKHP4E5MyI5vktceDCmMtKcx92gup9CVif2Pv4ROaqzZK8
|
||||
FNyPzL6W7UTrpASb2H/fXgNsAudFbGyP2V/d8Ne34D1qeRoe4GwKxRxIqoYftpZ/
|
||||
E096i66pcsqCeINiSsWRbb6JesmgwbEzAScOBkECgYEA6O/Dibc9PaqRpaiE6Qut
|
||||
S3W/Rr1Pd1jbN4rOVI2TFCgMJQmc6jOdq2fCntR9acsa8HPx+djOlXTUBPKBZ/Ae
|
||||
p8umRdXVWcNMnwWVWHt7tsEuR/gYkxQ5xjXeS1VDPnEre9+EaevMBuVs8HdRsKQO
|
||||
uzvNGeAFEfqwIqn7CFQ+ndU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -7,8 +7,10 @@ RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/assets.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
|
||||
@@ -42,11 +42,11 @@ http {
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_static on;
|
||||
gzip_comp_level 4;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
@@ -110,6 +110,8 @@ http {
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
|
||||
include /etc/nginx/overrides/assets.d/*.conf;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
@@ -142,24 +144,15 @@ http {
|
||||
location / {
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
||||
add_header Cache-Control "max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/your-account">
|
||||
<h2>Your account →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Access your account settings and manage personal access tokens</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create and manage your teams</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/comments/">
|
||||
<h2>Comments →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Give and receive feedback right over your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/assets">
|
||||
<h2>Assets →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Store elements and styles to easily reuse them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries">
|
||||
<h2>Libraries →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components">
|
||||
<h2>Components →</h2>
|
||||
<p>Speed your design workflow</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants">
|
||||
<h2>Variants →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens">
|
||||
<h2>Design Tokens →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -5,7 +5,7 @@ desc: Use Penpot's libraries for reusable design elements! Learn to create, mana
|
||||
---
|
||||
<h1 id="libraries">Libraries</h1>
|
||||
|
||||
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
<p class="main-paragraph">Libraries may include components, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
|
||||
<h3 id="file-libraries">File libraries</h3>
|
||||
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/designing/workspace-basics">
|
||||
<h2>Workspace basics →</h2>
|
||||
<p>Workspace basics</p>
|
||||
<p>Get to know the Workspace, where designs are created</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers">
|
||||
<h2>Layers →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/color-stroke/">
|
||||
<h2>Color & Strokes→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling options available for each layer</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/text-typo">
|
||||
<h2>Text & Typography→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling text content & using custom fonts</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts">
|
||||
<h2>Flexible layouts →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/export-import/export-import-files/">
|
||||
<h2>Export/Import Penpot files →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>How to export and import your Penpot files</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/export-import/exporting-layers/">
|
||||
<h2>Exporting layers →</h2>
|
||||
<p>Exporting layers</p>
|
||||
<p>How to export elements from your design into different file formats</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -16,7 +16,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/the-interface">
|
||||
<h2>Interface tour →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Take a tour of Penpot's main areas</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -28,7 +28,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/info">
|
||||
<h2>Tutorials & info →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Useful resources to better understand Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -22,49 +22,49 @@ eleventyNavigation:
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers/">
|
||||
<h2>Layers</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts/">
|
||||
<h2>Flexible layouts</h2>
|
||||
<p>Create designs that adapt automatically.</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components/">
|
||||
<h2>Components</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants/">
|
||||
<h2>Variants</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens/">
|
||||
<h2>Design Tokens</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/dev-tools/#inspect-design">
|
||||
<h2>Inspect design</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Get production-ready code</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping/">
|
||||
<h2>Prototyping</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries/">
|
||||
<h2>Libraries</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping">
|
||||
<h2>Prototyping →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/testing-view-mode">
|
||||
<h2>Testing: View mode →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Test your designs and play the interactions</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -7,4 +7,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
|
||||
bb -i '(babashka.wait/wait-for-path "target/app.js")';
|
||||
sleep 2;
|
||||
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
exec node target/app.js
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
|
||||
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(p/fmap (fn [resource]
|
||||
(assoc exchange :response/body resource)))
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "unexpected error on export multiple"
|
||||
(l/error :hint "unexpected error on single export"
|
||||
:cause cause)
|
||||
(p/rejected cause))))))
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(l/error :hint "unexpected error on multiple exportation" :cause cause)
|
||||
(l/error :hint "unexpected error on multiple export" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
import '../resources/public/css/ds.css';
|
||||
|
||||
export const decorators = [
|
||||
|
||||
@@ -5947,8 +5947,8 @@
|
||||
"~:spread": "10",
|
||||
"~:color": "rgb(160, 73, 73)",
|
||||
"~:inset": true,
|
||||
"~:offsetX": "10",
|
||||
"~:offsetY": "10"
|
||||
"~:offset-x": "10",
|
||||
"~:offset-y": "10"
|
||||
}
|
||||
],
|
||||
"~:description": "",
|
||||
|
||||
@@ -73,7 +73,7 @@ export class BasePage {
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js";
|
||||
const url = "**/js/config.js*";
|
||||
return await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
|
||||
@@ -1239,8 +1239,12 @@ test.describe("Tokens: Apply token", () => {
|
||||
// Fill in the shadow values
|
||||
const offsetXInput = firstShadowFields.getByLabel("X");
|
||||
const offsetYInput = firstShadowFields.getByLabel("Y");
|
||||
const blurInput = firstShadowFields.getByLabel("Blur");
|
||||
const spreadInput = firstShadowFields.getByLabel("Spread");
|
||||
const blurInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
const spreadInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Spread",
|
||||
});
|
||||
|
||||
await offsetXInput.fill("2");
|
||||
await offsetYInput.fill("2");
|
||||
@@ -1261,9 +1265,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||
|
||||
// Verify that a color value was set
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
await expect(colorInput).toHaveValue(/^rgb(.*)$/);
|
||||
|
||||
// Wait for validation to complete
|
||||
await expect(
|
||||
@@ -1281,11 +1286,15 @@ test.describe("Tokens: Apply token", () => {
|
||||
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-0",
|
||||
);
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
|
||||
// User adds a second shadow
|
||||
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
|
||||
const addButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Add Shadow",
|
||||
});
|
||||
await addButton.click();
|
||||
|
||||
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
@@ -1294,8 +1303,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(secondShadowFields).toBeVisible();
|
||||
|
||||
// User adds a third shadow
|
||||
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
|
||||
await addButton2.click();
|
||||
await addButton.click();
|
||||
|
||||
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-2",
|
||||
@@ -1305,9 +1313,15 @@ test.describe("Tokens: Apply token", () => {
|
||||
// User adds values for the third shadow
|
||||
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
||||
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
||||
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
|
||||
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
|
||||
const thirdColorInput = thirdShadowFields.getByLabel("Color");
|
||||
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
const thirdSpreadInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Spread",
|
||||
});
|
||||
const thirdColorInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
|
||||
await thirdOffsetXInput.fill("10");
|
||||
await thirdOffsetYInput.fill("10");
|
||||
@@ -1316,15 +1330,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
await thirdColorInput.fill("#FF0000");
|
||||
|
||||
// User removes the 2nd shadow
|
||||
const removeButton2 = secondShadowFields.getByTestId(
|
||||
"shadow-remove-button-1",
|
||||
);
|
||||
const removeButton2 = secondShadowFields.getByRole("button", {
|
||||
name: "Remove Shadow",
|
||||
});
|
||||
await removeButton2.click();
|
||||
|
||||
// Verify second shadow is removed
|
||||
await expect(
|
||||
secondShadowFields.getByTestId("shadow-add-button-3"),
|
||||
).not.toBeVisible();
|
||||
// Verify that we have only two shadow fields
|
||||
await expect(thirdShadowFields).not.toBeVisible();
|
||||
|
||||
// Verify that the first shadow kept its values
|
||||
const firstOffsetXValue = await firstShadowFields
|
||||
@@ -1334,13 +1346,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const firstBlurValue = await firstShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const firstSpreadValue = await firstShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const firstColorValueAfter = await firstShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(firstOffsetXValue).toBe("2");
|
||||
@@ -1349,7 +1361,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(firstSpreadValue).toBe("0");
|
||||
await expect(firstColorValueAfter).toBe(firstColorValue);
|
||||
|
||||
// Verify that the third shadow (now second) kept its values
|
||||
// Verify that the second kept its values (after shadow 3)
|
||||
// After removing index 1, the third shadow becomes the second shadow at index 1
|
||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-1",
|
||||
@@ -1363,13 +1375,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const secondBlurValue = await newSecondShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const secondSpreadValue = await newSecondShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const secondColorValue = await newSecondShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(secondOffsetXValue).toBe("10");
|
||||
@@ -1386,7 +1398,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-1",
|
||||
);
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
|
||||
// Switch to reference tab
|
||||
@@ -1414,13 +1428,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const restoredFirstBlur = await firstShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const restoredFirstSpread = await firstShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const restoredFirstColor = await firstShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(restoredFirstOffsetX).toBe("2");
|
||||
@@ -1437,13 +1451,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const restoredSecondBlur = await newSecondShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const restoredSecondSpread = await newSecondShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const restoredSecondColor = await newSecondShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(restoredSecondOffsetX).toBe("10");
|
||||
|
||||
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 821 KiB |
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -746,20 +746,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@include flexCenter;
|
||||
height: $s-48;
|
||||
width: $s-48;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--empty-message-background-color);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
stroke: var(--empty-message-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.attr-title {
|
||||
div {
|
||||
margin-left: 0;
|
||||
|
||||
@@ -17,23 +17,25 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotTranslations = JSON.parse({{& translations}});
|
||||
globalThis.penpotVersion = "%version%";
|
||||
globalThis.penpotBuildDate = "%buildDate%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
<!--cookie-consent-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -46,7 +48,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& main}}";
|
||||
import { init } from "{{& app_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -9,7 +9,3 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
</script>
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
window.penpotVersion = "%version%";
|
||||
window.penpotBuildDate = "%buildDate%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& rasterizer}}";
|
||||
import { init } from "{{& rasterizer_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
window.penpotVersion = "%version%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
</head>
|
||||
<body>
|
||||
@@ -20,7 +22,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& render}}";
|
||||
import { init } from "{{& render_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -28,6 +28,8 @@ export function startWorker() {
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
|
||||
async function findFiles(basePath, predicate, options = {}) {
|
||||
predicate =
|
||||
@@ -47,8 +49,7 @@ async function findFiles(basePath, predicate, options = {}) {
|
||||
function syncDirs(originPath, destPath) {
|
||||
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
proc.exec(command, (cause, stdout) => {
|
||||
return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
|
||||
if (cause) {
|
||||
reject(cause);
|
||||
} else {
|
||||
@@ -187,37 +188,34 @@ async function readManifestFile() {
|
||||
}
|
||||
|
||||
async function readShadowManifest() {
|
||||
const ts = Date.now();
|
||||
try {
|
||||
const content = await readManifestFile();
|
||||
const index = {
|
||||
app_main: "./js/main.js",
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "./js/config.js",
|
||||
polyfills: "./js/polyfills.js",
|
||||
worker_main: "./js/worker/main.js",
|
||||
libs: "./js/libs.js",
|
||||
};
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
|
||||
for (let item of content) {
|
||||
index[item.name] = "./js/" + item["output-name"] + "";
|
||||
}
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return index;
|
||||
} catch (cause) {
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "./js/config.js",
|
||||
polyfills: "./js/polyfills.js",
|
||||
main: "./js/main.js",
|
||||
shared: "./js/shared.js",
|
||||
worker_main: "./js/worker/main.js",
|
||||
rasterizer: "./js/rasterizer.js",
|
||||
libs: "./js/libs.js",
|
||||
};
|
||||
|
||||
return index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
@@ -257,7 +255,7 @@ const markedOptions = {
|
||||
|
||||
marked.use(markedOptions);
|
||||
|
||||
async function readTranslations() {
|
||||
export async function compileTranslations() {
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -294,9 +292,10 @@ async function readTranslations() {
|
||||
["uk", "ukr_UA"],
|
||||
"ha",
|
||||
];
|
||||
const result = {};
|
||||
|
||||
for (let lang of langs) {
|
||||
const result = {};
|
||||
|
||||
let filename = `${lang}.po`;
|
||||
if (l.isArray(lang)) {
|
||||
filename = `${lang[1]}.po`;
|
||||
@@ -315,11 +314,6 @@ async function readTranslations() {
|
||||
for (let key of Object.keys(trdata)) {
|
||||
if (key === "") continue;
|
||||
const comments = trdata[key].comments || {};
|
||||
|
||||
if (l.isNil(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
|
||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||
|
||||
const msgs = trdata[key].msgstr;
|
||||
@@ -329,9 +323,9 @@ async function readTranslations() {
|
||||
message = marked.parseInline(message);
|
||||
}
|
||||
|
||||
result[key][lang] = message;
|
||||
result[key] = message;
|
||||
} else {
|
||||
result[key][lang] = msgs.map((item) => {
|
||||
result[key] = msgs.map((item) => {
|
||||
if (isMarkdown) {
|
||||
return marked.parseInline(item);
|
||||
} else {
|
||||
@@ -340,22 +334,12 @@ async function readTranslations() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function filterTranslations(translations, langs = [], keyFilter) {
|
||||
const filteredEntries = Object.entries(translations)
|
||||
.filter(([translationKey, _]) => keyFilter(translationKey))
|
||||
.map(([translationKey, value]) => {
|
||||
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
||||
langs.includes(lang),
|
||||
);
|
||||
return [translationKey, Object.fromEntries(langEntries)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(filteredEntries);
|
||||
}
|
||||
|
||||
async function generateSvgSprite(files, prefix) {
|
||||
@@ -407,14 +391,6 @@ async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
let translations = await readTranslations();
|
||||
const storybookTranslations = JSON.stringify(
|
||||
filterTranslations(translations, ["en"], (key) =>
|
||||
key.startsWith("labels."),
|
||||
),
|
||||
);
|
||||
translations = JSON.stringify(translations);
|
||||
|
||||
const manifest = await readShadowManifest();
|
||||
let content;
|
||||
|
||||
@@ -436,13 +412,16 @@ async function generateTemplates() {
|
||||
"../public/images/sprites/assets.svg": assetsSprite,
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/index.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
isDebug,
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
|
||||
@@ -450,41 +429,30 @@ async function generateTemplates() {
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/challenge.mustache",
|
||||
{},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./resources/public/challenge.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-body.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-body.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-head.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(storybookTranslations),
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-head.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/render.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
content = await renderTemplate("resources/templates/render.mustache", context);
|
||||
|
||||
await fs.writeFile("./resources/public/render.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
|
||||
|
||||
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||
}
|
||||
|
||||
@@ -38,15 +38,7 @@ fi
|
||||
yarn run build:app:libs || exit 1;
|
||||
yarn run build:app:assets || exit 1;
|
||||
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/index.html;
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/render.html;
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/rasterizer.html;
|
||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/index.html;
|
||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/rasterizer.html;
|
||||
|
||||
if [ "$INCLUDE_WASM" = "yes" ]; then
|
||||
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./resources/public/js/render_wasm.js;
|
||||
fi
|
||||
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
|
||||
|
||||
rsync -avr resources/public/ target/dist/;
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await fs.mkdir("resources/public/js", {recursive: true});
|
||||
|
||||
await h.compileStorybookStyles();
|
||||
await h.copyAssets();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
set -ex
|
||||
exec clojure $OPTIONS -M -m rebel-readline.main
|
||||
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||
|
||||
@@ -52,6 +52,7 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileTranslations();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
@@ -81,7 +82,7 @@ h.watch("resources/templates", null, async function (path) {
|
||||
log.info("watch: translations (~)");
|
||||
h.watch("translations", null, async function (path) {
|
||||
log.info("changed:", path);
|
||||
await h.compileTemplates();
|
||||
await h.compileTranslations();
|
||||
});
|
||||
|
||||
log.info("watch: assets (~)");
|
||||
|
||||
@@ -23,28 +23,28 @@
|
||||
|
||||
:util-highlight
|
||||
{:entries [app.util.code-highlight]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-auth
|
||||
{:entries [app.main.ui.auth
|
||||
app.main.ui.auth.verify-token]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-viewer
|
||||
{:entries [app.main.ui.viewer]
|
||||
:depends-on #{:main :main-auth}}
|
||||
:depends-on #{:shared :main-auth}}
|
||||
|
||||
:main-workspace
|
||||
{:entries [app.main.ui.workspace]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-dashboard
|
||||
{:entries [app.main.ui.dashboard]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-settings
|
||||
{:entries [app.main.ui.settings]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:render
|
||||
{:entries [app.render]
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
@@ -191,5 +190,4 @@
|
||||
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(let [uri (u/join public-uri path)]
|
||||
(assoc uri :query (dm/str "version=" (:full version)))))
|
||||
(u/join public-uri path))
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
(defn ^:export init
|
||||
[]
|
||||
(mw/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
(thr/init!)
|
||||
(init-ui)
|
||||
@@ -114,11 +114,4 @@
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
@@ -148,17 +148,17 @@
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 7 Pro"
|
||||
:width 1440
|
||||
:height 3120}
|
||||
:width 412
|
||||
:height 892}
|
||||
{:name "Google Pixel 6a/6"
|
||||
:width 1080
|
||||
:height 2400}
|
||||
:width 412
|
||||
:height 915}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S22"
|
||||
:width 1080
|
||||
:height 2340}
|
||||
:width 360
|
||||
:height 780}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
[]
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:app-version (:full cf/version)
|
||||
:locale @i18n/locale}
|
||||
{:version (:full cf/version)
|
||||
:locale i18n/*current-locale*}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
:browser-version (obj/get browser "version")})
|
||||
@@ -98,7 +98,9 @@
|
||||
(def context
|
||||
(atom (d/without-nils (collect-context))))
|
||||
|
||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
||||
(add-watch i18n/state "events"
|
||||
(fn [_ _ _ v]
|
||||
(swap! context assoc :locale (get v :locale))))
|
||||
|
||||
;; --- EVENT TRANSLATION
|
||||
|
||||
|
||||
@@ -53,11 +53,16 @@
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(plugins.register/init)))))
|
||||
|
||||
(def profile-fetched?
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
@@ -59,9 +59,15 @@
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
:else
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||
|
||||
(defn- numeric-string? [s]
|
||||
(and (string? s)
|
||||
@@ -77,7 +83,7 @@
|
||||
[value]
|
||||
(let [number? (or (number? value)
|
||||
(numeric-string? value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
|
||||
@@ -103,7 +109,7 @@
|
||||
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [parsed-value (cft/parse-token-value value)
|
||||
(let [parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
(if (and parsed-value (not out-of-bounds))
|
||||
@@ -120,8 +126,8 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond (and parsed-value (not out-of-scope))
|
||||
@@ -145,7 +151,7 @@
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-scope (< (:value parsed-value) 0)
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond
|
||||
@@ -244,7 +250,7 @@
|
||||
:font-size-value font-size-value})]
|
||||
(or error
|
||||
(try
|
||||
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
|
||||
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
|
||||
(case unit
|
||||
"%" (/ value 100)
|
||||
"px" (/ value font-size-value)
|
||||
@@ -368,8 +374,8 @@
|
||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||
(update shadow-result :errors concat
|
||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||
parsers {:offsetX parse-sd-token-general-value
|
||||
:offsetY parse-sd-token-general-value
|
||||
parsers {:offset-x parse-sd-token-general-value
|
||||
:offset-y parse-sd-token-general-value
|
||||
:blur parse-sd-token-shadow-blur
|
||||
:spread parse-sd-token-shadow-spread
|
||||
:color parse-sd-token-color-value
|
||||
@@ -389,35 +395,42 @@
|
||||
(defn- parse-sd-token-shadow-value
|
||||
"Parses shadow value and validates it."
|
||||
[value]
|
||||
(cond
|
||||
;; Reference value (string)
|
||||
(string? value) {:value value}
|
||||
(let [missing-references
|
||||
(when (string? value)
|
||||
(seq (cto/find-token-value-references value)))]
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
(string? value)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
|
||||
|
||||
;; Empty value
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
|
||||
;; Invalid value
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
|
||||
;; Array of shadows
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
;; Parse each shadow with its index
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
|
||||
;; Collect all errors from all shadows
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
|
||||
;; Collect all values from shadows that have values
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values}))))
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values})))))
|
||||
|
||||
(defn collect-shadow-errors [token shadow-index]
|
||||
(group-by :shadow-key
|
||||
|
||||
@@ -351,19 +351,31 @@
|
||||
(on-success))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:and
|
||||
[:map
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]
|
||||
[:invitations {:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:role [::sm/one-of ctt/valid-roles]]]]]
|
||||
[:team-id ::sm/uuid]
|
||||
[:resend? {:optional true} ::sm/boolean]]
|
||||
[:fn (fn [attrs]
|
||||
(or (contains? attrs :emails)
|
||||
(contains? attrs :invitations)))]])
|
||||
|
||||
(def ^:private check-create-invitations-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn create-invitations
|
||||
"Unified function to create invitations. Supports two parameter formats:
|
||||
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
|
||||
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
|
||||
[{:keys [emails role team-id invitations resend?] :as params}]
|
||||
|
||||
(assert (uuid? team-id))
|
||||
;; Validate input format - must have either emails+role OR invitations
|
||||
(assert (or (and emails role (sm/check-set-of-emails emails) (keyword? role))
|
||||
(and invitations
|
||||
(sm/check-set-of-emails (map :email invitations))
|
||||
(every? #(contains? ctt/valid-roles (:role %)) invitations)))
|
||||
"Must provide either emails+role or invitations with individual roles")
|
||||
(check-create-invitations-params params)
|
||||
|
||||
(ptk/reify ::create-invitations
|
||||
ev/Event
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcmt]
|
||||
@@ -551,7 +553,6 @@
|
||||
component-id (:component-id shape)
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
|
||||
(when valid?
|
||||
(if (ctc/is-variant-container? shape)
|
||||
;; Rename the full variant when it is a variant container
|
||||
@@ -566,6 +567,43 @@
|
||||
(dwl/rename-component component-id clean-name))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn rename-shape-or-variant
|
||||
([id name]
|
||||
(rename-shape-or-variant nil nil id name))
|
||||
([file-id page-id id name]
|
||||
(ptk/reify ::rename-shape-or-variant
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (d/nilv file-id (:current-file-id state))
|
||||
page-id (d/nilv page-id (:current-page-id state))
|
||||
|
||||
file-data (dsh/lookup-file-data state file-id)
|
||||
shape
|
||||
(-> (dsh/lookup-page-objects state file-id page-id)
|
||||
(get id))
|
||||
|
||||
is-variant? (ctc/is-variant? shape)
|
||||
variant-id (when is-variant? (:variant-id shape))
|
||||
variant-name (when is-variant? (:variant-name shape))
|
||||
component-id (:component-id shape)
|
||||
component (ctkl/get-component file-data (:component-id shape))
|
||||
variant-properties (:variant-properties component)]
|
||||
(cond
|
||||
(and variant-name (ctv/valid-properties-formula? name))
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id))
|
||||
|
||||
variant-name
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties {})
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id name))
|
||||
|
||||
:else
|
||||
(rx/of (end-rename-shape id name))))))))
|
||||
|
||||
;; --- Update Selected Shapes attrs
|
||||
|
||||
(defn update-selected-shapes
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.main.data.workspace.tokens.application
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.shape.layout :as ctsl]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
@@ -153,11 +153,11 @@
|
||||
(defn value->shadow
|
||||
"Transform a token shadow value into penpot shadow data structure"
|
||||
[value]
|
||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
||||
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||
{:id (random-uuid)
|
||||
:hidden false
|
||||
:offset-x offsetX
|
||||
:offset-y offsetY
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:blur blur
|
||||
:color (value->color color)
|
||||
:spread spread
|
||||
@@ -525,8 +525,8 @@
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cft/attributes-map attributes token)
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
@@ -585,7 +585,7 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
|
||||
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
|
||||
(dwsh/update-shapes
|
||||
shape-ids
|
||||
(fn [shape]
|
||||
@@ -613,7 +613,7 @@
|
||||
(get token-properties (:type token))
|
||||
|
||||
unapply-tokens?
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
|
||||
shape-ids (map :id shapes)]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.main.data.workspace.tokens.color
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.main.data.tinycolor :as tinycolor]))
|
||||
|
||||
(defn color-bullet-color [token-color-value]
|
||||
@@ -17,5 +17,5 @@
|
||||
(tinycolor/->hex-string tc))))
|
||||
|
||||
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
|
||||
(when (and resolved-value (cft/color-token? token))
|
||||
(when (and resolved-value (cfo/color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
@@ -108,6 +108,10 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-shadow
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
|
||||
|
||||
:error/unknown
|
||||
{:error/code :error/unknown
|
||||
:error/fn #(tr "labels.unknown-error")}})
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
:letter-spacing "Letter Spacing"
|
||||
:text-case "Text Case"
|
||||
:text-decoration "Text Decoration"
|
||||
:offsetX "X"
|
||||
:offsetY "Y"
|
||||
:offset-x "X"
|
||||
:offset-y "Y"
|
||||
:blur "Blur"
|
||||
:spread "Spread"
|
||||
:color "Color"
|
||||
|
||||
@@ -147,27 +147,30 @@
|
||||
|
||||
(defn create-token-set
|
||||
[token-set]
|
||||
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Clear possible local state
|
||||
(update state :workspace-tokens dissoc :token-set-new-path))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
|
||||
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn rename-token-set
|
||||
[token-set new-name]
|
||||
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
|
||||
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn rename-token-set-group
|
||||
[set-group-path set-group-fname]
|
||||
@@ -179,26 +182,6 @@
|
||||
(rx/of
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn update-token-set
|
||||
[token-set name]
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
name (ctob/normalize-set-name name (ctob/get-name token-set))
|
||||
tokens-lib (get data :tokens-lib)]
|
||||
|
||||
(if (ctob/get-set-by-name tokens-lib name)
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/rename-token-set (ctob/get-id token-set) name))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn duplicate-token-set
|
||||
[id]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
@@ -448,7 +431,7 @@
|
||||
(ctob/get-id token-set)
|
||||
token-id)]
|
||||
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
|
||||
unames (map :name tokens)
|
||||
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")
|
||||
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
|
||||
new-token (-> token
|
||||
|
||||
@@ -128,14 +128,16 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
prop-name (-> props (nth pos) :name)
|
||||
valid-pos? (> (count props) pos)
|
||||
prop-name (when valid-pos? (-> props (nth pos) :name))
|
||||
|
||||
changes (-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name))
|
||||
changes (when valid-pos?
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name)))
|
||||
undo-id (js/Symbol)]
|
||||
(when (not= prop-name new-name)
|
||||
(when (and valid-pos? (not= prop-name new-name))
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.main.ui.components.dropdown :refer [dropdown-content*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as tm]
|
||||
@@ -53,14 +54,13 @@
|
||||
(def ^:private valid-option?
|
||||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu*
|
||||
[{:keys [show on-close options selectable selected
|
||||
(mf/defc context-menu-inner*
|
||||
[{:keys [on-close options selectable selected
|
||||
top left fixed min-width origin width]
|
||||
:as props}]
|
||||
|
||||
(assert (every? valid-option? options) "expected valid options")
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
(assert (boolean? show) "missing `show` prop")
|
||||
(assert (vector? options) "missing `options` prop")
|
||||
|
||||
(let [width (d/nilv width "initial")
|
||||
@@ -80,14 +80,15 @@
|
||||
offset-x (get state :offset-x)
|
||||
offset-y (get state :offset-y)
|
||||
levels (get state :levels)
|
||||
internal-id (mf/use-id)
|
||||
|
||||
on-local-close
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(fn []
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
(swap! state* assoc :levels [{:parent nil :options options}])
|
||||
(when (fn? on-close)
|
||||
(on-close))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
@@ -216,11 +217,22 @@
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}]))
|
||||
|
||||
(mf/with-effect [internal-id]
|
||||
(ug/dispatch! (ug/event "penpot:context-menu:open" #js {:id internal-id})))
|
||||
|
||||
(mf/with-effect [internal-id on-local-close]
|
||||
(letfn [(on-event [event]
|
||||
(when-let [detail (unchecked-get event "detail")]
|
||||
(when (not= internal-id (unchecked-get detail "id"))
|
||||
(on-local-close event))))]
|
||||
(ug/listen "penpot:context-menu:open" on-event)
|
||||
(partial ug/unlisten "penpot:context-menu:open" on-event)))
|
||||
|
||||
(mf/with-effect [ids]
|
||||
(tm/schedule-on-idle
|
||||
#(dom/focus! (dom/get-element (first ids)))))
|
||||
|
||||
(when (and show (some? levels))
|
||||
(when (some? levels)
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
@@ -229,7 +241,7 @@
|
||||
[:div {:class (stl/css-case
|
||||
:is-selectable selectable
|
||||
:context-menu true
|
||||
:is-open show
|
||||
:is-open true
|
||||
:fixed fixed)
|
||||
:style {:top (+ top offset-y)
|
||||
:left (+ left offset-x)}
|
||||
@@ -241,7 +253,7 @@
|
||||
:role "menu"
|
||||
:ref check-menu-offscreen}
|
||||
|
||||
(when-let [parent (:parent level)]
|
||||
(when parent
|
||||
[:*
|
||||
[:li {:id "go-back-sub-option"
|
||||
:class (stl/css :context-menu-item)
|
||||
@@ -256,7 +268,7 @@
|
||||
|
||||
[:li {:class (stl/css :separator)}]])
|
||||
|
||||
(for [[index option] (d/enumerate (:options level))]
|
||||
(for [[index option] (d/enumerate options)]
|
||||
(let [name (:name option)
|
||||
id (:id option)
|
||||
sub-options (:options option)
|
||||
@@ -297,3 +309,12 @@
|
||||
:data-testid id}
|
||||
name
|
||||
[:span {:class (stl/css :submenu-icon)} deprecated-icon/arrow]])]))))]])])))
|
||||
|
||||
(mf/defc context-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [show] :as props}]
|
||||
|
||||
(assert (boolean? show) "expected `show` prop to be a boolean")
|
||||
|
||||
(when ^boolean show
|
||||
[:> context-menu-inner* props]))
|
||||
|
||||
@@ -151,14 +151,16 @@
|
||||
|
||||
(mf/defc menu-team-icon*
|
||||
[{:keys [subscription-type]}]
|
||||
[:span {:class (stl/css :subscription-icon)
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}
|
||||
(case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)])
|
||||
[:span {:class (stl/css :subscription-icon-wrapper)}
|
||||
[:> icon* {:icon-id (case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)
|
||||
:class (stl/css :subscription-icon)
|
||||
:size "s"
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}]])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.subscription-icon {
|
||||
@extend .button-icon;
|
||||
.subscription-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-primary);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
border-radius: 6px;
|
||||
border-radius: $br-6;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
width: var(--sp-xl);
|
||||
height: var(--sp-xl);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
}
|
||||
|
||||
svg {
|
||||
block-size: var(--sp-m);
|
||||
inline-size: var(--sp-m);
|
||||
}
|
||||
.subscription-icon {
|
||||
stroke: var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
(when (not= 0 count-libraries)
|
||||
(if (pos? (count references))
|
||||
[:*
|
||||
[:div
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:h3 {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]]
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:p {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]
|
||||
(when (and (string? hint) (not= hint ""))
|
||||
[:> context-notification* {:level :info
|
||||
:appearance :ghost}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "refactor/basic-rules.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
@@ -15,14 +16,19 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
gap: var(--sp-xxl);
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
.list-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineMediumTypography;
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
@@ -31,13 +37,16 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
@include t.use-typography("body-small");
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.element-list {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
overflow-y: scroll;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -55,10 +64,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scd-msg {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal-scd-msg,
|
||||
.modal-subtitle,
|
||||
.modal-msg {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.ui.ds
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
|
||||
@@ -33,6 +32,7 @@
|
||||
[app.main.ui.ds.product.avatar :refer [avatar*]]
|
||||
[app.main.ui.ds.product.cta :refer [cta*]]
|
||||
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.ds.product.milestone :refer [milestone*]]
|
||||
@@ -44,8 +44,6 @@
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(i18n/init! cf/translations)
|
||||
|
||||
(def default
|
||||
"A export used for storybook"
|
||||
(mf/object
|
||||
@@ -59,6 +57,7 @@
|
||||
:HintMessage hint-message*
|
||||
:InputWithMeta input-with-meta*
|
||||
:EmptyPlaceholder empty-placeholder*
|
||||
:EmptyState empty-state*
|
||||
:Loader loader*
|
||||
:RawSvg raw-svg*
|
||||
:Select select*
|
||||
@@ -80,6 +79,11 @@
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
(i18n/set-translations "en" data))
|
||||
|
||||
;; meta / misc
|
||||
:meta
|
||||
{:icons (clj->js (sort icon-list))
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps disabled)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not disabled
|
||||
(swap! is-open* not))))
|
||||
|
||||
@@ -53,10 +53,15 @@
|
||||
"true")
|
||||
:aria-describedby (when has-hint
|
||||
(str id "-hint"))
|
||||
:aria-labelledby tooltip-id
|
||||
:type (d/nilv type "text")
|
||||
:id id
|
||||
:max-length (d/nilv max-length max-input-length)})
|
||||
|
||||
props (if (and aria-label (not (some? icon)))
|
||||
(mf/spread-props props
|
||||
{:aria-label aria-label})
|
||||
(mf/spread-props props
|
||||
{:aria-labelledby tooltip-id}))
|
||||
inside-class (stl/css-case :input-wrapper true
|
||||
:has-hint has-hint
|
||||
:hint-type-hint (= hint-type "hint")
|
||||
|
||||
29
frontend/src/app/main/ui/ds/product/empty_state.cljs
Normal file
29
frontend/src/app/main/ui/ds/product/empty_state.cljs
Normal file
@@ -0,0 +1,29 @@
|
||||
;; 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.ds.product.empty-state
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:empty-state
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:icon [:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:text :string]])
|
||||
|
||||
(mf/defc empty-state*
|
||||
{::mf/schema schema:empty-state}
|
||||
[{:keys [class icon text] :rest props}]
|
||||
(let [props (mf/spread-props props {:class [class (stl/css :group)]})]
|
||||
[:> :div props
|
||||
[:div {:class (stl/css :icon-wrapper)}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "l"
|
||||
:class (stl/css :icon)}]]
|
||||
[:div {:class (stl/css :text)} text]]))
|
||||
35
frontend/src/app/main/ui/ds/product/empty_state.mdx
Normal file
35
frontend/src/app/main/ui/ds/product/empty_state.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
{ /* 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 { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as EmptyState from "./empty_state.stories";
|
||||
|
||||
<Meta title="Product/EmptyState" />
|
||||
|
||||
# EmptyState
|
||||
|
||||
An indication to the user that there is no data to display in the current view; often includes instructions on what to do next.
|
||||
|
||||
<Canvas of={EmptyState.Default} />
|
||||
|
||||
## Technical notes
|
||||
|
||||
The icon and the text are mandatory, and a class is optional.
|
||||
|
||||
```clj
|
||||
[:> empty-state* {:icon i/at
|
||||
:text "This is an empty state"}]
|
||||
```
|
||||
|
||||
## Usage guidelines (design)
|
||||
|
||||
### Where to use
|
||||
|
||||
Use in areas of the app where users can add data or elements, but where there is currently no available information.
|
||||
|
||||
### Interaction / Behavior
|
||||
|
||||
There is no interaction.
|
||||
36
frontend/src/app/main/ui/ds/product/empty_state.scss
Normal file
36
frontend/src/app/main/ui/ds/product/empty_state.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
inline-size: $sz-48;
|
||||
block-size: $sz-48;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
inline-size: $sz-32;
|
||||
block-size: $sz-32;
|
||||
}
|
||||
|
||||
.text {
|
||||
@include t.use-typography("body-small");
|
||||
text-align: center;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
32
frontend/src/app/main/ui/ds/product/empty_state.stories.jsx
Normal file
32
frontend/src/app/main/ui/ds/product/empty_state.stories.jsx
Normal file
@@ -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 * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { EmptyState } = Components;
|
||||
const { icons } = Components.meta;
|
||||
|
||||
export default {
|
||||
title: "Product/EmptyState",
|
||||
component: Components.EmptyState,
|
||||
argTypes: {
|
||||
icon: {
|
||||
options: icons,
|
||||
control: { type: "select" },
|
||||
},
|
||||
text: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
icon: "help",
|
||||
text: "This is an empty state",
|
||||
},
|
||||
render: ({ ...args }) => <EmptyState {...args} />,
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
@@ -173,7 +173,7 @@
|
||||
[:id {:optional true} :string]
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
[:content [:or fn? :string]]
|
||||
[:content [:or fn? :string map?]]
|
||||
[:placement {:optional true}
|
||||
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.swatch {
|
||||
--border-color: var(--color-background-quaternary);
|
||||
--border-color: var(--color-accent-primary-muted);
|
||||
--border-radius: #{$br-4};
|
||||
--border-color-active: var(--color-foreground-primary);
|
||||
--border-color-active-inset: var(--color-background-primary);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
|
||||
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})
|
||||
|
||||
(mf/defc attributes
|
||||
(mf/defc attributes*
|
||||
[{:keys [page-id file-id shapes frame from libraries share-id objects color-space]}]
|
||||
(let [shapes (hooks/use-equal-memo shapes)
|
||||
first-shape (first shapes)
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
embed-images? (replace-map images-data))]
|
||||
(str/format page-template style-code markup-code)))
|
||||
|
||||
(mf/defc code
|
||||
(mf/defc code*
|
||||
[{:keys [shapes frame on-expand from]}]
|
||||
(let [style-type* (mf/use-state "css")
|
||||
markup-type* (mf/use-state "html")
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.inspect.attributes :refer [attributes]]
|
||||
[app.main.ui.inspect.code :refer [code]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.inspect.attributes :refer [attributes*]]
|
||||
[app.main.ui.inspect.code :refer [code*]]
|
||||
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
|
||||
[app.main.ui.inspect.styles :refer [styles-tab*]]
|
||||
[app.util.dom :as dom]
|
||||
@@ -122,8 +123,7 @@
|
||||
(fn []
|
||||
(if (seq shapes)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "inspect-mode-click-element"}))
|
||||
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))
|
||||
(reset! color-space* "hex")))
|
||||
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))))
|
||||
|
||||
[:aside {:class (stl/css-case :settings-bar-right true
|
||||
:viewer-code (= from :viewer))}
|
||||
@@ -189,52 +189,49 @@
|
||||
:libraries libraries
|
||||
:file-id file-id}]
|
||||
:computed
|
||||
[:& attributes {:color-space color-space
|
||||
:page-id page-id
|
||||
:objects objects
|
||||
:file-id file-id
|
||||
:frame frame
|
||||
:shapes shapes
|
||||
:from from
|
||||
:libraries libraries
|
||||
:share-id share-id}]
|
||||
[:> attributes* {:color-space color-space
|
||||
:page-id page-id
|
||||
:objects objects
|
||||
:file-id file-id
|
||||
:frame frame
|
||||
:shapes shapes
|
||||
:from from
|
||||
:libraries libraries
|
||||
:share-id share-id}]
|
||||
|
||||
:code
|
||||
[:& code {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand handle-expand
|
||||
:from from}])]
|
||||
[:> code* {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand handle-expand
|
||||
:from from}])]
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:selected (name @section)
|
||||
:on-change handle-change-tab
|
||||
:class (stl/css :viewer-tab-switcher)}
|
||||
(case @section
|
||||
:info
|
||||
[:& attributes {:page-id page-id
|
||||
:objects objects
|
||||
:file-id file-id
|
||||
:frame frame
|
||||
:shapes shapes
|
||||
:from from
|
||||
:libraries libraries
|
||||
:share-id share-id}]
|
||||
[:> attributes* {:page-id page-id
|
||||
:objects objects
|
||||
:file-id file-id
|
||||
:frame frame
|
||||
:shapes shapes
|
||||
:from from
|
||||
:libraries libraries
|
||||
:share-id share-id}]
|
||||
|
||||
:code
|
||||
[:& code {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand handle-expand
|
||||
:from from}])])]]
|
||||
[:div {:class (stl/css :empty)}
|
||||
[:div {:class (stl/css :code-info)}
|
||||
[:span {:class (stl/css :placeholder-icon)}
|
||||
deprecated-icon/code]
|
||||
[:span {:class (stl/css :placeholder-label)}
|
||||
(tr "inspect.empty.select")]]
|
||||
[:div {:class (stl/css :help-info)}
|
||||
[:span {:class (stl/css :placeholder-icon)}
|
||||
deprecated-icon/help]
|
||||
[:span {:class (stl/css :placeholder-label)}
|
||||
(tr "inspect.empty.help")]]
|
||||
[:button {:class (stl/css :more-info-btn)
|
||||
:on-click navigate-to-help}
|
||||
(tr "inspect.empty.more-info")]])]))
|
||||
[:> code* {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand handle-expand
|
||||
:from from}])])]]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css :empty)}
|
||||
[:> empty-state* {:icon i/code
|
||||
:text (tr "inspect.empty.select")}]
|
||||
[:> empty-state* {:icon i/help
|
||||
:text (tr "inspect.empty.help")}]]
|
||||
[:div {:class (stl/css :empty-button)}
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click navigate-to-help}
|
||||
(tr "inspect.empty.more")]]])]))
|
||||
|
||||
@@ -81,36 +81,14 @@
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sz-40;
|
||||
padding-top: $sz-24;
|
||||
gap: var(--sp-xxxl);
|
||||
margin: var(--sp-xxxl) auto;
|
||||
inline-size: $sz-200;
|
||||
}
|
||||
|
||||
.code-info,
|
||||
.help-info {
|
||||
@include deprecated.flexColumn;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--sp-m);
|
||||
margin-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
@extend .empty-icon;
|
||||
}
|
||||
|
||||
.placeholder-label {
|
||||
@include deprecated.bodySmallTypography;
|
||||
text-align: center;
|
||||
inline-size: px2rem(200);
|
||||
color: var(--empty-message-foreground-color);
|
||||
}
|
||||
|
||||
.more-info-btn {
|
||||
@extend .button-secondary;
|
||||
@include deprecated.uppercaseTitleTipography;
|
||||
block-size: $sz-32;
|
||||
padding: var(--sp-s) var(--sp-xxl);
|
||||
.empty-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher {
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
(swap! shorthands* assoc (:panel shorthand) (:property shorthand))))]
|
||||
[:ol {:class (stl/css :styles-tab) :aria-label (tr "labels.styles")}
|
||||
;; TOKENS PANEL
|
||||
(when (or active-themes active-sets)
|
||||
(when (or (seq active-themes) (seq active-sets))
|
||||
[:li
|
||||
[:> style-box* {:panel :token}
|
||||
[:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]])
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
// TODO: this must be a custom property in the design system
|
||||
:global(.light) {
|
||||
--low-emphasis-background: #fafafa;
|
||||
}
|
||||
|
||||
:global(.default) {
|
||||
--low-emphasis-background: #121214;
|
||||
}
|
||||
|
||||
.style-box {
|
||||
--title-gap: var(--sp-xs);
|
||||
--title-padding: var(--sp-s);
|
||||
@@ -13,12 +22,9 @@
|
||||
--arrow-color: var(--color-foreground-secondary);
|
||||
--box-border-color: var(--color-background-primary);
|
||||
|
||||
// TODO: this must be a custom property in the design system
|
||||
--lowEmphasis-background: #121214;
|
||||
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline: var(--sp-m);
|
||||
background-color: var(--lowEmphasis-background);
|
||||
background-color: var(--low-emphasis-background);
|
||||
|
||||
border-block-end: 2px solid var(--box-border-color);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-10]
|
||||
[app.main.ui.releases.v2-11]
|
||||
[app.main.ui.releases.v2-12]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.main.ui.releases.v2-4]
|
||||
@@ -102,4 +103,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.11")))
|
||||
(rc/render-release-notes (assoc params :version "2.12")))
|
||||
|
||||
162
frontend/src/app/main/ui/releases/v2_12.cljs
Normal file
162
frontend/src/app/main/ui/releases/v2_12.cljs
Normal file
@@ -0,0 +1,162 @@
|
||||
;; 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.releases.v2-12
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.12"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-slide-0.jpg"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot 2.12 is here!"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Better tokens visibility and more!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This release focuses on making your everyday workflow feel clearer, faster and more intuitive. Tokens are now easier to see and apply, appearing directly where you work and giving the designs better context during code inspection. Variants gain a more natural flow thanks to simple boolean toggles that remove friction when switching states. And PDF export becomes more flexible, letting you choose exactly which boards to share so your files match the story you want to tell."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Together, these enhancements bring greater control and fluidity to your entire design process."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-tokens-sidebar.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Better tokens visibility"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Better tokens visibility"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Design systems should be both powerful and effortless to use. This release brings tokens closer to where you work, making them easier to apply and easier to understand."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Apply color tokens right from the sidebar"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Your color tokens now appear directly in the properties sidebar, making it faster to apply or unapply tokens from the design tab. No more digging: now you can use tokens within your design flow."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"See token names in the Inspect panel"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Developers now get a clearer context during handoff. The Inspect panel shows the actual token used in your design, in a similar way to how styles are displayed. This small detail reduces ambiguity, aligns everyone on the same language, and strengthens collaboration across the team."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-variants.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Simpler boolean variants"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Simpler boolean variants"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Variants are central to building flexible, scalable components. With this release, boolean properties become far easier to work with."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"A simple toggle for boolean values"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Binary states now use a clean toggle, to be able to switch visually, instead of a dropdown. This makes adjusting component states more intuitive and speeds up working with multiple instances."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a subtle improvement, but it removes friction you feel hundreds of times a week, and makes component work flow more naturally."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
|
||||
2
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-export-pdf.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Smarter PDF export"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Smarter PDF export"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Exporting your work is now more precise and flexible."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Select specific boards when exporting"]
|
||||
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"You’re now in control of which boards make it into your PDF. Share just the final screens, just a flow, just the workshop materials. This streamlined export flow adapts to the way real teams work: share the story you want to tell, with exactly the boards you need."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
102
frontend/src/app/main/ui/releases/v2_12.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_12.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: deprecated.$s-324 1fr;
|
||||
height: deprecated.$s-500;
|
||||
width: deprecated.$s-888;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: deprecated.$s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: deprecated.$s-324;
|
||||
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: deprecated.$s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr deprecated.$s-32;
|
||||
gap: deprecated.$s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include deprecated.flexCenter;
|
||||
@include deprecated.headlineSmallTypography;
|
||||
height: deprecated.$s-32;
|
||||
width: deprecated.$s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-16;
|
||||
width: deprecated.$s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: deprecated.$s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
[app.main.ui.workspace.coordinates :as coordinates]
|
||||
[app.main.ui.workspace.libraries]
|
||||
[app.main.ui.workspace.nudge]
|
||||
[app.main.ui.workspace.palette :refer [palette]]
|
||||
[app.main.ui.workspace.palette :refer [palette*]]
|
||||
[app.main.ui.workspace.plugins]
|
||||
[app.main.ui.workspace.sidebar :refer [sidebar*]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
|
||||
@@ -35,7 +35,7 @@
|
||||
[app.main.ui.workspace.tokens.export.modal]
|
||||
[app.main.ui.workspace.tokens.import]
|
||||
[app.main.ui.workspace.tokens.import.modal]
|
||||
[app.main.ui.workspace.tokens.management.create.modals]
|
||||
[app.main.ui.workspace.tokens.management.forms.modals]
|
||||
[app.main.ui.workspace.tokens.settings]
|
||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||
[app.main.ui.workspace.viewport :refer [viewport*]]
|
||||
@@ -84,8 +84,8 @@
|
||||
node-ref (use-resize-observer on-resize)]
|
||||
[:*
|
||||
(when (not ^boolean hide-ui?)
|
||||
[:& palette {:layout layout
|
||||
:on-change-palette-size on-resize-palette}])
|
||||
[:> palette* {:layout layout
|
||||
:on-change-size on-resize-palette}])
|
||||
|
||||
[:section
|
||||
{:key (dm/str "workspace-" page-id)
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
(let [{:keys [modal title]} (get dwta/token-properties :color)
|
||||
window-size (dom/get-window-size)
|
||||
left-sidebar (dom/get-element "left-sidebar-aside")
|
||||
x-size (dom/get-data left-sidebar "left-sidebar-width")
|
||||
x-size (dom/get-data left-sidebar "width")
|
||||
modal-height 392
|
||||
x (- (int x-size) 30)
|
||||
y (- (/ (:height window-size) 2) (/ modal-height 2))]
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -160,6 +161,5 @@
|
||||
:key (:page-id tgroup)}])]
|
||||
|
||||
[:div {:class (stl/css :thread-group-placeholder)}
|
||||
[:span {:class (stl/css :placeholder-icon)} deprecated-icon/comments]
|
||||
[:span {:class (stl/css :placeholder-label)}
|
||||
(tr "labels.no-comments-available")]])]]))
|
||||
[:> empty-state* {:icon i/comments
|
||||
:text (tr "labels.no-comments-available")}]])]]))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.comments-section {
|
||||
@@ -128,29 +129,9 @@
|
||||
}
|
||||
|
||||
.thread-group-placeholder {
|
||||
@include deprecated.flexColumn;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: deprecated.$s-36;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
@include deprecated.flexCenter;
|
||||
height: deprecated.$s-48;
|
||||
width: deprecated.$s-48;
|
||||
border-radius: deprecated.$br-circle;
|
||||
background-color: var(--empty-message-background-color);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
height: deprecated.$s-28;
|
||||
width: deprecated.$s-28;
|
||||
stroke: var(--empty-message-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-label {
|
||||
@include deprecated.bodySmallTypography;
|
||||
text-align: center;
|
||||
width: deprecated.$s-184;
|
||||
color: var(--empty-message-foreground-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxxl);
|
||||
margin: var(--sp-xxxl) auto;
|
||||
inline-size: $sz-200;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user