Compare commits

..

10 Commits

Author SHA1 Message Date
Andrey Antukh
07334bd355 Memoize variant props on layer-item 2026-02-06 13:08:27 +01:00
Andrey Antukh
daaf7ac4b0 Reduce watchers for layer-item rename mechanism 2026-02-06 13:08:26 +01:00
Andrey Antukh
2053151da0 Add micro optimizations to layer-item component 2026-02-06 13:07:18 +01:00
Andrey Antukh
f904c461ca 💄 Add minor cosmetic changes to filters-tree component 2026-02-06 12:58:44 +01:00
Andrey Antukh
bc7a83ab49 Add more selective debouncing for layers-tree 2026-02-06 12:57:36 +01:00
Andrey Antukh
a6f41de9aa Reduce allocation on layers-tree component 2026-02-06 12:57:36 +01:00
Andrey Antukh
5eb9b8750b 💄 Add minor cosmetic changes on event listening 2026-02-06 12:57:36 +01:00
Andrey Antukh
1a10321806 💄 Fix component naming style related to layer-item 2026-02-06 12:57:36 +01:00
Andrey Antukh
4ae4790c3a 💄 Change props naming on layer-item and related components 2026-02-06 12:57:34 +01:00
Andrey Antukh
eeac2977ed Remove usage of use-var on layer-item
Focus on use more basic primitves on performance
sensitive components.
2026-02-06 12:54:35 +01:00
305 changed files with 30814 additions and 39399 deletions

View File

@@ -59,7 +59,6 @@ jobs:
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd
- name: Set up Docker Buildx
@@ -90,7 +89,6 @@ jobs:
backend
exporter
storybook
mcp
labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }}
@@ -154,21 +152,6 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master

View File

@@ -87,11 +87,7 @@ jobs:
- name: Build runtime
working-directory: ./plugins
run: pnpm run build:runtime
- name: Build doc
working-directory: ./plugins
run: pnpm run build:doc
run: pnpm run build
- name: Build plugins
working-directory: ./plugins

View File

@@ -10,7 +10,6 @@
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
@@ -35,19 +34,6 @@
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
## 2.13.2
### :bug: Bugs fixed
- Fix security issue (Path Traversal Vulnerability) on fonts related RPC method
## 2.13.1
### :bug: Bugs fixed
- Fix PDF Exporter outputs empty page when board has A4 format [Taiga #13181](https://tree.taiga.io/project/penpot/issue/13181)
## 2.13.0
### :heart: Community contributions (Thank you!)

View File

@@ -2,30 +2,4 @@
## Reporting a Vulnerability
We take the security of this project seriously. If you have discovered
a security vulnerability, please do **not** open a public issue.
Please report vulnerabilities via email to: **[support@penpot.app]**
### What to include:
* A brief description of the vulnerability.
* Steps to reproduce the issue.
* Potential impact if exploited.
We appreciate your patience and your commitment to **responsible disclosure**.
---
## Security Contributors
We are incredibly grateful to the following individuals and
organizations for their help in keeping this project safe.
* **Ali Maharramli** for identifying critical path traversal vulnerability
> **Note:** This list is a work in progress. If you have contributed
> to the security of this project and would like to be recognized (or
> prefer to remain anonymous), please let us know.
Please report security issues to `support@penpot.app`

View File

@@ -275,8 +275,7 @@
::email/whitelist (ig/ref ::email/whitelist)}
::mgmt/routes
{::db/pool (ig/ref ::db/pool)
::setup/props (ig/ref ::setup/props)}
{::db/pool (ig/ref ::db/pool)}
:app.http/router
{::session/manager (ig/ref ::session/manager)

View File

@@ -35,7 +35,8 @@
javax.xml.parsers.SAXParserFactory
org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd
org.im4java.core.IMOperation))
org.im4java.core.IMOperation
org.im4java.core.Info))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@@ -223,18 +224,17 @@
;; If we are processing an animated gif we use the first frame with -scene 0
(let [dim-result (sh/sh "identify" "-format" "%w %h\n" path)
orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)]
(when (= 0 (:exit dim-result))
(if (and (= 0 (:exit dim-result))
(= 0 (:exit orient-result)))
(let [[w h] (-> (:out dim-result)
str/trim
(clojure.string/split #"\s+")
(->> (mapv #(Integer/parseInt %))))
orientation-exit (:exit orient-result)
orientation (-> orient-result :out str/trim)]
(if (= 0 orientation-exit)
(case orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
{:width w :height h}) ; Normal or unknown orientation
{:width w :height h}))))) ; If orientation can't be read, use dimensions as-is
orientation (-> orient-result :out str/trim)]
(case orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
{:width w :height h})) ; Normal or unknown orientation
nil)))
(defmethod process :info
[{:keys [input] :as params}]
@@ -247,37 +247,26 @@
:hint "uploaded svg does not provides dimensions"))
(merge input info {:ts (ct/now) :size (fs/size path)}))
(let [path-str (str path)
identify-res (sh/sh "identify" "-format" "image/%[magick]\n" path-str)
;; identify prints one line per frame (animated GIFs, etc.); we take the first one
mtype' (if (zero? (:exit identify-res))
(-> identify-res
:out
str/trim
(str/split #"\s+" 2)
first
str/lower)
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image"))
{:keys [width height]}
(or (get-dimensions-with-orientation path-str)
(do
(l/warn "Failed to read image dimensions with orientation" {:path path})
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image")))]
(let [instance (Info. (str path))
mtype' (.getProperty instance "Mime type")]
(when (and (string? mtype)
(not= (str/lower mtype) mtype'))
(not= mtype mtype'))
(ex/raise :type :validation
:code :media-type-mismatch
:hint (str "Seems like you are uploading a file whose content does not match the extension."
"Expected: " mtype ". Got: " mtype')))
(assoc input
:width width
:height height
:size (fs/size path)
:ts (ct/now))))))
(let [{:keys [width height]}
(or (get-dimensions-with-orientation (str path))
(do
(l/warn "Failed to read image dimensions with orientation; falling back to im4java"
{:path path})
{:width (.getPageWidth instance)
:height (.getPageHeight instance)}))]
(assoc input
:width width
:height height
:size (fs/size path)
:ts (ct/now)))))))
(defmethod process-error org.im4java.core.InfoException
[error]

View File

@@ -58,4 +58,3 @@
(when (nil? (:data file))
(migrate-file conn file)))
(db/exec-one! conn ["drop table page cascade;"])))

View File

@@ -89,8 +89,7 @@
(def ^:private schema:create-font-variant
[:map {:title "create-font-variant"}
[:team-id ::sm/uuid]
[:data [:map-of ::sm/text [:or ::sm/bytes
[::sm/vec ::sm/bytes]]]]
[:data [:map-of ::sm/text ::sm/any]]
[:font-id ::sm/uuid]
[:font-family ::sm/text]
[:font-weight [::sm/one-of {:format "number"} valid-weight]]

View File

@@ -9,7 +9,7 @@
organization management and token validation endpoints."
(:require
[app.common.schema :as sm]
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
[app.common.types.profile :refer [schema:profile]]
[app.common.types.team :refer [schema:team]]
[app.common.uuid :as uuid]
[app.db :as db]
@@ -80,35 +80,3 @@
:team-id id
:organization-id organization-id
:organization-name organization-name})))
;; ---- API: get-managed-profiles
(def ^:private sql:get-managed-profiles
"SELECT DISTINCT p.id, p.fullname as name, p.email
FROM profile p
JOIN team_profile_rel tpr_member
ON tpr_member.profile_id = p.id
WHERE p.id <> ?
AND EXISTS (
SELECT 1
FROM team_profile_rel tpr_owner
JOIN team t
ON t.id = tpr_owner.team_id
WHERE tpr_owner.profile_id = ?
AND tpr_owner.team_id = tpr_member.team_id
AND tpr_owner.is_owner IS TRUE
AND t.is_default IS FALSE
AND t.deleted_at IS NULL);")
(def schema:managed-profile-result
[:vector schema:basic-profile])
(sv/defmethod ::get-managed-profiles
"List profiles that belong to teams for which current user is owner"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:managed-profile-result}
[cfg {:keys [::rpc/profile-id]}]
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(db/exec! cfg [sql:get-managed-profiles current-user-id current-user-id])))

View File

@@ -274,30 +274,3 @@
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res)))))))
(t/deftest input-sanitization-1
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
(io/read*))
params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/ttf" "/etc/passwd"}}
out (th/command! params)]
(t/is (= 0 (:call-count @mock)))
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))))))

View File

@@ -55,7 +55,6 @@
"design-tokens/v1"
"text-editor/v2-html-paste"
"text-editor/v2"
"text-editor-wasm/v1"
"render-wasm/v1"
"variants/v1"})
@@ -79,7 +78,6 @@
"plugins/runtime"
"text-editor/v2-html-paste"
"text-editor/v2"
"text-editor-wasm/v1"
"tokens/numeric-input"
"render-wasm/v1"})
@@ -129,7 +127,6 @@
:feature-design-tokens "design-tokens/v1"
:feature-text-editor-v2 "text-editor/v2"
:feature-text-editor-v2-html-paste "text-editor/v2-html-paste"
:feature-text-editor-wasm "text-editor-wasm/v1"
:feature-render-wasm "render-wasm/v1"
:feature-variants "variants/v1"
:feature-token-input "tokens/numeric-input"

View File

@@ -27,7 +27,6 @@
[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]
@@ -379,7 +378,7 @@
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe cto/schema:token-attrs]]]]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -8,228 +8,8 @@
(: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]
[malli.core :as m]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token value
(defn- token-value-empty-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(def schema:token-value-generic
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-composite-ref
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-font-family
[:vector :string])
(def schema:token-value-typography-map
[:map
[:font-family {:optional true} schema:token-value-font-family]
[:font-weight {:optional true} schema:token-value-generic]
[:font-size {:optional true} schema:token-value-generic]
[:line-height {:optional true} schema:token-value-generic]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
[:text-case {:optional true} schema:token-value-generic]])
(def schema:token-value-typography
[:or
schema:token-value-typography-map
schema:token-value-composite-ref])
(def schema:token-value-shadow-vector
[:vector
[:map
[:offset-x :string]
[:offset-y :string]
[:blur
[:and
:string
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-blur-value-error")}
(fn [blur]
(let [n (d/parse-double blur)]
(or (nil? n) (not (< n 0)))))]]]
[:spread
[:and
:string
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")}
(fn [spread]
(let [n (d/parse-double spread)]
(or (nil? n) (not (< n 0)))))]]]
[:color :string]
[:inset {:optional true} :boolean]]])
(def schema:token-value-shadow
[:or
schema:token-value-shadow-vector
schema:token-value-composite-ref])
(defn make-token-value-schema
[token-type]
[:multi {:dispatch (constantly token-type)
:title "Token Value"}
[:font-family schema:token-value-font-family]
[:typography schema:token-value-typography]
[:shadow schema:token-value-shadow]
[::m/default schema:token-value-generic]])
;; 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
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(and (some? tokens-tree)
(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 token-type]
[:and
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:value (make-token-value-schema token-type)]
[:description {:optional true} schema:token-description]])
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]])
(defn convert-dtcg-token
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
Eg. From this:
{'name' 'body-text'
'type' 'typography'
'value' {
'fontFamilies' ['Arial' 'Helvetica' 'sans-serif']
'fontSize' '16px'
'fontWeights' 'normal'}}
to this
{:name 'body-text'
:type :typography
:value {
:font-family ['Arial' 'Helvetica' 'sans-serif']
:font-size '16px'
:font-weight 'normal'}}"
[token-attrs]
(let [name (get token-attrs "name")
type (get token-attrs "type")
value (get token-attrs "value")
description (get token-attrs "description")
type (cto/dtcg-token-type->token-type type)
value (case type
:font-family (ctob/convert-dtcg-font-family value)
:typography (ctob/convert-dtcg-typography-composite value)
:shadow (ctob/convert-dtcg-shadow-composite value)
value)]
(d/without-nils {:name name
:type type
:value value
:description 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]
(or (nil? tokens-lib)
(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]
(or (nil? tokens-lib)
(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]
(or (nil? tokens-lib)
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[cuerdas.core :as str]))
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
@@ -300,6 +80,56 @@
(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))

View File

@@ -1,15 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.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)

View File

@@ -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-tokens-from-shape % token-attrs))
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
changes)))
check-shape

View File

@@ -11,7 +11,6 @@
#?(:clj [malli.dev.pretty :as mdp])
#?(:clj [malli.dev.virhe :as v])
[app.common.data :as d]
[app.common.json :as json]
[app.common.math :as mth]
[app.common.pprint :as pp]
[app.common.schema.generators :as sg]
@@ -93,31 +92,6 @@
[& items]
(apply mu/merge (map schema items)))
(defn assoc-key
"Add a key & value to a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and add the key there."
([s k v]
(assoc-key s k {} v))
([s k opts v] ;; change order of opts and v to match static schema defintions (e.g. [:something {:optional true} ::sm/integer])
(let [s (schema s)
v (schema v)]
(if (= (m/type s) :map)
(mu/assoc s k v opts)
(if-let [path (mu/find-first s (fn [s' path _] (when (= (m/type s') :map) path)))]
(mu/assoc-in s (conj path k) v opts)
s)))))
(defn dissoc-key
"Remove a key from a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and remove the key there."
[s k]
(let [s (schema s)]
(if (= (m/type s) :map)
(mu/dissoc s k)
(if-let [path (mu/find-first s (fn [s' path _] (when (= (m/type s') :map) path)))]
(mu/update-in s path mu/dissoc k)
s))))
(defn ref?
[s]
(m/-ref-schema? s))
@@ -296,13 +270,6 @@
(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)
@@ -883,32 +850,6 @@
:encode/string str
::oapi/type "boolean"}})
(defn parse-keyword
[v]
(if (string? v)
(-> v (json/read-kebab-key) (keyword))
v))
(defn format-keyword
[v]
(if (keyword? v)
(-> v (name) (json/write-camel-key))
v))
(register!
{:type ::keyword
:pred keyword?
:type-properties
{:title "keyword"
:description "keyword"
:error/message "expected keyword"
:error/code "errors.invalid-keyword"
:gen/gen sg/keyword
:decode/string parse-keyword
:decode/json parse-keyword
:encode/string format-keyword
::oapi/type "string"}})
(register!
{:type ::contains-any
:min 1
@@ -1068,15 +1009,6 @@
{:title "agent"
:description "instance of clojure agent"}}))
#?(:clj
(register!
{:type ::bytes
:pred bytes?
:type-properties
{:title "bytes"
:description "bytes array"}}))
(register! ::any (mu/update-properties :any assoc :gen/gen sg/any))
;; ---- PREDICATES

View File

@@ -21,10 +21,3 @@
;; Only present on resolved profile objects, the resolve process
;; takes the photo-id or geneates an image from the name
[:photo-url {:optional true} :string]])
(def schema:basic-profile
[:map {:title "Basic profile"}
[:id ::sm/uuid]
[:name {:optional true} :string]
[:email {:optional true} :string]])

View File

@@ -9,13 +9,13 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.time :as ct]
[clojure.data :as data]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GENERAL HELPERS
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- schema-keys
@@ -45,7 +45,7 @@
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
(boolean self-reference?)))
self-reference?))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
@@ -59,33 +59,14 @@
(some true? (map #(references-token? % token-name) value))
:else false))
(defn composite-token-reference?
"Predicate if a composite token is a reference value - a string pointing to another token."
[token-value]
(string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token types
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
@@ -96,7 +77,6 @@
:opacity "opacity"
:other "other"
:rotation "rotation"
:shadow "shadow"
:sizing "sizing"
:spacing "spacing"
:string "string"
@@ -114,13 +94,14 @@
"boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type
"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."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Same as above, in the opposite direction."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
@@ -128,114 +109,86 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-name-validation-regex
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
(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}
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
token-name-validation-regex])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)
(dtcg-token-type->token-type type)
type))}
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} 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:color
[:map
[:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
[:fill {:optional true} token-name-ref]
[:stroke-color {:optional true} token-name-ref]])
(def color-keys (schema-keys schema:color))
(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]])
(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
[:map
[:stroke-width {:optional true} token-name-ref]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} schema:token-name]
[:height {:optional true} schema:token-name]])
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]])
(def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"}
[: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))
[: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]])
(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} schema:token-name]
[:column-gap {:optional true} schema:token-name]])
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"}
[: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
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
[: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} 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))
[: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
@@ -243,13 +196,16 @@
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))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def stroke-width-keys (schema-keys schema:stroke-width))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing
@@ -260,109 +216,91 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:font-family
(def ^:private schema:axis
[:map
[:font-family {:optional true} schema:token-name]])
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} schema:token-name]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} schema:token-name]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(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 line-height-keys (schema-keys schema:line-height))
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} schema:token-name]])
[: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]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} token-name-ref]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} token-name-ref]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography
[:map
[:typography {:optional true} token-name-ref]])
(def typography-token-keys (schema-keys schema:typography))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} token-name-ref]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(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:number
(-> (reduce mu/union [schema:line-height
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
(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
(def all-keys (set/union color-keys
border-radius-keys
color-keys
dimensions-keys
number-keys
opacity-keys
rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
dimensions-keys
axis-keys
rotation-keys
typography-keys
typography-token-keys))
typography-token-keys
number-keys))
(def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}])
@@ -383,28 +321,11 @@
schema:text-decoration
schema:dimensions])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for conversion between token attrs and shape attrs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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
@@ -446,13 +367,21 @@
(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))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by shape type
;; TOKEN SHAPE ATTRIBUTES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private position-attributes #{:x :y})
(def position-attributes #{:x :y})
(def ^:private generic-attributes
(def generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
@@ -461,22 +390,20 @@
shadow-keys
position-attributes))
(def ^:private rect-attributes
(def rect-attributes
(set/union generic-attributes
border-radius-keys))
(def ^:private frame-with-layout-attributes
(def frame-with-layout-attributes
(set/union rect-attributes
spacing-gap-padding-keys))
(def ^:private text-attributes
(def 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
@@ -492,14 +419,12 @@
nil))
(defn appliable-attrs-for-shape
"Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Returns intersection of shape `attributes` for `shape-type`."
[attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape?
"Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Checks if `token-type` supports given shape `attributes`."
[attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -510,6 +435,42 @@
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}
@@ -539,33 +500,7 @@
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for tokens application
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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 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
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
@@ -628,3 +563,32 @@
(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))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))

View File

@@ -114,19 +114,25 @@
[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 cto/schema:token-attrs)
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
(sg/fmap #(make-token %)))}
(sm/required-keys cto/schema:token-attrs)
(sm/required-keys schema:token-attrs)
[:fn token?]])
(def ^:private check-token-attrs
(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))
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
(def check-token
(sm/check-fn schema:token :hint "expected valid token"))
@@ -311,18 +317,10 @@
[o]
(instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-name
[:and
:string
[:fn #(normalized-set-name? %)]]) ;; The #() is necessary because the function is only declared, not defined
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name schema:token-set-name]
[:name :string]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true
@@ -344,6 +342,8 @@
: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,25 +404,12 @@
(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.
@@ -1383,13 +1370,10 @@ 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 (tokens-lib? o) (valid? o)))
(and (instance? TokensLib o)
(valid? o)))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
@@ -1451,50 +1435,6 @@ 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:

View File

@@ -6,34 +6,34 @@
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[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} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(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/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/is (nil? (cft/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/is (nil? (cft/parse-token-value ".3")))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/is (nil? (cft/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 (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
expected (cft/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,21 +62,34 @@
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(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)))
(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)))
(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? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (true? (cft/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? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (nil? (cft/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"}}}}))))

View File

@@ -255,28 +255,28 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
(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])))
(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])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-tokens-from-shape [:font-family])))
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(:objects page)
{})
(cls/generate-update-shapes [(:id circle1)]
(fn [shape]
(-> shape
(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])))
(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])))
(:objects page)
{}))

View File

@@ -8,19 +8,20 @@
(: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/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")))
(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")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
;; Disallow any special characters
(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"))))
(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"))))

View File

@@ -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 #{}))
@@ -2013,11 +2013,3 @@
(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"}}}}))))

View File

@@ -51,11 +51,6 @@ services:
- 4401:4401
- 4402:4402
# Plugins
- 4200:4200
- 4201:4201
- 4202:4202
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}
# SMTP setup

View File

@@ -121,7 +121,7 @@ http {
proxy_http_version 1.1;
}
location /plugins/mcp {
location /mcp {
alias /home/penpot/penpot/mcp/packages/plugin/dist;
proxy_http_version 1.1;
}
@@ -133,11 +133,6 @@ http {
proxy_http_version 1.1;
}
location /management {
proxy_pass http://127.0.0.1:6060/management;
proxy_http_version 1.1;
}
location /mcp/stream {
proxy_pass http://127.0.0.1:4401/mcp;
proxy_http_version 1.1;

View File

@@ -24,7 +24,6 @@ RUN set -e; \
libltdl-dev \
liblzma-dev \
libopenexr-dev \
libxml2-dev \
libpng-dev \
librsvg2-dev \
libtiff-dev \
@@ -53,7 +52,6 @@ RUN set -e; \
libfftw3-dev \
libheif-dev \
libjpeg-dev \
libxml2-dev \
liblcms2-dev \
libltdl-dev \
liblzma-dev \
@@ -79,7 +77,6 @@ RUN set -e; \
libopenjp2-7 \
libpng16-16 \
librsvg2-2 \
libxml2 \
libtiff6 \
libwebp7 \
libwebpdemux2 \

View File

@@ -125,7 +125,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=penpotapp/imagemagick:7.1.2-13 /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
ARG BUNDLE_PATH="./bundle-backend/"
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/

View File

@@ -107,7 +107,7 @@ RUN set -eux; \
ARG BUNDLE_PATH="./bundle-exporter/"
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
COPY --from=penpotapp/imagemagick:7.1.2-13 /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
WORKDIR /opt/penpot/exporter
USER penpot:penpot

View File

@@ -198,6 +198,13 @@ services:
## Valkey (or previously Redis) is used for the websockets notifications.
PENPOT_REDIS_URI: redis://penpot-valkey/0
penpot-mcp:
image: penpotapp/mcp:${PENPOT_VERSION:-latest}
restart: always
networks:
- penpot
penpot-postgres:
image: "postgres:15"
restart: always

View File

@@ -1 +1 @@
resolver $PENPOT_INTERNAL_RESOLVER valid=10s;
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off valid=10s;

View File

@@ -73,7 +73,6 @@ http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
server_name _;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
@@ -130,11 +129,6 @@ http {
proxy_buffering off;
}
location /plugins {
alias /var/www/app/plugins;
proxy_http_version 1.1;
}
location /readyz {
access_log off;
proxy_pass $PENPOT_BACKEND_URI$request_uri;

View File

@@ -8,7 +8,9 @@ desc: Customize your Penpot instance today. Learn how to install with Elestio, D
This guide explains how to get your own Penpot instance, running on a machine you control,
to test it, use it by you or your team, or even customize and extend it any way you like.
For additional context, see the post <a href="https://penpot.app/blog/how-to-self-host-penpot/" target="_blank">How to self-host Penpot: A technical implementation guide</a> on the Penpot blog.
If you need more context you can look at the <a
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
about self-hosting</a> in Penpot community.
<strong>The experience stays the same, whether you use
Penpot <a href="https://design.penpot.app" target="_blank">in the cloud</a>

View File

@@ -14,7 +14,7 @@ Keep in mind that database size doesn't grow strictly proportionally with user c
# About Valkey / Redis requirements
Valkey is mainly used for coordinating websocket notifications and, since Penpot 2.11, as a cache. Therefore, disk storage will not be necessary as it will use the instance's RAM.
"Valkey is mainly used for coordinating websocket notifications and, since Penpot 2.11, as a cache. Therefore, disk storage will not be necessary as it will use the instance's RAM.
To prevent the cache from hogging all the system's RAM usage, it is recommended to use two configuration parameters which, both in the docker-compose.yaml provided by Penpot and in the official Helm Chart, come with default parameters that should be sufficient for most deployments:

View File

@@ -38,24 +38,6 @@
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))))
(sync-page-size! [dom]
(bw/eval! dom
(fn [elem]
;; IMPORTANT: No CLJS runtime allowed. Use only JS
;; primitives. This runs in a context without access to
;; cljs.core. Avoid any functions that transpile to
;; cljs.core/* calls, as they will break in the browser
;; runtime.
(let [width (.getAttribute ^js elem "width")
height (.getAttribute ^js elem "height")
style-node (let [node (.createElement js/document "style")]
(.appendChild (.-head js/document) node)
node)]
(set! (.-textContent style-node)
(dm/str "@page { size: " width "px " height "px; margin: 0; }\n"
"html, body, #app { margin: 0; padding: 0; width: " width "px; height: " height "px; overflow: visible; }"))))))
(render-object [page base-uri {:keys [id] :as object}]
(p/let [uri (prepare-uri base-uri id)
path (sh/tempfile :prefix "penpot.tmp.pdf." :suffix (mime/get-extension type))]
@@ -63,7 +45,6 @@
(bw/nav! page uri)
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
(bw/wait-for dom)
(sync-page-size! dom)
(bw/screenshot dom {:full-page? true})
(bw/sleep page 2000) ; the good old fix with sleep
(bw/pdf page {:path path})

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
"browserslist": [
"defaults"
],
@@ -47,11 +47,10 @@
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/tokenscript": "workspace:./packages/tokenscript",
"@penpot/plugins-runtime": "1.4.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",
"@penpot/tokenscript": "workspace:./packages/tokenscript",
"@penpot/ui": "workspace:./packages/ui",
"@playwright/test": "1.58.0",
"@storybook/addon-docs": "10.1.11",
"@storybook/addon-themes": "10.1.11",
@@ -103,7 +102,6 @@
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.1",
"scheduler": "^0.27.0",
"source-map-support": "^0.5.21",
"storybook": "10.1.11",
"style-dictionary": "5.0.0-rc.1",

View File

@@ -8,6 +8,6 @@
"author": "Andrey Antukh",
"license": "MPL-2.0",
"dependencies": {
"@tokens-studio/tokenscript-interpreter": "^0.26.0"
"@tokens-studio/tokenscript-interpreter": "^0.23.1"
}
}

View File

@@ -1,12 +1,12 @@
// Auto-generated by @tokens-studio/tokenscript-schemas
// Version: @tokens-studio/tokenscript-schemas@v0.4.0
// Version: @tokens-studio/tokenscript-schemas@v0.1.2
// GitHub: https://github.com/tokens-studio/tokenscript-schemas
// Command: npx @tokens-studio/tokenscript-schemas bundle preset:css preset:cssColors --output ./schemas.js
// Generated: 2026-02-11T08:46:40.467Z
// Command: npx @tokens-studio/tokenscript-schemas bundle preset:css --output ./tokenscript-schemas.js
// Generated: 2026-01-07T09:21:11.478Z
import { Config } from "@tokens-studio/tokenscript-interpreter";
export const SCHEMAS = [
const SCHEMAS = [
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/hex-color/0/",
schema: {
@@ -31,127 +31,7 @@ export const SCHEMAS = [
}
}
],
"conversions": [
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-color/0/",
"target": "$self",
"description": "Converts sRGB (0-1) to Hex format",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// sRGB to Hex Conversion\n// Converts sRGB (0-1) to hexadecimal string format\n//\n// Examples:\n// sRGB(1, 0, 0) → #ff0000\n// sRGB(0, 1, 0.5) → #00ff80\n\nvariable hex: String = \"#\";\nvariable value: Number = 0;\n\n// Red channel\nvalue = round({input}.r * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Green channel\nvalue = round({input}.g * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Blue channel\nvalue = round({input}.b * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\nreturn hex;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/p3-color/0/",
"target": "$self",
"description": "Converts Display P3 to Hex format (clamps to sRGB gamut)",
"lossless": false,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Display P3 to Hex Conversion\n// Converts P3 (0-1) to hexadecimal string format\n// Note: P3 colors may be out of sRGB gamut, values are clamped to 0-1\n//\n// Examples:\n// P3(1, 0, 0) → #ff0000\n// P3(0, 1, 0.5) → #00ff80\n\nvariable hex: String = \"#\";\nvariable value: Number = 0;\n\n// Red channel (clamp P3 to sRGB range)\nvalue = {input}.r;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Green channel\nvalue = {input}.g;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Blue channel\nvalue = {input}.b;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\nreturn hex;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/hsl-color/0/",
"target": "$self",
"description": "Converts HSL to Hex format",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// HSL to Hex Conversion\n// Converts HSL to hexadecimal string format\n// Reference: Standard HSL to RGB algorithm\n//\n// Input: Color.HSL with h (0-360), s (0-1), l (0-1)\n// Output: Hex string #rrggbb\n\n// Get input HSL values\nvariable h: Number = {input}.h;\nvariable s: Number = {input}.s;\nvariable l: Number = {input}.l;\n\n// Normalize hue to 0-1 range\nvariable hue: Number = h / 360;\n\n// RGB values (default to achromatic)\nvariable r: Number = l;\nvariable g: Number = l;\nvariable b: Number = l;\n\n// Only calculate if there's saturation\nif (s > 0) [\n variable q: Number = 0;\n if (l < 0.5) [\n q = l * (1 + s);\n ] else [\n q = l + s - l * s;\n ];\n\n variable p: Number = 2 * l - q;\n\n // Red (hue + 1/3)\n variable tr: Number = hue + 0.333333333333333;\n if (tr < 0) [ tr = tr + 1; ];\n if (tr > 1) [ tr = tr - 1; ];\n\n if (tr < 0.166666666666667) [\n r = p + (q - p) * 6 * tr;\n ] else [\n if (tr < 0.5) [\n r = q;\n ] else [\n if (tr < 0.666666666666667) [\n r = p + (q - p) * (0.666666666666667 - tr) * 6;\n ] else [\n r = p;\n ];\n ];\n ];\n\n // Green (hue)\n variable tg: Number = hue;\n if (tg < 0) [ tg = tg + 1; ];\n if (tg > 1) [ tg = tg - 1; ];\n\n if (tg < 0.166666666666667) [\n g = p + (q - p) * 6 * tg;\n ] else [\n if (tg < 0.5) [\n g = q;\n ] else [\n if (tg < 0.666666666666667) [\n g = p + (q - p) * (0.666666666666667 - tg) * 6;\n ] else [\n g = p;\n ];\n ];\n ];\n\n // Blue (hue - 1/3)\n variable tb: Number = hue - 0.333333333333333;\n if (tb < 0) [ tb = tb + 1; ];\n if (tb > 1) [ tb = tb - 1; ];\n\n if (tb < 0.166666666666667) [\n b = p + (q - p) * 6 * tb;\n ] else [\n if (tb < 0.5) [\n b = q;\n ] else [\n if (tb < 0.666666666666667) [\n b = p + (q - p) * (0.666666666666667 - tb) * 6;\n ] else [\n b = p;\n ];\n ];\n ];\n];\n\n// Convert RGB to hex\nvariable hex: String = \"#\";\nvariable value: Number = 0;\n\n// Red\nvalue = round(r * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Green\nvalue = round(g * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Blue\nvalue = round(b * 255);\nif (value < 0) [ value = 0; ];\nif (value > 255) [ value = 255; ];\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\nreturn hex;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/oklch-color/0/",
"target": "$self",
"description": "Converts OKLCH to Hex format (clamps to sRGB gamut)",
"lossless": false,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// OKLCH to Hex Conversion\n// Converts OKLCH perceptual color to hexadecimal string format\n// Path: OKLCH → OKLab → XYZ-D65 → Linear sRGB → sRGB → Hex\n//\n// Input: Color.OKLCH with l (0-1), c, h (0-360)\n// Output: Hex string #rrggbb\n\n// Get input OKLCH values\nvariable ok_l: Number = {input}.l;\nvariable ok_c: Number = {input}.c;\nvariable ok_h: Number = {input}.h;\n\n// === Step 1: OKLCH to OKLab (polar to cartesian) ===\nvariable pi: Number = pi();\nvariable deg_to_rad: Number = pi / 180;\nvariable h_rad: Number = ok_h * deg_to_rad;\n\nvariable lab_a: Number = ok_c * cos(h_rad);\nvariable lab_b: Number = ok_c * sin(h_rad);\n\n// === Step 2: OKLab to XYZ-D65 ===\n// Inverse Lab-to-LMS matrix\nvariable lms_l: Number = 1.0 * ok_l + 0.3963377773761749 * lab_a + 0.2158037573099136 * lab_b;\nvariable lms_m: Number = 1.0 * ok_l + -0.1055613458156586 * lab_a + -0.0638541728258133 * lab_b;\nvariable lms_s: Number = 1.0 * ok_l + -0.0894841775298119 * lab_a + -1.2914855480194092 * lab_b;\n\n// Cube the values (inverse of cube root)\nvariable lms_l_cubed: Number = lms_l * lms_l * lms_l;\nvariable lms_m_cubed: Number = lms_m * lms_m * lms_m;\nvariable lms_s_cubed: Number = lms_s * lms_s * lms_s;\n\n// Inverse LMS-to-XYZ matrix\nvariable xyz_x: Number = 1.2268798758459243 * lms_l_cubed + -0.5578149944602171 * lms_m_cubed + 0.2813910456659647 * lms_s_cubed;\nvariable xyz_y: Number = -0.0405757452148008 * lms_l_cubed + 1.1122868032803170 * lms_m_cubed + -0.0717110580655164 * lms_s_cubed;\nvariable xyz_z: Number = -0.0763729366746601 * lms_l_cubed + -0.4214933324022432 * lms_m_cubed + 1.5869240198367816 * lms_s_cubed;\n\n// === Step 3: XYZ-D65 to Linear sRGB ===\nvariable linear_r: Number = 3.2409699419045226 * xyz_x + -1.537383177570094 * xyz_y + -0.4986107602930034 * xyz_z;\nvariable linear_g: Number = -0.9692436362808796 * xyz_x + 1.8759675015077202 * xyz_y + 0.04155505740717559 * xyz_z;\nvariable linear_b: Number = 0.05563007969699366 * xyz_x + -0.20397695888897652 * xyz_y + 1.0569715142428786 * xyz_z;\n\n// === Step 4: Linear sRGB to sRGB (gamma correction) ===\nvariable threshold: Number = 0.0031308;\nvariable linear_scale: Number = 12.92;\nvariable gamma_offset: Number = 0.055;\nvariable gamma_scale: Number = 1.055;\nvariable gamma_exp: Number = 0.416666666666667;\n\nvariable srgb_r: Number = 0;\nif (linear_r <= threshold) [\n srgb_r = linear_r * linear_scale;\n] else [\n if (linear_r > 0) [\n srgb_r = gamma_scale * pow(linear_r, gamma_exp) - gamma_offset;\n ] else [\n srgb_r = 0;\n ];\n];\n\nvariable srgb_g: Number = 0;\nif (linear_g <= threshold) [\n srgb_g = linear_g * linear_scale;\n] else [\n if (linear_g > 0) [\n srgb_g = gamma_scale * pow(linear_g, gamma_exp) - gamma_offset;\n ] else [\n srgb_g = 0;\n ];\n];\n\nvariable srgb_b: Number = 0;\nif (linear_b <= threshold) [\n srgb_b = linear_b * linear_scale;\n] else [\n if (linear_b > 0) [\n srgb_b = gamma_scale * pow(linear_b, gamma_exp) - gamma_offset;\n ] else [\n srgb_b = 0;\n ];\n];\n\n// === Step 5: sRGB to Hex ===\nvariable hex: String = \"#\";\nvariable value: Number = 0;\n\n// Red (clamp to 0-1)\nvalue = srgb_r;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Green\nvalue = srgb_g;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\n// Blue\nvalue = srgb_b;\nif (value < 0) [ value = 0; ];\nif (value > 1) [ value = 1; ];\nvalue = round(value * 255);\nif (value < 16) [\n hex = hex.concat(\"0\").concat(value.to_string(16));\n] else [\n hex = hex.concat(value.to_string(16));\n];\n\nreturn hex;"
}
}
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-color/0/",
schema: {
"name": "SRGB",
"type": "color",
"description": "sRGB color space with normalized 0-1 range. The standard color space for web and displays.",
"schema": {
"type": "object",
"properties": {
"r": {
"type": "number",
"description": "Red channel (0-1)"
},
"g": {
"type": "number",
"description": "Green channel (0-1)"
},
"b": {
"type": "number",
"description": "Blue channel (0-1)"
}
},
"required": [
"r",
"g",
"b"
],
"order": [
"r",
"g",
"b"
],
"additionalProperties": false
},
"initializers": [
{
"title": "sRGB Color Initializer",
"keyword": "srgb",
"description": "Creates an sRGB color from normalized 0-1 values",
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// sRGB Color Initializer\n// Creates an sRGB color from normalized 0-1 values\n// Input: List of [r, g, b] or [r, g, b, alpha] values in 0-1 range\n\nvariable color_values: List = {input};\nvariable output: Color.SRGB;\n\noutput.r = color_values.get(0);\noutput.g = color_values.get(1);\noutput.b = color_values.get(2);\n\n// Set alpha if provided as 4th parameter\nif (color_values.length() > 3) [\n output.alpha = color_values.get(3);\n];\n\nreturn output;"
}
}
],
"conversions": [
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/rgb-color/0/",
"target": "$self",
"description": "Converts RGB (0-255) to sRGB (0-1) by normalizing",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// RGB to sRGB Conversion\n// Converts RGB (0-255) to sRGB (0-1) by normalizing\n// Input: Color.Rgb with r, g, b in 0-255 range\n// Output: Color.SRGB with r, g, b in 0-1 range\n// Lossless: Yes (simple division)\n\nvariable r_normalized: Number = {input}.r / 255;\nvariable g_normalized: Number = {input}.g / 255;\nvariable b_normalized: Number = {input}.b / 255;\n\nvariable output: Color.SRGB;\noutput.r = r_normalized;\noutput.g = g_normalized;\noutput.b = b_normalized;\n\nreturn output;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/hsl-color/0/",
"target": "$self",
"description": "Converts HSL to sRGB",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// HSL to sRGB Conversion\n// Reference: https://github.com/color-js/color.js/blob/main/src/spaces/hsl.js\n//\n// Algorithm:\n// 1. If saturation is 0, it's achromatic: R=G=B=L\n// 2. Otherwise use the HSL to RGB formula:\n// - Calculate intermediate values based on L\n// - Use hue to determine RGB components\n//\n// Input: Color.HSL with h (0-360), s (0-1), l (0-1)\n// Output: Color.SRGB with r, g, b in 0-1 range\n\n// Get input HSL values\nvariable h: Number = {input}.h;\nvariable s: Number = {input}.s;\nvariable l: Number = {input}.l;\n\n// Normalize hue to 0-1 range\nvariable hue: Number = h / 360;\n\n// Output values\nvariable r: Number = l;\nvariable g: Number = l;\nvariable b: Number = l;\n\n// Only calculate if there's saturation (not achromatic)\nif (s > 0) [\n // Calculate intermediate value\n variable q: Number = 0;\n if (l < 0.5) [\n q = l * (1 + s);\n ] else [\n q = l + s - l * s;\n ];\n \n variable p: Number = 2 * l - q;\n \n // Helper function logic inlined for R (hue + 1/3)\n variable tr: Number = hue + 0.333333333333333;\n if (tr < 0) [ tr = tr + 1; ];\n if (tr > 1) [ tr = tr - 1; ];\n \n if (tr < 0.166666666666667) [\n r = p + (q - p) * 6 * tr;\n ] else [\n if (tr < 0.5) [\n r = q;\n ] else [\n if (tr < 0.666666666666667) [\n r = p + (q - p) * (0.666666666666667 - tr) * 6;\n ] else [\n r = p;\n ];\n ];\n ];\n \n // Helper function logic inlined for G (hue)\n variable tg: Number = hue;\n if (tg < 0) [ tg = tg + 1; ];\n if (tg > 1) [ tg = tg - 1; ];\n \n if (tg < 0.166666666666667) [\n g = p + (q - p) * 6 * tg;\n ] else [\n if (tg < 0.5) [\n g = q;\n ] else [\n if (tg < 0.666666666666667) [\n g = p + (q - p) * (0.666666666666667 - tg) * 6;\n ] else [\n g = p;\n ];\n ];\n ];\n \n // Helper function logic inlined for B (hue - 1/3)\n variable tb: Number = hue - 0.333333333333333;\n if (tb < 0) [ tb = tb + 1; ];\n if (tb > 1) [ tb = tb - 1; ];\n \n if (tb < 0.166666666666667) [\n b = p + (q - p) * 6 * tb;\n ] else [\n if (tb < 0.5) [\n b = q;\n ] else [\n if (tb < 0.666666666666667) [\n b = p + (q - p) * (0.666666666666667 - tb) * 6;\n ] else [\n b = p;\n ];\n ];\n ];\n];\n\n// Create output\nvariable output: Color.SRGB;\noutput.r = r;\noutput.g = g;\noutput.b = b;\n\nreturn output;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-linear-color/0/",
"target": "$self",
"description": "Converts Linear sRGB to sRGB by applying gamma correction",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Linear sRGB to sRGB Conversion\n// Applies gamma correction (transfer function)\n// Reference: IEC 61966-2-1:1999 (sRGB specification)\n//\n// Algorithm:\n// if linear ≤ 0.0031308: srgb = linear * 12.92\n// else: srgb = 1.055 * linear^(1/2.4) - 0.055\n//\n// Input: Color.LinearSRGB with r, g, b in linear 0-1 range\n// Output: Color.SRGB with r, g, b in gamma-corrected 0-1 range\n\n// Gamma correction constants (IEC 61966-2-1)\nvariable threshold: Number = 0.0031308;\nvariable linear_scale: Number = 12.92;\nvariable gamma_offset: Number = 0.055;\nvariable gamma_scale: Number = 1.055;\nvariable gamma_exponent: Number = 0.416666666666667;\n\n// Get input linear values\nvariable linear_r: Number = {input}.r;\nvariable linear_g: Number = {input}.g;\nvariable linear_b: Number = {input}.b;\n\n// Convert red channel\nvariable srgb_r: Number = 0;\nif (linear_r <= threshold) [\n srgb_r = linear_r * linear_scale;\n] else [\n srgb_r = gamma_scale * pow(linear_r, gamma_exponent) - gamma_offset;\n];\n\n// Convert green channel\nvariable srgb_g: Number = 0;\nif (linear_g <= threshold) [\n srgb_g = linear_g * linear_scale;\n] else [\n srgb_g = gamma_scale * pow(linear_g, gamma_exponent) - gamma_offset;\n];\n\n// Convert blue channel\nvariable srgb_b: Number = 0;\nif (linear_b <= threshold) [\n srgb_b = linear_b * linear_scale;\n] else [\n srgb_b = gamma_scale * pow(linear_b, gamma_exponent) - gamma_offset;\n];\n\n// Create output\nvariable output: Color.SRGB;\noutput.r = srgb_r;\noutput.g = srgb_g;\noutput.b = srgb_b;\n\nreturn output;"
}
}
]
"conversions": []
}
},
{
@@ -297,6 +177,85 @@ export const SCHEMAS = [
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-color/0/",
schema: {
"name": "SRGB",
"type": "color",
"description": "sRGB color space with normalized 0-1 range. The standard color space for web and displays.",
"schema": {
"type": "object",
"properties": {
"r": {
"type": "number",
"description": "Red channel (0-1)"
},
"g": {
"type": "number",
"description": "Green channel (0-1)"
},
"b": {
"type": "number",
"description": "Blue channel (0-1)"
}
},
"required": [
"r",
"g",
"b"
],
"order": [
"r",
"g",
"b"
],
"additionalProperties": false
},
"initializers": [
{
"title": "sRGB Color Initializer",
"keyword": "srgb",
"description": "Creates an sRGB color from normalized 0-1 values",
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// sRGB Color Initializer\n// Creates an sRGB color from normalized 0-1 values\n// Input: List of [r, g, b] or [r, g, b, alpha] values in 0-1 range\n\nvariable color_values: List = {input};\nvariable output: Color.SRGB;\n\noutput.r = color_values.get(0);\noutput.g = color_values.get(1);\noutput.b = color_values.get(2);\n\n// Set alpha if provided as 4th parameter\nif (color_values.length() > 3) [\n output.alpha = color_values.get(3);\n];\n\nreturn output;"
}
}
],
"conversions": [
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/rgb-color/0/",
"target": "$self",
"description": "Converts RGB (0-255) to sRGB (0-1) by normalizing",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// RGB to sRGB Conversion\n// Converts RGB (0-255) to sRGB (0-1) by normalizing\n// Input: Color.Rgb with r, g, b in 0-255 range\n// Output: Color.SRGB with r, g, b in 0-1 range\n// Lossless: Yes (simple division)\n\nvariable r_normalized: Number = {input}.r / 255;\nvariable g_normalized: Number = {input}.g / 255;\nvariable b_normalized: Number = {input}.b / 255;\n\nvariable output: Color.SRGB;\noutput.r = r_normalized;\noutput.g = g_normalized;\noutput.b = b_normalized;\n\nreturn output;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/hsl-color/0/",
"target": "$self",
"description": "Converts HSL to sRGB",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// HSL to sRGB Conversion\n// Reference: https://github.com/color-js/color.js/blob/main/src/spaces/hsl.js\n//\n// Algorithm:\n// 1. If saturation is 0, it's achromatic: R=G=B=L\n// 2. Otherwise use the HSL to RGB formula:\n// - Calculate intermediate values based on L\n// - Use hue to determine RGB components\n//\n// Input: Color.HSL with h (0-360), s (0-1), l (0-1)\n// Output: Color.SRGB with r, g, b in 0-1 range\n\n// Get input HSL values\nvariable h: Number = {input}.h;\nvariable s: Number = {input}.s;\nvariable l: Number = {input}.l;\n\n// Normalize hue to 0-1 range\nvariable hue: Number = h / 360;\n\n// Output values\nvariable r: Number = l;\nvariable g: Number = l;\nvariable b: Number = l;\n\n// Only calculate if there's saturation (not achromatic)\nif (s > 0) [\n // Calculate intermediate value\n variable q: Number = 0;\n if (l < 0.5) [\n q = l * (1 + s);\n ] else [\n q = l + s - l * s;\n ];\n \n variable p: Number = 2 * l - q;\n \n // Helper function logic inlined for R (hue + 1/3)\n variable tr: Number = hue + 0.333333333333333;\n if (tr < 0) [ tr = tr + 1; ];\n if (tr > 1) [ tr = tr - 1; ];\n \n if (tr < 0.166666666666667) [\n r = p + (q - p) * 6 * tr;\n ] else [\n if (tr < 0.5) [\n r = q;\n ] else [\n if (tr < 0.666666666666667) [\n r = p + (q - p) * (0.666666666666667 - tr) * 6;\n ] else [\n r = p;\n ];\n ];\n ];\n \n // Helper function logic inlined for G (hue)\n variable tg: Number = hue;\n if (tg < 0) [ tg = tg + 1; ];\n if (tg > 1) [ tg = tg - 1; ];\n \n if (tg < 0.166666666666667) [\n g = p + (q - p) * 6 * tg;\n ] else [\n if (tg < 0.5) [\n g = q;\n ] else [\n if (tg < 0.666666666666667) [\n g = p + (q - p) * (0.666666666666667 - tg) * 6;\n ] else [\n g = p;\n ];\n ];\n ];\n \n // Helper function logic inlined for B (hue - 1/3)\n variable tb: Number = hue - 0.333333333333333;\n if (tb < 0) [ tb = tb + 1; ];\n if (tb > 1) [ tb = tb - 1; ];\n \n if (tb < 0.166666666666667) [\n b = p + (q - p) * 6 * tb;\n ] else [\n if (tb < 0.5) [\n b = q;\n ] else [\n if (tb < 0.666666666666667) [\n b = p + (q - p) * (0.666666666666667 - tb) * 6;\n ] else [\n b = p;\n ];\n ];\n ];\n];\n\n// Create output\nvariable output: Color.SRGB;\noutput.r = r;\noutput.g = g;\noutput.b = b;\n\nreturn output;"
}
},
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-linear-color/0/",
"target": "$self",
"description": "Converts Linear sRGB to sRGB by applying gamma correction",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Linear sRGB to sRGB Conversion\n// Applies gamma correction (transfer function)\n// Reference: IEC 61966-2-1:1999 (sRGB specification)\n//\n// Algorithm:\n// if linear ≤ 0.0031308: srgb = linear * 12.92\n// else: srgb = 1.055 * linear^(1/2.4) - 0.055\n//\n// Input: Color.LinearSRGB with r, g, b in linear 0-1 range\n// Output: Color.SRGB with r, g, b in gamma-corrected 0-1 range\n\n// Gamma correction constants (IEC 61966-2-1)\nvariable threshold: Number = 0.0031308;\nvariable linear_scale: Number = 12.92;\nvariable gamma_offset: Number = 0.055;\nvariable gamma_scale: Number = 1.055;\nvariable gamma_exponent: Number = 0.416666666666667;\n\n// Get input linear values\nvariable linear_r: Number = {input}.r;\nvariable linear_g: Number = {input}.g;\nvariable linear_b: Number = {input}.b;\n\n// Convert red channel\nvariable srgb_r: Number = 0;\nif (linear_r <= threshold) [\n srgb_r = linear_r * linear_scale;\n] else [\n srgb_r = gamma_scale * pow(linear_r, gamma_exponent) - gamma_offset;\n];\n\n// Convert green channel\nvariable srgb_g: Number = 0;\nif (linear_g <= threshold) [\n srgb_g = linear_g * linear_scale;\n] else [\n srgb_g = gamma_scale * pow(linear_g, gamma_exponent) - gamma_offset;\n];\n\n// Convert blue channel\nvariable srgb_b: Number = 0;\nif (linear_b <= threshold) [\n srgb_b = linear_b * linear_scale;\n] else [\n srgb_b = gamma_scale * pow(linear_b, gamma_exponent) - gamma_offset;\n];\n\n// Create output\nvariable output: Color.SRGB;\noutput.r = srgb_r;\noutput.g = srgb_g;\noutput.b = srgb_b;\n\nreturn output;"
}
}
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/srgb-linear-color/0/",
schema: {
@@ -770,65 +729,6 @@ export const SCHEMAS = [
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/p3-color/0/",
schema: {
"name": "P3",
"type": "color",
"description": "Display-P3 color space with sRGB transfer function. Wider gamut than sRGB, common on modern Apple displays.",
"schema": {
"type": "object",
"properties": {
"r": {
"type": "number",
"description": "Red channel (0-1, can exceed for out-of-gamut)"
},
"g": {
"type": "number",
"description": "Green channel (0-1, can exceed for out-of-gamut)"
},
"b": {
"type": "number",
"description": "Blue channel (0-1, can exceed for out-of-gamut)"
}
},
"required": [
"r",
"g",
"b"
],
"order": [
"r",
"g",
"b"
],
"additionalProperties": false
},
"initializers": [
{
"title": "Display-P3 Color Initializer",
"keyword": "p3",
"description": "Creates a Display-P3 color from 0-1 values",
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Display-P3 Color Initializer\n// Creates a Display-P3 color from 0-1 values\n// Input: List of [r, g, b] or [r, g, b, alpha] values\n\nvariable color_values: List = {input};\nvariable output: Color.P3;\n\noutput.r = color_values.get(0);\noutput.g = color_values.get(1);\noutput.b = color_values.get(2);\n\n// Set alpha if provided as 4th parameter\nif (color_values.length() > 3) [\n output.alpha = color_values.get(3);\n];\n\nreturn output;"
}
}
],
"conversions": [
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/p3-linear-color/0/",
"target": "$self",
"description": "Converts Linear P3 to P3 by applying sRGB transfer function",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Linear P3 to P3 Conversion\n// Applies sRGB transfer function (gamma encoding)\n// P3 uses the same transfer function as sRGB\n// Reference: CSS Color Level 4\n//\n// Algorithm (same as sRGB):\n// if linear ≤ 0.0031308: encoded = 12.92 × linear\n// else: encoded = 1.055 × linear^(1/2.4) - 0.055\n//\n// Input: Color.LinearP3 with linear r, g, b values\n// Output: Color.P3 with gamma-encoded r, g, b values\n\n// Transfer function constants (same as sRGB)\nvariable threshold: Number = 0.0031308;\nvariable linear_scale: Number = 12.92;\nvariable gamma_scale: Number = 1.055;\nvariable gamma_offset: Number = 0.055;\nvariable gamma_exponent: Number = 0.4166666666666667;\n\n// Get linear values\nvariable linear_r: Number = {input}.r;\nvariable linear_g: Number = {input}.g;\nvariable linear_b: Number = {input}.b;\n\n// Convert red channel\nvariable encoded_r: Number = 0;\nif (linear_r <= threshold) [\n encoded_r = linear_scale * linear_r;\n] else [\n encoded_r = gamma_scale * pow(linear_r, gamma_exponent) - gamma_offset;\n];\n\n// Convert green channel\nvariable encoded_g: Number = 0;\nif (linear_g <= threshold) [\n encoded_g = linear_scale * linear_g;\n] else [\n encoded_g = gamma_scale * pow(linear_g, gamma_exponent) - gamma_offset;\n];\n\n// Convert blue channel\nvariable encoded_b: Number = 0;\nif (linear_b <= threshold) [\n encoded_b = linear_scale * linear_b;\n] else [\n encoded_b = gamma_scale * pow(linear_b, gamma_exponent) - gamma_offset;\n];\n\n// Create output\nvariable output: Color.P3;\noutput.r = encoded_r;\noutput.g = encoded_g;\noutput.b = encoded_b;\n\nreturn output;"
}
}
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/css-color/0/",
schema: {
@@ -1286,6 +1186,65 @@ export const SCHEMAS = [
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/p3-color/0/",
schema: {
"name": "P3",
"type": "color",
"description": "Display-P3 color space with sRGB transfer function. Wider gamut than sRGB, common on modern Apple displays.",
"schema": {
"type": "object",
"properties": {
"r": {
"type": "number",
"description": "Red channel (0-1, can exceed for out-of-gamut)"
},
"g": {
"type": "number",
"description": "Green channel (0-1, can exceed for out-of-gamut)"
},
"b": {
"type": "number",
"description": "Blue channel (0-1, can exceed for out-of-gamut)"
}
},
"required": [
"r",
"g",
"b"
],
"order": [
"r",
"g",
"b"
],
"additionalProperties": false
},
"initializers": [
{
"title": "Display-P3 Color Initializer",
"keyword": "p3",
"description": "Creates a Display-P3 color from 0-1 values",
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Display-P3 Color Initializer\n// Creates a Display-P3 color from 0-1 values\n// Input: List of [r, g, b] or [r, g, b, alpha] values\n\nvariable color_values: List = {input};\nvariable output: Color.P3;\n\noutput.r = color_values.get(0);\noutput.g = color_values.get(1);\noutput.b = color_values.get(2);\n\n// Set alpha if provided as 4th parameter\nif (color_values.length() > 3) [\n output.alpha = color_values.get(3);\n];\n\nreturn output;"
}
}
],
"conversions": [
{
"source": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/p3-linear-color/0/",
"target": "$self",
"description": "Converts Linear P3 to P3 by applying sRGB transfer function",
"lossless": true,
"script": {
"type": "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
"script": "// Linear P3 to P3 Conversion\n// Applies sRGB transfer function (gamma encoding)\n// P3 uses the same transfer function as sRGB\n// Reference: CSS Color Level 4\n//\n// Algorithm (same as sRGB):\n// if linear ≤ 0.0031308: encoded = 12.92 × linear\n// else: encoded = 1.055 × linear^(1/2.4) - 0.055\n//\n// Input: Color.LinearP3 with linear r, g, b values\n// Output: Color.P3 with gamma-encoded r, g, b values\n\n// Transfer function constants (same as sRGB)\nvariable threshold: Number = 0.0031308;\nvariable linear_scale: Number = 12.92;\nvariable gamma_scale: Number = 1.055;\nvariable gamma_offset: Number = 0.055;\nvariable gamma_exponent: Number = 0.4166666666666667;\n\n// Get linear values\nvariable linear_r: Number = {input}.r;\nvariable linear_g: Number = {input}.g;\nvariable linear_b: Number = {input}.b;\n\n// Convert red channel\nvariable encoded_r: Number = 0;\nif (linear_r <= threshold) [\n encoded_r = linear_scale * linear_r;\n] else [\n encoded_r = gamma_scale * pow(linear_r, gamma_exponent) - gamma_offset;\n];\n\n// Convert green channel\nvariable encoded_g: Number = 0;\nif (linear_g <= threshold) [\n encoded_g = linear_scale * linear_g;\n] else [\n encoded_g = gamma_scale * pow(linear_g, gamma_exponent) - gamma_offset;\n];\n\n// Convert blue channel\nvariable encoded_b: Number = 0;\nif (linear_b <= threshold) [\n encoded_b = linear_scale * linear_b;\n] else [\n encoded_b = gamma_scale * pow(linear_b, gamma_exponent) - gamma_offset;\n];\n\n// Create output\nvariable output: Color.P3;\noutput.r = encoded_r;\noutput.g = encoded_g;\noutput.b = encoded_b;\n\nreturn output;"
}
}
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/function/lighten/0/",
schema: {
@@ -1465,165 +1424,6 @@ export const SCHEMAS = [
]
}
},
{
uri: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/constants/css-hex-colors/0/",
schema: {
"name": "CSS Hex Colors",
"type": "constants",
"description": "CSS named colors mapped to their hex values (CSS Color Level 4)",
"inline": true,
"values": {
"aliceblue": "#F0F8FF",
"antiquewhite": "#FAEBD7",
"aqua": "#00FFFF",
"aquamarine": "#7FFFD4",
"azure": "#F0FFFF",
"beige": "#F5F5DC",
"bisque": "#FFE4C4",
"black": "#000000",
"blanchedalmond": "#FFEBCD",
"blue": "#0000FF",
"blueviolet": "#8A2BE2",
"brown": "#A52A2A",
"burlywood": "#DEB887",
"cadetblue": "#5F9EA0",
"chartreuse": "#7FFF00",
"chocolate": "#D2691E",
"coral": "#FF7F50",
"cornflowerblue": "#6495ED",
"cornsilk": "#FFF8DC",
"crimson": "#DC143C",
"cyan": "#00FFFF",
"darkblue": "#00008B",
"darkcyan": "#008B8B",
"darkgoldenrod": "#B8860B",
"darkgray": "#A9A9A9",
"darkgreen": "#006400",
"darkgrey": "#A9A9A9",
"darkkhaki": "#BDB76B",
"darkmagenta": "#8B008B",
"darkolivegreen": "#556B2F",
"darkorange": "#FF8C00",
"darkorchid": "#9932CC",
"darkred": "#8B0000",
"darksalmon": "#E9967A",
"darkseagreen": "#8FBC8F",
"darkslateblue": "#483D8B",
"darkslategray": "#2F4F4F",
"darkslategrey": "#2F4F4F",
"darkturquoise": "#00CED1",
"darkviolet": "#9400D3",
"deeppink": "#FF1493",
"deepskyblue": "#00BFFF",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1E90FF",
"firebrick": "#B22222",
"floralwhite": "#FFFAF0",
"forestgreen": "#228B22",
"fuchsia": "#FF00FF",
"gainsboro": "#DCDCDC",
"ghostwhite": "#F8F8FF",
"gold": "#FFD700",
"goldenrod": "#DAA520",
"gray": "#808080",
"green": "#008000",
"greenyellow": "#ADFF2F",
"grey": "#808080",
"honeydew": "#F0FFF0",
"hotpink": "#FF69B4",
"indianred": "#CD5C5C",
"indigo": "#4B0082",
"ivory": "#FFFFF0",
"khaki": "#F0E68C",
"lavender": "#E6E6FA",
"lavenderblush": "#FFF0F5",
"lawngreen": "#7CFC00",
"lemonchiffon": "#FFFACD",
"lightblue": "#ADD8E6",
"lightcoral": "#F08080",
"lightcyan": "#E0FFFF",
"lightgoldenrodyellow": "#FAFAD2",
"lightgray": "#D3D3D3",
"lightgreen": "#90EE90",
"lightgrey": "#D3D3D3",
"lightpink": "#FFB6C1",
"lightsalmon": "#FFA07A",
"lightseagreen": "#20B2AA",
"lightskyblue": "#87CEFA",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#B0C4DE",
"lightyellow": "#FFFFE0",
"lime": "#00FF00",
"limegreen": "#32CD32",
"linen": "#FAF0E6",
"magenta": "#FF00FF",
"maroon": "#800000",
"mediumaquamarine": "#66CDAA",
"mediumblue": "#0000CD",
"mediumorchid": "#BA55D3",
"mediumpurple": "#9370DB",
"mediumseagreen": "#3CB371",
"mediumslateblue": "#7B68EE",
"mediumspringgreen": "#00FA9A",
"mediumturquoise": "#48D1CC",
"mediumvioletred": "#C71585",
"midnightblue": "#191970",
"mintcream": "#F5FFFA",
"mistyrose": "#FFE4E1",
"moccasin": "#FFE4B5",
"navajowhite": "#FFDEAD",
"navy": "#000080",
"oldlace": "#FDF5E6",
"olive": "#808000",
"olivedrab": "#6B8E23",
"orange": "#FFA500",
"orangered": "#FF4500",
"orchid": "#DA70D6",
"palegoldenrod": "#EEE8AA",
"palegreen": "#98FB98",
"paleturquoise": "#AFEEEE",
"palevioletred": "#DB7093",
"papayawhip": "#FFEFD5",
"peachpuff": "#FFDAB9",
"peru": "#CD853F",
"pink": "#FFC0CB",
"plum": "#DDA0DD",
"powderblue": "#B0E0E6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#FF0000",
"rosybrown": "#BC8F8F",
"royalblue": "#4169E1",
"saddlebrown": "#8B4513",
"salmon": "#FA8072",
"sandybrown": "#F4A460",
"seagreen": "#2E8B57",
"seashell": "#FFF5EE",
"sienna": "#A0522D",
"silver": "#C0C0C0",
"skyblue": "#87CEEB",
"slateblue": "#6A5ACD",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#FFFAFA",
"springgreen": "#00FF7F",
"steelblue": "#4682B4",
"tan": "#D2B48C",
"teal": "#008080",
"thistle": "#D8BFD8",
"tomato": "#FF6347",
"turquoise": "#40E0D0",
"violet": "#EE82EE",
"wheat": "#F5DEB3",
"white": "#FFFFFF",
"whitesmoke": "#F5F5F5",
"yellow": "#FFFF00",
"yellowgreen": "#9ACD32"
}
}
},
];
export function makeConfig() {

View File

@@ -1,4 +0,0 @@
{
"presets": [],
"plugins": []
}

View File

@@ -1 +0,0 @@
dist/

View File

@@ -1,23 +0,0 @@
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/lib/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: [],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {
builder: {
viteConfigPath: 'vite.config.mts',
},
},
},
};
function getAbsolutePath(value: string): any {
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
}
export default config;

View File

View File

@@ -1,11 +0,0 @@
# UI
A React component library with TypeScript for the Penpot ecosystem.
## Commands
Run from workspace root:
- **`pnpm storybook:ui`** - Start Storybook for component development
- **`pnpm build:ui`** - Build the library for production
- **`pnpm start:ui`** - Build in watch mode for development

View File

@@ -1,39 +0,0 @@
{
"name": "@penpot/ui",
"version": "0.0.1",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js"
},
"./style.css": "./dist/style.css"
},
"scripts": {
"watch": "vite build --watch",
"build": "vite build"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@storybook/react": "10.2.0",
"@storybook/react-vite": "10.2.0",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.1",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "7.0.1",
"react-compiler-runtime": "^1.0.0",
"storybook": "10.2.0",
"vite-plugin-dts": "^4.5.4"
},
"peerDependencies": {
"react": ">=19.2",
"react-dom": ">=19.2"
}
}

View File

@@ -1 +0,0 @@
export * from './lib/example/Example';

View File

@@ -1,5 +0,0 @@
.container {
background-color: #f0f0f0;
padding: 16px;
border: 2px solid #000;
}

View File

@@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import Example from './Example';
describe('Example', () => {
it('should render successfully', () => {
const { baseElement } = render(<Example />);
expect(baseElement).toBeTruthy();
});
});

View File

@@ -1,12 +0,0 @@
import { Example } from './Example';
import type { Meta, StoryObj } from '@storybook/react-vite';
const meta = {
title: 'UI/Example',
component: Example,
} satisfies Meta<typeof Example>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};

View File

@@ -1,21 +0,0 @@
import { useState } from 'react';
import styles from './Example.module.css';
export function Example() {
const [count, setCount] = useState(0);
return (
<div className={styles.container}>
<h1>Example!</h1>
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
</div>
);
}
export default Example;

View File

@@ -1,33 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"jsx": "react-jsx",
"types": ["vite/client", "vitest"],
"baseUrl": "."
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}
]
}

View File

@@ -1,37 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"vite/client"
]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"**/*.stories.ts",
"**/*.stories.js",
"**/*.stories.jsx",
"**/*.stories.tsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}

View File

@@ -1,28 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@@ -1,28 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"outDir": "",
"module": "esnext",
"moduleResolution": "bundler"
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.jsx",
"src/**/*.test.js"
],
"include": [
"src/**/*.stories.ts",
"src/**/*.stories.js",
"src/**/*.stories.jsx",
"src/**/*.stories.tsx",
"src/**/*.stories.mdx",
".storybook/*.js",
".storybook/*.ts"
]
}

View File

@@ -1,66 +0,0 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { copyFileSync } from 'node:fs';
const copyCssPlugin = () => ({
name: 'copy-css',
closeBundle: () => {
try {
copyFileSync(
'dist/index.css',
'../../resources/public/css/ui.css',
);
} catch (e) {
console.log('Error copying css file', e);
}
},
});
export default defineConfig(() => ({
root: import.meta.dirname,
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
dts({
entryRoot: 'src',
tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json'),
pathsToAliases: false,
}),
copyCssPlugin(),
],
build: {
outDir: 'dist/',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
entry: 'src/index.ts',
name: 'ui',
fileName: 'index',
formats: ['es' as const],
},
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},
test: {
name: 'ui',
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../coverage/libs/ui',
provider: 'v8' as const,
},
},
}));

View File

@@ -1,147 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"text-editor/v2",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~ud7430f09-4f59-8049-8007-6277bb7586f6",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "test_color_blending",
"~:revn": 78,
"~:modified-at": "~m1770820738388",
"~:vern": 0,
"~:id": "~ub15901d7-d46d-8056-8007-8d5e34fc1f0c",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ud7430f09-4f59-8049-8007-6277bb765abd",
"~:created-at": "~m1770741329904",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~ub15901d7-d46d-8056-8007-8d5e34fc1f0d"
],
"~:pages-index": {
"~ub15901d7-d46d-8056-8007-8d5e34fc1f0d": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~udb80df91-a3a3-803b-8007-8e37a71c9d28\",\"~udb80df91-a3a3-803b-8007-8e384d8c53b9\",\"~udb80df91-a3a3-803b-8007-8e37c09b4084\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c4\",\"~u097859f1-ca3b-80ba-8007-8e8beb99a3f5\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c5\",\"~u097859f1-ca3b-80ba-8007-8e8bfca43303\",\"~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c6\"]]]",
"~u097859f1-ca3b-80ba-8007-8e8bfca43303": "[\"~#shape\",[\"^ \",\"~:y\",-637.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-637.000005722046]],[\"^<\",[\"^ \",\"~:x\",636.9999995231628,\"~:y\",-637.000005722046]],[\"^<\",[\"^ \",\"~:x\",636.9999995231628,\"~:y\",-337.00000858306885]],[\"^<\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-337.00000858306885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u097859f1-ca3b-80ba-8007-8e8bfca43303\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:center\",\"~:stroke-width\",10,\"~:stroke-color\",\"#4bff00\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:outer\",\"^N\",10,\"^O\",\"#333fbd\",\"^P\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",336.9999895095825,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-637.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",336.9999895095825,\"~:y1\",-637.0000057220459,\"~:x2\",636.9999995231628,\"~:y2\",-337.00000858306885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^11\",\"#ff0000\",\"^12\",1]],\"~:flip-x\",null,\"^W\",299.99999713897705,\"~:flip-y\",null]]",
"~udb80df91-a3a3-803b-8007-8e384d8c53b9": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",1321.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",1321.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37b7ddd15c\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"^@\",\"~udb80df91-a3a3-803b-8007-8e384d8c53b9\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",20,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1]],\"~:x\",1021.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",1021.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",1321.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^W\",\"#ff0000\",\"^X\",1]],\"~:flip-x\",null,\"^Q\",300.0000162124634,\"~:flip-y\",null]]",
"~udb80df91-a3a3-803b-8007-8e379b5fd50f": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",986.7500224724797,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",986.7500224724797,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",686.7500124588994,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",686.7500124588994,\"~:y1\",82.00000368146124,\"~:x2\",986.7500224724797,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^P\",\"#ff0000\",\"^Q\",1]],\"~:flip-x\",null,\"^J\",299.99999713897705,\"~:flip-y\",null]]",
"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297": "[\"~#shape\",[\"^ \",\"~:y\",81.9999960520667,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",81.99999605206669]],[\"^<\",[\"^ \",\"~:x\",637.0000301018742,\"~:y\",81.99999605206669]],[\"^<\",[\"^ \",\"~:x\",637.0000301018742,\"~:y\",381.99999319104376]],[\"^<\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",381.99999319104376]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~u432cbb09-2ee7-80bf-8007-8d660b2f52ad\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",337.0000200882939,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",81.9999960520667,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",337.0000200882939,\"~:y1\",81.9999960520667,\"~:x2\",637.0000301018742,\"~:y2\",381.99999319104376]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^T\",\"#ff0000\",\"^U\",1]],\"~:flip-x\",null,\"^N\",299.99999713897705,\"~:flip-y\",null]]",
"~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7": "[\"~#shape\",[\"^ \",\"~:y\",-629.9999999999998,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1037,\"~:y\",-630]],[\"^<\",[\"^ \",\"~:x\",1337.0000100135803,\"~:y\",-630]],[\"^<\",[\"^ \",\"~:x\",1337.0000100135803,\"~:y\",-330.0000028610228]],[\"^<\",[\"^ \",\"~:x\",1037,\"~:y\",-330.0000028610228]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:outer\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:center\",\"^N\",10,\"^O\",\"#4bff00\",\"^P\",1]],\"~:x\",1037,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1037,\"~:y\",-629.9999999999998,\"^8\",300.0000100135803,\"~:height\",299.999997138977,\"~:x1\",1037,\"~:y1\",-629.9999999999998,\"~:x2\",1337.0000100135803,\"~:y2\",-330.0000028610228]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^11\",\"#ff0000\",\"^12\",1]],\"~:flip-x\",null,\"^W\",299.999997138977,\"~:flip-y\",null]]",
"~u097859f1-ca3b-80ba-8007-8e8beb99a3f5": "[\"~#shape\",[\"^ \",\"~:y\",-626.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-626.000005722046]],[\"^<\",[\"^ \",\"~:x\",987.0000224113464,\"~:y\",-626.000005722046]],[\"^<\",[\"^ \",\"~:x\",987.0000224113464,\"~:y\",-326.00000858306885]],[\"^<\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-326.00000858306885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u097859f1-ca3b-80ba-8007-8e8beb99a3f5\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"^M\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",687.0000123977661,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-626.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",687.0000123977661,\"~:y1\",-626.0000057220459,\"~:x2\",987.0000224113464,\"~:y2\",-326.00000858306885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]",
"~udb80df91-a3a3-803b-8007-8e37a71c9d28": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",637.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",637.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37b7ddd15c\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"^@\",\"~udb80df91-a3a3-803b-8007-8e37a71c9d28\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",337.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",337.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",637.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^P\",\"#ff0000\",\"^Q\",1]],\"~:flip-x\",null,\"^J\",300.0000162124634,\"~:flip-y\",null]]",
"~u18522c44-655d-8050-8007-8e89f4bdc0c5": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",637.000030040741,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",637.000030040741,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c5\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:outer\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",337.00002002716064,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",337.00002002716064,\"~:y1\",-287.0000057220459,\"~:x2\",637.000030040741,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^10\",\"#ff0000\",\"^11\",1]],\"~:flip-x\",null,\"^V\",299.99999713897705,\"~:flip-y\",null]]",
"~udb80df91-a3a3-803b-8007-8e37c09b4084": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",979.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",979.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37c09b4084\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",679.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",679.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",979.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^L\",\"#ff0000\",\"^M\",1]],\"~:flip-x\",null,\"^F\",300.0000162124634,\"~:flip-y\",null]]",
"~u18522c44-655d-8050-8007-8e89f4bdc0c4": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",986.7500224113464,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",986.7500224113464,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",0.5],[\"^ \",\"^J\",\"^K\",\"^L\",\"^M\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",686.7500123977661,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",686.7500123977661,\"~:y1\",-287.0000057220459,\"~:x2\",986.7500224113464,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]",
"~udb80df91-a3a3-803b-8007-8e38034ff7c8": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1336.5000148430852,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1336.5000148430852,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1]],\"~:x\",1036.5000048295049,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",1036.5000048295049,\"~:y1\",82.00000368146124,\"~:x2\",1336.5000148430852,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]",
"~u18522c44-655d-8050-8007-8e89f4bdc0c6": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",1336.500014781952,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",1336.500014781952,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c6\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:outer\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",1036.5000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",1036.5000047683716,\"~:y1\",-287.0000057220459,\"~:x2\",1336.500014781952,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^10\",\"#ff0000\",\"^11\",1]],\"~:flip-x\",null,\"^V\",299.99999713897705,\"~:flip-y\",null]]"
}
},
"~:id": "~ub15901d7-d46d-8056-8007-8d5e34fc1f0d",
"~:name": "Page 1"
}
},
"~:id": "~ub15901d7-d46d-8056-8007-8d5e34fc1f0c",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ export const WASM_FLAGS = [
export class WasmWorkspacePage extends WorkspacePage {
static async init(page) {
await super.init(page);
await WasmWorkspacePage.mockConfigFlags(page, WASM_FLAGS);
await WorkspacePage.mockConfigFlags(page, WASM_FLAGS);
await page.addInitScript(() => {
document.addEventListener("penpot:wasm:loaded", () => {
@@ -27,14 +27,6 @@ export class WasmWorkspacePage extends WorkspacePage {
});
}
static async mockConfigFlags(page, flags) {
await super.mockConfigFlags(page, [...WASM_FLAGS, ...flags]);
}
async mockConfigFlags(flags) {
return WasmWorkspacePage.mockConfigFlags(this.page, flags);
}
constructor(page) {
super(page);
this.canvas = page.getByTestId("canvas-wasm-shapes");

View File

@@ -165,7 +165,6 @@ test("Updates canvas background", async ({ page }) => {
});
await canvasBackgroundInput.fill("FABADA");
await workspace.page.keyboard.press("Enter");
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});
@@ -197,7 +196,7 @@ test("Renders a file with blurs applied to any kind of shape", async ({
test("Renders a file with shadows applied to any kind of shape", async ({
page,
}) => {
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-shadows.json");
@@ -291,24 +290,6 @@ test("Renders a file with nested clipping frames", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders clipped frames with strokes correctly (no double painting)", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-frame-strokes-opacity.json",
);
await workspace.goToWorkspace({
id: "3144ac7c-a5cc-80e8-8007-8bbb29a4e56e",
pageId: "3144ac7c-a5cc-80e8-8007-8bbb29a510ac",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a clipped frame with a large blur drop shadow", async ({
page,
}) => {
@@ -324,35 +305,3 @@ test("Renders a clipped frame with a large blur drop shadow", async ({
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with solid, dotted, dashed and mixed stroke styles", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-stroke-styles.json");
await workspace.goToWorkspace({
id: "b888b894-3697-80d3-8006-51cc8a55c200",
pageId: "b888b894-3697-80d3-8006-51cc8a55c210",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders shapes with multiple fills and blur", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-fill-blend-blurs.json");
await workspace.goToWorkspace({
id: "b15901d7-d46d-8056-8007-8d5e34fc1f0c",
pageId: "b15901d7-d46d-8056-8007-8d5e34fc1f0d",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 360 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 124 KiB

3866
frontend/pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -8,4 +8,3 @@ packages:
- "packages/mousetrap"
- "packages/tokenscript"
- "text-editor"
- "packages/ui"

View File

@@ -18,7 +18,6 @@
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}}

View File

@@ -4,6 +4,8 @@
set -ex
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
export INCLUDE_WASM=${BUILD_WASM:-yes};
export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
export BUILD_DATE=$(date -R);
@@ -16,8 +18,6 @@ export VERSION_TAG="${VERSION}-${BUILD_TS}";
# performant code on macros (example: rumext)
export NODE_ENV=production;
rm -rf node_modules;
corepack enable;
corepack install;
pnpm install;
@@ -28,17 +28,10 @@ rm -rf resources/public;
mkdir -p resources/public;
mkdir -p target/dist;
# Build render wasm binary
pushd ../render-wasm;
./build
popd
pushd ../mcp;
rm -rf node_modules;
./scripts/setup
WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build:multi-user
popd;
pnpm run build:app:main $EXTRA_PARAMS;
pnpm run build:app:libs;
pnpm run build:app:assets;
@@ -47,6 +40,8 @@ sed -i "s/\.\/render.js/.\/render.js?version=$VERSION_TAG/g" resources/public/js
rsync -avr resources/public/ target/dist/
# Include the MCP plugin on the bundle
mkdir -p target/dist/plugins/mcp/;
rsync -avr ../mcp/packages/plugin/dist/ target/dist/plugins/mcp/
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook
pnpm run build:storybook || exit 1;
rsync -avr storybook-static/ target/dist/storybook-static;
fi

View File

@@ -1,20 +1,6 @@
import * as esbuild from "esbuild";
import { readFile } from "node:fs/promises";
/**
* esbuild plugin to watch a directory recursively
*/
const watchExtraDirPlugin = {
name: 'watch-extra-dir',
setup(build) {
build.onLoad({ filter: /target\/index.js/, namespace: 'file' }, async (args) => {
return {
watchDirs: ["packages/ui/dist"],
};
});
}
};
const filter =
/react-virtualized[/\\]dist[/\\]es[/\\]WindowScroller[/\\]utils[/\\]onScroll\.js$/;
@@ -50,7 +36,7 @@ const config = {
js: '"use strict";\nvar global = globalThis;',
},
outfile: "resources/public/js/libs.js",
plugins: [fixReactVirtualized, rebuildNotify, watchExtraDirPlugin],
plugins: [fixReactVirtualized, rebuildNotify],
};
async function watch() {

View File

@@ -13,7 +13,7 @@ export VERSION_TAG="${VERSION}-${BUILD_TS}";
export NODE_ENV=production;
corepack enable;
corepack install;
pnpm install;
corepack install || exit 1;
pnpm install || exit 1;
pnpm run build:storybook;
pnpm run build:storybook || exit 1;

View File

@@ -8,7 +8,7 @@
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
@@ -85,7 +85,7 @@
[value]
(let [number? (or (number? value)
(numeric-string? value))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
@@ -111,7 +111,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 (cfo/parse-token-value value)
(let [parsed-value (cft/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))
@@ -129,7 +129,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 (cfo/parse-token-value value)
parsed-value (cft/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))
@@ -153,7 +153,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 (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-scope (< (:value parsed-value) 0)]
(cond
(and parsed-value (not out-of-scope))
@@ -251,7 +251,7 @@
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
(case unit
"%" (/ value 100)
"px" (/ value font-size-value)

View File

@@ -32,7 +32,6 @@
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.router :as rt]
[app.render-wasm.api :as wasm.api]
[app.util.text-editor :as ted]
[app.util.text.content.styles :as styles]
[app.util.timers :as ts]
@@ -47,7 +46,6 @@
(def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)
(def ^function is-empty? editor.v2/isEmpty)
(def ^function dispose! editor.v2/dispose)
(declare v2-update-text-shape-content)
@@ -509,12 +507,12 @@
ptk/EffectEvent
(effect [_ state _]
(when (features/active-feature? state "text-editor/v2")
(when-let [instance (:workspace-editor state)]
(let [styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration :removed-mixed true)
((comp update-node-fn migrate-node))
(styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles))))))))
(let [instance (:workspace-editor state)
styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration :removed-mixed true)
((comp update-node-fn migrate-node))
(styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles)))))))
;; --- RESIZE UTILS
@@ -778,30 +776,17 @@
(rx/of (v2-update-text-editor-styles id attrs)))
(when (features/active-feature? state "render-wasm/v1")
(rx/concat
;; Apply style to selected spans and sync content
(when (wasm.api/text-editor-is-active?)
(let [span-attrs (select-keys attrs txt/text-node-attrs)]
(when (not (empty? span-attrs))
(let [result (wasm.api/apply-style-to-selection span-attrs)]
(when result
(rx/of (v2-update-text-shape-content
(:shape-id result) (:content result)
:update-name? true)))))))
;; Resize (with delay for font-id changes)
(cond->> (rx/of (dwwt/resize-wasm-text id))
(contains? attrs :font-id)
(rx/delay 200))))))))
(rx/of (dwwt/resize-wasm-text-debounce id)))))))
ptk/EffectEvent
(effect [_ state _]
(when (features/active-feature? state "text-editor/v2")
(when-let [instance (:workspace-editor state)]
(let [attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration))
overriden-attrs (merge attrs-to-override attrs)
styles (styles/attrs->styles overriden-attrs)]
(editor.v2/applyStylesToSelection instance styles)))))))
(let [instance (:workspace-editor state)
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration))
overriden-attrs (merge attrs-to-override attrs)
styles (styles/attrs->styles overriden-attrs)]
(editor.v2/applyStylesToSelection instance styles))))))
(defn update-all-attrs
[ids attrs]
@@ -916,22 +901,15 @@
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
(defn v2-update-text-shape-content
[id content & {:keys [update-name? name finalize? save-undo?]
:or {update-name? false name nil finalize? false save-undo? true}}]
[id content & {:keys [update-name? name finalize?]
:or {update-name? false name nil finalize? false}}]
(ptk/reify ::v2-update-text-shape-content
ptk/WatchEvent
(watch [_ state _]
(if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)
new-shape? (nil? (:content shape))
prev-content (:content shape)
has-prev-content? (not (nil? (:prev-content shape)))
has-content? (when-not new-shape?
(v2-content-has-text? content))
did-has-content? (when-not new-shape?
(v2-content-has-text? prev-content))]
new-shape? (nil? (:content shape))]
(rx/concat
(rx/of
(dwsh/update-shapes
@@ -939,16 +917,10 @@
(fn [shape]
(let [new-shape (-> shape
(assoc :content content)
(cond-> (and has-content?
has-prev-content?)
(dissoc :prev-content))
(cond-> (and did-has-content?
(not has-content?))
(assoc :prev-content prev-content))
(cond-> (and update-name? (some? name))
(assoc :name name)))]
new-shape))
{:save-undo? save-undo? :undo-group (when new-shape? id)})
{:undo-group (when new-shape? id)})
(if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers
@@ -961,16 +933,8 @@
(when finalize?
(rx/concat
(when (and (not has-content?) (some? id))
(when (and (not (v2-content-has-text? content)) (some? id))
(rx/of
(when has-prev-content?
(dwsh/update-shapes
[id]
(fn [shape]
(let [new-shape (-> shape
(assoc :content (:prev-content shape)))]
new-shape))
{:save-undo? false}))
(dws/deselect-shape id)
(dwsh/delete-shapes #{id})))
(rx/of (dwt/finish-transform))))))

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.tokens.application
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
@@ -648,11 +648,11 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
tokenized-attributes (cfo/attributes-map attributes token)
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
@@ -711,7 +711,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token-name %))]
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token-name %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
@@ -740,7 +740,7 @@
(get token-properties (:type token))
unapply-tokens?
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.config :as cf]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]))
@@ -22,5 +22,5 @@
(if (contains? cf/flags :tokenscript)
(when (and resolved-value (ts/color-symbol? resolved-value))
(ts/color-symbol->penpot-color resolved-value))
(when (and resolved-value (cfo/color-token? token))
(when (and resolved-value (cft/color-token? token))
(color-bullet-color resolved-value))))

View File

@@ -195,30 +195,27 @@
(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/WatchEvent
(watch [it state _]
(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))))))
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
(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))))))
(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))))))))
(defn rename-token-set-group
[set-group-path set-group-fname]
@@ -230,6 +227,26 @@
(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
@@ -505,7 +522,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) ;; TODO: add function duplicate-token in tokens-lib
unames (map :name tokens)
suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token

View File

@@ -409,7 +409,7 @@
modif-tree (dwm/build-modif-tree ids objects get-modifier)]
(if (features/active-feature? state "render-wasm/v1")
(rx/of (dwm/apply-wasm-modifiers modif-tree (assoc options :ignore-snap-pixel true)))
(rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true}))
(let [modif-tree (gm/set-objects-modifiers modif-tree objects)]
(rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))))
@@ -621,7 +621,7 @@
(->> stream
(rx/filter (ptk/type? ::dws/duplicate-selected))
(rx/take 1)
(rx/map #(start-move from-position nil true))))))
(rx/map #(start-move from-position))))))
(defn get-drop-cell
[target-frame objects position]
@@ -641,9 +641,8 @@
(dom/set-property! node "transform" (gmt/translate-matrix move-vector))))))
(defn start-move
([from-position] (start-move from-position nil false))
([from-position ids] (start-move from-position ids false))
([from-position ids from-duplicate?]
([from-position] (start-move from-position nil))
([from-position ids]
(ptk/reify ::start-move
ptk/UpdateEvent
(update [_ state]
@@ -751,47 +750,38 @@
(rx/share))]
(if (features/active-feature? state "render-wasm/v1")
(let [duplicate-stopper
(->> ms/mouse-position-alt
(rx/mapcat
(fn [alt?]
(if (and alt? (not from-duplicate?))
(rx/of true)
(rx/empty)))))]
(rx/merge
(->> modifiers-stream
(rx/take-until duplicate-stopper)
(rx/map
(fn [[modifiers snap-ignore-axis]]
(dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis))))
(rx/merge
(->> modifiers-stream
(rx/map
(fn [[modifiers snap-ignore-axis]]
(dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis))))
(->> move-stream
(rx/with-latest-from ms/mouse-position-alt)
(rx/filter (fn [[_ alt?]] alt?))
(rx/take 1)
(rx/mapcat
(fn [[_ alt?]]
(if (and (not from-duplicate?) alt?)
(rx/of (start-move-duplicate from-position)
(dws/duplicate-selected false true))
(rx/empty)))))
(->> move-stream
(rx/with-latest-from ms/mouse-position-alt)
(rx/filter (fn [[_ alt?]] alt?))
(rx/take 1)
(rx/mapcat
(fn [[_ alt?]]
(if (and (not duplicate-move-started?) alt?)
(rx/of (start-move-duplicate from-position)
(dws/duplicate-selected false true))
(rx/empty)))))
;; Last event will write the modifiers creating the changes
(->> move-stream
(rx/last)
(rx/take-until duplicate-stopper)
(rx/with-latest-from modifiers-stream)
(rx/mapcat
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]]
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwm/apply-wasm-modifiers modifiers
:snap-ignore-axis snap-ignore-axis
:undo-transation? false)
(move-shapes-to-frame ids target-frame drop-index drop-cell)
(finish-transform)
(dwu/commit-undo-transaction undo-id))))))))
;; Last event will write the modifiers creating the changes
(->> move-stream
(rx/last)
(rx/with-latest-from modifiers-stream)
(rx/mapcat
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]]
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwm/apply-wasm-modifiers modifiers
:snap-ignore-axis snap-ignore-axis
:undo-transation? false)
(move-shapes-to-frame ids target-frame drop-index drop-cell)
(finish-transform)
(dwu/commit-undo-transaction undo-id)))))))
(rx/merge
(->> modifiers-stream

View File

@@ -183,9 +183,6 @@
[id]
(l/derived #(contains? % id) selected-shapes))
(def highlighted-shapes
(l/derived :highlighted workspace-local))
(def export-in-progress?
(l/derived :export-in-progress? export))

View File

@@ -49,6 +49,7 @@
(mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}]
(let [form (mf/use-ctx context)
disabled? (or (and (some? form)
(or (not (:valid @form))

View File

@@ -12,7 +12,7 @@
[app.common.types.component :as ctk]
[app.main.data.viewer :as dv]
[app.main.store :as st]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner*]]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[okulary.core :as l]
@@ -26,7 +26,6 @@
(mf/defc layer-item
[{:keys [item selected objects depth component-child? hide-toggle?] :as props}]
(let [id (:id item)
hidden? (:hidden item)
selected? (contains? selected id)
item-ref (mf/use-ref nil)
depth (+ depth 1)
@@ -68,18 +67,17 @@
(when (and (= (count selected) 1) selected?)
(dom/scroll-into-view-if-needed! (mf/ref-val item-ref) true))))
[:& layer-item-inner
[:> layer-item-inner*
{:ref item-ref
:item item
:depth depth
:read-only? true
:highlighted? false
:selected? selected?
:component-tree? component-tree?
:hidden? hidden?
:filtered? false
:expanded? expanded?
:hide-toggle? hide-toggle?
:is-read-only true
:is-highlighted false
:is-selected selected?
:is-component-tree component-tree?
:is-filtered false
:is-expanded expanded?
:hide-toggle hide-toggle?
:on-select-shape select-shape
:on-toggle-collapse toggle-collapse}

View File

@@ -11,7 +11,6 @@
[app.common.math :as mth]
[app.common.types.color :as cc]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
(defn parse-hex
@@ -68,60 +67,34 @@
(when (some? val)
(setup-hex-color val))))
apply-property-change
(fn [property val]
(let [val (case property
:s (/ val 100)
:v (value->hsv-value val)
:alpha (/ val 100)
val)]
(cond
(= property :alpha)
(on-change {:alpha val})
(#{:r :g :b} property)
(let [{:keys [r g b]} (merge color (hash-map property val))
hex (cc/rgb->hex [r g b])
[h s v] (cc/hex->hsv hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b}))
:else
(let [{:keys [h s v]} (merge color (hash-map property val))
hex (cc/hsv->hex [h s v])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b})))))
on-change-property
(fn [property max-value]
(fn [e]
(let [val (-> e dom/get-target-val d/parse-double (mth/clamp 0 max-value))]
(when (some? val)
(apply-property-change property val)))))
(let [val (-> e dom/get-target-val d/parse-double (mth/clamp 0 max-value))
val (case property
:s (/ val 100)
:v (value->hsv-value val)
val)]
(when (not (nil? val))
(if (#{:r :g :b} property)
(let [{:keys [r g b]} (merge color (hash-map property val))
hex (cc/rgb->hex [r g b])
[h s v] (cc/hex->hsv hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b}))
on-key-down-step
(fn [max-value on-step]
(fn [e]
(let [up? (kbd/up-arrow? e)
down? (kbd/down-arrow? e)]
(when (and (or up? down?)
(or (kbd/shift? e) (kbd/alt? e)))
(dom/prevent-default e)
(when-let [current-value (-> e dom/get-target-val d/parse-double)]
(let [step (cond
(kbd/shift? e) (if up? 10 -10)
(kbd/alt? e) (if up? 0.1 -0.1))
new-value (mth/clamp (+ current-value step) 0 max-value)
node (dom/get-target e)]
(dom/set-value! node new-value)
(on-step new-value)))))))
(let [{:keys [h s v]} (merge color (hash-map property val))
hex (cc/hsv->hex [h s v])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b})))))))
on-key-down-property
(fn [property max-value]
(on-key-down-step max-value #(apply-property-change property %)))]
on-change-opacity
(fn [e]
(when-let [new-alpha (-> e dom/get-target-val (mth/clamp 0 100) (/ 100))]
(on-change {:alpha new-alpha})))]
;; Updates the inputs values when a property is changed in the parent
@@ -154,8 +127,7 @@
:min 0
:max 255
:default-value red
:on-change (on-change-property :r 255)
:on-key-down (on-key-down-property :r 255)}]]
:on-change (on-change-property :r 255)}]]
[:div {:class (stl/css :input-wrapper)}
[:label {:for "green-value" :class (stl/css :input-label)} "G"]
[:input {:id "green-value"
@@ -164,8 +136,7 @@
:min 0
:max 255
:default-value green
:on-change (on-change-property :g 255)
:on-key-down (on-key-down-property :g 255)}]]
:on-change (on-change-property :g 255)}]]
[:div {:class (stl/css :input-wrapper)}
[:label {:for "blue-value" :class (stl/css :input-label)} "B"]
[:input {:id "blue-value"
@@ -174,8 +145,7 @@
:min 0
:max 255
:default-value blue
:on-change (on-change-property :b 255)
:on-key-down (on-key-down-property :b 255)}]]]
:on-change (on-change-property :b 255)}]]]
[:*
[:div {:class (stl/css :input-wrapper)}
@@ -186,8 +156,7 @@
:min 0
:max 360
:default-value hue
:on-change (on-change-property :h 360)
:on-key-down (on-key-down-property :h 360)}]]
:on-change (on-change-property :h 360)}]]
[:div {:class (stl/css :input-wrapper)}
[:label {:for "saturation-value" :class (stl/css :input-label)} "S"]
[:input {:id "saturation-value"
@@ -197,8 +166,7 @@
:max 100
:step 1
:default-value saturation
:on-change (on-change-property :s 100)
:on-key-down (on-key-down-property :s 100)}]]
:on-change (on-change-property :s 100)}]]
[:div {:class (stl/css :input-wrapper)}
[:label {:for "value-value" :class (stl/css :input-label)} "V"]
[:input {:id "value-value"
@@ -207,8 +175,7 @@
:min 0
:max 100
:default-value value
:on-change (on-change-property :v 100)
:on-key-down (on-key-down-property :v 100)}]]])]
:on-change (on-change-property :v 100)}]]])]
[:div {:class (stl/css :hex-alpha-wrapper)}
[:div {:class (stl/css-case :input-wrapper true
:hex true)}
@@ -228,5 +195,4 @@
:step 1
:max 100
:default-value (if (= alpha :multiple) "" alpha)
:on-change (on-change-property :alpha 100)
:on-key-down (on-key-down-property :alpha 100)}]])]]))
:on-change on-change-opacity}]])]]))

View File

@@ -117,8 +117,7 @@
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? update-name?
:name generated-name
:finalize? true
:save-undo? false))))
:finalize? true))))
(let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 0)))
@@ -136,21 +135,15 @@
on-needs-layout
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? false)))
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true)))
;; FIXME: We need to find a better way to trigger layout changes.
#_(st/emit!
(dwt/v2-update-text-shape-position-data shape-id [])))
on-change
(fn []
(let [is-empty? (dwt/is-empty? instance)
save-undo? (not is-empty?)]
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? save-undo?)))))
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true))))
on-clipboard-change
(fn [event]
@@ -254,16 +247,16 @@
:ref container-ref
:data-testid "text-editor-container"
:style {:width "var(--editor-container-width)"
:height "var(--editor-container-height)"}}
;; We hide the editor when is blurred because otherwise the
;; selection won't let us see the underlying text. Use opacity
;; because display or visibility won't allow to recover focus
;; afterwards.
;; IMPORTANT! This is now done through DOM mutations (see
;; on-blur and on-focus) but I keep this for future references.
;; :opacity (when @blurred 0)}}
:height "var(--editor-container-height)"}
;; We hide the editor when is blurred because otherwise the
;; selection won't let us see the underlying text. Use opacity
;; because display or visibility won't allow to recover focus
;; afterwards.
;; IMPORTANT! This is now done through DOM mutations (see
;; on-blur and on-focus) but I keep this for future references.
;; :opacity (when @blurred 0)}}
}
[:div
{:class (dm/str
"mousetrap "
@@ -352,9 +345,11 @@
max-height (max height selrect-height)
valign (-> shape :content :vertical-align)
y (:y selrect)
y (case valign
"bottom" (+ y (- selrect-height height))
"center" (+ y (/ (- selrect-height height) 2))
y (if (and valign (> height selrect-height))
(case valign
"bottom" (- y (- height selrect-height))
"center" (- y (/ (- height selrect-height) 2))
y)
y)]
[(assoc selrect :y y :width max-width :height max-height) transform])

View File

@@ -29,23 +29,6 @@
color: transparent;
// Match Skia's text layout precision: prevent browser text-size
// adjustments and ensure consistent kerning across browsers.
text-size-adjust: none;
-webkit-text-size-adjust: none;
font-kerning: normal;
&::selection,
*::selection {
color: transparent;
-webkit-text-fill-color: transparent; // WebKit/Safari
}
&::-moz-selection,
*::-moz-selection {
color: transparent;
}
[data-itype="paragraph"] {
line-height: inherit;
user-select: text;

View File

@@ -10,11 +10,13 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.math :as mth]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.common.weak :as weak]
[app.main.data.workspace :as dw]
[app.main.data.workspace.collapse :as dwc]
[app.main.refs :as refs]
@@ -37,6 +39,8 @@
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
(defonce ^:private sidebar-hover-pending? (atom false))
(def ^:const default-chunk-size 50)
(defn- schedule-sidebar-hover-flush []
(when (compare-and-set! sidebar-hover-pending? false true)
(ts/raf
@@ -48,12 +52,11 @@
(when (seq enter)
(apply st/emit! (map dw/highlight-shape enter))))))))
(mf/defc layer-item-inner
{::mf/wrap-props false}
[{:keys [item depth parent-size name-ref children ref style
(mf/defc layer-item-inner*
[{:keys [item depth parent-size name-ref children ref style rename-id
;; Flags
read-only? highlighted? selected? component-tree?
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
is-read-only is-highlighted is-selected is-component-tree
is-filtered is-expanded dnd-over dnd-over-top dnd-over-bot hide-toggle
;; Callbacks
on-select-shape on-context-menu on-pointer-enter on-pointer-leave on-zoom-to-selected
on-toggle-collapse on-enable-drag on-disable-drag on-toggle-visibility on-toggle-blocking]}]
@@ -64,7 +67,7 @@
hidden? (:hidden item)
has-shapes? (-> item :shapes seq boolean)
touched? (-> item :touched seq boolean)
parent-board? (and (cfh/frame-shape? item)
root-board? (and (cfh/frame-shape? item)
(= uuid/zero (:parent-id item)))
absolute? (ctl/item-absolute? item)
is-variant? (ctk/is-variant? item)
@@ -73,9 +76,11 @@
variant-name (when is-variant? (:variant-name item))
variant-error (when is-variant? (:variant-error item))
data (deref refs/workspace-data)
component (ctkl/get-component data (:component-id item))
variant-properties (:variant-properties component)
component-id (get item :component-id)
data (mf/deref refs/workspace-data)
variant-properties (-> (ctkl/get-component data component-id)
(get :variant-properties))
icon-shape (usi/get-shape-icon item)]
[:*
@@ -86,27 +91,27 @@
:data-testid "layer-row"
:class (stl/css-case
:layer-row true
:highlight highlighted?
:highlight is-highlighted
:component (ctk/instance-head? item)
:masked (:masked-group item)
:selected selected?
:selected is-selected
:type-frame (cfh/frame-shape? item)
:type-bool (cfh/bool-shape? item)
:type-comp (or component-tree? is-variant-container?)
:type-comp (or is-component-tree is-variant-container?)
:hidden hidden?
:dnd-over dnd-over?
:dnd-over-top dnd-over-top?
:dnd-over-bot dnd-over-bot?
:root-board parent-board?)
:dnd-over dnd-over
:dnd-over-top dnd-over-top
:dnd-over-bot dnd-over-bot
:root-board root-board?)
:style style}
[:span {:class (stl/css-case
:tab-indentation true
:filtered filtered?)
:filtered is-filtered)
:style {"--depth" depth}}]
[:div {:class (stl/css-case
:element-list-body true
:filtered filtered?
:selected selected?
:filtered is-filtered
:selected is-selected
:icon-layer (= (:type item) :icon))
:style {"--depth" depth}
:on-pointer-enter on-pointer-enter
@@ -115,12 +120,12 @@
(if (< 0 (count (:shapes item)))
[:div {:class (stl/css :button-content)}
(when (and (not hide-toggle?) (not filtered?))
(when (and (not hide-toggle) (not is-filtered))
[:button {:class (stl/css-case
:toggle-content true
:inverse expanded?)
:inverse is-expanded)
:data-testid "toggle-content"
:aria-expanded expanded?
:aria-expanded is-expanded
:on-click on-toggle-collapse}
deprecated-icon/arrow])
@@ -131,7 +136,7 @@
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]]
[:div {:class (stl/css :button-content)}
(when (not ^boolean filtered?)
(when (not ^boolean is-filtered)
[:span {:class (stl/css :toggle-content)}])
[:div {:class (stl/css :icon-shape)
:on-double-click on-zoom-to-selected}
@@ -140,25 +145,26 @@
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]])
[:> layer-name* {:ref name-ref
:rename-id rename-id
:shape-id id
:shape-name name
:is-shape-touched touched?
:disabled-double-click read-only?
:disabled-double-click is-read-only
:on-start-edit on-disable-drag
:on-stop-edit on-enable-drag
:depth depth
:is-blocked blocked?
:parent-size parent-size
:is-selected selected?
:type-comp (or component-tree? is-variant-container?)
:is-selected is-selected
:type-comp (or is-component-tree is-variant-container?)
:type-frame (cfh/frame-shape? item)
:variant-id variant-id
:variant-name variant-name
:variant-properties variant-properties
:variant-error variant-error
:component-id (:id component)
:component-id component-id
:is-hidden hidden?}]]
(when (not read-only?)
(when (not ^boolean is-read-only)
[:div {:class (stl/css-case
:element-actions true
:is-parent has-shapes?
@@ -183,41 +189,86 @@
children]))
;; Memoized for performance
(mf/defc layer-item
{::mf/props :obj
::mf/wrap [mf/memo]}
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
:or {render-children? true}}]
(let [id (:id item)
blocked? (:blocked item)
hidden? (:hidden item)
(mf/defc layer-item*
{::mf/wrap [mf/memo]}
[{:keys [index item selected objects rename-id
is-sortable is-filtered depth is-component-child
highlighted style render-children parent-size]
:or {render-children true}}]
(let [id (get item :id)
blocked? (get item :blocked)
hidden? (get item :hidden)
shapes (get item :shapes)
shapes (mf/with-memo [shapes objects]
(loop [counter 0
shapes (seq shapes)
result (list)]
(if-let [id (first shapes)]
(if-let [obj (get objects id)]
(do
;; NOTE: this is a bit hacky, but reduces substantially
;; the allocation; If we use enumeration, we allocate
;; new sequence and add one iteration on each render,
;; independently if objects are changed or not. If we
;; store counter on metadata, we still need to create a
;; new allocation for each shape; with this method we
;; bypass this by mutating a private property on the
;; object removing extra allocation and extra iteration
;; on every request.
(unchecked-set obj "__$__counter" counter)
(recur (inc counter)
(rest shapes)
(conj result obj)))
(recur (inc counter)
(rest shapes)
result))
(-> result vec not-empty))))
drag-disabled* (mf/use-state false)
drag-disabled? (deref drag-disabled*)
scroll-to-middle? (mf/use-var true)
scroll-middle-ref (mf/use-ref true)
expanded-iref (mf/with-memo [id]
(-> (l/in [:expanded id])
(l/derived refs/workspace-local)))
expanded? (mf/deref expanded-iref)
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
is-expanded (mf/deref expanded-iref)
selected? (contains? selected id)
highlighted? (contains? highlighted id)
is-selected (contains? selected id)
is-highlighted (contains? highlighted id)
container? (or (cfh/frame-shape? item)
(cfh/group-shape? item))
read-only? (mf/use-ctx ctx/workspace-read-only?)
parent-board? (and (cfh/frame-shape? item)
is-read-only (mf/use-ctx ctx/workspace-read-only?)
root-board? (and (cfh/frame-shape? item)
(= uuid/zero (:parent-id item)))
name-node-ref (mf/use-ref)
depth (+ depth 1)
is-component-tree (or ^boolean is-component-child
^boolean (ctk/instance-root? item)
^boolean (ctk/instance-head? item))
enable-drag (mf/use-fn #(reset! drag-disabled* false))
disable-drag (mf/use-fn #(reset! drag-disabled* true))
;; Lazy loading of child elements via IntersectionObserver
children-count* (mf/use-state 0)
children-count (deref children-count*)
lazy-ref (mf/use-ref nil)
observer-ref (mf/use-ref nil)
toggle-collapse
(mf/use-fn
(mf/deps expanded?)
(mf/deps is-expanded)
(fn [event]
(dom/stop-propagation event)
(if (and expanded? (kbd/shift? event))
(if (and is-expanded (kbd/shift? event))
(st/emit! (dwc/collapse-all))
(st/emit! (dwc/toggle-collapse id)))))
@@ -242,13 +293,13 @@
select-shape
(mf/use-fn
(mf/deps id filtered? objects)
(mf/deps id is-filtered objects)
(fn [event]
(dom/prevent-default event)
(reset! scroll-to-middle? false)
(mf/set-ref-val! scroll-middle-ref false)
(cond
(kbd/shift? event)
(if filtered?
(if is-filtered
(st/emit! (dw/shift-select-shapes id objects))
(st/emit! (dw/shift-select-shapes id)))
@@ -283,11 +334,11 @@
on-context-menu
(mf/use-fn
(mf/deps item read-only?)
(mf/deps item is-read-only)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not read-only?
(when-not is-read-only
(let [pos (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position pos :shape item}))))))
@@ -300,7 +351,7 @@
on-drop
(mf/use-fn
(mf/deps id objects expanded? selected)
(mf/deps id objects is-expanded selected)
(fn [side _data]
(let [single? (= (count selected) 1)
same? (and single? (= (first selected) id))]
@@ -313,32 +364,34 @@
(= side :center)
id
(and expanded? (= side :bot) (d/not-empty? (:shapes shape)))
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape)))
id
:else
(cfh/get-parent-id objects id))
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
[parent-id _]
(ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
parent (get objects parent-id)
current-index (d/index-of (:shapes parent) id)
to-index (cond
(= side :center) 0
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
;; target not found in parent (while lazy loading)
(neg? current-index) nil
(= side :top) (inc current-index)
:else current-index)]
(when (some? to-index)
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
on-hold
(mf/use-fn
(mf/deps id expanded?)
(mf/deps id is-expanded)
(fn []
(when-not expanded?
(when-not is-expanded
(st/emit! (dwc/toggle-collapse id)))))
zoom-to-selected
@@ -359,112 +412,114 @@
:data {:id (:id item)
:index index
:name (:name item)}
:draggable? (and
sortable?
(not read-only?)
(not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies
;; We don't want to change the structure of component copies
:draggable? (and ^boolean is-sortable
^boolean (not is-read-only)
^boolean (not (ctn/has-any-copy-parent? objects item))))]
ref (mf/use-ref)
depth (+ depth 1)
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
enable-drag (mf/use-fn #(reset! drag-disabled* false))
disable-drag (mf/use-fn #(reset! drag-disabled* true))
;; Lazy loading of child elements via IntersectionObserver
children-count* (mf/use-state 0)
children-count (deref children-count*)
lazy-ref (mf/use-ref nil)
observer-var (mf/use-var nil)
chunk-size 50]
(mf/with-effect [selected? selected]
(mf/with-effect [is-selected selected]
(let [single? (= (count selected) 1)
node (mf/ref-val ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
parent-node (dom/get-parent-at node 2)
first-child-node (dom/get-first-child parent-node)
node (mf/ref-val name-node-ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
parent-node (dom/get-parent-at node 2)
first-child-node (dom/get-first-child parent-node)
scroll-to-middle? (mf/ref-val scroll-middle-ref)
subid
(when (and single? selected? @scroll-to-middle?)
(when (and ^boolean single?
^boolean is-selected
^boolean scroll-to-middle?)
(ts/schedule
100
#(when (and node scroll-node)
(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node)
scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")]
(dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
(reset! scroll-to-middle? true)))))]
(mf/set-ref-val! scroll-middle-ref true)))))]
#(when (some? subid)
(rx/dispose! subid))))
;; Setup scroll-driven lazy loading when expanded
;; and ensures selected children are loaded immediately
(mf/with-effect [expanded? (:shapes item) selected]
(let [shapes-vec (:shapes item)
total (count shapes-vec)]
(if expanded?
(mf/with-effect [is-expanded shapes selected]
(let [total (count shapes)]
(if ^boolean is-expanded
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
;; Find if any selected id is a direct child and get its render index
selected-child-render-idx
(when (and (> total chunk-size) (seq selected))
(let [shapes-reversed (vec (reverse shapes-vec))]
(some (fn [sel-id]
(let [idx (.indexOf shapes-reversed sel-id)]
(when (>= idx 0) idx)))
selected)))
(when (> total default-chunk-size)
(some (fn [sel-id]
(let [idx (.indexOf shapes sel-id)]
(when (>= idx 0) idx)))
selected))
;; Load at least enough to include the selected child plus extra
;; for context (so it can be centered in the scroll view)
min-count (if selected-child-render-idx
(+ selected-child-render-idx chunk-size)
chunk-size)
current @children-count*
new-count (min total (max current chunk-size min-count))]
min-count
(if selected-child-render-idx
(+ selected-child-render-idx default-chunk-size)
default-chunk-size)
current-count
@children-count*
new-count
(mth/min total (mth/max current-count default-chunk-size min-count))]
(reset! children-count* new-count))
(reset! children-count* 0))))
(reset! children-count* 0))
(fn []
(when-let [obs (mf/ref-val observer-ref)]
(.disconnect obs)
(mf/set-ref-val! obs nil)))))
;; Re-observe sentinel whenever children-count changes (sentinel moves)
;; and (shapes item) to reconnect observer after shape changes
(mf/with-effect [children-count expanded? (:shapes item)]
(let [total (count (:shapes item))
node (mf/ref-val ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
lazy-node (mf/ref-val lazy-ref)]
(mf/with-effect [children-count is-expanded shapes]
(let [total (count shapes)
name-node (mf/ref-val name-node-ref)
scroll-node (dom/get-parent-with-data name-node "scroll-container")
lazy-node (mf/ref-val lazy-ref)]
;; Disconnect previous observer
(when-let [obs ^js @observer-var]
(when-let [obs (mf/ref-val observer-ref)]
(.disconnect obs)
(reset! observer-var nil))
(mf/set-ref-val! observer-ref nil))
;; Setup new observer if there are more children to load
(when (and expanded?
(< children-count total)
scroll-node
lazy-node)
(when (and ^boolean is-expanded
^boolean (< children-count total)
^boolean scroll-node
^boolean lazy-node)
(let [cb (fn [entries]
(when (and (seq entries)
(.-isIntersecting (first entries)))
(when (and (pos? (alength entries))
(.-isIntersecting ^js (aget entries 0)))
;; Load next chunk when sentinel intersects
(let [current @children-count*
next-count (min total (+ current chunk-size))]
(let [next-count (mth/min total (+ children-count default-chunk-size))]
(reset! children-count* next-count))))
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
(.observe observer lazy-node)
(reset! observer-var observer)))))
(mf/set-ref-val! observer-ref observer)))))
[:& layer-item-inner
[:> layer-item-inner*
{:ref dref
:item item
:depth depth
:parent-size parent-size
:name-ref ref
:read-only? read-only?
:highlighted? highlighted?
:selected? selected?
:component-tree? component-tree?
:filtered? filtered?
:expanded? expanded?
:dnd-over? (= (:over dprops) :center)
:dnd-over-top? (= (:over dprops) :top)
:dnd-over-bot? (= (:over dprops) :bot)
:name-ref name-node-ref
:rename-id rename-id
:is-read-only is-read-only
:is-highlighted is-highlighted
:is-selected is-selected
:is-component-tree is-component-tree
:is-filtered is-filtered
:is-expanded is-expanded
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot)
:on-select-shape select-shape
:on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter
@@ -477,29 +532,28 @@
:on-toggle-blocking toggle-blocking
:style style}
(when (and render-children?
(:shapes item)
expanded?)
(when (and ^boolean render-children
^boolean shapes
^boolean is-expanded)
[:div {:class (stl/css-case
:element-children true
:parent-selected selected?
:sticky-children parent-board?)
:parent-selected is-selected
:sticky-children root-board?)
:data-testid (dm/str "children-" id)}
(let [all-children (reverse (d/enumerate (:shapes item)))
visible (take children-count all-children)]
(for [[index id] visible]
(when-let [item (get objects id)]
[:& layer-item
{:item item
:highlighted highlighted
:selected selected
:index index
:objects objects
:key (dm/str id)
:sortable? sortable?
:depth depth
:parent-size parent-size
:component-child? component-tree?}])))
(when (< children-count (count (:shapes item)))
(for [item (take children-count shapes)]
[:> layer-item*
{:item item
:rename-id rename-id
:highlighted highlighted
:selected selected
:index (unchecked-get item "__$__counter")
:objects objects
:key (weak/weak-key item)
:is-sortable is-sortable
:depth depth
:parent-size parent-size
:is-component-child is-component-tree}])
(when (< children-count (count shapes))
[:div {:ref lazy-ref
:class (stl/css :lazy-load-sentinel)}])])]))

View File

@@ -16,39 +16,35 @@
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private space-for-icons 110)
(def lens:shape-for-rename
(-> (l/in [:workspace-local :shape-for-rename])
(l/derived st/state)))
(def ^:private ^:const space-for-icons 110)
(mf/defc layer-name*
{::mf/wrap-props false
::mf/forward-ref true}
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
[{:keys [shape-id rename-id shape-name is-shape-touched disabled-double-click
on-start-edit on-stop-edit depth parent-size is-selected
type-comp type-frame component-id is-hidden is-blocked
variant-id variant-name variant-properties variant-error]} external-ref]
variant-id variant-name variant-properties variant-error ref]}]
(let [edition* (mf/use-state false)
edition? (deref edition*)
local-ref (mf/use-ref)
ref (d/nilv external-ref local-ref)
ref (d/nilv ref local-ref)
shape-for-rename (mf/deref lens:shape-for-rename)
shape-name
(if variant-id
(d/nilv variant-error variant-name)
shape-name)
shape-name (if variant-id
(d/nilv variant-error variant-name)
shape-name)
default-value
(mf/with-memo [variant-id variant-error variant-properties]
(if variant-id
(or variant-error (ctv/properties-map->formula variant-properties))
shape-name))
default-value (if variant-id
(or variant-error (ctv/properties-map->formula variant-properties))
shape-name)
has-path? (str/includes? shape-name "/")
has-path?
(str/includes? shape-name "/")
start-edit
(mf/use-fn
@@ -85,10 +81,11 @@
(when (kbd/enter? event) (accept-edit))
(when (kbd/esc? event) (cancel-edit))))
parent-size (dm/str (- parent-size space-for-icons) "px")]
parent-size
(dm/str (- parent-size space-for-icons) "px")]
(mf/with-effect [shape-for-rename edition? start-edit shape-id]
(when (and (= shape-for-rename shape-id)
(mf/with-effect [rename-id edition? start-edit shape-id]
(when (and (= rename-id shape-id)
(not ^boolean edition?))
(start-edit)))
@@ -110,21 +107,24 @@
:auto-focus true
:id (dm/str "layer-name-" shape-id)
:default-value (d/nilv default-value "")}]
[:*
[:span
{:class (stl/css-case
:element-name true
:left-ellipsis has-path?
:selected is-selected
:hidden is-hidden
:type-comp type-comp
:type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size}
:ref ref
:on-double-click start-edit}
(if (dbg/enabled? :show-ids)
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
[:span {:class (stl/css-case
:element-name true
:left-ellipsis has-path?
:selected is-selected
:hidden is-hidden
:type-comp type-comp
:type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size}
:ref ref
:on-double-click start-edit}
(if ^boolean (dbg/enabled? :show-ids)
(dm/str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
(d/nilv shape-name ""))]
(when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
(when (and ^boolean (dbg/enabled? :show-touched)
^boolean is-shape-touched)
[:span {:class (stl/css :element-name-touched)} "*"])])))

View File

@@ -12,6 +12,7 @@
[app.common.files.helpers :as cfh]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.common.weak :as weak]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -21,7 +22,7 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item*]]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
@@ -31,92 +32,160 @@
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf])
(:import
goog.events.EventType))
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private ref:highlighted-shapes
(l/derived (fn [local]
(-> local
(get :highlighted)
(not-empty)))
refs/workspace-local))
(def ^:private ref:shape-for-rename
(l/derived (l/key :shape-for-rename) refs/workspace-local))
(defn- use-selected-shapes
"A convencience hook wrapper for get selected shapes"
[]
(let [selected (mf/deref refs/selected-shapes)]
(hooks/use-equal-memo selected)))
;; This components is a piece for sharding equality check between top
;; level frames and try to avoid rerender frames that are does not
;; affected by the selected set.
(mf/defc frame-wrapper
{::mf/props :obj}
(mf/defc frame-wrapper*
[{:keys [selected] :as props}]
(let [pending-selected (mf/use-var selected)
current-selected (mf/use-state selected)
props (mf/spread-object props {:selected @current-selected})
(let [pending-selected-ref
(mf/use-ref selected)
current-selected
(mf/use-state selected)
props
(mf/spread-object props {:selected @current-selected})
set-selected
(mf/use-memo
(fn []
(throttle-fn
50
#(when-let [pending-selected @pending-selected]
(reset! current-selected pending-selected)))))]
(mf/with-memo []
(throttle-fn 50 #(when-let [pending-selected (mf/ref-val pending-selected-ref)]
(reset! current-selected pending-selected))))]
(mf/with-effect [selected set-selected]
(reset! pending-selected selected)
(set-selected)
(mf/set-ref-val! pending-selected-ref selected)
(^function set-selected)
(fn []
(reset! pending-selected nil)
#(rx/dispose! set-selected)))
(mf/set-ref-val! pending-selected-ref nil)
(rx/dispose! set-selected)))
[:> layer-item props]))
[:> layer-item* props]))
(mf/defc layers-tree*
{::mf/wrap [mf/memo]}
[{:keys [objects is-filtered parent-size] :as props}]
(let [selected (use-selected-shapes)
highlighted (mf/deref ref:highlighted-shapes)
root (get objects uuid/zero)
rename-id (mf/deref ref:shape-for-rename)
shapes (get root :shapes)
shapes (mf/with-memo [shapes objects]
(loop [counter 0
shapes (seq shapes)
result (list)]
(if-let [id (first shapes)]
(if-let [obj (get objects id)]
(do
;; NOTE: this is a bit hacky, but reduces substantially
;; the allocation; If we use enumeration, we allocate
;; new sequence and add one iteration on each render,
;; independently if objects are changed or not. If we
;; store counter on metadata, we still need to create a
;; new allocation for each shape; with this method we
;; bypass this by mutating a private property on the
;; object removing extra allocation and extra iteration
;; on every request.
(unchecked-set obj "__$__counter" counter)
(recur (inc counter)
(rest shapes)
(conj result obj)))
(recur (inc counter)
(rest shapes)
result))
result)))]
(mf/defc layers-tree
{::mf/wrap [mf/memo #(mf/throttle % 200)]
::mf/wrap-props false}
[{:keys [objects filtered? parent-size] :as props}]
(let [selected (mf/deref refs/selected-shapes)
selected (hooks/use-equal-memo selected)
highlighted (mf/deref refs/highlighted-shapes)
highlighted (hooks/use-equal-memo highlighted)
root (get objects uuid/zero)]
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
[:> hooks/sortable-container* {}
(for [[index id] (reverse (d/enumerate (:shapes root)))]
(when-let [obj (get objects id)]
(if (cfh/frame-shape? obj)
[:& frame-wrapper
{:item obj
:selected selected
:highlighted highlighted
:index index
:objects objects
:key id
:sortable? true
:filtered? filtered?
:parent-size parent-size
:depth -1}]
[:& layer-item
{:item obj
:selected selected
:highlighted highlighted
:index index
:objects objects
:key id
:sortable? true
:filtered? filtered?
:depth -1
:parent-size parent-size}])))]]))
(for [obj shapes]
(if (cfh/frame-shape? obj)
[:> frame-wrapper*
{:item obj
:rename-id rename-id
:selected selected
:highlighted highlighted
:index (unchecked-get obj "__$__counter")
:objects objects
:key (weak/weak-key obj)
:is-sortable true
:is-filtered is-filtered
:parent-size parent-size
:depth -1}]
[:> layer-item*
{:item obj
:rename-id rename-id
:selected selected
:highlighted highlighted
:index (unchecked-get obj "__$__counter")
:objects objects
:key (weak/weak-key obj)
:is-sortable true
:is-filtered is-filtered
:depth -1
:parent-size parent-size}]))]]))
(mf/defc filters-tree
{::mf/wrap [mf/memo #(mf/throttle % 200)]
::mf/wrap-props false}
(mf/defc layers-tree-wrapper*
{::mf/private true}
[{:keys [objects] :as props}]
;; This is a performance sensitive componet, so we use lower-level primitives for
;; reduce residual allocation for this specific case
(let [state-tmp (mf/useState objects)
objects' (aget state-tmp 0)
set-objects (aget state-tmp 1)
subject-s (mf/with-memo []
(rx/subject))
changes-s (mf/with-memo [subject-s]
(->> subject-s
(rx/debounce 500)))
props (mf/spread-props props {:objects objects'})]
(mf/with-effect [objects subject-s]
(rx/push! subject-s objects))
(mf/with-effect [changes-s]
(let [sub (rx/subscribe changes-s set-objects)]
#(rx/dispose! sub)))
[:> layers-tree* props]))
(mf/defc filters-tree*
{::mf/wrap [mf/memo #(mf/throttle % 300)]
::mf/private true}
[{:keys [objects parent-size]}]
(let [selected (mf/deref refs/selected-shapes)
selected (hooks/use-equal-memo selected)
root (get objects uuid/zero)]
(let [selected (use-selected-shapes)
root (get objects uuid/zero)]
[:ul {:class (stl/css :element-list)}
(for [[index id] (d/enumerate (:shapes root))]
(when-let [obj (get objects id)]
[:& layer-item
[:> layer-item*
{:item obj
:selected selected
:index index
:objects objects
:key id
:sortable? false
:filtered? true
:is-sortable false
:is-filtered true
:depth -1
:parent-size parent-size}]))]))
@@ -132,6 +201,7 @@
keys
(filter #(not= uuid/zero %))
vec)]
(update reparented-objects uuid/zero assoc :shapes reparented-shapes)))
;; --- Layers Toolbox
@@ -277,9 +347,11 @@
(swap! state* update :num-items + 100))))]
(mf/with-effect []
(let [keys [(events/listen globals/document EventType.KEYDOWN on-key-down)
(events/listen globals/document EventType.CLICK hide-menu)]]
(fn [] (doseq [key keys] (events/unlistenByKey key)))))
(let [key1 (events/listen globals/document "keydown" on-key-down)
key2 (events/listen globals/document "click" hide-menu)]
(fn []
(events/unlistenByKey key1)
(events/unlistenByKey key2))))
[filtered-objects
handle-show-more
@@ -463,6 +535,8 @@
{::mf/wrap [mf/memo]}
[{:keys [size-parent]}]
(let [page (mf/deref refs/workspace-page)
page-id (get page :id)
focus (mf/deref refs/workspace-focus-selected)
objects (hooks/with-focus-objects (:objects page) focus)
@@ -472,7 +546,8 @@
observer-var (mf/use-var nil)
lazy-load-ref (mf/use-ref nil)
[filtered-objects show-more filter-component] (use-search page objects)
[filtered-objects show-more filter-component]
(use-search page objects)
intersection-callback
(fn [entries]
@@ -518,25 +593,25 @@
[:div {:class (stl/css :tool-window-content)
:data-scroll-container true
:ref on-render-container}
[:& filters-tree {:objects filtered-objects
:key (dm/str (:id page))
:parent-size size-parent}]
[:> filters-tree* {:objects filtered-objects
:key (dm/str page-id)
:parent-size size-parent}]
[:div {:ref lazy-load-ref}]]
[:div {:on-scroll on-scroll
:class (stl/css :tool-window-content)
:data-scroll-container true
:style {:display (when (some? filtered-objects) "none")}}
[:& layers-tree {:objects filtered-objects
:key (dm/str (:id page))
:filtered? true
:parent-size size-parent}]]]
[:> layers-tree-wrapper* {:objects filtered-objects
:key (dm/str page-id)
:is-filtered true
:parent-size size-parent}]]]
[:div {:on-scroll on-scroll
:class (stl/css :tool-window-content)
:data-scroll-container true
:style {:display (when (some? filtered-objects) "none")}}
[:& layers-tree {:objects objects
:key (dm/str (:id page))
:filtered? false
:parent-size size-parent}]])]))
[:> layers-tree-wrapper* {:objects objects
:key (dm/str page-id)
:is-filtered false
:parent-size size-parent}]])]))

View File

@@ -70,7 +70,7 @@
on-click
(mf/use-fn
(mf/deps id current-page-id)
(mf/deps id)
(fn []
;; For the wasm renderer, apply a blur effect to the viewport canvas
;; when we navigate to a different page.

View File

@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.main.data.modal :as modal]
@@ -47,9 +47,9 @@
;; Actions ---------------------------------------------------------------------
(defn attribute-actions [token selected-shapes attributes]
(let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} (map :id selected-shapes))]
{:all-selected? (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)
{:all-selected? (cft/shapes-applied-all? ids-by-attributes shape-ids attributes)
:shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))}))

View File

@@ -6,25 +6,48 @@
(ns app.main.ui.workspace.tokens.management.forms.color
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc form*
[{:keys [token token-type] :as props}]
(let [initial
(mf/with-memo [token-type token]
{:type token-type
:name (:name token "")
:value (:value token "")
:description (:description token "")
:color-result ""})
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 token-type)
(sm/dissoc-key :id)
(sm/assoc-key :color-result :string))
:initial initial
(defn- make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:color-result {:optional true} ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(nil? (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[props]
(let [props (mf/spread-props props {:make-schema make-schema
:input-component token.controls/color-input*})]
[:> generic/form* props]))

View File

@@ -156,6 +156,7 @@
color-resolved
(get-in @form [:data :color-result] "")
valid-color (or (tinycolor/valid-color value)
(tinycolor/valid-color color-resolved))

View File

@@ -6,8 +6,6 @@
(ns app.main.ui.workspace.tokens.management.forms.font-family
(:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
@@ -37,11 +35,6 @@
{:type token-type}))
props (mf/spread-props props {:token token
:token-type token-type
:make-schema #(-> (cfo/make-token-schema %1 token-type)
(sm/dissoc-key :id)
;; The value as edited in the form is a simple stirng.
;; It's converted to vector in the validator.
(sm/assoc-key :value cfo/schema:token-value-generic))
:validator validate-font-family-token
:input-component token.controls/fonts-combobox*})]
[:> generic/form* props]))

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.management.forms.form-container
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.types.tokens-lib :as ctob]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.management.forms.color :as color]
@@ -27,7 +28,7 @@
token-path
(mf/with-memo [token]
(ctob/get-token-path token))
(cft/token-name->path (:name token)))
tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set]

View File

@@ -8,8 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.helpers :as dh]
@@ -35,6 +36,13 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn get-value-for-validator
[active-tab value value-subfield form-type]
@@ -51,6 +59,29 @@
value))
(defn- default-make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(nil? (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token
validator
@@ -66,12 +97,11 @@
value-subfield
input-value-placeholder] :as props}]
(let [make-schema (or make-schema #(-> (cfo/make-token-schema % token-type)
(sm/dissoc-key :id)))
(let [make-schema (or make-schema default-make-schema)
input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token)
active-tab* (mf/use-state #(if (cfo/is-reference? token) :reference :composite))
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
token
@@ -102,10 +132,9 @@
(make-schema tokens-tree-in-selected-set active-tab))
initial
(mf/with-memo [token initial]
(mf/with-memo [token]
(or initial
{:type token-type
:name (:name token "")
{:name (:name token "")
:value (:value token "")
:description (:description token "")}))

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
@@ -51,7 +51,7 @@
;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil)
;; Validate refrence string
(cto/composite-token-reference? token-value) (default-validate-token params)
(cto/shadow-composite-token-reference? token-value) (default-validate-token params)
;; Validate composite token
:else
(let [params (-> params
@@ -253,7 +253,6 @@
[:> reference-form* {:token token
:tokens tokens}])]))
;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields
(defn- make-schema
[tokens-tree active-tab]
(sm/schema
@@ -263,10 +262,10 @@
[:and
[:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc
(sm/update-properties cto/token-name-ref assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value
[:map

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
@@ -48,7 +48,7 @@
;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil)
;; Validate refrence string
(cto/composite-token-reference? token-value) (default-validate-token props)
(cto/typography-composite-token-reference? token-value) (default-validate-token props)
;; Validate composite token
:else
(-> props
@@ -208,7 +208,6 @@
;; SCHEMA
;; TODO: use cfo/make-schema:token-value and extend it with typography and reference fields
(defn- make-schema
[tokens-tree active-tab]
(sm/schema
@@ -218,10 +217,10 @@
[:and
[:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc
(sm/update-properties cto/token-name-ref assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value
[:map

View File

@@ -1,11 +1,13 @@
(ns app.main.ui.workspace.tokens.management.forms.validators
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.workspace.tokens.errors :as wte]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
@@ -27,8 +29,7 @@
;; When creating a new token we dont have a name yet or invalid name,
;; but we still want to resolve the value to show in the form.
;; So we use a temporary token name that hopefully doesn't clash with any of the users token names
(not (sm/valid? cto/schema:token-name (:name token)))
(assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
(not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
tokens' (cond-> tokens
;; Remove previous token when renaming a token
(not= (:name token) (:name prev-token))
@@ -88,3 +89,23 @@
[token-name token-vals]
(when (some #(cto/token-value-self-reference? token-name %) token-vals)
(wte/get-error-code :error.token/direct-self-reference)))
;; This is used in plugins
(defn- make-token-name-schema
"Generate a dynamic schema validation to check if a token path derived
from the name already exists at `tokens-tree`."
[tokens-tree]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]])
(defn validate-token-name
[tokens-tree name]
(let [schema (make-token-name-schema tokens-tree)
explainer (sm/explainer schema)]
(-> name explainer sm/simplify not-empty)))

View File

@@ -10,7 +10,7 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.path-names :as cpn]
[app.common.types.token :as ctt]
[app.config :as cf]
@@ -157,9 +157,9 @@
(defn- applied-all-attributes?
[token selected-shapes attributes]
(let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} xf:map-id selected-shapes)]
(cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
[selected-shapes attrs & {:keys [selected-inside-layout?]}]
@@ -181,7 +181,7 @@
resolved-token (get active-theme-tokens (:name token))
has-selected? (pos? (count selected-shapes))
is-reference? (cfo/is-reference? token)
is-reference? (cft/is-reference? token)
contains-path? (str/includes? name ".")
attributes (as-> (get dwta/token-properties type) $
@@ -194,7 +194,7 @@
applied?
(if has-selected?
(cfo/shapes-token-applied? token selected-shapes attributes)
(cft/shapes-token-applied? token selected-shapes attributes)
false)
half-applied?
@@ -224,7 +224,7 @@
no-valid-value)
color
(when (cfo/color-token? token)
(when (cft/color-token? token)
(or (dwtc/resolved-token-bullet-color resolved-token)
(dwtc/resolved-token-bullet-color token)))

View File

@@ -63,8 +63,7 @@
(st/emit! (dwtl/start-token-set-edition id)))))]
[:> controlled-sets-list*
{:tokens-lib tokens-lib
:token-sets token-sets
{:token-sets token-sets
:is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active?
@@ -81,6 +80,6 @@
:on-toggle-token-set on-toggle-token-set-click
:on-toggle-token-set-group on-toggle-token-set-group-click
:on-update-token-set (partial sets-helpers/on-update-token-set tokens-lib)
:on-update-token-set sets-helpers/on-update-token-set
:on-update-token-set-group sets-helpers/on-update-token-set-group
:on-create-token-set sets-helpers/on-create-token-set}]))

View File

@@ -1,13 +1,9 @@
(ns app.main.ui.workspace.tokens.sets.helpers
(:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[potok.v2.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -15,18 +11,9 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn on-update-token-set
[tokens-lib token-set name]
(let [name (ctob/normalize-set-name name (ctob/get-name token-set))
errors (sm/validation-errors name (cfo/make-token-set-name-schema
tokens-lib
(ctob/get-id token-set)))]
(st/emit! (dwtl/clear-token-set-edition))
(if (empty? errors)
(st/emit! (dwtl/rename-token-set token-set name))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))
[token-set name]
(st/emit! (dwtl/clear-token-set-edition)
(dwtl/update-token-set token-set name)))
(defn on-update-token-set-group
[path name]
@@ -34,15 +21,15 @@
(dwtl/rename-token-set-group path name)))
(defn on-create-token-set
[tokens-lib parent-set name]
(let [name (ctob/make-child-name parent-set name)
errors (sm/validation-errors name (cfo/make-token-set-name-schema tokens-lib nil))]
[parent-set name]
(let [;; FIXME: this code should be reusable under helper under
;; common types namespace
name
(if-let [parent-path (ctob/get-set-path parent-set)]
(->> (concat parent-path (ctob/split-set-name name))
(ctob/join-set-path))
(ctob/normalize-set-name name))
token-set (ctob/make-token-set :name name)]
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(dwtl/clear-token-set-creation))
(if (empty? errors)
(let [token-set (ctob/make-token-set :name name)]
(st/emit! (dwtl/create-token-set token-set)))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))
(dwtl/create-token-set token-set))))

View File

@@ -321,7 +321,6 @@
on-select
on-toggle-set
on-toggle-set-group
tokens-lib
token-sets
new-path
edition-id]}]
@@ -409,7 +408,7 @@
:on-drop on-drop
:on-reset-edition on-reset-edition
:on-edit-submit (partial sets-helpers/on-create-token-set tokens-lib)}]
:on-edit-submit sets-helpers/on-create-token-set}]
:else
[:> sets-tree-set*
@@ -435,8 +434,7 @@
:on-edit-submit on-edit-submit-set}])))))
(mf/defc controlled-sets-list*
[{:keys [tokens-lib
token-sets
[{:keys [token-sets
selected
on-update-token-set
on-update-token-set-group
@@ -488,7 +486,6 @@
{:is-draggable draggable?
:new-path new-path
:edition-id edition-id
:tokens-lib tokens-lib
:token-sets token-sets
:selected selected
:on-select on-select

Some files were not shown because too many files have changed in this diff Show More