Compare commits

...

52 Commits

Author SHA1 Message Date
Andrés Moya
b786b8e07a 🔧 Use automatic validation in token proxies 2025-12-10 09:22:45 +01:00
Andrey Antukh
e8ff89836e 🔧 Refactor token validation schemas 2025-12-10 09:22:45 +01:00
Andrey Antukh
8a8f360c7f Merge remote-tracking branch 'origin/staging' into develop 2025-12-09 19:53:38 +01:00
Luis de Dios
e35fc85c3d 🎉 Create new empty-state component (#7903) 2025-12-09 16:48:12 +01:00
Yamila Moreno
1798461d21 🐳 Add override for assets (#7926) 2025-12-09 14:55:21 +01:00
Yamila Moreno
dde0fddd6f 🐳 Add missing override to Dockerfile.frontend (#7920) 2025-12-09 12:08:46 +01:00
Andrey Antukh
4637aced8c Add support auto decoding and validation syntax for obj/reify 2025-12-09 11:13:06 +01:00
Andrey Antukh
9dfe5b0865 🐛 Fix inconsistencies on using obj/reify on plugins 2025-12-09 11:13:06 +01:00
Andrey Antukh
33bcc9544a Update frontend repl script 2025-12-09 11:13:06 +01:00
Andrey Antukh
babd481b7f Make sm/coercer lazy 2025-12-09 11:13:06 +01:00
Andrey Antukh
a9733c792d Make check-fn completly lazy 2025-12-09 11:13:06 +01:00
Andrey Antukh
d04fdb5fbd Make the dist bundle use consistent and cache-aware uris (#7911) 2025-12-09 08:05:28 +01:00
Eva Marco
81e0e4f222 ♻️ Replace token form files (#7896)
* ♻️ Replace shadow form

* ♻️ Rename files and components

* ♻️ Replace offsetx and offsety names

* ♻️ Replace form file for new form component using new form system

* ♻️ Rename files and props
2025-12-05 17:04:07 +01:00
Yamila Moreno
f13b3c8737 🔧 Fix bug in Github Actions (#7908) 2025-12-04 20:24:33 +01:00
Yamila Moreno
a0f8559ffc 🔧 Add ci/cd for nitrate-module (#7905) 2025-12-04 16:02:29 +01:00
Andrey Antukh
416980f063 🐛 Fix issue on render template on dist bundle (#7899) 2025-12-03 20:48:02 +01:00
Andrey Antukh
f76710296c Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 18:52:28 +01:00
Andrey Antukh
d1379c55f6 Make i18n translation files load on demand 2025-12-03 16:44:37 +01:00
Andrey Antukh
b125c7b5a3 Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:55:01 +01:00
Andrey Antukh
496d37795b Adapt docker images nginx config template to latest changes (#7891) 2025-12-03 13:45:18 +01:00
Andrey Antukh
9f6899007a Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:10:30 +01:00
Marina López
641df77834 🐛 Fix wrong board size presets in Android (#7888) 2025-12-03 12:52:47 +01:00
Andrey Antukh
53aad7bc15 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 17:43:34 +01:00
Andrey Antukh
57297741f5 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 13:28:50 +01:00
Dalai Felinto
d63d692d34 🐛 Fix mask issues with component swap #7675
The logic to swap a component would delete the swapped out component
first before bringing in the new one.

In the process of doing so, the sanitization code would unmask the
group, now orphan of its mask shape component, when it was the first
element of the group.

The fix  was to pass an optional argument to the generate-delete-shapes
function to ignore mask in special cases like this.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-12-02 12:31:44 +01:00
Andrey Antukh
fe72d0af82 Add self-signed cert to caddy (#7872) 2025-12-02 10:45:26 +01:00
Luis de Dios
ef68081d1d 🎉 Add prototype tab UI tweaks (#7832)
* 🎉 Add prototype tab UI tweaks

* 📎 PR changes
2025-12-02 10:44:16 +01:00
Andrey Antukh
4ed49cdc5d Make devenv https and http2 capable (#7871)
Making it more similar on how it runs on production
environments and improves large amount of files loading
thanks to http2.
2025-12-01 20:43:23 +01:00
Andrey Antukh
50f9eedcdf Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 14:33:38 +01:00
Eva Marco
efe74e62e8 🎉 Replace font family form (#7825) 2025-12-01 11:17:25 +01:00
Eva Marco
456afe46de 🎉 Replace font family form (#7784) 2025-12-01 10:11:29 +01:00
Andrey Antukh
964ef799c2 🔥 Remove core.spec usage on common and frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
d34b6b88b6 Remove malli dev stuff from cljs build
It only used on backend.
2025-12-01 09:30:21 +01:00
Andrey Antukh
9a58f0e954 🔧 Disable code motion on shadow config 2025-12-01 09:30:21 +01:00
Andrey Antukh
adaf8be56d Use sm/coercer on app.render entry point 2025-12-01 09:30:21 +01:00
Andrey Antukh
2f1b99fa53 ♻️ Use ESM target for build frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
5080fcc594 🔥 Remove unused require of edn reader on loggin ns 2025-12-01 09:30:21 +01:00
Andrey Antukh
ea2d3758f0 Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 09:28:49 +01:00
Juanfran
94c15916e2 Merge pull request #7857 from penpot/niwinz-develop-prepare-for-pnpm
 Make automatic workflows not dependent on yarn
2025-11-28 13:07:30 +01:00
Andrey Antukh
ed0f3c3595 Make automatic workflows not dependent on yarn 2025-11-28 12:26:56 +01:00
alonso.torres
c2014a37b4 🐛 Fix problem when pasting elements in reverse flex layout 2025-11-27 18:02:34 +01:00
alonso.torres
6611fbd13b 🐛 Fix problem when drag+duplicate a full grid 2025-11-27 18:02:34 +01:00
Andrey Antukh
b5a6867058 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 18:01:08 +01:00
Andrey Antukh
0f88253dd5 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 16:11:36 +01:00
Andrey Antukh
8e3996fbb0 🔧 Change concirrency rules on tests github workflow 2025-11-27 13:16:08 +01:00
Alonso Torres
67762d9450 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 13:02:47 +01:00
Andrey Antukh
7f62652870 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 09:24:40 +01:00
Andrey Antukh
78d31ab11a 🐳 Update devenv docker and compose files
Reuse the already builded imagemagick instead of building
it again on the devenv.
2025-11-26 07:44:56 +01:00
Andrey Antukh
0a80c47901 Merge remote-tracking branch 'origin/staging' into develop 2025-11-26 07:30:42 +01:00
Yamila Moreno
77f1046fc8 🔧 Add MT notification when a docker image with final tag is built (#7824) 2025-11-25 16:39:42 +01:00
Andrey Antukh
553b73a83c ♻️ Replace CircleCI with Github Actions (#7789)
* ♻️ Replace circleci with github actions

* 📎 Add integration test sharding

* 📎 Reuse single build for integration tests shards
2025-11-24 10:44:04 +01:00
Andrey Antukh
00a45cb274 📎 Bump new version on changelog 2025-11-24 09:47:00 +01:00
184 changed files with 6110 additions and 3456 deletions

View File

@@ -1,305 +0,0 @@
version: 2.1
jobs:
lint:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
steps:
- checkout
- run:
name: "fmt check"
working_directory: "."
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "lint clj common"
working_directory: "."
command: |
yarn run lint:clj:common
- run:
name: "lint clj frontend"
working_directory: "."
command: |
yarn run lint:clj:frontend
- run:
name: "lint clj backend"
working_directory: "."
command: |
yarn run lint:clj:backend
- run:
name: "lint clj exporter"
working_directory: "."
command: |
yarn run lint:clj:exporter
- run:
name: "lint clj library"
working_directory: "."
command: |
yarn run lint:clj:library
test-common:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
- run:
name: "JVM tests"
working_directory: "./common"
command: |
clojure -M:dev:test
- run:
name: "NODE tests"
working_directory: "./common"
command: |
yarn install
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
test-frontend:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: "install dependencies"
working_directory: "./frontend"
# We install playwright here because the dependent tasks
# uses the same cache as this task so we prepopulate it
command: |
yarn install
yarn run playwright install chromium --with-deps
- run:
name: "lint scss on frontend"
working_directory: "./frontend"
command: |
yarn run lint:scss
- run:
name: "unit tests"
working_directory: "./frontend"
command: |
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
test-library:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies and build
working_directory: "./library"
command: |
yarn install
- run:
name: Build and Test
working_directory: "./library"
command: |
./scripts/build
yarn run test
test-components:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g -Xms2g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies
working_directory: "./frontend"
command: |
yarn install
yarn run playwright install chromium
- run:
name: Build Storybook
working_directory: "./frontend"
command: yarn run build:storybook
- run:
name: Serve Storybook and run tests
working_directory: "./frontend"
command: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"
test-backend:
docker:
- image: penpotapp/devenv:latest
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
- image: cimg/redis:7.0.5
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "backend/deps.edn" }}
- run:
name: "tests"
working_directory: "./backend"
command: |
clojure -M:dev:test --reporter kaocha.report/documentation
environment:
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
- save_cache:
paths:
- ~/.m2
- ~/.gitlibs
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
test-render-wasm:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
steps:
- checkout
- run:
name: "fmt check"
working_directory: "./render-wasm"
command: |
cargo fmt --check
- run:
name: "lint"
working_directory: "./render-wasm"
command: |
./lint
- run:
name: "cargo tests"
working_directory: "./render-wasm"
command: |
./test
workflows:
penpot:
jobs:
- test-frontend:
requires:
- lint: success
- test-library:
requires:
- lint: success
- test-components:
requires:
- lint: success
- test-backend:
requires:
- lint: success
- test-common:
requires:
- lint: success
- lint
- test-render-wasm

View File

@@ -0,0 +1,21 @@
name: _NITRATE MODULE
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "nitrate-module"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "nitrate-module"

View File

@@ -159,17 +159,7 @@ jobs:
- name: Build Bundle
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
- name: Build WASM
working-directory: "./render-wasm"
run: |
./build release
./scripts/build 0.0.0
- name: Store Bundle Cache
uses: actions/cache@v4
@@ -177,6 +167,7 @@ jobs:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04

View File

@@ -1,5 +1,26 @@
# CHANGELOG
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
### :sparkles: New features & Enhancements
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations

View File

@@ -14,8 +14,7 @@
[app.common.schema :as sm]
[clojure.core :as c]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[expound.alpha :as expound])
[cuerdas.core :as str])
#?(:clj
(:import
clojure.lang.IPersistentMap)))
@@ -110,13 +109,6 @@
(contains? data :explain))
(explain (:explain data) opts)
(and (contains? data ::s/problems)
(contains? data ::s/value)
(contains? data ::s/spec))
(binding [s/*explain-out* expound/printer]
(with-out-str
(s/explain-out (update data ::s/problems #(take (:length opts 10) %)))))
(contains? data ::sm/explain)
(sm/humanize-explain (::sm/explain data) opts)))

View File

@@ -27,6 +27,7 @@
[app.common.types.path :as path]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
@@ -378,7 +379,7 @@
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:attrs [:maybe cto/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -8,9 +8,112 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[clojure.set :as set]
[cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token
(defn make-token-name-schema
"Dynamically generates a schema to check a token name, adding translated error messages
and two additional validations:
- Min and max length.
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
[tokens-tree]
[:and
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]])
(def schema:token-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-schema
[tokens-tree]
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:description {:optional true} schema:token-description]]))
;; Token set
(defn make-token-set-name-schema
"Generates a dynamic schema to check a token set name:
- Validate name length.
- Checks if other token set with a path derived from the name already exists in the tokens lib."
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
(fn [name]
(let [set (ctob/get-set-by-name tokens-lib name)]
(or (nil? set) (= (ctob/get-id set) set-id))))]])
(def schema:token-set-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-set-schema
[tokens-lib set-id]
(sm/merge
ctob/schema:token-set-attrs
[:map
[:name [:and (make-token-set-name-schema tokens-lib set-id)
[:fn #(ctob/normalized-set-name? %)]]]
[:description {:optional true} schema:token-set-description]]))
;; Token theme
(defn make-token-theme-group-schema
"Generates a dynamic schema to check a token theme group:
- Validate group length.
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
[tokens-lib name theme-id]
[:and
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
(fn [group]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(defn make-token-theme-name-schema
"Generates a dynamic schema to check a token theme name:
- Validate name length.
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
[tokens-lib group theme-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
(fn [name]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(def schema:token-theme-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-theme-schema
[tokens-lib group name theme-id]
(sm/merge
ctob/schema:token-theme-attrs
[:map
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
[:description {:optional true} schema:token-theme-description]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
This regexp also trims whitespace around the value."
@@ -80,56 +183,6 @@
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
(defn token-name->path
"Splits token-name into a path vector split by `.` characters.
Will concatenate multiple `.` characters into one."
[token-name]
(str/split token-name #"\.+"))
(defn token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (token-name->path token-name)
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name token-names-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
token-names-tree path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
(defn color-token? [token]
(= (:type token) :color))

View File

@@ -0,0 +1,15 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.i18n
"Dummy i18n functions, to be used by code in common that needs translations.")
(defn tr
"This function will be monkeypatched at runtime with the real function in frontend i18n.
Here it just returns the key passed as argument. This way the result can be used in
unit tests or backend code for logs or error messages."
[key & _args]
key)

View File

@@ -43,8 +43,6 @@
"
#?(:cljs (:require-macros [app.common.logging :as l]))
(:require
#?(:clj [clojure.edn :as edn]
:cljs [cljs.reader :as edn])
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pprint :as pp]

View File

@@ -21,7 +21,6 @@
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
@@ -39,8 +38,7 @@
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
[clojure.set :as set]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
@@ -477,10 +475,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync file with library"
@@ -514,10 +512,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync local components with library"
@@ -2493,11 +2491,13 @@
(ctk/get-swap-slot))
(constantly false))
;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the
;; mask property as part of the sanitization check on generate-delete-shapes, passing "ignore-mask" to prevent this
[all-parents changes]
(-> changes
(cls/generate-delete-shapes
file page objects (d/ordered-set (:id shape))
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true}))
[new-shape changes]
(-> changes
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
@@ -2867,13 +2867,15 @@
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication of a variant, change its parent to root
;; so the copy is made as a child of root
;; If there is an alt-duplication we change to root
;; For variants so the copy is made as a child of root
;; This is because inside a variant-container can't be a copy
;; For other shape this way the layout won't be changed when duplicated
;; and if you move outside the layout will not change
shapes (map (fn [shape]
(if (and alt-duplication? (ctk/is-variant? shape))
(assoc shape :parent-id uuid/zero :frame-id nil)
shape))
(cond-> shape
alt-duplication?
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
shapes)

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-token-id % token-attrs))
(pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
changes)))
check-shape
@@ -123,8 +123,10 @@
;; ignore-children-fn is used to ignore some descendants
;; on the deletion process. It should receive a shape and
;; return a boolean
ignore-children-fn]
:or {ignore-children-fn (constantly false)}}]
ignore-children-fn
ignore-mask]
:or {ignore-children-fn (constantly false)
ignore-mask false}}]
(let [objects (pcb/get-objects changes)
data (pcb/get-library-data changes)
page-id (pcb/get-page-id changes)
@@ -162,18 +164,20 @@
lookup (d/getf objects)
groups-to-unmask
(reduce (fn [group-ids id]
;; When the shape to delete is the mask of a masked group,
;; the mask condition must be removed, and it must be
;; converted to a normal group.
(let [obj (lookup id)
parent (lookup (:parent-id obj))]
(if (and (:masked-group parent)
(= id (first (:shapes parent))))
(conj group-ids (:id parent))
group-ids)))
#{}
ids-to-delete)
(when-not ignore-mask
(reduce (fn [group-ids id]
;; When the shape to delete is the mask of a masked group,
;; the mask condition must be removed, and it must be
;; converted to a normal group.
(let [obj (lookup id)
parent (lookup (:parent-id obj))]
(if (and (:masked-group parent)
(= id (first (:shapes parent))))
(conj group-ids (:id parent))
group-ids)))
#{}
ids-to-delete)
[])
interacting-shapes
(filter (fn [shape]

View File

@@ -8,6 +8,8 @@
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
#?(:clj [malli.dev.pretty :as mdp])
#?(:clj [malli.dev.virhe :as v])
[app.common.data :as d]
[app.common.math :as mth]
[app.common.pprint :as pp]
@@ -19,8 +21,6 @@
[clojure.core :as c]
[cuerdas.core :as str]
[malli.core :as m]
[malli.dev.pretty :as mdp]
[malli.dev.virhe :as v]
[malli.error :as me]
[malli.generator :as mg]
[malli.registry :as mr]
@@ -92,6 +92,16 @@
[& items]
(apply mu/merge (map schema items)))
(defn assoc-key
"Add a key & value to a schema"
[s k v]
(mu/assoc (schema s) k v))
(defn dissoc-key
"Remove a key from a schema"
[s k]
(mu/dissoc (schema s) k))
(defn ref?
[s]
(m/-ref-schema? s))
@@ -245,27 +255,37 @@
:level (d/nilv level 8)
:length (d/nilv length 12)})))))
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]})
#?(:clj
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]}))
(defmethod v/-format ::explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]})
#?(:clj
(defmethod v/-format ::explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]}))
(defn pretty-explain
"A helper that allows print a console-friendly output for the
explain; should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options))))
#?(:clj
(defn pretty-explain
"A helper that allows print a console-friendly output for the explain;
should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(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]
@@ -312,6 +332,13 @@
::explain explain}))))
value))))
(defn coercer
[schema & {:as opts}]
(let [decode-fn (lazy-decoder schema json-transformer)
check-fn (check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))
(defn check
"A helper intended to be used on assertions for validate/check the
schema over provided data. Raises an assertion exception.
@@ -1006,6 +1033,9 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-safe-int?
(lazy-validator ::safe-int))
(def valid-text?
(validator ::text))

View File

@@ -9,13 +9,13 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[clojure.data :as data]
[app.common.time :as ct]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;; GENERAL HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- schema-keys
@@ -45,32 +45,33 @@
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
self-reference?))
(boolean self-reference?)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn composite-token-reference?
"Predicate if a composite token is a reference value - a string pointing to another token."
[token-value]
(string? token-value))
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
:font-size "fontSizes"
:font-weight "fontWeights"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:shadow "shadow"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "borderWidth"
:text-case "textCase"
:text-decoration "textDecoration"
:font-weight "fontWeights"
:typography "typography"})
(def dtcg-token-type->token-type
@@ -82,14 +83,13 @@
"boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
"When converting the type of one element inside a composite token, an additional type
:line-height is available, that is not allowed for a standalone token."
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
"Same as above, in the opposite direction."
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
@@ -97,93 +97,95 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:token-name
"A token name can contains letters, numbers, underscores the character $ and dots, but
not start with $ or end with a dot. The $ character does not have any special meaning,
but dots separate token groups (e.g. color.primary.background)."
[:re {:title "TokenName"
:gen/gen sg/text}
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
(def ^:private schema:color
[:map
[:fill {:optional true} token-name-ref]
[:stroke-color {:optional true} token-name-ref]])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)
(dtcg-token-type->token-type type)
type))}
(def color-keys (schema-keys schema:color))
token-types])
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name schema:token-name]
[:type schema:token-type]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token application to shape
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; All the following schemas define the `:applied-tokens` attribute of a shape.
;; This attribute is a map <token-attribute> -> <token-name>.
;; Token attributes approximately match shape attributes, but not always.
;; For each schema there is a `*keys` set including all the possible token attributes
;; to which a token of the corresponding type can be applied.
;; Some token types can be applied to some attributes only if the shape has a
;; particular condition (i.e. has a layout itself or is a layout item).
(def ^:private schema:border-radius
[:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} token-name-ref]
[:r2 {:optional true} token-name-ref]
[:r3 {:optional true} token-name-ref]
[:r4 {:optional true} token-name-ref]])
[:r1 {:optional true} schema:token-name]
[:r2 {:optional true} schema:token-name]
[:r3 {:optional true} schema:token-name]
[:r4 {:optional true} schema:token-name]])
(def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} token-name-ref]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:stroke-width
(def ^:private schema:color
[:map
[:stroke-width {:optional true} token-name-ref]])
[:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def color-keys (schema-keys schema:color))
(def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]])
[:width {:optional true} schema:token-name]
[:height {:optional true} schema:token-name]])
(def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"}
[:layout-item-min-w {:optional true} token-name-ref]
[:layout-item-max-w {:optional true} token-name-ref]
[:layout-item-min-h {:optional true} token-name-ref]
[:layout-item-max-h {:optional true} token-name-ref]])
[:layout-item-min-w {:optional true} schema:token-name]
[:layout-item-max-w {:optional true} schema:token-name]
[:layout-item-min-h {:optional true} schema:token-name]
[:layout-item-max-h {:optional true} schema:token-name]])
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def ^:private schema:sizing
(-> (reduce mu/union [schema:sizing-base
schema:sizing-layout-item])
(mu/update-properties assoc :title "SizingTokenAttrs")))
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def sizing-keys (schema-keys schema:sizing))
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing-gap
[:map {:title "SpacingGapTokenAttrs"}
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
[:row-gap {:optional true} schema:token-name]
[:column-gap {:optional true} schema:token-name]])
(def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"}
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
[:p1 {:optional true} schema:token-name]
[:p2 {:optional true} schema:token-name]
[:p3 {:optional true} schema:token-name]
[:p4 {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
@@ -192,6 +194,29 @@
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} schema:token-name]
[:m2 {:optional true} schema:token-name]
[:m3 {:optional true} schema:token-name]
[:m4 {:optional true} schema:token-name]])
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing
schema:spacing
@@ -201,91 +226,109 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:axis
[:map
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:font-family
[:map
[:font-family {:optional true} token-name-ref]])
[:font-family {:optional true} schema:token-name]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} token-name-ref]])
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} token-name-ref]])
[:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography
[:map
[:typography {:optional true} token-name-ref]])
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} token-name-ref]])
(def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
[:map {:title "LineHeightTokenAttrs"}
[:line-height {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def line-height-keys (schema-keys schema:line-height))
(def typography-keys (set/union font-size-keys
letter-spacing-keys
font-family-keys
font-weight-keys
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys
#{:line-height}))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} schema:token-name]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:number
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
(-> (reduce mu/union [schema:line-height
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
(def all-keys (set/union color-keys
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} schema:token-name]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} schema:token-name]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def ^:private schema:typography
[:map
[:typography {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def typography-keys (set/union font-family-keys
font-size-keys
font-weight-keys
font-weight-keys
letter-spacing-keys
line-height-keys
text-case-keys
text-decoration-keys
typography-token-keys))
(def ^:private schema:axis
[:map
[:x {:optional true} schema:token-name]
[:y {:optional true} schema:token-name]])
(def axis-keys (schema-keys schema:axis))
(def all-keys (set/union axis-keys
border-radius-keys
shadow-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
color-keys
dimensions-keys
axis-keys
number-keys
opacity-keys
rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
typography-keys
typography-token-keys
number-keys))
typography-token-keys))
(def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}])
@@ -306,11 +349,28 @@
schema:text-decoration
schema:dimensions])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by token type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn token-attr?
[attr]
(contains? all-keys attr))
(defn token-attr->shape-attr
"Returns the actual shape attribute affected when a token have been applied
to a given `token-attr`."
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
(defn shape-attr->token-attrs
"Returns the token-attr affected when a given attribute in a shape is changed.
The sub-attr is for attributes that may have multiple values, like strokes
(may be width or color) and layout padding & margin (may have 4 edges)."
([shape-attr] (shape-attr->token-attrs shape-attr nil))
([shape-attr changed-sub-attr]
(cond
@@ -352,21 +412,13 @@
(number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN SHAPE ATTRIBUTES
;; HELPERS for token attributes by shape type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def position-attributes #{:x :y})
(def ^:private position-attributes #{:x :y})
(def generic-attributes
(def ^:private generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
@@ -375,20 +427,22 @@
shadow-keys
position-attributes))
(def rect-attributes
(def ^:private rect-attributes
(set/union generic-attributes
border-radius-keys))
(def frame-with-layout-attributes
(def ^:private frame-with-layout-attributes
(set/union rect-attributes
spacing-gap-padding-keys))
(def text-attributes
(def ^:private text-attributes
(set/union generic-attributes
typography-keys
number-keys))
(defn shape-type->attributes
"Returns what token attributes may be applied to a shape depending on its type
and if it is a frame with a layout."
[type is-layout]
(case type
:bool generic-attributes
@@ -404,12 +458,14 @@
nil))
(defn appliable-attrs-for-shape
"Returns intersection of shape `attributes` for `shape-type`."
"Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape?
"Checks if `token-type` supports given shape `attributes`."
"Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -420,42 +476,6 @@
typography-keys
#{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- toggle-or-apply-token
"Remove any shape attributes from token if they exists.
Othewise apply token attributes."
[shape token]
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
(merge {} shape-leftover token-leftover)))
(defn- token-from-attributes [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
(let [token (token-from-attributes token attributes)]
(toggle-or-apply-token shape token)))
(defn apply-token-to-shape
[{:keys [shape token attributes] :as _props}]
(let [applied-tokens (apply-token-to-attributes {:shape shape
:token token
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-token-id shape layout-item-attrs)))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
@@ -481,7 +501,48 @@
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY
;; HELPERS for tokens application
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO it seems that this function is redundant, maybe?
;; (defn- toggle-or-apply-token
;; "Remove any shape attributes from token if they exists.
;; Othewise apply token attributes."
;; [shape token]
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
;; (merge {} only-in-shape only-in-token)))
(defn- generate-attr-map [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn apply-token-to-shape
"Applies the token to the given attributes in the shape."
[{:keys [shape token attributes] :as _props}]
(let [map-to-apply (generate-attr-map token attributes)]
(update shape :applied-tokens #(merge % map-to-apply))))
;; (defn apply-token-to-shape
;; [{:keys [shape token attributes] :as _props}]
;; (let [map-to-apply (generate-attr-map token attributes)
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
;; (update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-tokens-from-shape
"Removes any token applied to the given attributes in the shape."
[shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-tokens-from-shape shape layout-item-attrs)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for typography tokens
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
@@ -544,17 +605,3 @@
(when (font-weight-values weight)
(cond-> {:weight weight}
italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHADOW
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn shadow-composite-token-reference?
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))

View File

@@ -114,25 +114,19 @@
[o]
(instance? Token o))
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
(declare make-token)
(def schema:token
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
(sg/fmap #(make-token %)))}
(sm/required-keys schema:token-attrs)
(sm/required-keys cto/schema:token-attrs)
[:fn token?]])
(def ^:private check-token-attrs
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
(sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
(def decode-token-attrs
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
(def check-token
(sm/check-fn schema:token :hint "expected valid token"))
@@ -317,10 +311,13 @@
[o]
(instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name :string]
[:name [:and :string [:fn #(normalized-set-name? %)]]]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true
@@ -342,8 +339,6 @@
:string schema:token]
[:fn d/ordered-map?]]]])
(declare make-token-set)
(def schema:token-set
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
@@ -404,12 +399,25 @@
(split-set-name name))
(cpn/join-path :separator set-separator :with-spaces? false))))
(defn normalized-set-name?
"Check if a set name is normalized (no extra spaces)."
[name]
(= name (normalize-set-name name)))
(defn replace-last-path-name
"Replaces the last element in a `path` vector with `name`."
[path name]
(-> (into [] (drop-last path))
(conj name)))
(defn make-child-name
"Generate the name of a set child of `parent-set` adding the name `name`."
[parent-set name]
(if-let [parent-path (get-set-path parent-set)]
(->> (concat parent-path (split-set-name name))
(join-set-path))
(normalize-set-name name)))
;; The following functions will be removed after refactoring the internal structure of TokensLib,
;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
@@ -1365,10 +1373,13 @@ Will return a value that matches this schema:
(def ^:private check-tokens-lib-map
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
(defn tokens-lib?
[o]
(instance? TokensLib o))
(defn valid-tokens-lib?
[o]
(and (instance? TokensLib o)
(valid? o)))
(and (tokens-lib? o) (valid? o)))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
@@ -1430,6 +1441,50 @@ Will return a value that matches this schema:
(rename copy-name)
(reid (uuid/next))))))
(defn- token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (get-token-path {:name token-name})
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name tokens-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
tokens-tree
path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
;; === Import / Export from JSON format
;; Supported formats:
@@ -1575,10 +1630,10 @@ Will return a value that matches this schema:
(if (map? shadow)
(let [legacy-shadow-type (get "type" shadow)]
(-> shadow
(set/rename-keys {"x" :offsetX
"offsetX" :offsetX
"y" :offsetY
"offsetY" :offsetY
(set/rename-keys {"x" :offset-x
"offsetX" :offset-x
"y" :offset-y
"offsetY" :offset-y
"blur" :blur
"spread" :spread
"color" :color
@@ -1589,7 +1644,7 @@ Will return a value that matches this schema:
(= "false" %) false
(= legacy-shadow-type "innerShadow") true
:else false))
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
shadow))]
(cond
;; Reference value - keep as string
@@ -1860,8 +1915,8 @@ Will return a value that matches this schema:
(mapv (fn [shadow]
(if (map? shadow)
(-> shadow
(set/rename-keys {:offsetX "offsetX"
:offsetY "offsetY"
(set/rename-keys {:offset-x "offsetX"
:offset-y "offsetY"
:blur "blur"
:spread "spread"
:color "color"

View File

@@ -14,7 +14,8 @@
(defn parse
[data]
(cond
(str/starts-with? data "%")
(or (str/starts-with? data "%")
(= data "develop"))
{:full "develop"
:branch "develop"
:base "0.0.0"

View File

@@ -6,34 +6,34 @@
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[clojure.test :as t]))
(t/deftest test-parse-token-value
(t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cft/parse-token-value " -1.3a "))))
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cft/parse-token-value ".3")))))
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/deftest shapes-ids-by-applied-attributes
(t/testing "Returns set of matched attributes that fit the applied token"
@@ -54,7 +54,7 @@
shape-applied-x-y
shape-applied-all
shape-applied-none]
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
(t/is (= (:x expected) (shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all)))
@@ -62,34 +62,21 @@
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all))))
(t/deftest tokens-applied-test
(t/testing "is true when single shape matches the token and attributes"
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "b"}}]
#{:x}))))
(t/testing "is false when no shape matches the token or attributes"
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}]
#{:y})))))
(t/deftest name->path-test
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
(t/deftest token-name-path-exists?-test
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -255,28 +255,28 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:r1 :r2 :r3 :r4])
(cto/unapply-token-id [:rotation])
(cto/unapply-token-id [:opacity])
(cto/unapply-token-id [:stroke-width])
(cto/unapply-token-id [:stroke-color])
(cto/unapply-token-id [:fill])
(cto/unapply-token-id [:width :height])))
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
(cto/unapply-tokens-from-shape [:rotation])
(cto/unapply-tokens-from-shape [:opacity])
(cto/unapply-tokens-from-shape [:stroke-width])
(cto/unapply-tokens-from-shape [:stroke-color])
(cto/unapply-tokens-from-shape [:fill])
(cto/unapply-tokens-from-shape [:width :height])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-tokens-from-shape [:font-family])))
(:objects page)
{})
(cls/generate-update-shapes [(:id circle1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-token-id [:m1 :m2 :m3 :m4])))
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
(:objects page)
{}))

View File

@@ -8,20 +8,19 @@
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/deftest test-valid-token-name-schema
;; Allow regular namespace token names
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
(t/is (true? (sm/validate cto/token-name-ref "foo")))
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
(t/is (true? (sm/validate cto/schema:token-name "foo")))
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
;; Disallow any special characters
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))

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 #{}))
@@ -1897,15 +1897,15 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(:value token)))))
(t/testing "multiple shadow token"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(:value token)))))
(t/testing "shadow token with reference"
@@ -1918,7 +1918,7 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(:value token)))))
(t/testing "shadow token with description"
@@ -1937,14 +1937,14 @@
(ctob/make-token
{:name "shadow.single"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:description "A single shadow"})
"shadow.multiple"
(ctob/make-token
{:name "shadow.multiple"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
"shadow.ref"
(ctob/make-token
{:name "shadow.ref"
@@ -1991,7 +1991,7 @@
(ctob/make-token
{:name "shadow.test"
:type :shadow
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
:description "Round trip test"})
"shadow.ref"
(ctob/make-token
@@ -2013,3 +2013,11 @@
(t/is (some? imported-ref))
(t/is (= (:type original-ref) (:type imported-ref)))
(t/is (= (:value imported-ref) (:value original-ref))))))))
(t/deftest token-name-path-exists?-test
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -25,48 +25,6 @@ RUN set -ex; \
binutils \
build-essential autoconf libtool pkg-config
################################################################################
## IMAGE MAGICK
################################################################################
FROM base AS build-imagemagick
ENV IMAGEMAGICK_VERSION=7.1.1-47 \
DEBIAN_FRONTEND=noninteractive
RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
libltdl-dev \
libpng-dev \
libjpeg-dev \
libtiff-dev \
libwebp-dev \
libopenexr-dev \
libfftw3-dev \
libzip-dev \
liblcms2-dev \
liblzma-dev \
libzstd-dev \
libheif-dev \
librsvg2-dev \
; \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share;
################################################################################
## NODE SETUP
################################################################################
@@ -417,7 +375,7 @@ ENV LANG='C.UTF-8' \
RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=build-imagemagick /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --from=setup-jvm /opt/jdk /opt/jdk
COPY --from=setup-jvm /opt/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node

View File

@@ -69,6 +69,11 @@ services:
- PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
networks:
default:
aliases:
- main
minio:
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
command: minio server /mnt/data --console-address ":9001"
@@ -80,10 +85,6 @@ services:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
ports:
- 9000:9000
- 9001:9001
networks:
default:
aliases:

View File

@@ -38,11 +38,11 @@ http {
gzip_vary on;
gzip_proxied any;
gzip_comp_level 3;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
map $http_upgrade $connection_upgrade {
default upgrade;
@@ -223,16 +223,15 @@ http {
add_header X-Cache-Status $upstream_cache_status;
}
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Cache-Control "no-store";
# This header is what we need to use on prod
# add_header Cache-Control "public, must-revalidate, max-age=0";
add_header Cache-Control "no-store, no-cache, max-age=0" always;
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

@@ -7,8 +7,10 @@ RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /opt/data/assets; \
chown -R penpot:penpot /opt/data; \
mkdir -p /etc/nginx/overrides/main.d/; \
mkdir -p /etc/nginx/overrides/http.d/; \
mkdir -p /etc/nginx/overrides/server.d/; \
mkdir -p /etc/nginx/overrides/assets.d/; \
mkdir -p /etc/nginx/overrides/location.d/;
ARG BUNDLE_PATH="./bundle-frontend/"

View File

@@ -42,11 +42,11 @@ http {
gzip_vary on;
gzip_proxied any;
gzip_static on;
gzip_comp_level 4;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
@@ -110,6 +110,8 @@ http {
recursive_error_pages on;
proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirect;
include /etc/nginx/overrides/assets.d/*.conf;
}
location /internal/assets {
@@ -142,24 +144,15 @@ http {
location / {
include /etc/nginx/overrides/location.d/*.conf;
location ~ ^/js/config.js$ {
add_header Cache-Control "no-store, no-cache, max-age=0" always;
}
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
add_header Cache-Control "max-age=604800" always; # 7 days
}
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
if_modified_since off;
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

@@ -1,5 +1,10 @@
import { withThemeByClassName } from "@storybook/addon-themes";
import Components from "@target/components";
import translations from "@public/translation.en.js";
Components.setDefaultTranslations(translations);
import '../resources/public/css/ds.css';
export const decorators = [

View File

@@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.24"
:git/sha "17a0c94"
{:git/tag "v2.25"
:git/sha "27e5a1a"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}
@@ -42,7 +42,7 @@
:dev
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "3.2.0"}
{thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}

View File

@@ -45,9 +45,9 @@
"translations": "node ./scripts/translations.js",
"watch:app:assets": "node ./scripts/watch.js",
"watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main storybook",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs",
"watch:app": "yarn run clear:shadow-cache && yarn run build:app:worker && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch": "yarn run watch:app:assets",
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"

View File

@@ -22,9 +22,9 @@ export default defineConfig({
workers: 1,
/* Timeout for expects (longer in CI) */
timeout: 60000,
timeout: 80000,
expect: {
timeout: process.env.CI ? 30000 : 5000,
timeout: process.env.CI ? 40000 : 5000,
},
/* Reporter to use. See https://playwright.dev/docs/test-reporters */

View File

@@ -5947,8 +5947,8 @@
"~:spread": "10",
"~:color": "rgb(160, 73, 73)",
"~:inset": true,
"~:offsetX": "10",
"~:offsetY": "10"
"~:offset-x": "10",
"~:offset-y": "10"
}
],
"~:description": "",

View File

@@ -73,7 +73,7 @@ export class BasePage {
}
static async mockConfigFlags(page, flags) {
const url = "**/js/config.js?ts=*";
const url = "**/js/config.js*";
return await page.route(url, (route) =>
route.fulfill({
status: 200,

View File

@@ -303,7 +303,7 @@ test.describe("Tokens: Tokens Tab", () => {
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.pressSequentially(".changed");
await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click();
await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
@@ -1070,6 +1070,7 @@ test.describe("Tokens: Apply token", () => {
// Fill in values for all fields and verify they persist when switching tabs
await fontSizeField.fill("16");
await expect(saveButton).toBeEnabled();
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
@@ -1238,8 +1239,12 @@ test.describe("Tokens: Apply token", () => {
// Fill in the shadow values
const offsetXInput = firstShadowFields.getByLabel("X");
const offsetYInput = firstShadowFields.getByLabel("Y");
const blurInput = firstShadowFields.getByLabel("Blur");
const spreadInput = firstShadowFields.getByLabel("Spread");
const blurInput = firstShadowFields.getByRole("textbox", {
name: "Blur",
});
const spreadInput = firstShadowFields.getByRole("textbox", {
name: "Spread",
});
await offsetXInput.fill("2");
await offsetYInput.fill("2");
@@ -1260,9 +1265,10 @@ test.describe("Tokens: Apply token", () => {
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
// Verify that a color value was set
const colorInput = firstShadowFields.getByLabel("Color");
const firstColorValue = await colorInput.inputValue();
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
await expect(colorInput).toHaveValue(/^rgb(.*)$/);
// Wait for validation to complete
await expect(
@@ -1280,11 +1286,15 @@ test.describe("Tokens: Apply token", () => {
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-0",
);
const colorInput = firstShadowFields.getByLabel("Color");
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
const firstColorValue = await colorInput.inputValue();
// User adds a second shadow
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
const addButton = tokensUpdateCreateModal.getByRole("button", {
name: "Add Shadow",
});
await addButton.click();
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
@@ -1293,8 +1303,7 @@ test.describe("Tokens: Apply token", () => {
await expect(secondShadowFields).toBeVisible();
// User adds a third shadow
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
await addButton2.click();
await addButton.click();
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-2",
@@ -1304,9 +1313,15 @@ test.describe("Tokens: Apply token", () => {
// User adds values for the third shadow
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
const thirdColorInput = thirdShadowFields.getByLabel("Color");
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
name: "Blur",
});
const thirdSpreadInput = thirdShadowFields.getByRole("textbox", {
name: "Spread",
});
const thirdColorInput = thirdShadowFields.getByRole("textbox", {
name: "Color",
});
await thirdOffsetXInput.fill("10");
await thirdOffsetYInput.fill("10");
@@ -1315,15 +1330,13 @@ test.describe("Tokens: Apply token", () => {
await thirdColorInput.fill("#FF0000");
// User removes the 2nd shadow
const removeButton2 = secondShadowFields.getByTestId(
"shadow-remove-button-1",
);
const removeButton2 = secondShadowFields.getByRole("button", {
name: "Remove Shadow",
});
await removeButton2.click();
// Verify second shadow is removed
await expect(
secondShadowFields.getByTestId("shadow-add-button-3"),
).not.toBeVisible();
// Verify that we have only two shadow fields
await expect(thirdShadowFields).not.toBeVisible();
// Verify that the first shadow kept its values
const firstOffsetXValue = await firstShadowFields
@@ -1333,13 +1346,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const firstBlurValue = await firstShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const firstSpreadValue = await firstShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const firstColorValueAfter = await firstShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(firstOffsetXValue).toBe("2");
@@ -1348,7 +1361,7 @@ test.describe("Tokens: Apply token", () => {
await expect(firstSpreadValue).toBe("0");
await expect(firstColorValueAfter).toBe(firstColorValue);
// Verify that the third shadow (now second) kept its values
// Verify that the second kept its values (after shadow 3)
// After removing index 1, the third shadow becomes the second shadow at index 1
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-1",
@@ -1362,13 +1375,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const secondBlurValue = await newSecondShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const secondSpreadValue = await newSecondShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const secondColorValue = await newSecondShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(secondOffsetXValue).toBe("10");
@@ -1385,7 +1398,9 @@ test.describe("Tokens: Apply token", () => {
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-1",
);
const colorInput = firstShadowFields.getByLabel("Color");
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
const firstColorValue = await colorInput.inputValue();
// Switch to reference tab
@@ -1413,13 +1428,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const restoredFirstBlur = await firstShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const restoredFirstSpread = await firstShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const restoredFirstColor = await firstShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(restoredFirstOffsetX).toBe("2");
@@ -1436,13 +1451,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const restoredSecondBlur = await newSecondShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const restoredSecondSpread = await newSecondShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const restoredSecondColor = await newSecondShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(restoredSecondOffsetX).toBe("10");

View File

@@ -746,20 +746,6 @@
}
}
.empty-icon {
@include flexCenter;
height: $s-48;
width: $s-48;
border-radius: $br-circle;
background-color: var(--empty-message-background-color);
svg {
@extend .button-icon;
height: $s-28;
width: $s-28;
stroke: var(--empty-message-foreground-color);
}
}
.attr-title {
div {
margin-left: 0;

View File

@@ -17,23 +17,25 @@
<meta name="twitter:site" content="@penpotapp">
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
{{/isDebug}}
<link rel="icon" href="images/favicon.png" />
<script type="module">
globalThis.penpotVersion = "{{& version}}";
globalThis.penpotBuildDate = "{{& build_date}}";
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
</script>
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
<script defer src="{{& config}}"></script>
<script defer src="{{& polyfills}}"></script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
{{/manifest}}
<script>
window.penpotTranslations = JSON.parse({{& translations}});
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
</script>
<!--cookie-consent-->
</head>
<body>
@@ -44,9 +46,11 @@
<section id="modal"></section>
{{# manifest}}
<script defer src="js/libs.js?ts={{& ts}}"></script>
<script defer src="{{& shared}}"></script>
<script defer src="{{& main}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& app_main}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -1,4 +1,4 @@
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
<style>
body {
@@ -9,7 +9,3 @@
height: 100%;
}
</style>
<script>
window.penpotTranslations = JSON.parse({{& translations}});
</script>

View File

@@ -6,22 +6,24 @@
<link rel="icon" href="images/favicon.png" />
<script>
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
globalThis.penpotVersion = "{{& version}}";
globalThis.penpotBuildDate = "{{& build_date}}";
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
</script>
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
{{/manifest}}
</head>
<body>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& rasterizer}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& rasterizer_main}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -7,20 +7,24 @@
<link rel="icon" href="images/favicon.png" />
<script>
window.penpotVersion = "%version%";
globalThis.penpotVersion = "{{& version}}";
globalThis.penpotBuildDate = "{{& build_date}}";
</script>
{{# manifest}}
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
{{/manifest}}
</head>
<body>
<div id="app"></div>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& render}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& render_main}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -28,6 +28,8 @@ export function startWorker() {
}
export const isDebug = process.env.NODE_ENV !== "production";
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
async function findFiles(basePath, predicate, options = {}) {
predicate =
@@ -47,8 +49,7 @@ async function findFiles(basePath, predicate, options = {}) {
function syncDirs(originPath, destPath) {
const command = `rsync -ar --delete ${originPath} ${destPath}`;
return new Promise((resolve, reject) => {
proc.exec(command, (cause, stdout) => {
return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
if (cause) {
reject(cause);
} else {
@@ -187,33 +188,34 @@ async function readManifestFile() {
}
async function readShadowManifest() {
const ts = Date.now();
try {
const content = await readManifestFile();
const index = {
app_main: "./js/main.js",
render_main: "./js/render.js",
rasterizer_main: "./js/rasterizer.js",
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
};
config: "./js/config.js?version=" + CURRENT_VERSION,
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
libs: "./js/libs.js?version=" + CURRENT_VERSION,
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
for (let item of content) {
index[item.name] = "js/" + item["output-name"];
}
importmap: JSON.stringify({
"imports": {
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
}
})
};
return index;
} catch (cause) {
return {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
main: "js/main.js?ts=" + ts,
shared: "js/shared.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
rasterizer: "js/rasterizer.js?ts=" + ts,
};
}
return index;
}
async function renderTemplate(path, context = {}, partials = {}) {
@@ -253,7 +255,7 @@ const markedOptions = {
marked.use(markedOptions);
async function readTranslations() {
export async function compileTranslations() {
const langs = [
"ar",
"ca",
@@ -290,9 +292,10 @@ async function readTranslations() {
["uk", "ukr_UA"],
"ha",
];
const result = {};
for (let lang of langs) {
const result = {};
let filename = `${lang}.po`;
if (l.isArray(lang)) {
filename = `${lang[1]}.po`;
@@ -311,11 +314,6 @@ async function readTranslations() {
for (let key of Object.keys(trdata)) {
if (key === "") continue;
const comments = trdata[key].comments || {};
if (l.isNil(result[key])) {
result[key] = {};
}
const isMarkdown = l.includes(comments.flag, "markdown");
const msgs = trdata[key].msgstr;
@@ -325,9 +323,9 @@ async function readTranslations() {
message = marked.parseInline(message);
}
result[key][lang] = message;
result[key] = message;
} else {
result[key][lang] = msgs.map((item) => {
result[key] = msgs.map((item) => {
if (isMarkdown) {
return marked.parseInline(item);
} else {
@@ -336,22 +334,12 @@ async function readTranslations() {
});
}
}
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
const outputDir = "resources/public/js/";
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
await fs.writeFile(outputFile, esm);
}
return result;
}
function filterTranslations(translations, langs = [], keyFilter) {
const filteredEntries = Object.entries(translations)
.filter(([translationKey, _]) => keyFilter(translationKey))
.map(([translationKey, value]) => {
const langEntries = Object.entries(value).filter(([lang, _]) =>
langs.includes(lang),
);
return [translationKey, Object.fromEntries(langEntries)];
});
return Object.fromEntries(filteredEntries);
}
async function generateSvgSprite(files, prefix) {
@@ -403,14 +391,6 @@ async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true });
let translations = await readTranslations();
const storybookTranslations = JSON.stringify(
filterTranslations(translations, ["en"], (key) =>
key.startsWith("labels."),
),
);
translations = JSON.stringify(translations);
const manifest = await readShadowManifest();
let content;
@@ -432,13 +412,16 @@ async function generateTemplates() {
"../public/images/sprites/assets.svg": assetsSprite,
};
const context = {
manifest: manifest,
version: CURRENT_VERSION,
build_date: BUILD_DATE,
isDebug,
};
content = await renderTemplate(
"resources/templates/index.mustache",
{
manifest: manifest,
translations: JSON.stringify(translations),
isDebug,
},
context,
partials,
);
@@ -446,41 +429,30 @@ async function generateTemplates() {
content = await renderTemplate(
"resources/templates/challenge.mustache",
{},
context,
partials,
);
await fs.writeFile("./resources/public/challenge.html", content);
content = await renderTemplate(
"resources/templates/preview-body.mustache",
{
manifest: manifest,
},
context,
partials,
);
await fs.writeFile("./.storybook/preview-body.html", content);
content = await renderTemplate(
"resources/templates/preview-head.mustache",
{
manifest: manifest,
translations: JSON.stringify(storybookTranslations),
},
context,
partials,
);
await fs.writeFile("./.storybook/preview-head.html", content);
content = await renderTemplate("resources/templates/render.mustache", {
manifest: manifest,
translations: JSON.stringify(translations),
});
content = await renderTemplate("resources/templates/render.mustache", context);
await fs.writeFile("./resources/public/render.html", content);
content = await renderTemplate("resources/templates/rasterizer.mustache", {
manifest: manifest,
translations: JSON.stringify(translations),
});
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
await fs.writeFile("./resources/public/rasterizer.html", content);
}

View File

@@ -26,7 +26,10 @@ yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
mkdir -p resources/public;
mkdir -p target/dist;
yarn run build:app:main $EXTRA_PARAMS || exit 1
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
@@ -35,18 +38,9 @@ fi
yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
mkdir -p target/dist;
rsync -avr resources/public/ target/dist/
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/render.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/rasterizer.html;
if [ "$INCLUDE_WASM" = "yes" ]; then
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
fi
rsync -avr resources/public/ target/dist/;
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook

View File

@@ -4,5 +4,6 @@ await h.compileStyles();
await h.copyAssets();
await h.copyWasmPlayground();
await h.compileSvgSprites();
await h.compileTranslations();
await h.compileTemplates();
await h.compilePolyfills();

View File

@@ -31,9 +31,9 @@ const rebuildNotify = {
const config = {
entryPoints: ["target/index.js"],
bundle: true,
format: "iife",
format: "esm",
banner: {
js: '"use strict"; var global = globalThis;',
js: '"use strict";\nvar global = globalThis;',
},
outfile: "resources/public/js/libs.js",
plugins: [fixReactVirtualized, rebuildNotify],

View File

@@ -1,7 +1,11 @@
import fs from "node:fs/promises";
import * as h from "./_helpers.js";
await fs.mkdir("resources/public/js", {recursive: true});
await h.compileStorybookStyles();
await h.copyAssets();
await h.compileSvgSprites();
await h.compileTranslations();
await h.compileTemplates();
await h.compilePolyfills();

View File

@@ -1,6 +1,19 @@
#!/usr/bin/env bash
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
export OPTIONS="-A:dev"
set -ex
exec clojure $OPTIONS -M -m rebel-readline.main
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main

View File

@@ -52,6 +52,7 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
await compileSassAll();
await h.copyAssets();
await h.copyWasmPlayground();
await h.compileTranslations();
await h.compileSvgSprites();
await h.compileTemplates();
await h.compilePolyfills();
@@ -81,7 +82,7 @@ h.watch("resources/templates", null, async function (path) {
log.info("watch: translations (~)");
h.watch("translations", null, async function (path) {
log.info("changed:", path);
await h.compileTemplates();
await h.compileTranslations();
});
log.info("watch: assets (~)");

View File

@@ -6,13 +6,12 @@
:builds
{:main
{:target :browser
{:target :esm
:output-dir "resources/public/js/"
:asset-path "/js"
:devtools {:watch-dir "resources/public"
:reload-strategy :full}
:build-options {:manifest-name "manifest.json"}
:module-loader true
:modules
{:shared
{:entries []}
@@ -20,42 +19,42 @@
:main
{:entries [app.main app.plugins.api]
:depends-on #{:shared}
:init-fn app.main/init}
:exports {init app.main/init}}
:util-highlight
{:entries [app.util.code-highlight]
:depends-on #{:main}}
:depends-on #{:shared}}
:main-auth
{:entries [app.main.ui.auth
app.main.ui.auth.verify-token]
:depends-on #{:main}}
:depends-on #{:shared}}
:main-viewer
{:entries [app.main.ui.viewer]
:depends-on #{:main :main-auth}}
:depends-on #{:shared :main-auth}}
:main-workspace
{:entries [app.main.ui.workspace]
:depends-on #{:main}}
:depends-on #{:shared}}
:main-dashboard
{:entries [app.main.ui.dashboard]
:depends-on #{:main}}
:depends-on #{:shared}}
:main-settings
{:entries [app.main.ui.settings]
:depends-on #{:main}}
:depends-on #{:shared}}
:render
{:entries [app.render]
:depends-on #{:shared}
:init-fn app.render/init}
:exports {init app.render/init}}
:rasterizer
{:entries [app.rasterizer]
:depends-on #{:shared}
:init-fn app.rasterizer/init}}
:exports {init app.rasterizer/init}}}
:js-options
{:entry-keys ["module" "browser" "main"]
@@ -75,11 +74,10 @@
:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:output-wrapper true
:rename-prefix-namespace "PENPOT"
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
:cross-chunk-method-motion false
:source-map-detail-level :all}}}
:worker

View File

@@ -86,7 +86,6 @@
(def default-theme "default")
(def default-language "en")
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes"))
(def build-date (parse-build-date global))
@@ -191,5 +190,4 @@
(defn resolve-static-asset
[path]
(let [uri (u/join public-uri path)]
(assoc uri :query (dm/str "version=" (:full version)))))
(u/join public-uri path))

View File

@@ -92,7 +92,7 @@
(defn ^:export init
[]
(mw/init!)
(i18n/init! cf/translations)
(i18n/init)
(cur/init-styles)
(thr/init!)
(init-ui)
@@ -114,11 +114,4 @@
[]
(reinit))
;; Reload the UI when the language changes
(add-watch
i18n/locale "locale"
(fn [_ _ old-value current-value]
(when (not= old-value current-value)
(reinit))))
(set! (.-stackTraceLimit js/Error) 50)

View File

@@ -148,17 +148,17 @@
:width 768
:height 1024}
{:name "Google Pixel 7 Pro"
:width 1440
:height 3120}
:width 412
:height 892}
{:name "Google Pixel 6a/6"
:width 1080
:height 2400}
:width 412
:height 915}
{:name "Google Pixel 4a/5"
:width 393
:height 851}
{:name "Samsung Galaxy S22"
:width 1080
:height 2340}
:width 360
:height 780}
{:name "Samsung Galaxy S20+"
:width 384
:height 854}

View File

@@ -68,7 +68,7 @@
(let [uagent (new ua/UAParser)]
(merge
{:version (:full cf/version)
:locale @i18n/locale}
:locale i18n/*current-locale*}
(let [browser (.getBrowser uagent)]
{:browser (obj/get browser "name")
:browser-version (obj/get browser "version")})
@@ -98,7 +98,9 @@
(def context
(atom (d/without-nils (collect-context))))
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
(add-watch i18n/state "events"
(fn [_ _ _ v]
(swap! context assoc :locale (get v :locale))))
;; --- EVENT TRANSLATION

View File

@@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.profile :refer [schema:profile]]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -54,11 +53,16 @@
(assoc :profile-id id)
(assoc :profile profile)))
ptk/WatchEvent
(watch [_ state _]
(let [profile (:profile state)]
(->> (rx/from (i18n/set-locale (:lang profile)))
(rx/ignore))))
ptk/EffectEvent
(effect [_ state _]
(let [profile (:profile state)]
(swap! storage/user assoc :profile profile)
(i18n/set-locale! (:lang profile))
(plugins.register/init)))))
(def profile-fetched?
@@ -484,7 +488,7 @@
(defn delete-access-token
[{:keys [id] :as params}]
(us/assert! ::us/uuid id)
(assert (uuid? id))
(ptk/reify ::delete-access-token
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -8,7 +8,7 @@
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
@@ -59,9 +59,15 @@
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
If the value is not parseable and/or has missing references returns a map with `:errors`."
[value]
(if-let [tc (tinycolor/valid-color value)]
{:value value :unit (tinycolor/color-format tc)}
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
(let [missing-references (seq (cto/find-token-value-references value))]
(if-let [tc (tinycolor/valid-color value)]
{:value value :unit (tinycolor/color-format tc)}
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
:else
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
(defn- numeric-string? [s]
(and (string? s)
@@ -77,7 +83,7 @@
[value]
(let [number? (or (number? value)
(numeric-string? value))
parsed-value (cft/parse-token-value value)
parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
@@ -103,7 +109,7 @@
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
[value]
(let [parsed-value (cft/parse-token-value value)
(let [parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
(if (and parsed-value (not out-of-bounds))
@@ -120,8 +126,8 @@
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value)
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
parsed-value (cfo/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))]
(cond (and parsed-value (not out-of-scope))
@@ -145,7 +151,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value)
parsed-value (cfo/parse-token-value value)
out-of-scope (< (:value parsed-value) 0)
references (seq (cto/find-token-value-references value))]
(cond
@@ -244,7 +250,7 @@
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(case unit
"%" (/ value 100)
"px" (/ value font-size-value)
@@ -368,8 +374,8 @@
(let [add-keyed-errors (fn [shadow-result k errors]
(update shadow-result :errors concat
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
parsers {:offsetX parse-sd-token-general-value
:offsetY parse-sd-token-general-value
parsers {:offset-x parse-sd-token-general-value
:offset-y parse-sd-token-general-value
:blur parse-sd-token-shadow-blur
:spread parse-sd-token-shadow-spread
:color parse-sd-token-color-value
@@ -389,35 +395,42 @@
(defn- parse-sd-token-shadow-value
"Parses shadow value and validates it."
[value]
(cond
;; Reference value (string)
(string? value) {:value value}
(let [missing-references
(when (string? value)
(seq (cto/find-token-value-references value)))]
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
(string? value)
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
;; Empty value
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
;; Invalid value
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
;; Array of shadows
:else
(let [converted (js->clj value :keywordize-keys true)
:else
(let [converted (js->clj value :keywordize-keys true)
;; Parse each shadow with its index
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
;; Collect all errors from all shadows
all-errors (mapcat :errors parsed-shadows)
all-errors (mapcat :errors parsed-shadows)
;; Collect all values from shadows that have values
all-values (into [] (keep :value parsed-shadows))]
all-values (into [] (keep :value parsed-shadows))]
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values}))))
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values})))))
(defn collect-shadow-errors [token shadow-index]
(group-by :shadow-key

View File

@@ -706,53 +706,58 @@
(= 1 (count tree-root)))]
(cond
;; Paste next to selected frame, if selected is itself or of the same size as the copied
(and (selected-frame? state)
(or (any-same-frame-from-selected? state (keys pobjects))
(and only-one-root-shape?
(frame-same-size? pobjects (first tree-root)))))
(let [selected-frame-obj (get page-objects (first page-selected))
parent-id (:parent-id base)
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
paste-y (:y selected-frame-obj)
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
[parent-id delta index])
;; Paste inside selected frame otherwise
(selected-frame? state)
(let [selected-frame-obj (get page-objects (first page-selected))
origin-frame-id (:frame-id first-selected-obj)
origin-frame-object (get page-objects origin-frame-id)
(if (or (any-same-frame-from-selected? state (keys pobjects))
(and only-one-root-shape?
(frame-same-size? pobjects (first tree-root))))
;; Paste next to selected frame, if selected is itself or of the same size as the copied
(let [selected-frame-obj (get page-objects (first page-selected))
parent-id (:parent-id base)
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
paste-y (:y selected-frame-obj)
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width wrapper))))
[parent-id delta index])
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
;; Paste inside selected frame otherwise
(let [selected-frame-obj (get page-objects (first page-selected))
origin-frame-id (:frame-id first-selected-obj)
origin-frame-object (get page-objects origin-frame-id)
;; Pasted objects mustn't exceed the selected frame x limit
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
(:x frame-object))
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width wrapper))))
;; Pasted objects mustn't exceed the selected frame y limit
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
(:y frame-object))
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
delta (if (= origin-frame-id uuid/zero)
;; When the origin isn't in a frame the result is pasted in the center.
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
;; When pasting from one frame to another frame the object
;; position must be limited to container boundaries. If
;; the pasted object doesn't fit we try to:
;;
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right
;; and bottom in the original frame
(gpt/point paste-x paste-y))
;; Pasted objects mustn't exceed the selected frame x limit
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
(:x frame-object))
;; Pasted objects mustn't exceed the selected frame y limit
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
(:y frame-object))
delta (if (= origin-frame-id uuid/zero)
;; When the origin isn't in a frame the result is pasted in the center.
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
;; When pasting from one frame to another frame the object
;; position must be limited to container boundaries. If
;; the pasted object doesn't fit we try to:
;;
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right
;; and bottom in the original frame
(gpt/point paste-x paste-y))]
[frame-id delta (dec (count (:shapes selected-frame-obj)))]))
target-index
(if (and (ctl/flex-layout? selected-frame-obj) (ctl/reverse? selected-frame-obj))
(dec 0) ;; Before the first index 0
(count (:shapes selected-frame-obj)))]
[frame-id delta target-index])
(empty? page-selected)
(let [frame-id (ctst/top-nested-frame page-objects position)

View File

@@ -119,21 +119,6 @@
(let [page (dsh/lookup-page state)]
(rx/of (update-flow (:id page) flow-id #(assoc % :name name)))))))
(defn start-rename-flow
[id]
(dm/assert! (uuid? id))
(ptk/reify ::start-rename-flow
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :flow-for-rename] id))))
(defn end-rename-flow
[]
(ptk/reify ::end-rename-flow
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :flow-for-rename))))
;; --- Interactions
(defn- connected-frame?

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.tokens.application
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
@@ -153,11 +153,11 @@
(defn value->shadow
"Transform a token shadow value into penpot shadow data structure"
[value]
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
{:id (random-uuid)
:hidden false
:offset-x offsetX
:offset-y offsetY
:offset-x offset-x
:offset-y offset-y
:blur blur
:color (value->color color)
:spread spread
@@ -525,8 +525,8 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
tokenized-attributes (cfo/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
@@ -585,7 +585,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
@@ -613,7 +613,7 @@
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.main.data.tinycolor :as tinycolor]))
(defn color-bullet-color [token-color-value]
@@ -17,5 +17,5 @@
(tinycolor/->hex-string tc))))
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
(when (and resolved-value (cft/color-token? token))
(when (and resolved-value (cfo/color-token? token))
(color-bullet-color resolved-value)))

View File

@@ -108,6 +108,10 @@
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
:error.style-dictionary/invalid-token-value-shadow
{:error/code :error.style-dictionary/invalid-token-value-shadow
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
:error/unknown
{:error/code :error/unknown
:error/fn #(tr "labels.unknown-error")}})

View File

@@ -16,8 +16,8 @@
:letter-spacing "Letter Spacing"
:text-case "Text Case"
:text-decoration "Text Decoration"
:offsetX "X"
:offsetY "Y"
:offset-x "X"
:offset-y "Y"
:blur "Blur"
:spread "Spread"
:color "Color"

View File

@@ -147,27 +147,30 @@
(defn create-token-set
[token-set]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
(ptk/reify ::create-token-set
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib)
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(defn rename-token-set
[token-set new-name]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(defn rename-token-set-group
[set-group-path set-group-fname]
@@ -179,26 +182,6 @@
(rx/of
(dch/commit-changes changes))))))
(defn update-token-set
[token-set name]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
name (ctob/normalize-set-name name (ctob/get-name token-set))
tokens-lib (get data :tokens-lib)]
(if (ctob/get-set-by-name tokens-lib name)
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(defn duplicate-token-set
[id]
(ptk/reify ::duplicate-token-set
@@ -448,7 +431,7 @@
(ctob/get-id token-set)
token-id)]
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
unames (map :name tokens)
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token

View File

@@ -31,27 +31,28 @@
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.modules :as mod]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
(def auth-page
(mf/lazy-component app.main.ui.auth/auth))
(mf/lazy #(mod/load 'app.main.ui.auth/auth-page*)))
(def verify-token-page
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def verify-token-page*
(mf/lazy #(mod/load 'app.main.ui.auth.verify-token/verify-token-page*)))
(def viewer-page*
(mf/lazy-component app.main.ui.viewer/viewer*))
(mf/lazy #(mod/load 'app.main.ui.viewer/viewer-page*)))
(def dashboard-page*
(mf/lazy-component app.main.ui.dashboard/dashboard*))
(mf/lazy #(mod/load 'app.main.ui.dashboard/dashboard-page*)))
(def settings-page*
(mf/lazy-component app.main.ui.settings/settings*))
(mf/lazy #(mod/load 'app.main.ui.settings/settings-page*)))
(def workspace-page*
(mf/lazy-component app.main.ui.workspace/workspace*))
(mf/lazy #(mod/load 'app.main.ui.workspace/workspace-page*)))
(mf/defc workspace-legacy-redirect*
{::mf/props :obj
@@ -189,7 +190,7 @@
[:? [:& auth-page {:route route}]]
:auth-verify-token
[:? [:& verify-token-page {:route route}]]
[:? [:& verify-token-page* {:route route}]]
(:settings-profile
:settings-password

View File

@@ -19,8 +19,7 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc auth
{::mf/props :obj}
(mf/defc auth*
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
is-register (or
@@ -69,3 +68,9 @@
(when (= section :auth-register)
[:& terms-register])]]))
(mf/defc auth-page*
{::mf/lazy-load true}
[props]
[:> auth* props])

View File

@@ -61,9 +61,9 @@
(rt/nav :auth-login)
(ntf/warn (tr "errors.unexpected-token"))))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])
(mf/defc verify-token*
[{:keys [route]}]
(let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)]
(mf/with-effect []
@@ -99,3 +99,8 @@
[:> static/invalid-token {}]
[:> loader* {:title (tr "labels.loading")
:overlay true}])))
(mf/defc verify-token-page*
{::mf/lazy-load true}
[props]
[:> verify-token* props])

View File

@@ -8,24 +8,28 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.util.modules :as modules]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.lazy :as lazy]))
[rumext.v2 :as mf]))
(def highlight-fn
(lazy/loadable app.util.code-highlight/highlight!))
(delay (modules/load-fn 'app.util.code-highlight/highlight!)))
(mf/defc code-block
{::mf/wrap-props false}
[{:keys [code type]}]
(let [block-ref (mf/use-ref)
code (str/trim code)]
code (str/trim code)]
(mf/with-effect [code type]
(when-let [node (mf/ref-val block-ref)]
(p/let [highlight-fn (lazy/load highlight-fn)]
(highlight-fn node))))
(->> @highlight-fn
(p/fmap (fn [f] (f)))
(p/fnly (fn [f cause]
(if cause
(js/console.error cause)
(f node)))))))
[:pre {:class (dm/str type " " (stl/css :code-display)) :ref block-ref} code]))

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.formats :as fmt]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@@ -46,7 +47,7 @@
:disabled disabled)}
(if (some? icon)
[:span {:class icon-class} icon]
[:> icon* {:icon-id icon :class icon-class :aria-hidden true}]
[:span {:class (stl/css :title-name)} value])
[:input {:id id

View File

@@ -247,7 +247,6 @@
(swap! storage/session dissoc :template))))))
(mf/defc dashboard*
{::mf/props :obj}
[{:keys [profile project-id team-id search-term plugin-url template section]}]
(let [team (mf/deref refs/team)
projects (mf/deref refs/projects)
@@ -313,3 +312,8 @@
:section section
:search-term search-term
:team team}]]]))
(mf/defc dashboard-page*
{::mf/lazy-load true}
[props]
[:> dashboard* props])

View File

@@ -6,9 +6,9 @@
(ns app.main.ui.ds
(:require
[app.config :as cf]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
@@ -32,6 +32,7 @@
[app.main.ui.ds.product.avatar :refer [avatar*]]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.product.milestone :refer [milestone*]]
@@ -43,8 +44,6 @@
[app.util.i18n :as i18n]
[rumext.v2 :as mf]))
(i18n/init! cf/translations)
(def default
"A export used for storybook"
(mf/object
@@ -58,10 +57,12 @@
:HintMessage hint-message*
:InputWithMeta input-with-meta*
:EmptyPlaceholder empty-placeholder*
:EmptyState empty-state*
:Loader loader*
:RawSvg raw-svg*
:Select select*
:Switch switch*
:Checkbox checkbox*
:Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*
@@ -78,6 +79,11 @@
:Milestone milestone*
:MilestoneGroup milestone-group*
:Date date*
:set-default-translations
(fn [data]
(i18n/set-translations "en" data))
;; meta / misc
:meta
{:icons (clj->js (sort icon-list))

View File

@@ -11,12 +11,18 @@
%base-button {
--button-bg-color: initial;
--button-fg-color: initial;
--button-hover-bg-color: initial;
--button-hover-fg-color: initial;
--button-active-bg-color: initial;
--button-active-fg-color: initial;
--button-disabled-bg-color: initial;
--button-disabled-fg-color: initial;
--button-border-color: var(--button-bg-color);
--button-focus-inner-ring-color: initial;
--button-focus-outer-ring-color: initial;
@@ -38,8 +44,10 @@
--button-fg-color: var(--button-hover-fg-color);
}
&:active {
&:active,
&[aria-pressed="true"] {
--button-bg-color: var(--button-active-bg-color);
--button-fg-color: var(--button-active-fg-color);
}
&:focus-visible {
@@ -63,6 +71,7 @@
--button-hover-fg-color: var(--color-background-secondary);
--button-active-bg-color: var(--color-accent-tertiary);
--button-active-fg-color: var(--color-background-secondary);
--button-disabled-bg-color: var(--color-accent-primary-muted);
--button-disabled-fg-color: var(--color-background-secondary);
@@ -72,7 +81,8 @@
--button-focus-inner-ring-color: var(--color-background-secondary);
--button-focus-outer-ring-color: var(--color-accent-primary);
&:active {
&:active,
&[aria-pressed="true"] {
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
}
}
@@ -85,6 +95,7 @@
--button-hover-fg-color: var(--color-accent-primary);
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-foreground-secondary);
@@ -103,6 +114,7 @@
--button-hover-fg-color: var(--color-accent-primary);
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-accent-primary-muted);
@@ -121,6 +133,7 @@
--button-hover-fg-color: var(--color-foreground-primary);
--button-active-bg-color: var(--color-accent-error);
--button-active-fg-color: var(--color-foreground-primary);
--button-disabled-bg-color: var(--color-background-error);
--button-disabled-fg-color: var(--color-accent-error);
@@ -130,7 +143,8 @@
--button-focus-inner-ring-color: var(--color-background-primary);
--button-focus-outer-ring-color: var(--color-accent-primary);
&:active {
&:active,
&[aria-pressed="true"] {
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
}
}

View File

@@ -0,0 +1,45 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.controls.checkbox
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[rumext.v2 :as mf]))
(def ^:private schema:checkbox
[:map
[:id {:optional true} :string]
[:label {:optional true} :string]
[:checked {:optional true} :boolean]
[:on-change {:optional true} fn?]
[:disabled {:optional true} :boolean]])
(mf/defc checkbox*
{::mf/schema schema:checkbox}
[{:keys [id class label checked on-change disabled] :rest props}]
(let [props
(mf/spread-props props {:type "checkbox"
:class (stl/css :checkbox-input)
:id id
:checked checked
:on-change on-change
:disabled disabled})]
[:div {:class [class (stl/css :checkbox)]}
[:label {:for id
:class (stl/css :checkbox-label)}
[:div {:class (stl/css-case :checkbox-box true
:checked checked
:disabled disabled)}
(when checked
[:> icon* {:icon-id i/tick
:size "s"}])]
[:div {:class (stl/css :checkbox-text)} label]
[:> :input props]]]))

View File

@@ -0,0 +1,40 @@
{ /* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as Checkbox from "./checkbox.stories";
<Meta title="Controls/Checkbox" />
# Checkbox
The `checkbox*` component is a toggle control. It allows users to switch between boolean states (`false` or `true`).
<Canvas of={Checkbox.Default} />
<Canvas of={Checkbox.Checked} />
## Anatomy
The checkbox component consists of three main parts:
- **Label** (optional): the text that describes what the checkbox controls. Clicking on this text also works for toggling.
- **Box**: the box which shows the current state. Contains a check if the state is `true`.
- **Native element**: the native HTML element which holds the state. It remains hidden to the user.
## Usage Guidelines
### When to Use
- For boolean settings that take effect immediately.
- In preference panels and configuration screens.
### When Not to Use
- For actions that require confirmation (use buttons instead).
- For multiple choice selections (use radio buttons or select).
- For temporary states that need explicit "Apply" action.
- For ternary states.

View File

@@ -0,0 +1,86 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as *;
.checkbox {
--input-checkbox-border-color: var(--color-foreground-secondary);
--input-checkbox-border-color-focus: var(--color-accent-primary);
--input-checkbox-border-color-hover: var(--color-accent-primary-muted);
--input-checkbox-foreground-color: var(--color-foreground-primary);
--input-checkbox-background-color: var(--color-background-quaternary);
--input-checkbox-border-color-checked: var(--color-background-quaternary);
--input-checkbox-foreground-color-checked: var(--color-background-primary);
--input-checkbox-background-color-checked: var(--color-accent-primary);
--input-checkbox-foreground-color-disabled: var(--color-background-primary);
--input-checkbox-background-color-disabled: var(--color-foreground-secondary);
--input-checkbox-text-color: var(--color-foreground-secondary);
}
.checkbox-label {
display: grid;
grid-template-columns: var(--sp-l) 1fr 0;
align-items: center;
&:hover {
.checkbox-box {
border-color: var(--input-checkbox-border-color-hover);
}
}
&:focus,
&:focus-within {
.checkbox-box {
border-color: var(--input-checkbox-border-color-focus);
}
}
}
.checkbox-box {
display: flex;
align-items: center;
justify-content: center;
inline-size: $sz-16;
block-size: $sz-16;
border-radius: $br-4;
border: $b-1 solid var(--input-checkbox-border-color);
color: var(--input-checkbox-foreground-color);
background-color: var(--input-checkbox-background-color);
&.disabled {
border: 0;
}
&.checked {
--input-checkbox-border-color: var(--input-checkbox-border-color-checked);
--input-checkbox-foreground-color: var(--input-checkbox-foreground-color-checked);
--input-checkbox-background-color: var(--input-checkbox-background-color-checked);
&.disabled {
--input-checkbox-foreground-color: var(--input-checkbox-foreground-color-disabled);
--input-checkbox-background-color: var(--input-checkbox-background-color-disabled);
}
}
}
.checkbox-text {
@include use-typography("body-small");
padding-inline-start: var(--sp-s);
color: var(--input-checkbox-text-color);
}
.checkbox-input {
&:focus {
outline: 0;
inline-size: 0;
block-size: 0;
}
}

View File

@@ -0,0 +1,76 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
import * as React from "react";
import Components from "@target/components";
const { Checkbox } = Components;
export default {
title: "Controls/Checkbox",
component: Checkbox,
argTypes: {
label: {
control: { type: "text" },
description: "Label text displayed next to the checkbox",
},
checked: {
control: { type: "boolean" },
description: "Whether the checkbox is checked",
},
disabled: {
control: { type: "boolean" },
description: "Whether the checkbox is disabled",
},
},
args: {
checked: false,
disabled: false,
},
parameters: {
controls: {
exclude: ["id", "on-change"],
},
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const Default = {
args: {
label: "Toggle something",
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const Checked = {
args: {
label: "Toggle something",
checked: true,
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const WithoutLabel = {
args: {
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const WithLongLabel = {
args: {
label:
"This is a very long label that demonstrates how the checkbox component handles text wrapping and layout when the label content is extensive",
disabled: false,
},
render: ({ ...args }) => (
<div style={{ maxWidth: "300px" }}>
<Checkbox {...args} />
</div>
),
};

View File

@@ -26,14 +26,19 @@
[:max-length {:optional true} :int]
[:variant {:optional true} [:maybe [:enum "seamless" "dense" "comfortable"]]]
[:hint-message {:optional true} [:maybe :string]]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]])
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]
[:hint-formated {:optional true} :boolean]])
(mf/defc input*
{::mf/forward-ref true
::mf/schema schema:input}
[{:keys [id class label is-optional type max-length variant hint-message hint-type] :rest props} ref]
[{:keys [id class label is-optional type max-length variant hint-message hint-type hint-formated] :rest props} ref]
(let [id (or id (mf/use-id))
variant (d/nilv variant "dense")
hint-class (if (and (not= "error" hint-type)
hint-formated)
(stl/css :hint-formated)
"")
is-optional (d/nilv is-optional false)
type (d/nilv type "text")
max-length (d/nilv max-length max-input-length)
@@ -56,6 +61,7 @@
[:> input-field* props]
(when has-hint
[:> hint-message* {:id id
:class hint-class
:message hint-message
:type hint-type}])]))

View File

@@ -12,3 +12,7 @@
gap: var(--sp-xs);
inline-size: 100%;
}
.hint-formated {
white-space: pre;
}

View File

@@ -118,6 +118,7 @@
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not disabled
(swap! is-open* not))))

View File

@@ -53,10 +53,15 @@
"true")
:aria-describedby (when has-hint
(str id "-hint"))
:aria-labelledby tooltip-id
:type (d/nilv type "text")
:id id
:max-length (d/nilv max-length max-input-length)})
props (if (and aria-label (not (some? icon)))
(mf/spread-props props
{:aria-label aria-label})
(mf/spread-props props
{:aria-labelledby tooltip-id}))
inside-class (stl/css-case :input-wrapper true
:has-hint has-hint
:hint-type-hint (= hint-type "hint")

View File

@@ -300,6 +300,7 @@
"A collection of all icons"
(collect-icons))
(def ^:private ^:const icon-size-l 32)
(def ^:private ^:const icon-size-m 16)
(def ^:private ^:const icon-size-s 12)
@@ -308,7 +309,7 @@
[:class {:optional true} [:maybe :string]]
[:icon-id [:and :string [:fn #(contains? icon-list %)]]]
[:size {:optional true}
[:maybe [:enum "s" "m"]]]])
[:maybe [:enum "s" "m" "l"]]]])
(mf/defc icon*
{::mf/schema schema:icon}
@@ -317,10 +318,14 @@
{:class [class (stl/css :icon)]
:width icon-size-m
:height icon-size-m})
size-px (if (= size "s")
icon-size-s
icon-size-m)
offset (/ (- icon-size-m size-px) 2)]
size-px (cond (= size "l") icon-size-l
(= size "s") icon-size-s
:else icon-size-m)
offset (if (or (= size "s") (= size "m"))
(/ (- icon-size-m size-px) 2)
0)]
[:> :svg props
[:use {:href (dm/str "#icon-" icon-id)

View File

@@ -0,0 +1,29 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.product.empty-state
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[rumext.v2 :as mf]))
(def ^:private schema:empty-state
[:map
[:class {:optional true} :string]
[:icon [:and :string [:fn #(contains? icon-list %)]]]
[:text :string]])
(mf/defc empty-state*
{::mf/schema schema:empty-state}
[{:keys [class icon text] :rest props}]
(let [props (mf/spread-props props {:class [class (stl/css :group)]})]
[:> :div props
[:div {:class (stl/css :icon-wrapper)}
[:> icon* {:icon-id icon
:size "l"
:class (stl/css :icon)}]]
[:div {:class (stl/css :text)} text]]))

View File

@@ -0,0 +1,35 @@
{ /* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as EmptyState from "./empty_state.stories";
<Meta title="Product/EmptyState" />
# EmptyState
An indication to the user that there is no data to display in the current view; often includes instructions on what to do next.
<Canvas of={EmptyState.Default} />
## Technical notes
The icon and the text are mandatory, and a class is optional.
```clj
[:> empty-state* {:icon i/at
:text "This is an empty state"}]
```
## Usage guidelines (design)
### Where to use
Use in areas of the app where users can add data or elements, but where there is currently no available information.
### Interaction / Behavior
There is no interaction.

View File

@@ -0,0 +1,36 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/typography.scss" as t;
.group {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-m);
}
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
inline-size: $sz-48;
block-size: $sz-48;
}
.icon {
color: var(--color-foreground-secondary);
inline-size: $sz-32;
block-size: $sz-32;
}
.text {
@include t.use-typography("body-small");
text-align: center;
color: var(--color-foreground-secondary);
}

View File

@@ -0,0 +1,32 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
import * as React from "react";
import Components from "@target/components";
const { EmptyState } = Components;
const { icons } = Components.meta;
export default {
title: "Product/EmptyState",
component: Components.EmptyState,
argTypes: {
icon: {
options: icons,
control: { type: "select" },
},
text: {
control: { type: "text" },
},
},
args: {
icon: "help",
text: "This is an empty state",
},
render: ({ ...args }) => <EmptyState {...args} />,
};
export const Default = {};

View File

@@ -15,7 +15,7 @@
}
.swatch {
--border-color: var(--color-background-quaternary);
--border-color: var(--color-accent-primary-muted);
--border-radius: #{$br-4};
--border-color-active: var(--color-foreground-primary);
--border-color-active-inset: var(--color-background-primary);

View File

@@ -34,10 +34,9 @@
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value true))))
props
(mf/spread-props props {:on-change on-change
:default-value value})
:value value})
props
(if (and error touched?)

View File

@@ -12,10 +12,11 @@
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.inspect.attributes :refer [attributes*]]
[app.main.ui.inspect.code :refer [code*]]
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
@@ -223,17 +224,14 @@
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}
deprecated-icon/code]
[:span {:class (stl/css :placeholder-label)}
(tr "inspect.empty.select")]]
[:div {:class (stl/css :help-info)}
[:span {:class (stl/css :placeholder-icon)}
deprecated-icon/help]
[:span {:class (stl/css :placeholder-label)}
(tr "inspect.empty.help")]]
[:button {:class (stl/css :more-info-btn)
:on-click navigate-to-help}
(tr "inspect.empty.more-info")]])]))
[:*
[:div {:class (stl/css :empty)}
[:> empty-state* {:icon i/code
:text (tr "inspect.empty.select")}]
[:> empty-state* {:icon i/help
:text (tr "inspect.empty.help")}]]
[:div {:class (stl/css :empty-button)}
[:> button* {:variant "secondary"
:on-click navigate-to-help}
(tr "inspect.empty.more")]]])]))

View File

@@ -81,36 +81,14 @@
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: $sz-40;
padding-top: $sz-24;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}
.code-info,
.help-info {
@include deprecated.flexColumn;
align-items: center;
justify-content: flex-start;
gap: var(--sp-m);
margin-inline-end: var(--sp-s);
}
.placeholder-icon {
@extend .empty-icon;
}
.placeholder-label {
@include deprecated.bodySmallTypography;
text-align: center;
inline-size: px2rem(200);
color: var(--empty-message-foreground-color);
}
.more-info-btn {
@extend .button-secondary;
@include deprecated.uppercaseTitleTipography;
block-size: $sz-32;
padding: var(--sp-s) var(--sp-xxl);
.empty-button {
display: flex;
justify-content: center;
}
.inspect-tab-switcher {

View File

@@ -78,3 +78,9 @@
:settings-notifications
[:& notifications-page* {:profile profile}])]]]]))
(mf/defc settings-page*
{::mf/lazy-load true}
[props]
[:> settings* props])

View File

@@ -624,7 +624,6 @@
;; --- Component: Viewer
(mf/defc viewer*
{::mf/props :obj}
[{:keys [file-id share-id page-id] :as props}]
(mf/with-effect [file-id page-id share-id]
(let [params {:file-id file-id
@@ -643,3 +642,8 @@
[:> loader* {:title (tr "labels.loading")
:overlay true}]))
(mf/defc viewer-page*
{::mf/lazy-load true}
[props]
[:> viewer* props])

View File

@@ -35,7 +35,7 @@
[app.main.ui.workspace.tokens.export.modal]
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.create.modals]
[app.main.ui.workspace.tokens.management.forms.modals]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]
[app.main.ui.workspace.viewport :refer [viewport*]]
@@ -165,7 +165,7 @@
(dsh/lookup-page state file-id page-id))))
st/state))
(mf/defc workspace-page*
(mf/defc workspace-inner*
{::mf/private true}
[{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id]
@@ -252,10 +252,16 @@
:touch-action "none"}}
[:> context-menu*]
(if (and file-loaded? page-id)
[:> workspace-page*
[:> workspace-inner*
{:page-id page-id
:file-id file-id
:file file
:wglobal wglobal
:layout layout}]
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*
{::mf/lazy-load true}
[props]
[:> workspace* props])

View File

@@ -418,11 +418,11 @@
[:& radio-buttons {:selected color-style
:on-change toggle-token-color
:name "color-style"}
[:& radio-button {:icon deprecated-icon/swatches
[:& radio-button {:icon i/swatches
:value :direct-color
:title (tr "labels.color")
:id "opt-color"}]
[:& radio-button {:icon deprecated-icon/tokens
[:& radio-button {:icon i/tokens
:value :token-color
:title (tr "workspace.colorpicker.color-tokens")
:id "opt-token-color"}]])]

View File

@@ -18,6 +18,7 @@
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -160,6 +161,5 @@
:key (:page-id tgroup)}])]
[:div {:class (stl/css :thread-group-placeholder)}
[:span {:class (stl/css :placeholder-icon)} deprecated-icon/comments]
[:span {:class (stl/css :placeholder-label)}
(tr "labels.no-comments-available")]])]]))
[:> empty-state* {:icon i/comments
:text (tr "labels.no-comments-available")}]])]]))

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.comments-section {
@@ -128,29 +129,9 @@
}
.thread-group-placeholder {
@include deprecated.flexColumn;
align-items: center;
justify-content: flex-start;
margin-top: deprecated.$s-36;
}
.placeholder-icon {
@include deprecated.flexCenter;
height: deprecated.$s-48;
width: deprecated.$s-48;
border-radius: deprecated.$br-circle;
background-color: var(--empty-message-background-color);
svg {
@extend .button-icon;
height: deprecated.$s-28;
width: deprecated.$s-28;
stroke: var(--empty-message-foreground-color);
}
}
.placeholder-label {
@include deprecated.bodySmallTypography;
text-align: center;
width: deprecated.$s-184;
color: var(--empty-message-foreground-color);
display: flex;
flex-direction: column;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}

View File

@@ -33,6 +33,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.util.color :as uc]
@@ -49,9 +50,6 @@
(def ^:private add-icon
(deprecated-icon/icon-xref :add (stl/css :add-icon)))
(def ^:private library-icon
(deprecated-icon/icon-xref :library (stl/css :library-icon)))
(defn- get-library-summary
"Given a library data return a summary representation of this library"
[data]
@@ -493,9 +491,8 @@
[:div {:class (stl/css :update-section)}
(if (empty? libs-assets)
[:div {:class (stl/css :section-list-empty)}
[:span {:class (stl/css :empty-state-icon)}
library-icon]
(tr "workspace.libraries.no-libraries-need-sync")]
[:> empty-state* {:icon i/library
:text (tr "workspace.libraries.no-libraries-need-sync")}]]
[:*
[:div {:class (stl/css :section-title)} (tr "workspace.libraries.library-updates")]

View File

@@ -192,7 +192,6 @@
// empty state
.section-list-empty {
@include t.use-typography("body-medium");
display: grid;
grid-template-rows: auto 1fr;
justify-items: center;
@@ -200,17 +199,6 @@
text-align: center;
height: fit-content;
margin-block: var(--sp-l);
color: var(--modal-title-foreground-color);
}
.empty-state-icon {
display: flex;
justify-content: center;
align-items: center;
width: $sz-48;
height: $sz-48;
border-radius: $br-circle;
background-color: var(--pill-background-color);
}
.library-icon {

View File

@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.thumbnails :as thc]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
@@ -38,7 +37,6 @@
[app.util.i18n :as i18n :refer [c tr]]
[app.util.strings :refer [matches-search]]
[app.util.timers :as ts]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -97,10 +95,6 @@
(str (str/slice (:path asset) (count path)))
(cpn/merge-path-item (:name asset))))
(s/def ::asset-name ::us/not-empty-string)
(s/def ::name-group-form
(s/keys :req-un [::asset-name]))
(def initial-context-menu-state
{:open? false :top nil :left nil})

View File

@@ -28,7 +28,6 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.util.dom :as dom]
@@ -567,11 +566,11 @@
[:& radio-buttons {:selected (if is-listing-thumbs "grid" "list")
:on-change toggle-list-style
:name "listing-style"}
[:& radio-button {:icon deprecated-icon/view-as-list
[:& radio-button {:icon i/view-as-list
:value "list"
:title (tr "workspace.assets.list-view")
:id "opt-list"}]
[:& radio-button {:icon deprecated-icon/flex-grid
[:& radio-button {:icon i/flex-grid
:value "grid"
:title (tr "workspace.assets.grid-view")
:id "opt-grid"}]]])

View File

@@ -12,6 +12,8 @@
[app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :refer [tr] :as i18n]
@@ -327,8 +329,8 @@
[:div {:class (stl/css :history-toolbox)}
(if (empty? entries)
[:div {:class (stl/css :history-entry-empty)}
[:div {:class (stl/css :history-entry-empty-icon)} deprecated-icon/history]
[:div {:class (stl/css :history-entry-empty-msg)} (tr "workspace.undo.empty")]]
[:> empty-state* {:icon i/history
:text (tr "workspace.undo.empty")}]]
[:ul {:class (stl/css :history-entries)}
(for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)]
[:& history-entry {:key (str "entry-" idx-entry)

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.history-toolbox {
@@ -30,23 +31,11 @@
}
.history-entry-empty {
@include deprecated.flexCenter;
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
padding: deprecated.$s-28 deprecated.$s-16;
text-align: center;
}
.history-entry-empty-icon {
@extend .empty-icon;
svg {
margin-left: calc(-1 * deprecated.$s-2);
}
}
.history-entry-empty-msg {
@include deprecated.bodySmallTypography;
color: var(--empty-message-foreground-color);
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}
.history-entries {

View File

@@ -25,7 +25,7 @@
[app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options*]]
[app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu*]]
[app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell]
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]]
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu*]]
[app.main.ui.workspace.sidebar.options.menus.layout-container :as layout-container]
[app.main.ui.workspace.sidebar.options.page :as page]
[app.main.ui.workspace.sidebar.options.shapes.bool :as bool]
@@ -215,7 +215,7 @@
(case options-mode
:prototype
[:div {:class (stl/css :element-options :interaction-options)}
[:& interactions-menu {:shape (first shapes)}]]
[:> interactions-menu* {:shape (first shapes)}]]
:inspect
[:div {:class (stl/css :element-options :inspect-options)}

View File

@@ -13,6 +13,7 @@
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -99,10 +100,10 @@
:name "frame-orientation"
:wide true
:class (stl/css :radio-buttons)}
[:& radio-button {:icon deprecated-icon/size-vertical
[:& radio-button {:icon i/size-vertical
:value "vertical"
:id "size-vertical"}]
[:& radio-button {:icon deprecated-icon/size-horizontal
[:& radio-button {:icon i/size-horizontal
:value "horizontal"
:id "size-horizontal"}]]]))

View File

@@ -16,6 +16,7 @@
[app.main.features :as features]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -78,22 +79,22 @@
:class (stl/css :boolean-radio-btn)
:on-change on-change
:name "bool-options"}
[:& radio-button {:icon deprecated-icon/boolean-union
[:& radio-button {:icon i/boolean-union
:value "union"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")")
:id "bool-opt-union"}]
[:& radio-button {:icon deprecated-icon/boolean-difference
[:& radio-button {:icon i/boolean-difference
:value "difference"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")")
:id "bool-opt-differente"}]
[:& radio-button {:icon deprecated-icon/boolean-intersection
[:& radio-button {:icon i/boolean-intersection
:value "intersection"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")")
:id "bool-opt-intersection"}]
[:& radio-button {:icon deprecated-icon/boolean-exclude
[:& radio-button {:icon i/boolean-exclude
:value "exclude"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")")

View File

@@ -42,7 +42,6 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.options.menus.variants-help-modal]
[app.util.debug :as dbg]
@@ -798,10 +797,10 @@
[:& radio-buttons {:selected (if (:listing-thumbs? filters) "grid" "list")
:on-change toggle-list-style
:name "swap-listing-style"}
[:& radio-button {:icon deprecated-icon/view-as-list
[:& radio-button {:icon i/view-as-list
:value "list"
:id "swap-opt-list"}]
[:& radio-button {:icon deprecated-icon/flex-grid
[:& radio-button {:icon i/flex-grid
:value "grid"
:id "swap-opt-grid"}]]]

View File

@@ -206,7 +206,7 @@
:data-value "bottom"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]]]
[:div {:class (stl/css :contraints-selects)}
[:div {:class (stl/css :constraints-selects)}
[:div {:class (stl/css :horizontal-select) :data-testid "constraint-h-select"}
[:& select
{:default-value (if (not= constraints-h :multiple) (d/nilv (d/name constraints-h) "scale") "")

View File

@@ -120,7 +120,9 @@
}
.constraints-selects {
@include deprecated.flexColumn;
display: flex;
flex-direction: column;
gap: 4px;
}
.horizontal-select,

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