mirror of
https://github.com/penpot/penpot.git
synced 2026-01-03 11:58:46 -05:00
Compare commits
93 Commits
niwinz-dev
...
tokens-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b786b8e07a | ||
|
|
e8ff89836e | ||
|
|
8a8f360c7f | ||
|
|
e35fc85c3d | ||
|
|
a614207f7e | ||
|
|
1798461d21 | ||
|
|
6ce3249c6d | ||
|
|
dde0fddd6f | ||
|
|
4637aced8c | ||
|
|
9dfe5b0865 | ||
|
|
33bcc9544a | ||
|
|
babd481b7f | ||
|
|
a9733c792d | ||
|
|
b0351be724 | ||
|
|
d04fdb5fbd | ||
|
|
81e0e4f222 | ||
|
|
b8392b3731 | ||
|
|
77dba477ca | ||
|
|
b6598d1f07 | ||
|
|
f13b3c8737 | ||
|
|
a0f8559ffc | ||
|
|
bf1dc21c75 | ||
|
|
46c20a993f | ||
|
|
0e0106f69a | ||
|
|
19bb69cc60 | ||
|
|
504eb70988 | ||
|
|
75a2331edf | ||
|
|
c2b4c9907d | ||
|
|
bd5bbcae26 | ||
|
|
84273508ad | ||
|
|
9245ba6bc2 | ||
|
|
4be046406d | ||
|
|
84c747cd31 | ||
|
|
0036a9a0cd | ||
|
|
2105c3a68c | ||
|
|
38efa88460 | ||
|
|
6e254c2cf4 | ||
|
|
416980f063 | ||
|
|
f76710296c | ||
|
|
6251fa6b22 | ||
|
|
aedd8cc11e | ||
|
|
d1379c55f6 | ||
|
|
b125c7b5a3 | ||
|
|
496d37795b | ||
|
|
2f0853f5cc | ||
|
|
648e660bcf | ||
|
|
9f6899007a | ||
|
|
bee2f70bfa | ||
|
|
00f8eac8fa | ||
|
|
df7caacb45 | ||
|
|
641df77834 | ||
|
|
49bbdfb257 | ||
|
|
94af978be8 | ||
|
|
feababe2a8 | ||
|
|
5ef06685fc | ||
|
|
57fcec5afc | ||
|
|
58f82da61e | ||
|
|
a28c5b61ca | ||
|
|
53aad7bc15 | ||
|
|
9123d199b7 | ||
|
|
57297741f5 | ||
|
|
eeaf28bb25 | ||
|
|
d63d692d34 | ||
|
|
6b8091bb90 | ||
|
|
fe72d0af82 | ||
|
|
ef68081d1d | ||
|
|
bba02473d5 | ||
|
|
4ed49cdc5d | ||
|
|
50f9eedcdf | ||
|
|
77c9d8a2c8 | ||
|
|
95b7784a42 | ||
|
|
4690f740b9 | ||
|
|
529c4eb38a | ||
|
|
c3a9919c4d | ||
|
|
efe74e62e8 | ||
|
|
10a2732a55 | ||
|
|
456afe46de | ||
|
|
964ef799c2 | ||
|
|
d34b6b88b6 | ||
|
|
9a58f0e954 | ||
|
|
adaf8be56d | ||
|
|
2f1b99fa53 | ||
|
|
5080fcc594 | ||
|
|
ea2d3758f0 | ||
|
|
40e3617138 | ||
|
|
b18c421415 | ||
|
|
e7029f2182 | ||
|
|
2c3becb408 | ||
|
|
94c15916e2 | ||
|
|
ed0f3c3595 | ||
|
|
a4e6aa0588 | ||
|
|
c2014a37b4 | ||
|
|
6611fbd13b |
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: _NITRATE MODULE
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
1
.github/workflows/build-tag.yml
vendored
1
.github/workflows/build-tag.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
|
||||
notify:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
|
||||
steps:
|
||||
|
||||
74
.github/workflows/tests.yml
vendored
74
.github/workflows/tests.yml
vendored
@@ -20,7 +20,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: "Code Linter"
|
||||
name: "Linter"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
@@ -30,10 +30,7 @@ jobs:
|
||||
|
||||
- name: Check clojure code format
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
./scripts/lint
|
||||
|
||||
test-common:
|
||||
name: "Common Tests"
|
||||
@@ -52,10 +49,7 @@ jobs:
|
||||
- name: Run tests on NODE
|
||||
working-directory: ./common
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
./scripts/test
|
||||
|
||||
test-frontend:
|
||||
name: "Frontend Tests"
|
||||
@@ -69,25 +63,12 @@ jobs:
|
||||
- name: Unit Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
./scripts/test
|
||||
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
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"
|
||||
|
||||
- name: Check SCSS Format
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run lint:scss;
|
||||
./scripts/test-components
|
||||
|
||||
test-render-wasm:
|
||||
name: "Render WASM Tests"
|
||||
@@ -164,11 +145,7 @@ jobs:
|
||||
- name: Run tests
|
||||
working-directory: ./library
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run build:bundle;
|
||||
yarn run test;
|
||||
./scripts/test
|
||||
|
||||
build-integration:
|
||||
name: "Build Integration Bundle"
|
||||
@@ -182,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
|
||||
@@ -200,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
|
||||
@@ -219,11 +187,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard="1/4";
|
||||
./scripts/test-e2e --shard="1/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -253,11 +217,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "2/4";
|
||||
./scripts/test-e2e --shard="2/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -287,11 +247,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "3/4";
|
||||
./scripts/test-e2e --shard="3/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -303,7 +259,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
test-integration-4:
|
||||
name: "Integration Tests 3/4"
|
||||
name: "Integration Tests 4/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
@@ -321,11 +277,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "4/4";
|
||||
./scripts/test-e2e --shard="4/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -80,3 +80,4 @@ node_modules
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/.yarn/*
|
||||
/.pnpm-store
|
||||
|
||||
14
CHANGES.md
14
CHANGES.md
@@ -8,10 +8,18 @@
|
||||
|
||||
### :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)
|
||||
|
||||
@@ -74,6 +82,7 @@ example. It's still usable as before, we just removed the example.
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
|
||||
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@@ -100,6 +109,11 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
|
||||
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
|
||||
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
|
||||
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
|
||||
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
|
||||
@@ -106,17 +106,17 @@
|
||||
(let [content-part (MimeBodyPart.)
|
||||
alternative-mpart (MimeMultipart. "alternative")]
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(when-let [content (get body "text/html")]
|
||||
(let [html-part (MimeBodyPart.)]
|
||||
(.setContent html-part ^String content
|
||||
(str "text/html; charset=" charset))
|
||||
(.addBodyPart alternative-mpart html-part)))
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(.setContent content-part alternative-mpart)
|
||||
(.addBodyPart mixed-mpart content-part))
|
||||
|
||||
|
||||
@@ -79,18 +79,6 @@
|
||||
(remove #(contains? reserved-props (key %))))
|
||||
props))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context {:external-session-id (::rpc/external-session-id params)
|
||||
:external-event-origin (::rpc/external-event-origin params)
|
||||
:triggered-by (::rpc/handler-name params)}]
|
||||
{::type "action"
|
||||
::profile-id (::rpc/profile-id params)
|
||||
::ip-addr (::rpc/ip-addr params)
|
||||
::context (d/without-nils context)}))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
@@ -99,13 +87,24 @@
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-external-event-origin
|
||||
(defn- get-client-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
(str/prune origin 200))))
|
||||
|
||||
(defn get-client-user-agent
|
||||
[request]
|
||||
(when-let [user-agent (yreq/get-header request "user-agent")]
|
||||
(str/prune user-agent 500)))
|
||||
|
||||
(defn- get-client-version
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-frontend-version")]
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
(str/prune origin 100))))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -134,6 +133,33 @@
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
token-id (::actoken/id request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:access-token-id (some-> token-id str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
@@ -148,18 +174,10 @@
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id
|
||||
(get-external-session-id request))
|
||||
(assoc :external-event-origin
|
||||
(get-external-event-origin request))
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
context (merge (::context resultm)
|
||||
(prepare-context-from-request request))
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
|
||||
7
common/scripts/test
Executable file
7
common/scripts/test
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
@@ -378,7 +379,7 @@
|
||||
[:type [:= :set-token]]
|
||||
[:set-id ::sm/uuid]
|
||||
[:token-id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-attrs]]]]
|
||||
[:attrs [:maybe cto/schema:token-attrs]]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
|
||||
@@ -8,9 +8,112 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.i18n :refer [tr]]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HIGH LEVEL SCHEMAS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Token
|
||||
|
||||
(defn make-token-name-schema
|
||||
"Dynamically generates a schema to check a token name, adding translated error messages
|
||||
and two additional validations:
|
||||
- Min and max length.
|
||||
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
|
||||
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
|
||||
[tokens-tree]
|
||||
[:and
|
||||
(-> cto/schema:token-name
|
||||
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (ctob/token-name-path-exists? % tokens-tree))]])
|
||||
|
||||
(def schema:token-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-schema
|
||||
[tokens-tree]
|
||||
(sm/merge
|
||||
cto/schema:token-attrs
|
||||
[:map
|
||||
[:name (make-token-name-schema tokens-tree)]
|
||||
[:description {:optional true} schema:token-description]]))
|
||||
|
||||
;; Token set
|
||||
|
||||
(defn make-token-set-name-schema
|
||||
"Generates a dynamic schema to check a token set name:
|
||||
- Validate name length.
|
||||
- Checks if other token set with a path derived from the name already exists in the tokens lib."
|
||||
[tokens-lib set-id]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
|
||||
(fn [name]
|
||||
(let [set (ctob/get-set-by-name tokens-lib name)]
|
||||
(or (nil? set) (= (ctob/get-id set) set-id))))]])
|
||||
|
||||
(def schema:token-set-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-set-schema
|
||||
[tokens-lib set-id]
|
||||
(sm/merge
|
||||
ctob/schema:token-set-attrs
|
||||
[:map
|
||||
[:name [:and (make-token-set-name-schema tokens-lib set-id)
|
||||
[:fn #(ctob/normalized-set-name? %)]]]
|
||||
[:description {:optional true} schema:token-set-description]]))
|
||||
|
||||
;; Token theme
|
||||
|
||||
(defn make-token-theme-group-schema
|
||||
"Generates a dynamic schema to check a token theme group:
|
||||
- Validate group length.
|
||||
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
|
||||
[tokens-lib name theme-id]
|
||||
[:and
|
||||
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
|
||||
(fn [group]
|
||||
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
|
||||
(or (nil? theme) (= (:id theme) theme-id))))]])
|
||||
|
||||
(defn make-token-theme-name-schema
|
||||
"Generates a dynamic schema to check a token theme name:
|
||||
- Validate name length.
|
||||
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
|
||||
[tokens-lib group theme-id]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
|
||||
(fn [name]
|
||||
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
|
||||
(or (nil? theme) (= (:id theme) theme-id))))]])
|
||||
|
||||
(def schema:token-theme-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
(defn make-token-theme-schema
|
||||
[tokens-lib group name theme-id]
|
||||
(sm/merge
|
||||
ctob/schema:token-theme-attrs
|
||||
[:map
|
||||
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
|
||||
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
|
||||
[:description {:optional true} schema:token-theme-description]]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def parseable-token-value-regexp
|
||||
"Regexp that can be used to parse a number value out of resolved token value.
|
||||
This regexp also trims whitespace around the value."
|
||||
@@ -80,56 +183,6 @@
|
||||
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
|
||||
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
|
||||
|
||||
(defn token-name->path
|
||||
"Splits token-name into a path vector split by `.` characters.
|
||||
|
||||
Will concatenate multiple `.` characters into one."
|
||||
[token-name]
|
||||
(str/split token-name #"\.+"))
|
||||
|
||||
(defn token-name->path-selector
|
||||
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
|
||||
|
||||
`:selector` is the last item of the names path
|
||||
`:path` is everything leading up the the `:selector`."
|
||||
[token-name]
|
||||
(let [path-segments (token-name->path token-name)
|
||||
last-idx (dec (count path-segments))
|
||||
[path [selector]] (split-at last-idx path-segments)]
|
||||
{:path (seq path)
|
||||
:selector selector}))
|
||||
|
||||
(defn token-name-path-exists?
|
||||
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
|
||||
|
||||
It's not allowed to create a token inside a token. E.g.:
|
||||
Creating a token with
|
||||
|
||||
{:name \"foo.bar\"}
|
||||
|
||||
in the tokens tree:
|
||||
|
||||
{\"foo\" {:name \"other\"}}"
|
||||
[token-name token-names-tree]
|
||||
(let [{:keys [path selector]} (token-name->path-selector token-name)
|
||||
path-target (reduce
|
||||
(fn [acc cur]
|
||||
(let [target (get acc cur)]
|
||||
(cond
|
||||
;; Path segment doesn't exist yet
|
||||
(nil? target) (reduced false)
|
||||
;; A token exists at this path
|
||||
(:name target) (reduced true)
|
||||
;; Continue traversing the true
|
||||
:else target)))
|
||||
token-names-tree path)]
|
||||
(cond
|
||||
(boolean? path-target) path-target
|
||||
(get path-target :name) true
|
||||
:else (-> (get path-target selector)
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
(defn color-token? [token]
|
||||
(= (:type token) :color))
|
||||
|
||||
|
||||
15
common/src/app/common/i18n.cljc
Normal file
15
common/src/app/common/i18n.cljc
Normal file
@@ -0,0 +1,15 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.i18n
|
||||
"Dummy i18n functions, to be used by code in common that needs translations.")
|
||||
|
||||
(defn tr
|
||||
"This function will be monkeypatched at runtime with the real function in frontend i18n.
|
||||
Here it just returns the key passed as argument. This way the result can be used in
|
||||
unit tests or backend code for logs or error messages."
|
||||
[key & _args]
|
||||
key)
|
||||
@@ -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]
|
||||
|
||||
@@ -12,13 +12,15 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
[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]
|
||||
@@ -26,6 +28,7 @@
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
@@ -35,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)
|
||||
@@ -473,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"
|
||||
@@ -510,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"
|
||||
@@ -1876,6 +1878,44 @@
|
||||
roperations'
|
||||
uoperations')))))))
|
||||
|
||||
(defn- set-path-new-values
|
||||
[current-shape prev-shape transform]
|
||||
(let [new-content (segment/transform-content
|
||||
(:content current-shape)
|
||||
(gmt/transform-in (gpt/point 0 0) transform))
|
||||
new-points (-> (segment/content->selrect new-content)
|
||||
(grc/rect->points))
|
||||
points-center (gco/points->center new-points)
|
||||
new-selrect (gsh/calculate-selrect new-points points-center)
|
||||
shape (assoc current-shape
|
||||
:content new-content
|
||||
:points new-points
|
||||
:selrect new-selrect)
|
||||
|
||||
prev-center (segment/content-center (:content prev-shape))
|
||||
delta (gpt/subtract points-center (first new-points))
|
||||
new-pos (gpt/subtract prev-center delta)]
|
||||
(gsh/absolute-move shape new-pos)))
|
||||
|
||||
(defn- switch-path-change-value
|
||||
[prev-shape ;; The shape before the switch
|
||||
current-shape ;; The shape after the switch (a clean copy)
|
||||
ref-shape ;; The referenced shape on the main component
|
||||
;; before the switch
|
||||
attr]
|
||||
(let [old-width (-> ref-shape :selrect :width)
|
||||
new-width (-> prev-shape :selrect :width)
|
||||
|
||||
old-height (-> ref-shape :selrect :height)
|
||||
new-height (-> prev-shape :selrect :height)
|
||||
|
||||
transform (-> (gpt/point (/ new-width old-width)
|
||||
(/ new-height old-height))
|
||||
(gmt/scale-matrix))
|
||||
|
||||
shape (set-path-new-values current-shape prev-shape transform)]
|
||||
(get shape attr)))
|
||||
|
||||
|
||||
(defn- switch-text-change-value
|
||||
[prev-content ;; The :content of the text before the switch
|
||||
@@ -2027,6 +2067,10 @@
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
path-change?
|
||||
(and (= :path (:type current-shape))
|
||||
(contains? #{:points :selrect :content} attr))
|
||||
|
||||
;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
;; so it's calculated again
|
||||
@@ -2055,6 +2099,12 @@
|
||||
(:content origin-ref-shape)
|
||||
touched)
|
||||
|
||||
path-change?
|
||||
(switch-path-change-value previous-shape
|
||||
current-shape
|
||||
origin-ref-shape
|
||||
attr)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
@@ -2441,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))]
|
||||
@@ -2815,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)
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
@@ -281,7 +301,20 @@
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [s (schema s)
|
||||
(let [s #?(:clj
|
||||
(schema s)
|
||||
:cljs
|
||||
(try
|
||||
(schema s)
|
||||
(catch :default cause
|
||||
(let [data (ex-data cause)]
|
||||
(if (= :malli.core/invalid-schema (:type data))
|
||||
(throw (ex-info
|
||||
(str "Invalid schema\n"
|
||||
(pp/pprint-str (:data data)))
|
||||
{}))
|
||||
(throw cause))))))
|
||||
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
@@ -299,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.
|
||||
@@ -993,6 +1033,9 @@
|
||||
(def valid-safe-number?
|
||||
(lazy-validator ::safe-number))
|
||||
|
||||
(def valid-safe-int?
|
||||
(lazy-validator ::safe-int))
|
||||
|
||||
(def valid-text?
|
||||
(validator ::text))
|
||||
|
||||
|
||||
@@ -267,3 +267,4 @@
|
||||
(-> (stp/convert-to-path shape objects)
|
||||
(update :content impl/path-data))))
|
||||
|
||||
(dm/export impl/decode-segments)
|
||||
|
||||
@@ -565,6 +565,9 @@
|
||||
(def check-content
|
||||
(sm/check-fn schema:content))
|
||||
|
||||
(def decode-segments
|
||||
(sm/lazy-decoder schema:segments sm/json-transformer))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CONSTRUCTORS & PREDICATES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[clojure.data :as data]
|
||||
[app.common.time :as ct]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[malli.util :as mu]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;; GENERAL HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- schema-keys
|
||||
@@ -45,32 +45,33 @@
|
||||
[token-name token-value]
|
||||
(let [token-references (find-token-value-references token-value)
|
||||
self-reference? (get token-references token-name)]
|
||||
self-reference?))
|
||||
(boolean self-reference?)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(defn composite-token-reference?
|
||||
"Predicate if a composite token is a reference value - a string pointing to another token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
(def token-type->dtcg-token-type
|
||||
{:boolean "boolean"
|
||||
:border-radius "borderRadius"
|
||||
:shadow "shadow"
|
||||
:color "color"
|
||||
:dimensions "dimension"
|
||||
:font-family "fontFamilies"
|
||||
:font-size "fontSizes"
|
||||
:font-weight "fontWeights"
|
||||
:letter-spacing "letterSpacing"
|
||||
:number "number"
|
||||
:opacity "opacity"
|
||||
:other "other"
|
||||
:rotation "rotation"
|
||||
:shadow "shadow"
|
||||
:sizing "sizing"
|
||||
:spacing "spacing"
|
||||
:string "string"
|
||||
:stroke-width "borderWidth"
|
||||
:text-case "textCase"
|
||||
:text-decoration "textDecoration"
|
||||
:font-weight "fontWeights"
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
@@ -82,14 +83,13 @@
|
||||
"boxShadow" :shadow)))
|
||||
|
||||
(def composite-token-type->dtcg-token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"When converting the type of one element inside a composite token, an additional type
|
||||
:line-height is available, that is not allowed for a standalone token."
|
||||
(assoc token-type->dtcg-token-type
|
||||
:line-height "lineHeights"))
|
||||
|
||||
(def composite-dtcg-token-type->token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"Same as above, in the opposite direction."
|
||||
(assoc dtcg-token-type->token-type
|
||||
"lineHeights" :line-height
|
||||
"lineHeight" :line-height))
|
||||
@@ -97,93 +97,95 @@
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA: Token
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:token-name
|
||||
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
||||
not start with $ or end with a dot. The $ character does not have any special meaning,
|
||||
but dots separate token groups (e.g. color.primary.background)."
|
||||
[:re {:title "TokenName"
|
||||
:gen/gen sg/text}
|
||||
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:fill {:optional true} token-name-ref]
|
||||
[:stroke-color {:optional true} token-name-ref]])
|
||||
(def schema:token-type
|
||||
[::sm/one-of {:decode/json (fn [type]
|
||||
(if (string? type)
|
||||
(dtcg-token-type->token-type type)
|
||||
type))}
|
||||
|
||||
(def color-keys (schema-keys schema:color))
|
||||
token-types])
|
||||
|
||||
(def schema:token-attrs
|
||||
[:map {:title "Token"}
|
||||
[:id ::sm/uuid]
|
||||
[:name schema:token-name]
|
||||
[:type schema:token-type]
|
||||
[:value ::sm/any]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA: Token application to shape
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; All the following schemas define the `:applied-tokens` attribute of a shape.
|
||||
;; This attribute is a map <token-attribute> -> <token-name>.
|
||||
;; Token attributes approximately match shape attributes, but not always.
|
||||
;; For each schema there is a `*keys` set including all the possible token attributes
|
||||
;; to which a token of the corresponding type can be applied.
|
||||
;; Some token types can be applied to some attributes only if the shape has a
|
||||
;; particular condition (i.e. has a layout itself or is a layout item).
|
||||
|
||||
(def ^:private schema:border-radius
|
||||
[:map {:title "BorderRadiusTokenAttrs"}
|
||||
[:r1 {:optional true} token-name-ref]
|
||||
[:r2 {:optional true} token-name-ref]
|
||||
[:r3 {:optional true} token-name-ref]
|
||||
[:r4 {:optional true} token-name-ref]])
|
||||
[:r1 {:optional true} schema:token-name]
|
||||
[:r2 {:optional true} schema:token-name]
|
||||
[:r3 {:optional true} schema:token-name]
|
||||
[:r4 {:optional true} schema:token-name]])
|
||||
|
||||
(def border-radius-keys (schema-keys schema:border-radius))
|
||||
|
||||
(def ^:private schema:shadow
|
||||
[:map {:title "ShadowTokenAttrs"}
|
||||
[:shadow {:optional true} token-name-ref]])
|
||||
|
||||
(def shadow-keys (schema-keys schema:shadow))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:stroke-width {:optional true} token-name-ref]])
|
||||
[:fill {:optional true} schema:token-name]
|
||||
[:stroke-color {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
(def color-keys (schema-keys schema:color))
|
||||
|
||||
(def ^:private schema:sizing-base
|
||||
[:map {:title "SizingBaseTokenAttrs"}
|
||||
[:width {:optional true} token-name-ref]
|
||||
[:height {:optional true} token-name-ref]])
|
||||
[:width {:optional true} schema:token-name]
|
||||
[:height {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:sizing-layout-item
|
||||
[:map {:title "SizingLayoutItemTokenAttrs"}
|
||||
[:layout-item-min-w {:optional true} token-name-ref]
|
||||
[:layout-item-max-w {:optional true} token-name-ref]
|
||||
[:layout-item-min-h {:optional true} token-name-ref]
|
||||
[:layout-item-max-h {:optional true} token-name-ref]])
|
||||
[:layout-item-min-w {:optional true} schema:token-name]
|
||||
[:layout-item-max-w {:optional true} schema:token-name]
|
||||
[:layout-item-min-h {:optional true} schema:token-name]
|
||||
[:layout-item-max-h {:optional true} schema:token-name]])
|
||||
|
||||
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
|
||||
|
||||
(def ^:private schema:sizing
|
||||
(-> (reduce mu/union [schema:sizing-base
|
||||
schema:sizing-layout-item])
|
||||
(mu/update-properties assoc :title "SizingTokenAttrs")))
|
||||
|
||||
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
|
||||
|
||||
(def sizing-keys (schema-keys schema:sizing))
|
||||
|
||||
(def ^:private schema:opacity
|
||||
[:map {:title "OpacityTokenAttrs"}
|
||||
[:opacity {:optional true} token-name-ref]])
|
||||
|
||||
(def opacity-keys (schema-keys schema:opacity))
|
||||
|
||||
(def ^:private schema:spacing-gap
|
||||
[:map {:title "SpacingGapTokenAttrs"}
|
||||
[:row-gap {:optional true} token-name-ref]
|
||||
[:column-gap {:optional true} token-name-ref]])
|
||||
[:row-gap {:optional true} schema:token-name]
|
||||
[:column-gap {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:spacing-padding
|
||||
[:map {:title "SpacingPaddingTokenAttrs"}
|
||||
[:p1 {:optional true} token-name-ref]
|
||||
[:p2 {:optional true} token-name-ref]
|
||||
[:p3 {:optional true} token-name-ref]
|
||||
[:p4 {:optional true} token-name-ref]])
|
||||
|
||||
(def ^:private schema:spacing-margin
|
||||
[:map {:title "SpacingMarginTokenAttrs"}
|
||||
[:m1 {:optional true} token-name-ref]
|
||||
[:m2 {:optional true} token-name-ref]
|
||||
[:m3 {:optional true} token-name-ref]
|
||||
[:m4 {:optional true} token-name-ref]])
|
||||
|
||||
(def ^:private schema:spacing
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
schema:spacing-padding
|
||||
schema:spacing-margin])
|
||||
(mu/update-properties assoc :title "SpacingTokenAttrs")))
|
||||
|
||||
(def spacing-margin-keys (schema-keys schema:spacing-margin))
|
||||
|
||||
(def spacing-keys (schema-keys schema:spacing))
|
||||
[:p1 {:optional true} schema:token-name]
|
||||
[:p2 {:optional true} schema:token-name]
|
||||
[:p3 {:optional true} schema:token-name]
|
||||
[:p4 {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:spacing-gap-padding
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
@@ -192,6 +194,29 @@
|
||||
|
||||
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
|
||||
|
||||
(def ^:private schema:spacing-margin
|
||||
[:map {:title "SpacingMarginTokenAttrs"}
|
||||
[:m1 {:optional true} schema:token-name]
|
||||
[:m2 {:optional true} schema:token-name]
|
||||
[:m3 {:optional true} schema:token-name]
|
||||
[:m4 {:optional true} schema:token-name]])
|
||||
|
||||
(def spacing-margin-keys (schema-keys schema:spacing-margin))
|
||||
|
||||
(def ^:private schema:spacing
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
schema:spacing-padding
|
||||
schema:spacing-margin])
|
||||
(mu/update-properties assoc :title "SpacingTokenAttrs")))
|
||||
|
||||
(def spacing-keys (schema-keys schema:spacing))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
[:map
|
||||
[:stroke-width {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
|
||||
(def ^:private schema:dimensions
|
||||
(-> (reduce mu/union [schema:sizing
|
||||
schema:spacing
|
||||
@@ -201,91 +226,109 @@
|
||||
|
||||
(def dimensions-keys (schema-keys schema:dimensions))
|
||||
|
||||
(def ^:private schema:axis
|
||||
[:map
|
||||
[:x {:optional true} token-name-ref]
|
||||
[:y {:optional true} token-name-ref]])
|
||||
|
||||
(def axis-keys (schema-keys schema:axis))
|
||||
|
||||
(def ^:private schema:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[:rotation {:optional true} token-name-ref]])
|
||||
|
||||
(def rotation-keys (schema-keys schema:rotation))
|
||||
|
||||
(def ^:private schema:font-size
|
||||
[:map {:title "FontSizeTokenAttrs"}
|
||||
[:font-size {:optional true} token-name-ref]])
|
||||
|
||||
(def font-size-keys (schema-keys schema:font-size))
|
||||
|
||||
(def ^:private schema:letter-spacing
|
||||
[:map {:title "LetterSpacingTokenAttrs"}
|
||||
[:letter-spacing {:optional true} token-name-ref]])
|
||||
|
||||
(def letter-spacing-keys (schema-keys schema:letter-spacing))
|
||||
|
||||
(def ^:private schema:font-family
|
||||
[:map
|
||||
[:font-family {:optional true} token-name-ref]])
|
||||
[:font-family {:optional true} schema:token-name]])
|
||||
|
||||
(def font-family-keys (schema-keys schema:font-family))
|
||||
|
||||
(def ^:private schema:text-case
|
||||
[:map
|
||||
[:text-case {:optional true} token-name-ref]])
|
||||
(def ^:private schema:font-size
|
||||
[:map {:title "FontSizeTokenAttrs"}
|
||||
[:font-size {:optional true} schema:token-name]])
|
||||
|
||||
(def text-case-keys (schema-keys schema:text-case))
|
||||
(def font-size-keys (schema-keys schema:font-size))
|
||||
|
||||
(def ^:private schema:font-weight
|
||||
[:map
|
||||
[:font-weight {:optional true} token-name-ref]])
|
||||
[:font-weight {:optional true} schema:token-name]])
|
||||
|
||||
(def font-weight-keys (schema-keys schema:font-weight))
|
||||
|
||||
(def ^:private schema:typography
|
||||
[:map
|
||||
[:typography {:optional true} token-name-ref]])
|
||||
(def ^:private schema:letter-spacing
|
||||
[:map {:title "LetterSpacingTokenAttrs"}
|
||||
[:letter-spacing {:optional true} schema:token-name]])
|
||||
|
||||
(def typography-token-keys (schema-keys schema:typography))
|
||||
(def letter-spacing-keys (schema-keys schema:letter-spacing))
|
||||
|
||||
(def ^:private schema:text-decoration
|
||||
[:map
|
||||
[:text-decoration {:optional true} token-name-ref]])
|
||||
(def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
|
||||
[:map {:title "LineHeightTokenAttrs"}
|
||||
[:line-height {:optional true} schema:token-name]])
|
||||
|
||||
(def text-decoration-keys (schema-keys schema:text-decoration))
|
||||
(def line-height-keys (schema-keys schema:line-height))
|
||||
|
||||
(def typography-keys (set/union font-size-keys
|
||||
letter-spacing-keys
|
||||
font-family-keys
|
||||
font-weight-keys
|
||||
text-case-keys
|
||||
text-decoration-keys
|
||||
font-weight-keys
|
||||
typography-token-keys
|
||||
#{:line-height}))
|
||||
(def ^:private schema:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[:rotation {:optional true} schema:token-name]])
|
||||
|
||||
(def rotation-keys (schema-keys schema:rotation))
|
||||
|
||||
(def ^:private schema:number
|
||||
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
|
||||
(-> (reduce mu/union [schema:line-height
|
||||
schema:rotation])
|
||||
(mu/update-properties assoc :title "NumberTokenAttrs")))
|
||||
|
||||
(def number-keys (schema-keys schema:number))
|
||||
|
||||
(def all-keys (set/union color-keys
|
||||
(def ^:private schema:opacity
|
||||
[:map {:title "OpacityTokenAttrs"}
|
||||
[:opacity {:optional true} schema:token-name]])
|
||||
|
||||
(def opacity-keys (schema-keys schema:opacity))
|
||||
|
||||
(def ^:private schema:shadow
|
||||
[:map {:title "ShadowTokenAttrs"}
|
||||
[:shadow {:optional true} schema:token-name]])
|
||||
|
||||
(def shadow-keys (schema-keys schema:shadow))
|
||||
|
||||
(def ^:private schema:text-case
|
||||
[:map
|
||||
[:text-case {:optional true} schema:token-name]])
|
||||
|
||||
(def text-case-keys (schema-keys schema:text-case))
|
||||
|
||||
(def ^:private schema:text-decoration
|
||||
[:map
|
||||
[:text-decoration {:optional true} schema:token-name]])
|
||||
|
||||
(def text-decoration-keys (schema-keys schema:text-decoration))
|
||||
|
||||
(def ^:private schema:typography
|
||||
[:map
|
||||
[:typography {:optional true} schema:token-name]])
|
||||
|
||||
(def typography-token-keys (schema-keys schema:typography))
|
||||
|
||||
(def typography-keys (set/union font-family-keys
|
||||
font-size-keys
|
||||
font-weight-keys
|
||||
font-weight-keys
|
||||
letter-spacing-keys
|
||||
line-height-keys
|
||||
text-case-keys
|
||||
text-decoration-keys
|
||||
typography-token-keys))
|
||||
|
||||
(def ^:private schema:axis
|
||||
[:map
|
||||
[:x {:optional true} schema:token-name]
|
||||
[:y {:optional true} schema:token-name]])
|
||||
|
||||
(def axis-keys (schema-keys schema:axis))
|
||||
|
||||
(def all-keys (set/union axis-keys
|
||||
border-radius-keys
|
||||
shadow-keys
|
||||
stroke-width-keys
|
||||
sizing-keys
|
||||
opacity-keys
|
||||
spacing-keys
|
||||
color-keys
|
||||
dimensions-keys
|
||||
axis-keys
|
||||
number-keys
|
||||
opacity-keys
|
||||
rotation-keys
|
||||
shadow-keys
|
||||
sizing-keys
|
||||
spacing-keys
|
||||
stroke-width-keys
|
||||
typography-keys
|
||||
typography-token-keys
|
||||
number-keys))
|
||||
typography-token-keys))
|
||||
|
||||
(def ^:private schema:tokens
|
||||
[:map {:title "GenericTokenAttrs"}])
|
||||
@@ -306,11 +349,28 @@
|
||||
schema:text-decoration
|
||||
schema:dimensions])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS for token attributes by token type
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn token-attr?
|
||||
[attr]
|
||||
(contains? all-keys attr))
|
||||
|
||||
(defn token-attr->shape-attr
|
||||
"Returns the actual shape attribute affected when a token have been applied
|
||||
to a given `token-attr`."
|
||||
[token-attr]
|
||||
(case token-attr
|
||||
:fill :fills
|
||||
:stroke-color :strokes
|
||||
:stroke-width :strokes
|
||||
token-attr))
|
||||
|
||||
(defn shape-attr->token-attrs
|
||||
"Returns the token-attr affected when a given attribute in a shape is changed.
|
||||
The sub-attr is for attributes that may have multiple values, like strokes
|
||||
(may be width or color) and layout padding & margin (may have 4 edges)."
|
||||
([shape-attr] (shape-attr->token-attrs shape-attr nil))
|
||||
([shape-attr changed-sub-attr]
|
||||
(cond
|
||||
@@ -352,21 +412,13 @@
|
||||
(number-keys shape-attr) #{shape-attr}
|
||||
(axis-keys shape-attr) #{shape-attr})))
|
||||
|
||||
(defn token-attr->shape-attr
|
||||
[token-attr]
|
||||
(case token-attr
|
||||
:fill :fills
|
||||
:stroke-color :strokes
|
||||
:stroke-width :strokes
|
||||
token-attr))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN SHAPE ATTRIBUTES
|
||||
;; HELPERS for token attributes by shape type
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def position-attributes #{:x :y})
|
||||
(def ^:private position-attributes #{:x :y})
|
||||
|
||||
(def generic-attributes
|
||||
(def ^:private generic-attributes
|
||||
(set/union color-keys
|
||||
stroke-width-keys
|
||||
rotation-keys
|
||||
@@ -375,20 +427,22 @@
|
||||
shadow-keys
|
||||
position-attributes))
|
||||
|
||||
(def rect-attributes
|
||||
(def ^:private rect-attributes
|
||||
(set/union generic-attributes
|
||||
border-radius-keys))
|
||||
|
||||
(def frame-with-layout-attributes
|
||||
(def ^:private frame-with-layout-attributes
|
||||
(set/union rect-attributes
|
||||
spacing-gap-padding-keys))
|
||||
|
||||
(def text-attributes
|
||||
(def ^:private text-attributes
|
||||
(set/union generic-attributes
|
||||
typography-keys
|
||||
number-keys))
|
||||
|
||||
(defn shape-type->attributes
|
||||
"Returns what token attributes may be applied to a shape depending on its type
|
||||
and if it is a frame with a layout."
|
||||
[type is-layout]
|
||||
(case type
|
||||
:bool generic-attributes
|
||||
@@ -404,12 +458,14 @@
|
||||
nil))
|
||||
|
||||
(defn appliable-attrs-for-shape
|
||||
"Returns intersection of shape `attributes` for `shape-type`."
|
||||
"Returns which ones of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes shape-type is-layout]
|
||||
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
|
||||
|
||||
(defn any-appliable-attr-for-shape?
|
||||
"Checks if `token-type` supports given shape `attributes`."
|
||||
"Returns if any of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes token-type is-layout]
|
||||
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
|
||||
|
||||
@@ -420,42 +476,6 @@
|
||||
typography-keys
|
||||
#{:fill}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS IN SHAPES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- toggle-or-apply-token
|
||||
"Remove any shape attributes from token if they exists.
|
||||
Othewise apply token attributes."
|
||||
[shape token]
|
||||
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
|
||||
(merge {} shape-leftover token-leftover)))
|
||||
|
||||
(defn- token-from-attributes [token attributes]
|
||||
(->> (map (fn [attr] [attr (:name token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
|
||||
(let [token (token-from-attributes token attributes)]
|
||||
(toggle-or-apply-token shape token)))
|
||||
|
||||
(defn apply-token-to-shape
|
||||
[{:keys [shape token attributes] :as _props}]
|
||||
(let [applied-tokens (apply-token-to-attributes {:shape shape
|
||||
:token token
|
||||
:attributes attributes})]
|
||||
(update shape :applied-tokens #(merge % applied-tokens))))
|
||||
|
||||
(defn unapply-token-id [shape attributes]
|
||||
(update shape :applied-tokens d/without-keys attributes))
|
||||
|
||||
(defn unapply-layout-item-tokens
|
||||
"Unapplies all layout item related tokens from shape."
|
||||
[shape]
|
||||
(let [layout-item-attrs (set/union sizing-layout-item-keys
|
||||
spacing-margin-keys)]
|
||||
(unapply-token-id shape layout-item-attrs)))
|
||||
|
||||
(def tokens-by-input
|
||||
"A map from input name to applicable token for that input."
|
||||
{:width #{:sizing :dimensions}
|
||||
@@ -481,7 +501,48 @@
|
||||
:stroke-color #{:color}})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TYPOGRAPHY
|
||||
;; HELPERS for tokens application
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; TODO it seems that this function is redundant, maybe?
|
||||
;; (defn- toggle-or-apply-token
|
||||
;; "Remove any shape attributes from token if they exists.
|
||||
;; Othewise apply token attributes."
|
||||
;; [shape token]
|
||||
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
|
||||
;; (merge {} only-in-shape only-in-token)))
|
||||
|
||||
(defn- generate-attr-map [token attributes]
|
||||
(->> (map (fn [attr] [attr (:name token)]) attributes)
|
||||
(into {})))
|
||||
|
||||
(defn apply-token-to-shape
|
||||
"Applies the token to the given attributes in the shape."
|
||||
[{:keys [shape token attributes] :as _props}]
|
||||
(let [map-to-apply (generate-attr-map token attributes)]
|
||||
(update shape :applied-tokens #(merge % map-to-apply))))
|
||||
|
||||
;; (defn apply-token-to-shape
|
||||
;; [{:keys [shape token attributes] :as _props}]
|
||||
;; (let [map-to-apply (generate-attr-map token attributes)
|
||||
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
|
||||
;; (update shape :applied-tokens #(merge % applied-tokens))))
|
||||
|
||||
(defn unapply-tokens-from-shape
|
||||
"Removes any token applied to the given attributes in the shape."
|
||||
[shape attributes]
|
||||
(update shape :applied-tokens d/without-keys attributes))
|
||||
|
||||
(defn unapply-layout-item-tokens
|
||||
"Unapplies all layout item related tokens from shape."
|
||||
[shape]
|
||||
(let [layout-item-attrs (set/union sizing-layout-item-keys
|
||||
spacing-margin-keys)]
|
||||
(unapply-tokens-from-shape shape layout-item-attrs)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS for typography tokens
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn split-font-family
|
||||
@@ -544,17 +605,3 @@
|
||||
(when (font-weight-values weight)
|
||||
(cond-> {:weight weight}
|
||||
italic? (assoc :style "italic")))))
|
||||
|
||||
(defn typography-composite-token-reference?
|
||||
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SHADOW
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn shadow-composite-token-reference?
|
||||
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
@@ -114,25 +114,19 @@
|
||||
[o]
|
||||
(instance? Token o))
|
||||
|
||||
(def schema:token-attrs
|
||||
[:map {:title "Token"}
|
||||
[:id ::sm/uuid]
|
||||
[:name cto/token-name-ref]
|
||||
[:type [::sm/one-of cto/token-types]]
|
||||
[:value ::sm/any]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
(declare make-token)
|
||||
|
||||
(def schema:token
|
||||
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
|
||||
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
|
||||
(sg/fmap #(make-token %)))}
|
||||
(sm/required-keys schema:token-attrs)
|
||||
(sm/required-keys cto/schema:token-attrs)
|
||||
[:fn token?]])
|
||||
|
||||
(def ^:private check-token-attrs
|
||||
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
|
||||
(sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
|
||||
|
||||
(def decode-token-attrs
|
||||
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
|
||||
|
||||
(def check-token
|
||||
(sm/check-fn schema:token :hint "expected valid token"))
|
||||
@@ -317,10 +311,13 @@
|
||||
[o]
|
||||
(instance? TokenSetLegacy o))
|
||||
|
||||
(declare make-token-set)
|
||||
(declare normalized-set-name?)
|
||||
|
||||
(def schema:token-set-attrs
|
||||
[:map {:title "TokenSet"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:name [:and :string [:fn #(normalized-set-name? %)]]]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:tokens {:optional true
|
||||
@@ -342,8 +339,6 @@
|
||||
:string schema:token]
|
||||
[:fn d/ordered-map?]]]])
|
||||
|
||||
(declare make-token-set)
|
||||
|
||||
(def schema:token-set
|
||||
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
|
||||
(sg/fmap #(make-token-set %)))}
|
||||
@@ -404,12 +399,25 @@
|
||||
(split-set-name name))
|
||||
(cpn/join-path :separator set-separator :with-spaces? false))))
|
||||
|
||||
(defn normalized-set-name?
|
||||
"Check if a set name is normalized (no extra spaces)."
|
||||
[name]
|
||||
(= name (normalize-set-name name)))
|
||||
|
||||
(defn replace-last-path-name
|
||||
"Replaces the last element in a `path` vector with `name`."
|
||||
[path name]
|
||||
(-> (into [] (drop-last path))
|
||||
(conj name)))
|
||||
|
||||
(defn make-child-name
|
||||
"Generate the name of a set child of `parent-set` adding the name `name`."
|
||||
[parent-set name]
|
||||
(if-let [parent-path (get-set-path parent-set)]
|
||||
(->> (concat parent-path (split-set-name name))
|
||||
(join-set-path))
|
||||
(normalize-set-name name)))
|
||||
|
||||
;; The following functions will be removed after refactoring the internal structure of TokensLib,
|
||||
;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
|
||||
|
||||
@@ -1365,10 +1373,13 @@ Will return a value that matches this schema:
|
||||
(def ^:private check-tokens-lib-map
|
||||
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
|
||||
|
||||
(defn tokens-lib?
|
||||
[o]
|
||||
(instance? TokensLib o))
|
||||
|
||||
(defn valid-tokens-lib?
|
||||
[o]
|
||||
(and (instance? TokensLib o)
|
||||
(valid? o)))
|
||||
(and (tokens-lib? o) (valid? o)))
|
||||
|
||||
(defn- ensure-hidden-theme
|
||||
"A helper that is responsible to ensure that the hidden theme always
|
||||
@@ -1430,6 +1441,50 @@ Will return a value that matches this schema:
|
||||
(rename copy-name)
|
||||
(reid (uuid/next))))))
|
||||
|
||||
(defn- token-name->path-selector
|
||||
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
|
||||
|
||||
`:selector` is the last item of the names path
|
||||
`:path` is everything leading up the the `:selector`."
|
||||
[token-name]
|
||||
(let [path-segments (get-token-path {:name token-name})
|
||||
last-idx (dec (count path-segments))
|
||||
[path [selector]] (split-at last-idx path-segments)]
|
||||
{:path (seq path)
|
||||
:selector selector}))
|
||||
|
||||
(defn token-name-path-exists?
|
||||
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
|
||||
|
||||
It's not allowed to create a token inside a token. E.g.:
|
||||
Creating a token with
|
||||
|
||||
{:name \"foo.bar\"}
|
||||
|
||||
in the tokens tree:
|
||||
|
||||
{\"foo\" {:name \"other\"}}"
|
||||
[token-name tokens-tree]
|
||||
(let [{:keys [path selector]} (token-name->path-selector token-name)
|
||||
path-target (reduce
|
||||
(fn [acc cur]
|
||||
(let [target (get acc cur)]
|
||||
(cond
|
||||
;; Path segment doesn't exist yet
|
||||
(nil? target) (reduced false)
|
||||
;; A token exists at this path
|
||||
(:name target) (reduced true)
|
||||
;; Continue traversing the true
|
||||
:else target)))
|
||||
tokens-tree
|
||||
path)]
|
||||
(cond
|
||||
(boolean? path-target) path-target
|
||||
(get path-target :name) true
|
||||
:else (-> (get path-target selector)
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
;; === Import / Export from JSON format
|
||||
|
||||
;; Supported formats:
|
||||
@@ -1575,10 +1630,10 @@ Will return a value that matches this schema:
|
||||
(if (map? shadow)
|
||||
(let [legacy-shadow-type (get "type" shadow)]
|
||||
(-> shadow
|
||||
(set/rename-keys {"x" :offsetX
|
||||
"offsetX" :offsetX
|
||||
"y" :offsetY
|
||||
"offsetY" :offsetY
|
||||
(set/rename-keys {"x" :offset-x
|
||||
"offsetX" :offset-x
|
||||
"y" :offset-y
|
||||
"offsetY" :offset-y
|
||||
"blur" :blur
|
||||
"spread" :spread
|
||||
"color" :color
|
||||
@@ -1589,7 +1644,7 @@ Will return a value that matches this schema:
|
||||
(= "false" %) false
|
||||
(= legacy-shadow-type "innerShadow") true
|
||||
:else false))
|
||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
||||
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||
shadow))]
|
||||
(cond
|
||||
;; Reference value - keep as string
|
||||
@@ -1860,8 +1915,8 @@ Will return a value that matches this schema:
|
||||
(mapv (fn [shadow]
|
||||
(if (map? shadow)
|
||||
(-> shadow
|
||||
(set/rename-keys {:offsetX "offsetX"
|
||||
:offsetY "offsetY"
|
||||
(set/rename-keys {:offset-x "offsetX"
|
||||
:offset-y "offsetY"
|
||||
:blur "blur"
|
||||
:spread "spread"
|
||||
:color "color"
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
(defn parse
|
||||
[data]
|
||||
(cond
|
||||
(str/starts-with? data "%")
|
||||
(or (str/starts-with? data "%")
|
||||
(= data "develop"))
|
||||
{:full "develop"
|
||||
:branch "develop"
|
||||
:base "0.0.0"
|
||||
|
||||
@@ -6,34 +6,34 @@
|
||||
|
||||
(ns common-tests.files.tokens-test
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-parse-token-value
|
||||
(t/testing "parses double from a token value"
|
||||
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
|
||||
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
|
||||
(t/testing "trims white-space"
|
||||
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
|
||||
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
|
||||
(t/testing "parses unit: px"
|
||||
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
|
||||
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
|
||||
(t/testing "parses unit: %"
|
||||
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
|
||||
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
|
||||
(t/testing "parses unit: px")
|
||||
(t/testing "returns nil for any invalid characters"
|
||||
(t/is (nil? (cft/parse-token-value " -1.3a "))))
|
||||
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
|
||||
(t/testing "doesnt accept invalid double"
|
||||
(t/is (nil? (cft/parse-token-value ".3")))))
|
||||
(t/is (nil? (cfo/parse-token-value ".3")))))
|
||||
|
||||
(t/deftest token-applied-test
|
||||
(t/testing "matches passed token with `:token-attributes`"
|
||||
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match empty token"
|
||||
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "does't match passed token `:id`"
|
||||
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match passed `:token-attributes`"
|
||||
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
|
||||
(t/deftest shapes-ids-by-applied-attributes
|
||||
(t/testing "Returns set of matched attributes that fit the applied token"
|
||||
@@ -54,7 +54,7 @@
|
||||
shape-applied-x-y
|
||||
shape-applied-all
|
||||
shape-applied-none]
|
||||
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
(t/is (= (:x expected) (shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
@@ -62,34 +62,21 @@
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
(t/is (= (:z expected) (shape-ids shape-applied-all)))
|
||||
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
|
||||
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
|
||||
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
|
||||
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
|
||||
(shape-ids shape-applied-x
|
||||
shape-applied-x-y
|
||||
shape-applied-all))))
|
||||
|
||||
(t/deftest tokens-applied-test
|
||||
(t/testing "is true when single shape matches the token and attributes"
|
||||
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x}))))
|
||||
(t/testing "is false when no shape matches the token or attributes"
|
||||
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x})))
|
||||
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
{:applied-tokens {:x "a"}}]
|
||||
#{:y})))))
|
||||
|
||||
(t/deftest name->path-test
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
|
||||
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
|
||||
|
||||
(t/deftest token-name-path-exists?-test
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
||||
|
||||
@@ -255,28 +255,28 @@
|
||||
(cls/generate-update-shapes [(:id frame1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:r1 :r2 :r3 :r4])
|
||||
(cto/unapply-token-id [:rotation])
|
||||
(cto/unapply-token-id [:opacity])
|
||||
(cto/unapply-token-id [:stroke-width])
|
||||
(cto/unapply-token-id [:stroke-color])
|
||||
(cto/unapply-token-id [:fill])
|
||||
(cto/unapply-token-id [:width :height])))
|
||||
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
|
||||
(cto/unapply-tokens-from-shape [:rotation])
|
||||
(cto/unapply-tokens-from-shape [:opacity])
|
||||
(cto/unapply-tokens-from-shape [:stroke-width])
|
||||
(cto/unapply-tokens-from-shape [:stroke-color])
|
||||
(cto/unapply-tokens-from-shape [:fill])
|
||||
(cto/unapply-tokens-from-shape [:width :height])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id text1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:font-size])
|
||||
(cto/unapply-token-id [:letter-spacing])
|
||||
(cto/unapply-token-id [:font-family])))
|
||||
(cto/unapply-tokens-from-shape [:font-size])
|
||||
(cto/unapply-tokens-from-shape [:letter-spacing])
|
||||
(cto/unapply-tokens-from-shape [:font-family])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id circle1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
|
||||
(cto/unapply-token-id [:m1 :m2 :m3 :m4])))
|
||||
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
|
||||
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
|
||||
(:objects page)
|
||||
{}))
|
||||
|
||||
|
||||
@@ -8,20 +8,19 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-valid-token-name-schema
|
||||
;; Allow regular namespace token names
|
||||
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "foo")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
|
||||
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
|
||||
;; Disallow trailing tokens
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
|
||||
;; Disallow multiple separator dots
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
|
||||
;; Disallow any special characters
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))
|
||||
|
||||
@@ -678,35 +678,35 @@
|
||||
|
||||
(t/deftest list-active-themes-tokens-bug-taiga-10617
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode / Dark"
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode/Dark"
|
||||
:tokens {"red"
|
||||
(ctob/make-token :name "red"
|
||||
:type :color
|
||||
:value "#700000")}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode / Light"
|
||||
(ctob/add-set (ctob/make-token-set :name "Mode/Light"
|
||||
:tokens {"red"
|
||||
(ctob/make-token :name "red"
|
||||
:type :color
|
||||
:value "#ff0000")}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Device / Desktop"
|
||||
(ctob/add-set (ctob/make-token-set :name "Device/Desktop"
|
||||
:tokens {"border1"
|
||||
(ctob/make-token :name "border1"
|
||||
:type :border-radius
|
||||
:value 30)}))
|
||||
(ctob/add-set (ctob/make-token-set :name "Device / Mobile"
|
||||
(ctob/add-set (ctob/make-token-set :name "Device/Mobile"
|
||||
:tokens {"border1"
|
||||
(ctob/make-token :name "border1"
|
||||
:type :border-radius
|
||||
:value 50)}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "App"
|
||||
:name "Mobile"
|
||||
:sets #{"Mode / Dark" "Device / Mobile"}))
|
||||
:sets #{"Mode/Dark" "Device/Mobile"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "App"
|
||||
:name "Web"
|
||||
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"}))
|
||||
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "Brand"
|
||||
:name "Brand A"
|
||||
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"}))
|
||||
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"}))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "Brand"
|
||||
:name "Brand B"
|
||||
:sets #{}))
|
||||
@@ -1897,15 +1897,15 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "multiple shadow token"
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with reference"
|
||||
@@ -1918,7 +1918,7 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with description"
|
||||
@@ -1937,14 +1937,14 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.single"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:description "A single shadow"})
|
||||
"shadow.multiple"
|
||||
(ctob/make-token
|
||||
{:name "shadow.multiple"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
{:name "shadow.ref"
|
||||
@@ -1991,7 +1991,7 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.test"
|
||||
:type :shadow
|
||||
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:description "Round trip test"})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
@@ -2013,3 +2013,11 @@
|
||||
(t/is (some? imported-ref))
|
||||
(t/is (= (:type original-ref) (:type imported-ref)))
|
||||
(t/is (= (:value imported-ref) (:value original-ref))))))))
|
||||
|
||||
(t/deftest token-name-path-exists?-test
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
||||
|
||||
@@ -59,6 +59,38 @@ RUN set -eux; \
|
||||
corepack enable; \
|
||||
rm -rf /tmp/nodejs.tar.gz;
|
||||
|
||||
|
||||
################################################################################
|
||||
## CADDYSERVER SETUP
|
||||
################################################################################
|
||||
|
||||
FROM base AS setup-caddy
|
||||
|
||||
ENV CADDY_VERSION=2.10.2
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
curl -LfsSo /tmp/caddy.tar.gz ${BINARY_URL}; \
|
||||
mkdir -p /tmp/caddy; \
|
||||
cd /tmp/caddy; \
|
||||
tar -xf /tmp/caddy.tar.gz; \
|
||||
chown -R root /tmp/caddy; \
|
||||
mv /tmp/caddy/caddy /usr/bin/; \
|
||||
rm -rf /tmp/caddy.tar.gz; \
|
||||
rm -rf /tmp/caddy;
|
||||
|
||||
################################################################################
|
||||
## JVM SETUP
|
||||
################################################################################
|
||||
@@ -351,6 +383,7 @@ COPY --from=setup-utils /opt/utils /opt/utils
|
||||
COPY --from=setup-rust /opt/cargo /opt/cargo
|
||||
COPY --from=setup-rust /opt/rustup /opt/rustup
|
||||
COPY --from=setup-rust /opt/emsdk /opt/emsdk
|
||||
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY files/nginx-mime.types /etc/nginx/mime.types
|
||||
@@ -361,6 +394,9 @@ COPY files/vimrc /root/.vimrc
|
||||
COPY files/tmux.conf /root/.tmux.conf
|
||||
COPY files/sudoers /etc/sudoers
|
||||
|
||||
COPY files/Caddyfile /home/
|
||||
COPY files/selfsigned.crt /home/
|
||||
COPY files/selfsigned.key /home/
|
||||
COPY files/start-tmux.sh /home/start-tmux.sh
|
||||
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
|
||||
COPY files/entrypoint.sh /home/entrypoint.sh
|
||||
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
- 3447:3447
|
||||
- 3448:3448
|
||||
- 3449:3449
|
||||
- 3449:3449/udp
|
||||
- 3450:3450
|
||||
- 6006:6006
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
|
||||
12
docker/devenv/files/Caddyfile
Normal file
12
docker/devenv/files/Caddyfile
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
localhost:3449 {
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
set -e
|
||||
nginx
|
||||
tail -f /dev/null
|
||||
caddy start -c /home/Caddyfile
|
||||
tail -f /dev/null;
|
||||
|
||||
@@ -12,7 +12,7 @@ http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 0;
|
||||
keepalive_timeout 100;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
@@ -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;
|
||||
@@ -55,7 +55,7 @@ http {
|
||||
proxy_cache_key "$host$request_uri";
|
||||
|
||||
server {
|
||||
listen 3449 default_server;
|
||||
listen 4449 default_server;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 300M;
|
||||
@@ -223,17 +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";
|
||||
add_header Connection close always;
|
||||
# This header is what we need to use on prod
|
||||
# add_header Cache-Control "public, must-revalidate, max-age=0";
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
22
docker/devenv/files/selfsigned.crt
Normal file
22
docker/devenv/files/selfsigned.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuzCCAqOgAwIBAgIUa3THJQSn1+ErK65g1jDL0tjUkBYwDQYJKoZIhvcNAQEL
|
||||
BQAwXzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
|
||||
bDEOMAwGA1UECgwFTG9jYWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MB4XDTI1MTIwMjA4MjUyM1oXDTI2MTIwMjA4MjUyM1owXzELMAkGA1UEBhMC
|
||||
VVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2NhbDEOMAwGA1UECgwFTG9j
|
||||
YWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVIlfpIPE+QyL/q7IQOilEA7wEOZ6wbsh2Fr
|
||||
59H1gSLFvgoCxI6RVUkQ/MFRnw/r1ZbAqRpc2xAl5a9Ml14q20Zlj6dAHsWX6O2J
|
||||
EwNsD18dQmX3BncnjV3yCZM2iQcMFKuXG4KQNdIQNNvdIgtlrHYp0ohS9s3XC7cj
|
||||
KxNrm/pW9EAXfn9AYDd/qER090L2E4ipP9m/5l3MjinNc4l2kpH9rLOgb79H0RLt
|
||||
PK3/KP8ErZhAvzdmDBAdM5Z5K37b+TfB/kSVNUKL6qyw5CCjlShERLhBNprlnRfz
|
||||
tHNIQ1RHq3qJJN19ZnJrLqICuQ5ztvj7hBDiOSV0LnmyKgXr6wIDAQABo28wbTAd
|
||||
BgNVHQ4EFgQUPL8WGf6z/wB8TimJBx1zybsIeikwHwYDVR0jBBgwFoAUPL8WGf6z
|
||||
/wB8TimJBx1zybsIeikwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
|
||||
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBACMMVyR3kbNxnzuUc2lahKH4
|
||||
cPXVWOsvCvnDtjzm41XmKjUJTbtjn3p5d/ZmLbZ4zzIQULfWXO3XG/HevkvVo0g6
|
||||
6pJXTXc6C6ZhFG0rIYMcPPzmGmalDV5n+lUaCVx5XbFFxvRQ7893auwhRATdwGs+
|
||||
xiMyYbE2w9otKqyDItmJZJ5nW6vmXJ42YHxlXF18u9U88xqtOSMd5xZahbsmw7Gg
|
||||
A4/o4TPoAX5QfA306sL443WaczsF7bmsTf9qcYa/3xxQkP5Seyqx8ePWpS22qysE
|
||||
jG6XPpymxb6sb2mVaFBAzhEMb/eBvE9nRAopxmB7uV4TbqC51K/U3uo6jFX4Jbw=
|
||||
-----END CERTIFICATE-----
|
||||
28
docker/devenv/files/selfsigned.key
Normal file
28
docker/devenv/files/selfsigned.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUiV+kg8T5DIv
|
||||
+rshA6KUQDvAQ5nrBuyHYWvn0fWBIsW+CgLEjpFVSRD8wVGfD+vVlsCpGlzbECXl
|
||||
r0yXXirbRmWPp0AexZfo7YkTA2wPXx1CZfcGdyeNXfIJkzaJBwwUq5cbgpA10hA0
|
||||
290iC2WsdinSiFL2zdcLtyMrE2ub+lb0QBd+f0BgN3+oRHT3QvYTiKk/2b/mXcyO
|
||||
Kc1ziXaSkf2ss6Bvv0fREu08rf8o/wStmEC/N2YMEB0zlnkrftv5N8H+RJU1Qovq
|
||||
rLDkIKOVKEREuEE2muWdF/O0c0hDVEereokk3X1mcmsuogK5DnO2+PuEEOI5JXQu
|
||||
ebIqBevrAgMBAAECggEABqtE+LNn8nW9v98jcc2IBjc2g4D5yVJaZYWxqGVJJ7T6
|
||||
Lfhw7Qf4AoZAHM9en9FMM7Ahw7hO2SboynoLJHyHGOp1FNQqiJptFNdBkjKr0rqI
|
||||
4pk0HK+3zLQO/4gz50gne0vP3qZtlorV5Jpf8e/Et3jWm9XOQcTB2e6AKL4k827B
|
||||
dv4Tld+/7PoZVXjahfrUWuIZr5mzyF1eUkD8sPOpdr3HJxSueqsOMjbG8XMRqCQ+
|
||||
5eCWWSW5yPQlMr7M7cXM+a0k73Xn1sKl7fP3/9byji25zxGUaMu5RA1kw0Oqseid
|
||||
RXuRxGphGZgnx1aFxDAPg3FtmGch7/Cc6WfqboOL0QKBgQD4GZO1gGaE8cg4lvuo
|
||||
ZUX2YJu6UJuNOmuhfvG3ui4WO9PHy3btc2q+3kutSuBcyIjhi+qbXasBcX/QOOJF
|
||||
udyTZc5PopNkJojS4JdXAZCiu5sKI3lp4DIt9qNISlXGgrJgdxGUO+DzarBctXdn
|
||||
BSwXFw5hcjJjl7wsPGQl1tBTQwKBgQDPuz5MEM5ZeUe9CT5sQDq/ld0u4aL5AHmx
|
||||
aaA2gzDgd9l2R5wHX6wLzjoVWXOmeqaYzJopt2JN4iXrtbjWkyePgZeZMyWoyJ/v
|
||||
clW9bi8HM9f9EpPr7czSj9sLUnsjd9cuTD+JuXK//jRGbRpw7r7nWtLHImjj6d2v
|
||||
APZRq0v2OQKBgBcESG/OObSbubeGSlKVEqiIzem7ELNJeDLDVCl3XE8zvbILbj0Z
|
||||
OA39EYhCKg5xjEFgeaNwTS0VGoZ2wIc3dv81sq4wpvvjl035CBFKU+DFBt0p7Vml
|
||||
MwKQnxVV0B9agLHyWe8mnvf2LeZr72ffUvfRa8QelA4pRYvVDnV0OF+BAoGAW6rM
|
||||
+tQPuvwB5DFIEozlX9XKHP4E5MyI5vktceDCmMtKcx92gup9CVif2Pv4ROaqzZK8
|
||||
FNyPzL6W7UTrpASb2H/fXgNsAudFbGyP2V/d8Ne34D1qeRoe4GwKxRxIqoYftpZ/
|
||||
E096i66pcsqCeINiSsWRbb6JesmgwbEzAScOBkECgYEA6O/Dibc9PaqRpaiE6Qut
|
||||
S3W/Rr1Pd1jbN4rOVI2TFCgMJQmc6jOdq2fCntR9acsa8HPx+djOlXTUBPKBZ/Ae
|
||||
p8umRdXVWcNMnwWVWHt7tsEuR/gYkxQ5xjXeS1VDPnEre9+EaevMBuVs8HdRsKQO
|
||||
uzvNGeAFEfqwIqn7CFQ+ndU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -7,8 +7,10 @@ RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/assets.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
|
||||
@@ -42,11 +42,11 @@ http {
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_static on;
|
||||
gzip_comp_level 4;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
@@ -110,6 +110,8 @@ http {
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
|
||||
include /etc/nginx/overrides/assets.d/*.conf;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
@@ -142,24 +144,15 @@ http {
|
||||
location / {
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
||||
add_header Cache-Control "max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/your-account">
|
||||
<h2>Your account →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Access your account settings and manage personal access tokens</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create and manage your teams</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/comments/">
|
||||
<h2>Comments →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Give and receive feedback right over your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/assets">
|
||||
<h2>Assets →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Store elements and styles to easily reuse them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries">
|
||||
<h2>Libraries →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components">
|
||||
<h2>Components →</h2>
|
||||
<p>Speed your design workflow</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants">
|
||||
<h2>Variants →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens">
|
||||
<h2>Design Tokens →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -5,7 +5,7 @@ desc: Use Penpot's libraries for reusable design elements! Learn to create, mana
|
||||
---
|
||||
<h1 id="libraries">Libraries</h1>
|
||||
|
||||
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
<p class="main-paragraph">Libraries may include components, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
|
||||
<h3 id="file-libraries">File libraries</h3>
|
||||
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/designing/workspace-basics">
|
||||
<h2>Workspace basics →</h2>
|
||||
<p>Workspace basics</p>
|
||||
<p>Get to know the Workspace, where designs are created</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers">
|
||||
<h2>Layers →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/color-stroke/">
|
||||
<h2>Color & Strokes→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling options available for each layer</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/text-typo">
|
||||
<h2>Text & Typography→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling text content & using custom fonts</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts">
|
||||
<h2>Flexible layouts →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/export-import/export-import-files/">
|
||||
<h2>Export/Import Penpot files →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>How to export and import your Penpot files</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/export-import/exporting-layers/">
|
||||
<h2>Exporting layers →</h2>
|
||||
<p>Exporting layers</p>
|
||||
<p>How to export elements from your design into different file formats</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -16,7 +16,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/the-interface">
|
||||
<h2>Interface tour →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Take a tour of Penpot's main areas</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -28,7 +28,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/info">
|
||||
<h2>Tutorials & info →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Useful resources to better understand Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -22,49 +22,49 @@ eleventyNavigation:
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers/">
|
||||
<h2>Layers</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts/">
|
||||
<h2>Flexible layouts</h2>
|
||||
<p>Create designs that adapt automatically.</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components/">
|
||||
<h2>Components</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants/">
|
||||
<h2>Variants</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens/">
|
||||
<h2>Design Tokens</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/dev-tools/#inspect-design">
|
||||
<h2>Inspect design</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Get production-ready code</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping/">
|
||||
<h2>Prototyping</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries/">
|
||||
<h2>Libraries</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping">
|
||||
<h2>Prototyping →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/testing-view-mode">
|
||||
<h2>Testing: View mode →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Test your designs and play the interactions</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -18,4 +18,15 @@ cp ../.yarnrc.yml target/;
|
||||
cp yarn.lock target/;
|
||||
cp package.json target/;
|
||||
|
||||
cat <<EOF | tee target/setup
|
||||
#/usr/bin/env bash
|
||||
set -e;
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install
|
||||
yarn run playwright install chromium;
|
||||
EOF
|
||||
|
||||
chmod +x target/setup;
|
||||
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/app.js;
|
||||
|
||||
@@ -7,4 +7,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
|
||||
bb -i '(babashka.wait/wait-for-path "target/app.js")';
|
||||
sleep 2;
|
||||
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
exec node target/app.js
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
|
||||
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(p/fmap (fn [resource]
|
||||
(assoc exchange :response/body resource)))
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "unexpected error on export multiple"
|
||||
(l/error :hint "unexpected error on single export"
|
||||
:cause cause)
|
||||
(p/rejected cause))))))
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(l/error :hint "unexpected error on multiple exportation" :cause cause)
|
||||
(l/error :hint "unexpected error on multiple export" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
import '../resources/public/css/ds.css';
|
||||
|
||||
export const decorators = [
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -5947,8 +5947,8 @@
|
||||
"~:spread": "10",
|
||||
"~:color": "rgb(160, 73, 73)",
|
||||
"~:inset": true,
|
||||
"~:offsetX": "10",
|
||||
"~:offsetY": "10"
|
||||
"~:offset-x": "10",
|
||||
"~:offset-y": "10"
|
||||
}
|
||||
],
|
||||
"~:description": "",
|
||||
|
||||
@@ -73,7 +73,7 @@ export class BasePage {
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js?ts=*";
|
||||
const url = "**/js/config.js*";
|
||||
return await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
|
||||
@@ -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");
|
||||
|
||||
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 821 KiB |
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -746,20 +746,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@include flexCenter;
|
||||
height: $s-48;
|
||||
width: $s-48;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--empty-message-background-color);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
stroke: var(--empty-message-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.attr-title {
|
||||
div {
|
||||
margin-left: 0;
|
||||
|
||||
@@ -17,23 +17,25 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -9,7 +9,3 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
</script>
|
||||
|
||||
@@ -6,22 +6,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await fs.mkdir("resources/public/js", {recursive: true});
|
||||
|
||||
await h.compileStorybookStyles();
|
||||
await h.copyAssets();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
set -ex
|
||||
exec clojure $OPTIONS -M -m rebel-readline.main
|
||||
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||
|
||||
9
frontend/scripts/test
Executable file
9
frontend/scripts/test
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
|
||||
yarn run lint:scss;
|
||||
yarn run test;
|
||||
13
frontend/scripts/test-components
Executable file
13
frontend/scripts/test-components
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
exec 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"
|
||||
8
frontend/scripts/test-e2e
Executable file
8
frontend/scripts/test-e2e
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list "$@";
|
||||
@@ -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 (~)");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
@@ -191,5 +190,4 @@
|
||||
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(let [uri (u/join public-uri path)]
|
||||
(assoc uri :query (dm/str "version=" (:full version)))))
|
||||
(u/join public-uri path))
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
(defn ^:export init
|
||||
[]
|
||||
(mw/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
(thr/init!)
|
||||
(init-ui)
|
||||
@@ -114,11 +114,4 @@
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
@@ -148,17 +148,17 @@
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 7 Pro"
|
||||
:width 1440
|
||||
:height 3120}
|
||||
:width 412
|
||||
:height 892}
|
||||
{:name "Google Pixel 6a/6"
|
||||
:width 1080
|
||||
:height 2400}
|
||||
:width 412
|
||||
:height 915}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S22"
|
||||
:width 1080
|
||||
:height 2340}
|
||||
:width 360
|
||||
:height 780}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
[]
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:app-version (:full cf/version)
|
||||
:locale @i18n/locale}
|
||||
{:version (:full cf/version)
|
||||
:locale i18n/*current-locale*}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
:browser-version (obj/get browser "version")})
|
||||
@@ -98,7 +98,9 @@
|
||||
(def context
|
||||
(atom (d/without-nils (collect-context))))
|
||||
|
||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
||||
(add-watch i18n/state "events"
|
||||
(fn [_ _ _ v]
|
||||
(swap! context assoc :locale (get v :locale))))
|
||||
|
||||
;; --- EVENT TRANSLATION
|
||||
|
||||
|
||||
@@ -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 [_ _ _]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
@@ -59,9 +59,15 @@
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
:else
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||
|
||||
(defn- numeric-string? [s]
|
||||
(and (string? s)
|
||||
@@ -77,7 +83,7 @@
|
||||
[value]
|
||||
(let [number? (or (number? value)
|
||||
(numeric-string? value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
|
||||
@@ -103,7 +109,7 @@
|
||||
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [parsed-value (cft/parse-token-value value)
|
||||
(let [parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
(if (and parsed-value (not out-of-bounds))
|
||||
@@ -120,8 +126,8 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond (and parsed-value (not out-of-scope))
|
||||
@@ -145,7 +151,7 @@
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-scope (< (:value parsed-value) 0)
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond
|
||||
@@ -244,7 +250,7 @@
|
||||
:font-size-value font-size-value})]
|
||||
(or error
|
||||
(try
|
||||
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
|
||||
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
|
||||
(case unit
|
||||
"%" (/ value 100)
|
||||
"px" (/ value font-size-value)
|
||||
@@ -368,8 +374,8 @@
|
||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||
(update shadow-result :errors concat
|
||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||
parsers {:offsetX parse-sd-token-general-value
|
||||
:offsetY parse-sd-token-general-value
|
||||
parsers {:offset-x parse-sd-token-general-value
|
||||
:offset-y parse-sd-token-general-value
|
||||
:blur parse-sd-token-shadow-blur
|
||||
:spread parse-sd-token-shadow-spread
|
||||
:color parse-sd-token-color-value
|
||||
@@ -389,35 +395,42 @@
|
||||
(defn- parse-sd-token-shadow-value
|
||||
"Parses shadow value and validates it."
|
||||
[value]
|
||||
(cond
|
||||
;; Reference value (string)
|
||||
(string? value) {:value value}
|
||||
(let [missing-references
|
||||
(when (string? value)
|
||||
(seq (cto/find-token-value-references value)))]
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
(string? value)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
|
||||
|
||||
;; Empty value
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
|
||||
;; Invalid value
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
|
||||
;; Array of shadows
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
;; Parse each shadow with its index
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
|
||||
;; Collect all errors from all shadows
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
|
||||
;; Collect all values from shadows that have values
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values}))))
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values})))))
|
||||
|
||||
(defn collect-shadow-errors [token shadow-index]
|
||||
(group-by :shadow-key
|
||||
|
||||
@@ -351,19 +351,31 @@
|
||||
(on-success))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:and
|
||||
[:map
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]
|
||||
[:invitations {:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:role [::sm/one-of ctt/valid-roles]]]]]
|
||||
[:team-id ::sm/uuid]
|
||||
[:resend? {:optional true} ::sm/boolean]]
|
||||
[:fn (fn [attrs]
|
||||
(or (contains? attrs :emails)
|
||||
(contains? attrs :invitations)))]])
|
||||
|
||||
(def ^:private check-create-invitations-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn create-invitations
|
||||
"Unified function to create invitations. Supports two parameter formats:
|
||||
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
|
||||
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
|
||||
[{:keys [emails role team-id invitations resend?] :as params}]
|
||||
|
||||
(assert (uuid? team-id))
|
||||
;; Validate input format - must have either emails+role OR invitations
|
||||
(assert (or (and emails role (sm/check-set-of-emails emails) (keyword? role))
|
||||
(and invitations
|
||||
(sm/check-set-of-emails (map :email invitations))
|
||||
(every? #(contains? ctt/valid-roles (:role %)) invitations)))
|
||||
"Must provide either emails+role or invitations with individual roles")
|
||||
(check-create-invitations-params params)
|
||||
|
||||
(ptk/reify ::create-invitations
|
||||
ev/Event
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcmt]
|
||||
@@ -551,7 +553,6 @@
|
||||
component-id (:component-id shape)
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
|
||||
(when valid?
|
||||
(if (ctc/is-variant-container? shape)
|
||||
;; Rename the full variant when it is a variant container
|
||||
@@ -566,6 +567,43 @@
|
||||
(dwl/rename-component component-id clean-name))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn rename-shape-or-variant
|
||||
([id name]
|
||||
(rename-shape-or-variant nil nil id name))
|
||||
([file-id page-id id name]
|
||||
(ptk/reify ::rename-shape-or-variant
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (d/nilv file-id (:current-file-id state))
|
||||
page-id (d/nilv page-id (:current-page-id state))
|
||||
|
||||
file-data (dsh/lookup-file-data state file-id)
|
||||
shape
|
||||
(-> (dsh/lookup-page-objects state file-id page-id)
|
||||
(get id))
|
||||
|
||||
is-variant? (ctc/is-variant? shape)
|
||||
variant-id (when is-variant? (:variant-id shape))
|
||||
variant-name (when is-variant? (:variant-name shape))
|
||||
component-id (:component-id shape)
|
||||
component (ctkl/get-component file-data (:component-id shape))
|
||||
variant-properties (:variant-properties component)]
|
||||
(cond
|
||||
(and variant-name (ctv/valid-properties-formula? name))
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id))
|
||||
|
||||
variant-name
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties {})
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id name))
|
||||
|
||||
:else
|
||||
(rx/of (end-rename-shape id name))))))))
|
||||
|
||||
;; --- Update Selected Shapes attrs
|
||||
|
||||
(defn update-selected-shapes
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.main.data.workspace.tokens.application
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.shape.layout :as ctsl]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
@@ -153,11 +153,11 @@
|
||||
(defn value->shadow
|
||||
"Transform a token shadow value into penpot shadow data structure"
|
||||
[value]
|
||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
||||
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||
{:id (random-uuid)
|
||||
:hidden false
|
||||
:offset-x offsetX
|
||||
:offset-y offsetY
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:blur blur
|
||||
:color (value->color color)
|
||||
:spread spread
|
||||
@@ -525,8 +525,8 @@
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cft/attributes-map attributes token)
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
@@ -585,7 +585,7 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
|
||||
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
|
||||
(dwsh/update-shapes
|
||||
shape-ids
|
||||
(fn [shape]
|
||||
@@ -613,7 +613,7 @@
|
||||
(get token-properties (:type token))
|
||||
|
||||
unapply-tokens?
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
|
||||
shape-ids (map :id shapes)]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.main.data.workspace.tokens.color
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.main.data.tinycolor :as tinycolor]))
|
||||
|
||||
(defn color-bullet-color [token-color-value]
|
||||
@@ -17,5 +17,5 @@
|
||||
(tinycolor/->hex-string tc))))
|
||||
|
||||
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
|
||||
(when (and resolved-value (cft/color-token? token))
|
||||
(when (and resolved-value (cfo/color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
@@ -108,6 +108,10 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-shadow
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
|
||||
|
||||
:error/unknown
|
||||
{:error/code :error/unknown
|
||||
:error/fn #(tr "labels.unknown-error")}})
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
:letter-spacing "Letter Spacing"
|
||||
:text-case "Text Case"
|
||||
:text-decoration "Text Decoration"
|
||||
:offsetX "X"
|
||||
:offsetY "Y"
|
||||
:offset-x "X"
|
||||
:offset-y "Y"
|
||||
:blur "Blur"
|
||||
:spread "Spread"
|
||||
:color "Color"
|
||||
|
||||
@@ -147,27 +147,30 @@
|
||||
|
||||
(defn create-token-set
|
||||
[token-set]
|
||||
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Clear possible local state
|
||||
(update state :workspace-tokens dissoc :token-set-new-path))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
|
||||
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn rename-token-set
|
||||
[token-set new-name]
|
||||
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
|
||||
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn rename-token-set-group
|
||||
[set-group-path set-group-fname]
|
||||
@@ -179,26 +182,6 @@
|
||||
(rx/of
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn update-token-set
|
||||
[token-set name]
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
name (ctob/normalize-set-name name (ctob/get-name token-set))
|
||||
tokens-lib (get data :tokens-lib)]
|
||||
|
||||
(if (ctob/get-set-by-name tokens-lib name)
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/rename-token-set (ctob/get-id token-set) name))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn duplicate-token-set
|
||||
[id]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
@@ -448,7 +431,7 @@
|
||||
(ctob/get-id token-set)
|
||||
token-id)]
|
||||
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
|
||||
unames (map :name tokens)
|
||||
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")
|
||||
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
|
||||
new-token (-> token
|
||||
|
||||
@@ -128,14 +128,16 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
prop-name (-> props (nth pos) :name)
|
||||
valid-pos? (> (count props) pos)
|
||||
prop-name (when valid-pos? (-> props (nth pos) :name))
|
||||
|
||||
changes (-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name))
|
||||
changes (when valid-pos?
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name)))
|
||||
undo-id (js/Symbol)]
|
||||
(when (not= prop-name new-name)
|
||||
(when (and valid-pos? (not= prop-name new-name))
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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]))
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.main.ui.components.dropdown :refer [dropdown-content*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as tm]
|
||||
@@ -53,14 +54,13 @@
|
||||
(def ^:private valid-option?
|
||||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu*
|
||||
[{:keys [show on-close options selectable selected
|
||||
(mf/defc context-menu-inner*
|
||||
[{:keys [on-close options selectable selected
|
||||
top left fixed min-width origin width]
|
||||
:as props}]
|
||||
|
||||
(assert (every? valid-option? options) "expected valid options")
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
(assert (boolean? show) "missing `show` prop")
|
||||
(assert (vector? options) "missing `options` prop")
|
||||
|
||||
(let [width (d/nilv width "initial")
|
||||
@@ -80,14 +80,15 @@
|
||||
offset-x (get state :offset-x)
|
||||
offset-y (get state :offset-y)
|
||||
levels (get state :levels)
|
||||
internal-id (mf/use-id)
|
||||
|
||||
on-local-close
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(fn []
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
(swap! state* assoc :levels [{:parent nil :options options}])
|
||||
(when (fn? on-close)
|
||||
(on-close))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
@@ -216,11 +217,22 @@
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}]))
|
||||
|
||||
(mf/with-effect [internal-id]
|
||||
(ug/dispatch! (ug/event "penpot:context-menu:open" #js {:id internal-id})))
|
||||
|
||||
(mf/with-effect [internal-id on-local-close]
|
||||
(letfn [(on-event [event]
|
||||
(when-let [detail (unchecked-get event "detail")]
|
||||
(when (not= internal-id (unchecked-get detail "id"))
|
||||
(on-local-close event))))]
|
||||
(ug/listen "penpot:context-menu:open" on-event)
|
||||
(partial ug/unlisten "penpot:context-menu:open" on-event)))
|
||||
|
||||
(mf/with-effect [ids]
|
||||
(tm/schedule-on-idle
|
||||
#(dom/focus! (dom/get-element (first ids)))))
|
||||
|
||||
(when (and show (some? levels))
|
||||
(when (some? levels)
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
@@ -229,7 +241,7 @@
|
||||
[:div {:class (stl/css-case
|
||||
:is-selectable selectable
|
||||
:context-menu true
|
||||
:is-open show
|
||||
:is-open true
|
||||
:fixed fixed)
|
||||
:style {:top (+ top offset-y)
|
||||
:left (+ left offset-x)}
|
||||
@@ -241,7 +253,7 @@
|
||||
:role "menu"
|
||||
:ref check-menu-offscreen}
|
||||
|
||||
(when-let [parent (:parent level)]
|
||||
(when parent
|
||||
[:*
|
||||
[:li {:id "go-back-sub-option"
|
||||
:class (stl/css :context-menu-item)
|
||||
@@ -256,7 +268,7 @@
|
||||
|
||||
[:li {:class (stl/css :separator)}]])
|
||||
|
||||
(for [[index option] (d/enumerate (:options level))]
|
||||
(for [[index option] (d/enumerate options)]
|
||||
(let [name (:name option)
|
||||
id (:id option)
|
||||
sub-options (:options option)
|
||||
@@ -297,3 +309,12 @@
|
||||
:data-testid id}
|
||||
name
|
||||
[:span {:class (stl/css :submenu-icon)} deprecated-icon/arrow]])]))))]])])))
|
||||
|
||||
(mf/defc context-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [show] :as props}]
|
||||
|
||||
(assert (boolean? show) "expected `show` prop to be a boolean")
|
||||
|
||||
(when ^boolean show
|
||||
[:> context-menu-inner* props]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -151,14 +151,16 @@
|
||||
|
||||
(mf/defc menu-team-icon*
|
||||
[{:keys [subscription-type]}]
|
||||
[:span {:class (stl/css :subscription-icon)
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}
|
||||
(case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)])
|
||||
[:span {:class (stl/css :subscription-icon-wrapper)}
|
||||
[:> icon* {:icon-id (case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)
|
||||
:class (stl/css :subscription-icon)
|
||||
:size "s"
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}]])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.subscription-icon {
|
||||
@extend .button-icon;
|
||||
.subscription-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-primary);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
border-radius: 6px;
|
||||
border-radius: $br-6;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
width: var(--sp-xl);
|
||||
height: var(--sp-xl);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
}
|
||||
|
||||
svg {
|
||||
block-size: var(--sp-m);
|
||||
inline-size: var(--sp-m);
|
||||
}
|
||||
.subscription-icon {
|
||||
stroke: var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
(when (not= 0 count-libraries)
|
||||
(if (pos? (count references))
|
||||
[:*
|
||||
[:div
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:h3 {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]]
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:p {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]
|
||||
(when (and (string? hint) (not= hint ""))
|
||||
[:> context-notification* {:level :info
|
||||
:appearance :ghost}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "refactor/basic-rules.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
@@ -15,14 +16,19 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
gap: var(--sp-xxl);
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
.list-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineMediumTypography;
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
@@ -31,13 +37,16 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
@include t.use-typography("body-small");
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.element-list {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
overflow-y: scroll;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -55,10 +64,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scd-msg {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal-scd-msg,
|
||||
.modal-subtitle,
|
||||
.modal-msg {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user