Compare commits

..

53 Commits

Author SHA1 Message Date
Andrey Antukh
347a758831 WIP 2025-12-11 12:22:28 +01:00
Andrey Antukh
4aea00a76f WIP 2025-12-11 12:16:39 +01:00
Andrey Antukh
5a260294a1 🔧 Update build-tag.yml github workflow 2025-12-11 12:00:42 +01:00
Andrey Antukh
3f6e44316e 🐛 Add missing node depes install on render-wasm 2025-12-11 11:51:47 +01:00
Eva Marco
77ef8e6fe6 🐛 Fix scroll on move library modal (#7952) 2025-12-11 10:46:54 +01:00
Alejandro Alonso
916b7709dc Update Pencil Penpot Design System System template in carousel (#7948) 2025-12-10 15:09:28 +01:00
Eva Marco
443e41fea4 🐛 Fix multiple selection with color tokens (#7941) 2025-12-10 14:36:08 +01:00
Alejandro Alonso
c7c9b04095 Merge pull request #7944 from penpot/niwinz-staging-exporter-fix
🐛 Fix incorrect resource lifetime handling on exporter
2025-12-10 14:35:20 +01:00
Eva Marco
c61a0c0332 📚 Add line to changelog (#7945) 2025-12-10 13:58:18 +01:00
Eva Marco
8707ff6511 🎉 Add spanish translation 2025-12-10 13:12:30 +01:00
Florian Schroedl
3d8a251741 🐛 Disallow font-family referencing composite token 2025-12-10 13:12:30 +01:00
Andrey Antukh
34e84ee3c8 🐛 Fix incorrect resource lifetime handling on exporter 2025-12-10 13:02:31 +01:00
Alejandro Alonso
e8201402a7 Merge pull request #7938 from penpot/niwinz-staging-bugfix-5
🐛 Fix several issues
2025-12-10 12:05:42 +01:00
Aitor Moreno
8a22477b96 Merge pull request #7932 from penpot/niwinz-staging-worker-wasm-load
🐛 Fix WASM loading strategy on worker
2025-12-10 11:47:31 +01:00
Alejandro Alonso
3e684ea54f ⬆️ Update svgo dependency on frontend (#7936) 2025-12-10 10:07:02 +01:00
Andrey Antukh
98039f13d8 🐛 Fix main toolbar z-index 2025-12-10 09:47:40 +01:00
Alejandro Alonso
40c27591f6 🐛 Fix svg import (#7925) 2025-12-10 08:36:54 +01:00
Andrey Antukh
91d20a46d1 💄 Add cosmetic changes to exports assets progress component 2025-12-10 08:23:05 +01:00
Andrey Antukh
50bead7c56 🐛 Fix react warning on having p inside p on assets export progress 2025-12-10 08:22:41 +01:00
Andrey Antukh
b75b999903 📎 Fix devenv jvm warning 2025-12-10 08:22:05 +01:00
Andrey Antukh
810f1721c8 🐛 Fix recursion render on subscription modal 2025-12-10 07:54:52 +01:00
Andrey Antukh
a4646373cf ♻️ Refactor wasm loading strategy on worker 2025-12-09 19:41:19 +01:00
Andrey Antukh
f111cbb2a4 Add better cache config on devenv nginx 2025-12-09 19:38:30 +01:00
Aitor Moreno
a614207f7e 🐛 Fix exporter failing with HTTPS 2025-12-09 16:08:20 +01:00
Luis de Dios
6ce3249c6d 🐛 Fix color format does not switch in the view mode (#7923)
* 🐛 Fix color format does not switch in the inspect mode of the view mode

* ♻️ Update components
2025-12-09 14:38:15 +01:00
Pablo Alba
b0351be724 🐛 Fix switch variants with paths 2025-12-09 11:08:55 +01:00
Andrey Antukh
b8392b3731 🐛 Fix regression on sending team invitations (#7912) 2025-12-05 12:36:06 +01:00
Andrey Antukh
77dba477ca 🔧 Backport build-tag github workflow from develop 2025-12-05 10:25:03 +01:00
Eva Marco
b6598d1f07 🐛 Fix scrollbar on color modal (#7906) 2025-12-05 09:55:41 +01:00
Xaviju
bf1dc21c75 💄 Hide themes & sets panels when none active (#7902) 2025-12-04 14:11:57 +01:00
Alejandro Alonso
46c20a993f Merge pull request #7904 from penpot/niwinz-staging-fix-invitation-resend
🐛 Fix exception on resending invitation
2025-12-04 11:56:07 +01:00
Andrey Antukh
0e0106f69a 🐛 Add correct assertion on create-invitation fn 2025-12-04 11:38:32 +01:00
Andrey Antukh
19bb69cc60 Improve invalid schema error report 2025-12-04 11:38:16 +01:00
Alejandro Alonso
504eb70988 Merge pull request #7885 from penpot/niwinz-staging-bugfix-2
🐛 Make workspace palette reposition on left sidebar collapse
2025-12-04 11:19:20 +01:00
Xaviju
75a2331edf 💄 Set low-emphasis color for both light/dark modes (#7884) 2025-12-04 11:04:07 +01:00
Alejandro Alonso
c2b4c9907d Merge pull request #7886 from penpot/niwinz-staging-bugfix-3
🐛 Fix casing on a translation of export files modal option
2025-12-04 10:59:51 +01:00
Alejandro Alonso
bd5bbcae26 Merge pull request #7894 from penpot/niwinz-staging-bugfix-4
🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar
2025-12-04 10:58:54 +01:00
Andrey Antukh
84273508ad 🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar 2025-12-04 10:56:29 +01:00
Andrey Antukh
9245ba6bc2 💄 Adapt component style for assets-local-library on sidebar assets 2025-12-04 10:55:57 +01:00
Andrey Antukh
4be046406d Pass direct args instead of a vector to toggle-values on sidebar assets 2025-12-04 10:55:57 +01:00
Alejandro Alonso
84c747cd31 Merge pull request #7883 from penpot/niwinz-staging-bugfix
🐛 Fix exception on paste text on comments input
2025-12-04 10:32:07 +01:00
Alejandro Alonso
0036a9a0cd Merge pull request #7865 from penpot/niwinz-staging-audit
 Add minor improvements to the audit module
2025-12-04 10:04:00 +01:00
Alejandro Alonso
2105c3a68c Merge pull request #7866 from penpot/niwinz-staging-fix-emails
🐛 Change internal ordering on how email parts are assembled
2025-12-04 09:56:22 +01:00
Belén Albeza
38efa88460 🐛 Fix unpublish library modal not scrolling file list (#7892)
* 🐛 Fix unpublish library modal not scrolling when the linked files list is too long

* 💄 Remove deprecated tokens in unpublish library modal

* 🔧 Update CHANGELOG
2025-12-03 22:41:20 +01:00
Pablo Alba
6e254c2cf4 🐛 Fix change of library on swap (#7898) 2025-12-03 22:40:23 +01:00
Andrey Antukh
94af978be8 🐛 Fix casing on a translation of export files modal option 2025-12-03 10:22:45 +01:00
Andrey Antukh
feababe2a8 🐛 Make workspace palette reposition on left sidebar collapse 2025-12-03 09:56:14 +01:00
Andrey Antukh
5ef06685fc 💄 Add cosmetic improvements to workspace palette component 2025-12-03 09:38:23 +01:00
Andrey Antukh
57fcec5afc 🐛 Make from-synthetic-clipboard-event function return always a stream
Causes an execption on steam processing when it returns nil
2025-12-03 08:32:38 +01:00
Andrey Antukh
58f82da61e 🐛 Fix exception on paste text on comments input 2025-12-03 08:20:58 +01:00
Andrey Antukh
a28c5b61ca 💄 Adapt viewport paste code codestyle
And remove some not necessary constructions
2025-12-03 08:09:13 +01:00
Andrey Antukh
95b7784a42 🐛 Change internal ordering on how email parts are assembled
This fixes the html email rendering on gmail. Other clients (like proton,
emailcatcher) properly renders html independently of the order of parts
on the multipart email structure but gmail requires that html should be
the last one.
2025-12-01 14:27:21 +01:00
Andrey Antukh
4690f740b9 Add minor improvements to the audit module 2025-12-01 13:57:55 +01:00
224 changed files with 6404 additions and 6042 deletions

305
.circleci/config.yml Normal file
View File

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

View File

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

View File

@@ -11,7 +11,7 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_wasm: "yes"
build_storybook: "yes"
build-docker:

View File

@@ -159,7 +159,17 @@ jobs:
- name: Build Bundle
working-directory: ./frontend
run: |
./scripts/build 0.0.0
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
- name: Store Bundle Cache
uses: actions/cache@v4
@@ -167,7 +177,6 @@ jobs:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04

View File

@@ -1,26 +1,5 @@
# CHANGELOG
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
### :sparkles: New features & Enhancements
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations
@@ -111,6 +90,10 @@ example. It's still usable as before, we just removed the example.
- 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)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
## 2.11.1

View File

@@ -3,7 +3,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System | Pencil"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}

View File

@@ -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))

View File

@@ -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)

View File

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

View File

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

View File

@@ -12,12 +12,16 @@
[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]
@@ -25,6 +29,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]
@@ -34,7 +39,8 @@
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[clojure.set :as set]))
[clojure.set :as set]
[clojure.spec.alpha :as s]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
@@ -471,10 +477,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]
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-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)
(container-log :info asset-id
:msg "Sync file with library"
@@ -508,10 +514,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]
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-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)
(container-log :info asset-id
:msg "Sync local components with library"
@@ -1874,6 +1880,44 @@
roperations'
uoperations')))))))
(defn- set-path-new-values
[current-shape prev-shape transform]
(let [new-content (segment/transform-content
(:content current-shape)
(gmt/transform-in (gpt/point 0 0) transform))
new-points (-> (segment/content->selrect new-content)
(grc/rect->points))
points-center (gco/points->center new-points)
new-selrect (gsh/calculate-selrect new-points points-center)
shape (assoc current-shape
:content new-content
:points new-points
:selrect new-selrect)
prev-center (segment/content-center (:content prev-shape))
delta (gpt/subtract points-center (first new-points))
new-pos (gpt/subtract prev-center delta)]
(gsh/absolute-move shape new-pos)))
(defn- switch-path-change-value
[prev-shape ;; The shape before the switch
current-shape ;; The shape after the switch (a clean copy)
ref-shape ;; The referenced shape on the main component
;; before the switch
attr]
(let [old-width (-> ref-shape :selrect :width)
new-width (-> prev-shape :selrect :width)
old-height (-> ref-shape :selrect :height)
new-height (-> prev-shape :selrect :height)
transform (-> (gpt/point (/ new-width old-width)
(/ new-height old-height))
(gmt/scale-matrix))
shape (set-path-new-values current-shape prev-shape transform)]
(get shape attr)))
(defn- switch-text-change-value
[prev-content ;; The :content of the text before the switch
@@ -2025,6 +2069,10 @@
(= :content attr)
(touched attr-group))
path-change?
(and (= :path (:type current-shape))
(contains? #{:points :selrect :content} attr))
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
@@ -2053,6 +2101,12 @@
(:content origin-ref-shape)
touched)
path-change?
(switch-path-change-value previous-shape
current-shape
origin-ref-shape
attr)
:else
(get previous-shape attr)))
@@ -2439,13 +2493,11 @@
(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 :ignore-mask true}))
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
[new-shape changes]
(-> changes
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
@@ -2815,15 +2867,13 @@
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication we change to root
;; For variants so the copy is made as a child of root
;; If there is an alt-duplication of a variant, change its parent to root
;; 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]
(cond-> shape
alt-duplication?
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
(if (and alt-duplication? (ctk/is-variant? shape))
(assoc shape :parent-id uuid/zero :frame-id nil)
shape))
shapes)

View File

@@ -123,10 +123,8 @@
;; 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
ignore-mask]
:or {ignore-children-fn (constantly false)
ignore-mask false}}]
ignore-children-fn]
:or {ignore-children-fn (constantly false)}}]
(let [objects (pcb/get-objects changes)
data (pcb/get-library-data changes)
page-id (pcb/get-page-id changes)
@@ -164,20 +162,18 @@
lookup (d/getf objects)
groups-to-unmask
(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)
[])
(reduce (fn [group-ids id]
;; When the shape to delete is the mask of a masked group,
;; the mask condition must be removed, and it must be
;; converted to a normal group.
(let [obj (lookup id)
parent (lookup (:parent-id obj))]
(if (and (:masked-group parent)
(= id (first (:shapes parent))))
(conj group-ids (:id parent))
group-ids)))
#{}
ids-to-delete)
interacting-shapes
(filter (fn [shape]

View File

@@ -8,8 +8,6 @@
(: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]
@@ -21,6 +19,8 @@
[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]
@@ -245,30 +245,27 @@
:level (d/nilv level 8)
:length (d/nilv length 12)})))))
#?(: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 ::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 ::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)]}))
(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
(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 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))))
(defmacro ignoring
[expr]
@@ -284,9 +281,22 @@
(defn check-fn
"Create a predefined check function"
[s & {:keys [hint type code]}]
(let [s (delay (schema s))
validator* (delay (m/validator @s))
explainer* (delay (m/explainer @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")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
@@ -302,13 +312,6 @@
::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.
@@ -1003,9 +1006,6 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-safe-int?
(lazy-validator ::safe-int))
(def valid-text?
(validator ::text))

View File

@@ -1575,10 +1575,10 @@ Will return a value that matches this schema:
(if (map? shadow)
(let [legacy-shadow-type (get "type" shadow)]
(-> shadow
(set/rename-keys {"x" :offset-x
"offsetX" :offset-x
"y" :offset-y
"offsetY" :offset-y
(set/rename-keys {"x" :offsetX
"offsetX" :offsetX
"y" :offsetY
"offsetY" :offsetY
"blur" :blur
"spread" :spread
"color" :color
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
(= "false" %) false
(= legacy-shadow-type "innerShadow") true
:else false))
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
shadow))]
(cond
;; Reference value - keep as string
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
(mapv (fn [shadow]
(if (map? shadow)
(-> shadow
(set/rename-keys {:offset-x "offsetX"
:offset-y "offsetY"
(set/rename-keys {:offsetX "offsetX"
:offsetY "offsetY"
:blur "blur"
:spread "spread"
:color "color"

View File

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

View File

@@ -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 (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(t/is (= [{:offsetX "0", :offsetY "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 (= [{: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}]
(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}]
(: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 (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(t/is (= [{:offsetX "0", :offsetY "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 [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:description "A single shadow"})
"shadow.multiple"
(ctob/make-token
{:name "shadow.multiple"
:type :shadow
: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"}]})
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offsetX "0" :offsetY "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 [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
:description "Round trip test"})
"shadow.ref"
(ctob/make-token

View File

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

View File

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

View File

@@ -38,11 +38,11 @@ http {
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_comp_level 3;
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 application/wasm;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
map $http_upgrade $connection_upgrade {
default upgrade;
@@ -223,15 +223,19 @@ http {
add_header X-Cache-Status $upstream_cache_status;
}
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~* \.(js|css|wasm)$ {
add_header Cache-Control "no-store" always;
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Cache-Control "no-store, no-cache, max-age=0" always;
add_header Cache-Control "no-store" always;
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

@@ -30,18 +30,13 @@ tmux select-window -t penpot:1
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn run watch:app' enter
tmux new-window -t penpot:2 -n 'frontend ts'
tmux new-window -t penpot:2 -n 'frontend storybook'
tmux select-window -t penpot:2
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn watch:ts' enter
tmux new-window -t penpot:3 -n 'frontend storybook'
tmux select-window -t penpot:3
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn run watch:storybook' enter
tmux new-window -t penpot:4 -n 'exporter'
tmux select-window -t penpot:4
tmux new-window -t penpot:3 -n 'exporter'
tmux select-window -t penpot:3
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot 'yarn run watch' enter
@@ -50,8 +45,8 @@ tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
tmux new-window -t penpot:5 -n 'backend'
tmux select-window -t penpot:5
tmux new-window -t penpot:4 -n 'backend'
tmux select-window -t penpot:4
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter

View File

@@ -7,7 +7,6 @@ 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/location.d/;

View File

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

View File

@@ -21,6 +21,7 @@
"raw-body": "^3.0.1",
"source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0",
"xml-js": "^1.6.11",
"xregexp": "^5.1.2"
},

View File

@@ -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)

View File

@@ -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
@@ -107,12 +107,12 @@
:on-progress on-progress)
append (fn [{:keys [filename path] :as resource}]
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
proc (->> exports
(map (fn [export] (rd/render export append)))
(p/all)
(p/fnly (fn [_] (.finalize zip)))
(p/mcat (fn [_] (rsc/close-zip zip)))
(p/fmap (constantly resource))
(p/mcat (partial rsc/upload-resource auth-token))
(p/fmap (fn [resource]

View File

@@ -11,6 +11,7 @@
["node:fs" :as fs]
["node:fs/promises" :as fsp]
["node:path" :as path]
["undici" :as http]
[app.common.exceptions :as ex]
[app.common.transit :as t]
[app.common.uri :as u]
@@ -53,30 +54,40 @@
(.pipe zip out)
zip))
(defn add-to-zip!
(defn add-to-zip
[zip path name]
(.file ^js zip path #js {:name name}))
(defn close-zip!
(defn close-zip
[zip]
(.finalize ^js zip))
(p/create (fn [resolve]
(.on ^js zip "close" resolve)
(.finalize ^js zip))))
(defn upload-resource
[auth-token resource]
(->> (fsp/readFile (:path resource))
(p/fmap (fn [buffer]
(js/console.log buffer)
(new js/Blob #js [buffer] #js {:type (:mtype resource)})))
(p/mcat (fn [blob]
(let [fdata (new js/FormData)
uri (-> (cf/get :public-uri)
(u/ensure-path-slash)
(u/join "api/management/methods/upload-tempfile")
(str))]
(let [fdata (new http/FormData)
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
headers #js {"X-Shared-Key" cf/management-key
"Authorization" (str "Bearer " auth-token)}
request #js {:headers headers
:method "POST"
:body fdata
:dispatcher agent}
uri (-> (cf/get :public-uri)
(u/ensure-path-slash)
(u/join "api/management/methods/upload-tempfile")
(str))]
(.append fdata "content" blob (:filename resource))
(js/fetch uri #js {:headers #js {"X-Shared-Key" cf/management-key
"Authorization" (str "Bearer " auth-token)}
:method "POST"
:body fdata}))))
(http/fetch uri request))))
(p/mcat (fn [response]
(if (not= (.-status response) 200)
(ex/raise :type :internal

View File

@@ -75,7 +75,8 @@
[path]
(->> (.stat fs/promises path)
(p/fmap (fn [data]
{:created-at (inst-ms (.-ctime ^js data))
{:path path
:created-at (inst-ms (.-ctime ^js data))
:size (.-size data)}))
(p/merr (fn [_cause]
(p/resolved nil)))))

View File

@@ -582,6 +582,7 @@ __metadata:
raw-body: "npm:^3.0.1"
source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0"
ws: "npm:^8.18.3"
xml-js: "npm:^1.6.11"
xregexp: "npm:^5.1.2"
@@ -1513,6 +1514,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.16.0":
version: 7.16.0
resolution: "undici@npm:7.16.0"
checksum: 10c0/efd867792e9f233facf9efa0a087e2d9c3e4415c0b234061b9b40307ca4fa01d945fee4d43c7b564e1b80e0d519bcc682f9f6e0de13c717146c00a80e2f1fb0f
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"

2
frontend/.gitignore vendored
View File

@@ -11,4 +11,4 @@ node_modules/
/blob-report/
/playwright/.cache/
/playwright/**/visual-specs/**/*.png
/ts/dist/

View File

@@ -1,6 +1,6 @@
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../ts/src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../resources/public"],
addons: [
"@storybook/addon-themes",

View File

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

View File

@@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.25"
:git/sha "27e5a1a"
{:git/tag "v2.24"
:git/sha "17a0c94"
: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.2"}
{thheller/shadow-cljs {:mvn/version "3.2.0"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -50,5 +50,8 @@
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
:jvm-opts ["--sun-misc-unsafe-memory-access=allow" "-Dpenpot.wasm.profile-marks=true"]}
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
"-Dpenpot.wasm.profile-marks=true"
"-XX:+UnlockExperimentalVMOptions"
"-XX:CompileCommand=blackhole,criterium.blackhole.Blackhole::consume"]}
}}

View File

@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['ts/dist']),
{
files: ['ts/src/**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -17,8 +17,7 @@
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"@vitejs/plugin-react": "^4.2.0",
"playwright": "1.52.0",
"playwright-core": "1.52.0",
"globals": "^16.5.0"
"playwright-core": "1.52.0"
},
"scripts": {
"build:app:assets": "node ./scripts/build-app-assets.js",
@@ -33,9 +32,8 @@
"e2e:server": "node ./scripts/e2e-server.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -w",
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -c --check",
"lint:ts": "eslint ts/",
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
"lint:clj": "clj-kondo --parallel --lint src/",
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
@@ -52,7 +50,6 @@
"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:ts": "vite build --watch",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
},
"devDependencies": {
@@ -62,16 +59,11 @@
"@storybook/addon-vitest": "10.0.4",
"@storybook/react-vite": "10.0.4",
"@types/node": "^22.15.21",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "3.2.4",
"autoprefixer": "^10.4.21",
"concurrently": "^9.2.1",
"esbuild": "^0.25.9",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"express": "^5.1.0",
"fancy-log": "^2.0.0",
"getopts": "^2.3.0",
@@ -103,7 +95,6 @@
"storybook": "10.0.4",
"svg-sprite": "^2.0.4",
"typescript": "^5.9.2",
"typescript-eslint": "^8.45.0",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
@@ -115,11 +106,9 @@
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.1",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@penpot/ts": "portal:./ts",
"@tokens-studio/sd-transforms": "1.2.11",
"@vitejs/plugin-react": "4.2.0",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"compression": "^1.8.1",
"date-fns": "^4.1.0",

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,26 +17,23 @@
<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?version={{& version}}" rel="stylesheet" type="text/css" />
<link href="css/ts-style.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
<link href="css/debug.css?ts={{& ts}}" 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 src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
<script defer src="{{& config}}"></script>
<script defer src="{{& polyfills}}"></script>
{{/manifest}}
<script>
window.penpotTranslations = JSON.parse({{& translations}});
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
</script>
<!--cookie-consent-->
</head>
<body>
@@ -47,11 +44,9 @@
<section id="modal"></section>
{{# manifest}}
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& app_main}}";
init();
</script>
<script defer src="js/libs.js?ts={{& ts}}"></script>
<script defer src="{{& shared}}"></script>
<script defer src="{{& main}}"></script>
{{/manifest}}
</body>
</html>

View File

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

View File

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

View File

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

View File

@@ -28,8 +28,6 @@ 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 =
@@ -49,7 +47,8 @@ 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 {
@@ -181,41 +180,44 @@ export async function watch(baseDir, predicate, callback) {
});
}
async function readManifestFile() {
const manifestPath = "resources/public/js/manifest.json";
async function readManifestFile(resource) {
const manifestPath = "resources/public/" + resource;
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
return JSON.parse(content);
}
async function readShadowManifest() {
const index = {
app_main: "./js/main.js",
render_main: "./js/render.js",
rasterizer_main: "./js/rasterizer.js",
const ts = Date.now();
try {
const content = await readManifestFile("js/manifest.json");
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,
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
};
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
}
})
};
for (let item of content) {
index[item.name] = "js/" + item["output-name"];
}
return index;
const content2 = await readManifestFile("js/worker/manifest.json");
for (let item of content2) {
index["worker_" + item.name] = "js/worker/" + item["output-name"];
}
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,
};
}
}
async function renderTemplate(path, context = {}, partials = {}) {
@@ -255,7 +257,7 @@ const markedOptions = {
marked.use(markedOptions);
export async function compileTranslations() {
async function readTranslations() {
const langs = [
"ar",
"ca",
@@ -292,10 +294,9 @@ export async function compileTranslations() {
["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`;
@@ -314,6 +315,11 @@ export async function compileTranslations() {
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;
@@ -323,9 +329,9 @@ export async function compileTranslations() {
message = marked.parseInline(message);
}
result[key] = message;
result[key][lang] = message;
} else {
result[key] = msgs.map((item) => {
result[key][lang] = msgs.map((item) => {
if (isMarkdown) {
return marked.parseInline(item);
} else {
@@ -334,12 +340,22 @@ export async function compileTranslations() {
});
}
}
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) {
@@ -391,6 +407,14 @@ 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;
@@ -412,16 +436,13 @@ 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",
context,
{
manifest: manifest,
translations: JSON.stringify(translations),
isDebug,
},
partials,
);
@@ -429,30 +450,41 @@ 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",
context,
{
manifest: manifest,
},
partials,
);
await fs.writeFile("./.storybook/preview-body.html", content);
content = await renderTemplate(
"resources/templates/preview-head.mustache",
context,
{
manifest: manifest,
translations: JSON.stringify(storybookTranslations),
},
partials,
);
await fs.writeFile("./.storybook/preview-head.html", content);
content = await renderTemplate("resources/templates/render.mustache", context);
content = await renderTemplate("resources/templates/render.mustache", {
manifest: manifest,
translations: JSON.stringify(translations),
});
await fs.writeFile("./resources/public/render.html", content);
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
content = await renderTemplate("resources/templates/rasterizer.mustache", {
manifest: manifest,
translations: JSON.stringify(translations),
});
await fs.writeFile("./resources/public/rasterizer.html", content);
}

View File

@@ -20,27 +20,31 @@ echo $PATH
set -ex
corepack enable;
corepack install || exit 1;
corepack install;
yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
mkdir -p resources/public;
mkdir -p target/dist;
yarn run build:app:main $EXTRA_PARAMS || exit 1
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS;
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
pushd ../render-wasm;
./build
popd
fi
yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
mkdir -p target/dist;
rsync -avr resources/public/ target/dist/
rsync -avr resources/public/ target/dist/;
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_STORYBOOK" = "yes" ]; then
# build storybook

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,6 @@
#!/usr/bin/env bash
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"
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
set -ex
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
exec clojure $OPTIONS -M -m rebel-readline.main

View File

@@ -52,7 +52,6 @@ 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();
@@ -82,7 +81,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.compileTranslations();
await h.compileTemplates();
});
log.info("watch: assets (~)");

View File

@@ -6,12 +6,13 @@
:builds
{:main
{:target :esm
{:target :browser
: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 []}
@@ -19,42 +20,42 @@
:main
{:entries [app.main app.plugins.api]
:depends-on #{:shared}
:exports {init app.main/init}}
:init-fn app.main/init}
:util-highlight
{:entries [app.util.code-highlight]
:depends-on #{:shared}}
:depends-on #{:main}}
:main-auth
{:entries [app.main.ui.auth
app.main.ui.auth.verify-token]
:depends-on #{:shared}}
:depends-on #{:main}}
:main-viewer
{:entries [app.main.ui.viewer]
:depends-on #{:shared :main-auth}}
:depends-on #{:main :main-auth}}
:main-workspace
{:entries [app.main.ui.workspace]
:depends-on #{:shared}}
:depends-on #{:main}}
:main-dashboard
{:entries [app.main.ui.dashboard]
:depends-on #{:shared}}
:depends-on #{:main}}
:main-settings
{:entries [app.main.ui.settings]
:depends-on #{:shared}}
:depends-on #{:main}}
:render
{:entries [app.render]
:depends-on #{:shared}
:exports {init app.render/init}}
:init-fn app.render/init}
:rasterizer
{:entries [app.rasterizer]
:depends-on #{:shared}
:exports {init app.rasterizer/init}}}
:init-fn app.rasterizer/init}}
:js-options
{:entry-keys ["module" "browser" "main"]
@@ -74,14 +75,15 @@
: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
{:target :esm
{:target :browser
:output-dir "resources/public/js/worker/"
:asset-path "/js/worker"
:devtools {:browser-inject :main
@@ -92,6 +94,7 @@
{:main
{:entries [app.worker]
:web-worker true
:prepend-js "importScripts('/js/worker/render.js');"
:depends-on #{}}}
:js-options

View File

@@ -86,6 +86,7 @@
(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))
@@ -126,7 +127,7 @@
public-uri))
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
(obj/get global "penpotWorkerURI" "/js/worker/main.js"))
(defn external-feature-flag
[flag value]
@@ -188,6 +189,11 @@
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
(false? thumbnail?) (u/join (dm/str id)))))))
(defn resolve-static-asset
[path]
(u/join public-uri path))
(defn resolve-href
[resource]
(let [version (get version :full)
href (-> public-uri
(u/ensure-path-slash)
(u/join resource)
(get :path))]
(str href "?version=" version)))

View File

@@ -6,7 +6,6 @@
(ns app.main
(:require
["@penpot/ts" :refer [setTranslation]]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.types.objects-map]
@@ -39,8 +38,6 @@
(log/setup! {:app :info})
(log/set-level! :debug)
(setTranslation i18n/tr)
(when (= :browser cf/target)
(log/inf :version (:full cf/version)
:asserts *assert*
@@ -95,7 +92,7 @@
(defn ^:export init
[]
(mw/init!)
(i18n/init)
(i18n/init! cf/translations)
(cur/init-styles)
(thr/init!)
(init-ui)
@@ -117,4 +114,11 @@
[]
(reinit))
;; Reload the UI when the language changes
(add-watch
i18n/locale "locale"
(fn [_ _ old-value current-value]
(when (not= old-value current-value)
(reinit))))
(set! (.-stackTraceLimit js/Error) 50)

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
(: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]
@@ -53,16 +54,11 @@
(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?
@@ -488,7 +484,7 @@
(defn delete-access-token
[{:keys [id] :as params}]
(assert (uuid? id))
(us/assert! ::us/uuid id)
(ptk/reify ::delete-access-token
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -59,15 +59,9 @@
"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]
(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)]}))))
(if-let [tc (tinycolor/valid-color value)]
{:value value :unit (tinycolor/color-format tc)}
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
(defn- numeric-string? [s]
(and (string? s)
@@ -126,7 +120,7 @@
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 (seq (cto/find-token-value-references value)))
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))]
@@ -261,14 +255,19 @@
(defn- parse-sd-token-font-family-value
[value]
(let [missing-references (seq (some cto/find-token-value-references value))]
(let [value (-> (js->clj value) (flatten))
valid-font-family (or (string? value) (every? string? value))
missing-references (seq (some cto/find-token-value-references value))]
(cond
(not valid-font-family)
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)]}
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
:else
{:value (-> (js->clj value) (flatten))})))
{:value value})))
(defn parse-atomic-typography-value [token-type token-value]
(case token-type
@@ -374,8 +373,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 {:offset-x parse-sd-token-general-value
:offset-y parse-sd-token-general-value
parsers {:offsetX parse-sd-token-general-value
:offsetY parse-sd-token-general-value
:blur parse-sd-token-shadow-blur
:spread parse-sd-token-shadow-spread
:color parse-sd-token-color-value
@@ -395,42 +394,35 @@
(defn- parse-sd-token-shadow-value
"Parses shadow value and validates it."
[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)]}
(cond
;; Reference value (string)
(string? value) {:value value}
;; Empty value
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
;; Invalid value
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
;; Array of shadows
:else
(let [converted (js->clj value :keywordize-keys true)
:else
(let [converted (js->clj value :keywordize-keys true)
;; Parse each shadow with its index
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
;; Collect all errors from all shadows
all-errors (mapcat :errors parsed-shadows)
all-errors (mapcat :errors parsed-shadows)
;; Collect all values from shadows that have values
all-values (into [] (keep :value parsed-shadows))]
all-values (into [] (keep :value parsed-shadows))]
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values})))))
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values}))))
(defn collect-shadow-errors [token shadow-index]
(group-by :shadow-key

View File

@@ -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

View File

@@ -706,58 +706,53 @@
(= 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)
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width wrapper))))
(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-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
[parent-id delta index])
;; 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))
;; 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 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-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width 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))
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
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])
;; 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)))]))
(empty? page-selected)
(let [frame-id (ctst/top-nested-frame page-objects position)

View File

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

View File

@@ -153,11 +153,11 @@
(defn value->shadow
"Transform a token shadow value into penpot shadow data structure"
[value]
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
{:id (random-uuid)
:hidden false
:offset-x offset-x
:offset-y offset-y
:offset-x offsetX
:offset-y offsetY
:blur blur
:color (value->color color)
:spread spread

View File

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

View File

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

View File

@@ -31,28 +31,27 @@
[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 #(mod/load 'app.main.ui.auth/auth-page*)))
(mf/lazy-component app.main.ui.auth/auth))
(def verify-token-page*
(mf/lazy #(mod/load 'app.main.ui.auth.verify-token/verify-token-page*)))
(def verify-token-page
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def viewer-page*
(mf/lazy #(mod/load 'app.main.ui.viewer/viewer-page*)))
(mf/lazy-component app.main.ui.viewer/viewer*))
(def dashboard-page*
(mf/lazy #(mod/load 'app.main.ui.dashboard/dashboard-page*)))
(mf/lazy-component app.main.ui.dashboard/dashboard*))
(def settings-page*
(mf/lazy #(mod/load 'app.main.ui.settings/settings-page*)))
(mf/lazy-component app.main.ui.settings/settings*))
(def workspace-page*
(mf/lazy #(mod/load 'app.main.ui.workspace/workspace-page*)))
(mf/lazy-component app.main.ui.workspace/workspace*))
(mf/defc workspace-legacy-redirect*
{::mf/props :obj
@@ -190,7 +189,7 @@
[:? [:& auth-page {:route route}]]
:auth-verify-token
[:? [:& verify-token-page* {:route route}]]
[:? [:& verify-token-page {:route route}]]
(:settings-profile
:settings-password

View File

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

View File

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

View File

@@ -8,28 +8,24 @@
(: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]))
[rumext.v2 :as mf]
[shadow.lazy :as lazy]))
(def highlight-fn
(delay (modules/load-fn 'app.util.code-highlight/highlight!)))
(lazy/loadable 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)]
(->> @highlight-fn
(p/fmap (fn [f] (f)))
(p/fnly (fn [f cause]
(if cause
(js/console.error cause)
(f node)))))))
(p/let [highlight-fn (lazy/load highlight-fn)]
(highlight-fn node))))
[:pre {:class (dm/str type " " (stl/css :code-display)) :ref block-ref} code]))

View File

@@ -9,7 +9,6 @@
(: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]))
@@ -47,7 +46,7 @@
:disabled disabled)}
(if (some? icon)
[:> icon* {:icon-id icon :class icon-class :aria-hidden true}]
[:span {:class icon-class} icon]
[:span {:class (stl/css :title-name)} value])
[:input {:id id

View File

@@ -247,6 +247,7 @@
(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)
@@ -312,8 +313,3 @@
:section section
:search-term search-term
:team team}]]]))
(mf/defc dashboard-page*
{::mf/lazy-load true}
[props]
[:> dashboard* props])

View File

@@ -7,7 +7,6 @@
(ns app.main.ui.dashboard.projects
(:require-macros [app.main.style :as stl])
(:require
["@penpot/ts" :refer [TestTsxComponent]]
[app.common.geom.point :as gpt]
[app.common.time :as ct]
[app.main.data.common :as dcm]
@@ -51,11 +50,8 @@
::mf/props :obj
::mf/private true}
[{:keys [can-edit]}]
;; (js/console.log "xxxx" TestTsxComponent)
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div
[:> TestTsxComponent]]
[:div#dashboard-projects-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.projects-title")]]
(when can-edit

View File

@@ -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}

View File

@@ -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: auto;
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;
}

View File

@@ -6,9 +6,9 @@
(ns app.main.ui.ds
(:require
[app.config :as cf]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
@@ -43,6 +43,8 @@
[app.util.i18n :as i18n]
[rumext.v2 :as mf]))
(i18n/init! cf/translations)
(def default
"A export used for storybook"
(mf/object
@@ -60,7 +62,6 @@
:RawSvg raw-svg*
:Select select*
:Switch switch*
:Checkbox checkbox*
:Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*
@@ -77,11 +78,6 @@
:Milestone milestone*
:MilestoneGroup milestone-group*
:Date date*
:set-default-translations
(fn [data]
(i18n/set-translations "en" data))
;; meta / misc
:meta
{:icons (clj->js (sort icon-list))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -223,24 +223,30 @@
circ (* 2 Math/PI 12)
pct (- circ (* circ (/ progress total)))
pwidth (if error?
280
(/ (* progress 280) total))
color (cond
error? clr/new-danger
healthy? (if is-default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
pwidth
(if error?
280
(/ (* progress 280) total))
background-clr (if is-default-theme?
clr/background-quaternary
clr/background-quaternary-light)
title (cond
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
color
(cond
error? clr/new-danger
healthy? (if is-default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if is-default-theme?
clr/background-quaternary
clr/background-quaternary-light)
title
(cond
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
retry-last-export
(mf/use-fn #(st/emit! (de/retry-last-export)))
@@ -284,7 +290,7 @@
:on-click retry-last-export}
(tr "workspace.options.retry")]
[:p {:class (stl/css :progress)}
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)

View File

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

View File

@@ -36,7 +36,7 @@
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})
(mf/defc attributes
(mf/defc attributes*
[{:keys [page-id file-id shapes frame from libraries share-id objects color-space]}]
(let [shapes (hooks/use-equal-memo shapes)
first-shape (first shapes)

View File

@@ -96,7 +96,7 @@
embed-images? (replace-map images-data))]
(str/format page-template style-code markup-code)))
(mf/defc code
(mf/defc code*
[{:keys [shapes frame on-expand from]}]
(let [style-type* (mf/use-state "css")
markup-type* (mf/use-state "html")

View File

@@ -16,8 +16,8 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.inspect.attributes :refer [attributes]]
[app.main.ui.inspect.code :refer [code]]
[app.main.ui.inspect.attributes :refer [attributes*]]
[app.main.ui.inspect.code :refer [code*]]
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
[app.main.ui.inspect.styles :refer [styles-tab*]]
[app.util.dom :as dom]
@@ -122,8 +122,7 @@
(fn []
(if (seq shapes)
(st/emit! (ptk/event ::ev/event {::ev/name "inspect-mode-click-element"}))
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))
(reset! color-space* "hex")))
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))))
[:aside {:class (stl/css-case :settings-bar-right true
:viewer-code (= from :viewer))}
@@ -189,41 +188,41 @@
:libraries libraries
:file-id file-id}]
:computed
[:& attributes {:color-space color-space
:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
[:> attributes* {:color-space color-space
:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]
[:> code* {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]
[:> tab-switcher* {:tabs tabs
:selected (name @section)
:on-change handle-change-tab
:class (stl/css :viewer-tab-switcher)}
(case @section
:info
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
[:> attributes* {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:> code* {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}

View File

@@ -133,7 +133,7 @@
(swap! shorthands* assoc (:panel shorthand) (:property shorthand))))]
[:ol {:class (stl/css :styles-tab) :aria-label (tr "labels.styles")}
;; TOKENS PANEL
(when (or active-themes active-sets)
(when (or (seq active-themes) (seq active-sets))
[:li
[:> style-box* {:panel :token}
[:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]])

View File

@@ -6,6 +6,15 @@
@use "ds/typography.scss" as *;
// TODO: this must be a custom property in the design system
:global(.light) {
--low-emphasis-background: #fafafa;
}
:global(.default) {
--low-emphasis-background: #121214;
}
.style-box {
--title-gap: var(--sp-xs);
--title-padding: var(--sp-s);
@@ -13,12 +22,9 @@
--arrow-color: var(--color-foreground-secondary);
--box-border-color: var(--color-background-primary);
// TODO: this must be a custom property in the design system
--lowEmphasis-background: #121214;
padding-block: var(--sp-s);
padding-inline: var(--sp-m);
background-color: var(--lowEmphasis-background);
background-color: var(--low-emphasis-background);
border-block-end: 2px solid var(--box-border-color);
}

View File

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

View File

@@ -77,7 +77,7 @@
[:button {:class (stl/css :cta-button :bottom-link)
:on-click cta-link-trial} cta-text-trial])])
(defn schema:seats-form [min-editors]
(defn- make-management-form-schema [min-editors]
[:map {:title "SeatsForm"}
[:min-members [::sm/number {:min min-editors
:max 9999}]]
@@ -87,7 +87,6 @@
{::mf/register modal/components
::mf/register-as :management-dialog}
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
(let [unlimited-modal-step*
(mf/use-state 1)
@@ -112,9 +111,12 @@
{:min-members min-editors
:redirect-to-payment-details false})
schema
(mf/with-memo [min-editors]
(make-management-form-schema min-editors))
form
(fm/use-form :schema (schema:seats-form min-editors)
:initial initial)
(fm/use-form :schema schema :initial initial)
submit-in-progress
(mf/use-ref false)
@@ -334,11 +336,15 @@
[:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]]
[:div {:class (stl/css :modal-end)}
[:div {:class (stl/css :modal-title)} (tr "subscription.settings.sucess.dialog.title" subscription-name)]
[:div {:class (stl/css :modal-title)}
(tr "subscription.settings.sucess.dialog.title" subscription-name)]
(when (not= subscription-name "professional")
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.thanks" subscription-name)])
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.sucess.dialog.footer")]
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.thanks" subscription-name)])
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.sucess.dialog.footer")]
[:div {:class (stl/css :success-action-buttons)}
[:input
@@ -418,7 +424,11 @@
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? success-modal-is-trial? subscription]
(mf/with-effect [authenticated?
show-subscription-success-modal?
show-trial-subscription-modal?
success-modal-is-trial?
subscription]
(when ^boolean authenticated?
(cond
^boolean show-trial-subscription-modal?

View File

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

View File

@@ -27,7 +27,7 @@
[app.main.ui.workspace.coordinates :as coordinates]
[app.main.ui.workspace.libraries]
[app.main.ui.workspace.nudge]
[app.main.ui.workspace.palette :refer [palette]]
[app.main.ui.workspace.palette :refer [palette*]]
[app.main.ui.workspace.plugins]
[app.main.ui.workspace.sidebar :refer [sidebar*]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
@@ -35,7 +35,7 @@
[app.main.ui.workspace.tokens.export.modal]
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.forms.modals]
[app.main.ui.workspace.tokens.management.create.modals]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]
[app.main.ui.workspace.viewport :refer [viewport*]]
@@ -84,8 +84,8 @@
node-ref (use-resize-observer on-resize)]
[:*
(when (not ^boolean hide-ui?)
[:& palette {:layout layout
:on-change-palette-size on-resize-palette}])
[:> palette* {:layout layout
:on-change-size on-resize-palette}])
[:section
{:key (dm/str "workspace-" page-id)
@@ -165,7 +165,7 @@
(dsh/lookup-page state file-id page-id))))
st/state))
(mf/defc workspace-inner*
(mf/defc workspace-page*
{::mf/private true}
[{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id]
@@ -252,16 +252,10 @@
:touch-action "none"}}
[:> context-menu*]
(if (and file-loaded? page-id)
[:> workspace-inner*
[:> workspace-page*
{:page-id page-id
:file-id file-id
:file file
:wglobal wglobal
:layout layout}]
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*
{::mf/lazy-load true}
[props]
[:> workspace* props])

View File

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

View File

@@ -156,7 +156,7 @@
(let [{:keys [modal title]} (get dwta/token-properties :color)
window-size (dom/get-window-size)
left-sidebar (dom/get-element "left-sidebar-aside")
x-size (dom/get-data left-sidebar "left-sidebar-width")
x-size (dom/get-data left-sidebar "width")
modal-height 392
x (- (int x-size) 30)
y (- (/ (:height window-size) 2) (/ modal-height 2))]

View File

@@ -33,12 +33,13 @@
[okulary.core :as l]
[rumext.v2 :as mf]))
(def viewport
(def ^:private ref:viewport
(l/derived :vport refs/workspace-local))
(defn calculate-palette-padding [rulers?]
(defn- calculate-palette-style
[rulers?]
(let [left-sidebar (dom/get-element "left-sidebar-aside")
left-sidebar-size (-> (dom/get-data left-sidebar "left-sidebar-width")
left-sidebar-size (-> (dom/get-data left-sidebar "width")
(d/parse-integer))
rulers-width (if rulers? 22 0)
min-left-sidebar-width left-sidebar-default-width
@@ -48,36 +49,46 @@
#js {"paddingLeft" (dm/str calculate-padding-left "px")
"paddingRight" "322px"}))
(mf/defc palette
[{:keys [layout on-change-palette-size]}]
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil)
state* (mf/use-state {:show-menu false})
state (deref state*)
show-menu? (:show-menu state)
selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent)
selected-text* (mf/use-state :file)
selected-text (deref selected-text*)
on-select (mf/use-fn #(reset! selected %))
rulers? (mf/deref refs/rulers?)
{:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]}
(r/use-resize-hook :palette 72 54 80 :y true :bottom on-change-palette-size)
(mf/defc palette*
[{:keys [layout on-change-size]}]
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
vport (mf/deref viewport)
vport-width (:width vport)
read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil)
state* (mf/use-state #(-> {:show-menu false}))
state (deref state*)
show-menu? (:show-menu state)
selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent)
selected-text* (mf/use-state :file)
selected-text (deref selected-text*)
on-select (mf/use-fn #(reset! selected %))
rulers? (mf/deref refs/rulers?)
vport (mf/deref ref:viewport)
vport-width (get vport :width)
{:keys [on-pointer-down
on-lost-pointer-capture
on-pointer-move
parent-ref
size]}
(r/use-resize-hook :palette 72 54 80 :y true :bottom on-change-size)
on-resize
(mf/use-callback
(mf/use-fn
(fn [_]
(let [dom (mf/ref-val container)
width (obj/get dom "clientWidth")]
(swap! state* assoc :width width))))
on-close-menu
(mf/use-callback
(mf/use-fn
(fn [_]
(swap! state* assoc :show-menu false)))
@@ -100,7 +111,7 @@
(reset! selected-text* (:id lib)))))
toggle-palettes
(mf/use-callback
(mf/use-fn
(fn [_]
(r/set-resize-type! :top)
(dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down")
@@ -131,7 +142,9 @@
(vary-meta assoc ::ev/origin "workspace-left-toolbar"))))
(dom/blur! node))))
any-palette? (or color-palette? text-palette?)
any-palette?
(or color-palette? text-palette?)
size-classname
(cond
(<= size 64) (stl/css :small-palette)
@@ -142,16 +155,16 @@
(let [key1 (events/listen js/window "resize" on-resize)]
#(events/unlistenByKey key1)))
(mf/use-layout-effect
#(let [dom (mf/ref-val parent-ref)
(mf/with-layout-effect []
(let [dom (mf/ref-val parent-ref)
width (obj/get dom "clientWidth")]
(swap! state* assoc :width width)))
[:div {:class (stl/css :palette-wrapper)
:id "palette-wrapper"
:style (calculate-palette-padding rulers?)
:style (calculate-palette-style rulers?)
:data-testid "palette"}
(when-not workspace-read-only?
(when-not ^boolean read-only?
[:div {:ref parent-ref
:class (dm/str size-classname " " (stl/css-case :palettes true
:wide any-palette?

View File

@@ -27,7 +27,6 @@
[app.main.ui.workspace.left-header :refer [left-header*]]
[app.main.ui.workspace.right-header :refer [right-header*]]
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox*]]
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button*]]
[app.main.ui.workspace.sidebar.debug :refer [debug-panel*]]
[app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info*]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
@@ -44,19 +43,34 @@
;; --- Left Sidebar (Component)
(defn- on-collapse-left-sidebar
[]
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(def ^:private toggle-collapse-left-sidebar
(partial st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(mf/defc collapse-button*
{::mf/private true}
[]
;; NOTE: This custom button may be replace by an action button when this variant is designed
[:button {:class (stl/css :collapse-sidebar-button)
:on-click on-collapse-left-sidebar}
:on-click toggle-collapse-left-sidebar}
[:> icon* {:icon-id i/arrow
:size "s"
:aria-label (tr "workspace.sidebar.collapse")}]])
(mf/defc collapsed-button*
{::mf/memo true
::mf/private true}
[]
[:div {:id "left-sidebar-aside"
:data-width "0"
:class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button)
:title (tr "workspace.sidebar.expand")
:on-click toggle-collapse-left-sidebar}
[:> icon* {:icon-id i/arrow
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]])
(mf/defc layers-content*
{::mf/private true
::mf/memo true}
@@ -97,6 +111,7 @@
[:> layers-toolbox* {:size-parent width}]]))
(mf/defc left-sidebar*
{::mf/memo true}
[{:keys [layout file page-id tokens-lib active-tokens resolved-active-tokens]}]
@@ -161,7 +176,7 @@
[:aside {:ref parent-ref
:id "left-sidebar-aside"
:data-testid "left-sidebar"
:data-left-sidebar-width (str width)
:data-width (str width)
:class aside-class
:style {:--left-sidebar-width (dm/str width "px")}}

View File

@@ -116,6 +116,44 @@
}
}
.collapsed-sidebar {
@include deprecated.flexCenter;
position: absolute;
top: deprecated.$s-48;
left: 0;
padding: deprecated.$s-4;
border-radius: deprecated.$br-8;
background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
}
.collapsed-title {
@include deprecated.flexCenter;
height: deprecated.$s-36;
width: deprecated.$s-24;
border-radius: deprecated.$br-8;
background: var(--color-background-secondary);
}
.collapsed-button {
@include deprecated.buttonStyle;
height: deprecated.$s-24;
width: deprecated.$s-16;
padding: 0;
border-radius: deprecated.$br-5;
svg {
@include deprecated.flexCenter;
height: deprecated.$s-16;
width: deprecated.$s-16;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
&:hover {
svg {
stroke: var(--icon-foreground-hover);
}
}
}
.versions-tab {
width: 100%;
overflow: hidden;

View File

@@ -56,9 +56,8 @@
(update file :data dissoc :pages-index))
refs/file))
(mf/defc assets-local-library
{::mf/wrap [mf/memo]
::mf/wrap-props false}
(mf/defc assets-local-library*
{::mf/private true}
[{:keys [filters]}]
(let [file (mf/deref ref:local-library)]
[:> file-library*
@@ -68,7 +67,7 @@
:filters filters}]))
(defn- toggle-values
[v [a b]]
[v a b]
(if (= v a) b a))
(mf/defc assets-toolbox*
@@ -97,7 +96,7 @@
(mf/use-fn
(mf/deps ordering)
(fn []
(let [new-value (toggle-values ordering [:asc :desc])]
(let [new-value (toggle-values ordering :asc :desc)]
(swap! filters* assoc :ordering new-value)
(dwa/set-current-assets-ordering! new-value))))
@@ -105,7 +104,7 @@
(mf/use-fn
(mf/deps list-style)
(fn []
(let [new-value (toggle-values list-style [:thumbs :list])]
(let [new-value (toggle-values list-style :thumbs :list)]
(swap! filters* assoc :list-style new-value)
(dwa/set-current-assets-list-style! new-value))))
@@ -209,5 +208,5 @@
[:& (mf/provider cmm/assets-toggle-ordering) {:value toggle-ordering}
[:& (mf/provider cmm/assets-toggle-list-style) {:value toggle-list-style}
[:*
[:& assets-local-library {:filters filters}]
[:> assets-local-library* {:filters filters}]
[:> assets-libraries* {:filters filters}]]]]]]))

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