Compare commits

...

200 Commits

Author SHA1 Message Date
Andrey Antukh
69c880d00e 🐛 Fix importmap usage on firefox 2025-12-23 13:10:58 +01:00
Andrey Antukh
9eebc467ef Preload default translations 2025-12-23 13:10:58 +01:00
Andrey Antukh
b77712ce73 Move frontend/vendor to frontend/packages 2025-12-23 13:10:58 +01:00
Andrey Antukh
3d3e81f314 Replace tubax with more modern tooling 2025-12-23 13:10:58 +01:00
Andrey Antukh
fe6441bb24 Replace hightlight.js internal bundle with direct npm use 2025-12-23 13:10:58 +01:00
Andrey Antukh
e15f0baf30 Replace direct draft-js usage with internal module 2025-12-23 13:10:58 +01:00
Andrey Antukh
c040cbb784 🔥 Remove old gulp related dependencies 2025-12-23 13:10:58 +01:00
Andrey Antukh
7f674b78a9 📎 Move all deps to dev-dependencies on frontend package.json
All they only needed for build process.
2025-12-23 13:10:58 +01:00
Andrey Antukh
099b78affd 📎 Update frontend yarn.lock file 2025-12-23 13:10:58 +01:00
Andrey Antukh
78cc3f0aa4 📎 Add immutable dependency to vendor/draft-js 2025-12-23 13:10:58 +01:00
Andrey Antukh
76f5f12808 ⬆️ Update dependencies on exporter 2025-12-23 13:10:58 +01:00
Andrey Antukh
047483a70a 🐛 Fix deleted files thumbnails generation 2025-12-22 20:20:43 +01:00
Andrey Antukh
8cb2f27de8 ♻️ Move file permissions to binfile common ns 2025-12-22 20:16:41 +01:00
Andrey Antukh
0433336fc9 📎 Use correct criterium version on frontend deps 2025-12-22 20:16:41 +01:00
Andrey Antukh
ce234fbeda Allow get thumbnails for deleted files 2025-12-22 20:16:41 +01:00
Andrey Antukh
fc4d31eed7 Add minor efficiency improvements to deleted dashboard page 2025-12-22 20:16:41 +01:00
María Valderrama
c670aac339 🎉 Added deleted files to dashboard 2025-12-22 20:16:41 +01:00
Andrés Moya
1d3fb5434f Enable shadow tokens by default (#7996) 2025-12-22 18:17:29 +01:00
Andrey Antukh
f478399ae0 Merge remote-tracking branch 'origin/staging-render' into develop 2025-12-22 17:28:18 +01:00
Andrey Antukh
6a1854f180 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 17:28:01 +01:00
Andrés Moya
0858e297e5 🎉 Add composite tokens to plugins API (#7992) 2025-12-22 17:14:54 +01:00
Alejandro Alonso
5780a43fe0 🐛 Fix object added in different page (#7988) 2025-12-22 16:59:47 +01:00
Alejandro Alonso
737eceda3a 🐛 Fix unmasking shapes (#7989) 2025-12-22 16:59:04 +01:00
Alonso Torres
923c3c2dbd 🐛 Fix font weight token (#7991) 2025-12-22 16:58:26 +01:00
Alejandro Alonso
a14b4561e7 🐛 Fix comment bubbles (#7990) 2025-12-22 16:57:45 +01:00
Anonymous
105e1fe86c 🌐 Add translations for: Spanish
Currently translated at 97.1% (1940 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2025-12-22 16:34:43 +01:00
Yaron Shahrabani
3e0a916883 🌐 Add translations for: Hebrew
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-22 16:34:42 +01:00
Ahmad HosseinBor
4f80238bc2 🌐 Add translations for: Persian
Currently translated at 39.2% (783 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2025-12-22 16:34:42 +01:00
Alejandro Alonso
5156cc5d9a 🌐 Add translations for: Yoruba
Currently translated at 58.8% (1176 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2025-12-22 16:34:42 +01:00
Yessenia Villarte Vaca
42c46b6cfc 🌐 Add translations for: Spanish (Latin America)
Currently translated at 6.5% (131 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2025-12-22 16:34:41 +01:00
VKing9
8b3c40b35e 🌐 Add translations for: Hindi
Currently translated at 99.6% (1991 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-22 16:34:41 +01:00
Andy Li
d3996e5fb1 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 80.1% (1601 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2025-12-22 16:34:40 +01:00
Anonymous
0c42bca866 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Marius
e5685c1f1c 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Anonymous
2784209bde 🌐 Add translations for: Turkish
Currently translated at 99.5% (1989 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-12-22 16:34:40 +01:00
Alejandro Alonso
024f460e99 🌐 Add translations for: Igbo
Currently translated at 25.6% (512 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2025-12-22 16:34:39 +01:00
Anonymous
1d9b76b62a 🌐 Add translations for: Romanian
Currently translated at 97.2% (1942 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2025-12-22 16:34:39 +01:00
Shuaib Zahda
7e17a75b7d 🌐 Add translations for: Arabic
Currently translated at 56.4% (1127 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-12-22 16:34:39 +01:00
Anonymous
ca093d6fae 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Alexandre Pawlak
0f0b7562b5 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Anonymous
9cdc694697 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Dário
b972a4033b 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Anonymous
cbe9f4da51 🌐 Add translations for: Swedish
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-12-22 16:34:37 +01:00
Anonymous
c583bde9e3 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:37 +01:00
Vint Prox
3911ebdc4e 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:36 +01:00
Anonymous
3e3b18667b 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 69.9% (1396 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-12-22 16:34:36 +01:00
Anonymous
ed81c9b8df 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Sebastiaan Pasma
fbdf98d29c 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Anonymous
e603825a55 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Valentina Chapellu
1d724783e6 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Hosted Weblate
e0abe7dcb5 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2025-12-22 16:33:05 +01:00
Andrey Antukh
5c1bbf5be8 Merge remote-tracking branch 'weblate/develop' into develop 2025-12-22 16:32:30 +01:00
Pablo Alba
bbb0d58190 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:27:10 +01:00
Andrey Antukh
88dcf9d1fe 🐛 Mark rpc calls as authenticated when shared key is used (#7901) 2025-12-22 12:18:36 +01:00
Belén Albeza
20061067ad 🐛 Fix text editor not getting focus back after font variant change 2025-12-22 11:18:25 +01:00
Andrey Antukh
2acf15958b Merge branch 'staging-render' into develop 2025-12-22 09:24:04 +01:00
Andrey Antukh
08267de242 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 09:23:48 +01:00
Pablo Alba
35fb376a78 Add proxypass to caddyfile on devenv (#7985) 2025-12-22 09:21:22 +01:00
Dalai Felinto
13fcf3a9bb 💄 Set import Tokens default option to be Single JSON value (#7918)
This patches makes the default Tokens importing option to match the
current default Tokens exporting option (single JSON value). This way it
is more obvious and quick to export the tokens from a file and import
in new one,

---

While testing our design system we are often re-exporting and
re-importing the Tokens to the files using the design system components.

I'm aware that this may be addressed in the future so the Tokens are
brought in together with the library. Meanwhile (and even in the future)
I think it is sensible to have a symmetry between the export and import
defeault options.

Co-authored-by: Dalai Felinto <dalai@blender.org>
2025-12-19 10:44:05 +01:00
Henrik Steffens
dba6ae2820 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:25 +01:00
Marius
ada101c236 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:24 +01:00
Marius
ea48fb5825 🌐 Add translations for: German
Currently translated at 90.4% (1804 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-16 11:00:25 +01:00
alonso.torres
8fde6b28ed 🐛 Fix problems with alignments and margins 2025-12-12 13:21:04 +01:00
alonso.torres
63325ec796 🐛 Fix problem with flex fill size distribution 2025-12-12 13:21:04 +01:00
alonso.torres
84415476d0 🐛 Fix problem with reflow layout 2025-12-12 13:21:04 +01:00
Andrey Antukh
33c786498d Merge remote-tracking branch 'origin/staging-render' into develop 2025-12-12 12:19:49 +01:00
Andrey Antukh
1f886b1f88 Merge remote-tracking branch 'origin/staging' into develop 2025-12-12 12:16:41 +01:00
Aitor Moreno
5a922c6bd6 Merge pull request #7960 from penpot/superalex-fix-too-many-active-webgl-contexts
🐛 Fix too many active WEBGL contexts
2025-12-12 12:03:46 +01:00
Alejandro Alonso
1388865cfc 🐛 Fix too many active WEBGL contexts 2025-12-12 11:16:47 +01:00
Andrey Antukh
1738847694 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-12 10:20:21 +01:00
Aitor Moreno
ca1c3c799d Merge pull request #7968 from penpot/alotor-fix-border-radius
🐛 Fix problem with border radius to path
2025-12-12 10:18:07 +01:00
alonso.torres
ce5006ae84 🐛 Fix problem with border radius to path 2025-12-11 22:40:44 +01:00
Belén Albeza
0a7a65af5d ♻️ Make SerializableResult to depend on From traits 2025-12-11 16:00:03 +01:00
alonso.torres
ea4d0e1238 Calculate position data in wasm 2025-12-11 16:00:03 +01:00
Elena Torro
b705cf953a 🐛 Set layout data from set-object 2025-12-11 14:52:32 +01:00
Alejandro Alonso
90ce1f56e7 Merge pull request #7958 from penpot/superalex-fix-svg-extract-ids
🐛 Fix svg extract ids
2025-12-11 14:02:05 +01:00
Alejandro Alonso
ab0438cc6f 🐛 Fix svg extract ids 2025-12-11 13:47:00 +01:00
Aitor Moreno
c6aa9cc4b7 Merge pull request #7950 from penpot/ladybenko-12851-fix-text-selection
🐛 Fix text selection when editor regains focus
2025-12-11 13:45:29 +01:00
Andrey Antukh
5779adef33 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-11 13:30:59 +01:00
Elena Torró
ebf1758958 Merge pull request #7935 from penpot/superalex-improve-svg-import
🎉 Improve svg import
2025-12-11 13:21:29 +01:00
Elena Torró
e94c56bfa7 Merge pull request #7954 from penpot/azazeln28-fix-font-weight-mixed-value
🐛 Fix font weight mixed value
2025-12-11 12:43:53 +01:00
Alejandro Alonso
89d9591011 🎉 Improve svg import 2025-12-11 12:02:34 +01:00
Andrey Antukh
3becfcd723 🔧 Update build-tag.yml github workflow 2025-12-11 11:59:16 +01:00
Aitor Moreno
5501a2815f 🐛 Fix font-variant-id mixed value 2025-12-11 11:32:27 +01:00
Alejandro Alonso
1066438b02 Merge pull request #7922 from penpot/elenatorro-12855-improve-pan-rendering
🔧 Improve pan rendering
2025-12-10 15:58:59 +01:00
Alejandro Alonso
3b23a3ad19 Merge pull request #7947 from penpot/elenatorro-12880-fix-variant-ui
🔧 Support variants interactivity on the new render's UI
2025-12-10 15:27:48 +01:00
Andrey Antukh
7396f4bfb6 Merge remote-tracking branch 'origin/staging' into develop 2025-12-10 15:17:50 +01:00
Belén Albeza
5cf51f3d26 🐛 Fix text selection not being restore if it was only 1 word 2025-12-10 15:05:13 +01:00
Belén Albeza
25acad5154 🔧 Add formatting rules to the TextEditor 2025-12-10 15:04:34 +01:00
Elena Torro
0a212b6291 🔧 Support variants interactivity on the new render's UI 2025-12-10 14:39:59 +01:00
Andrey Antukh
eb1eeb4750 Merge remote-tracking branch 'origin/staging-render' into niwinz-develop-merge 2025-12-10 13:53:15 +01:00
Andrey Antukh
a78477592b Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-10 13:36:23 +01:00
Xaviju
0956b66281 💄 Group tokens by name path (#7775)
* 💄 Group tokens by name path
2025-12-10 12:34:19 +01:00
Luis de Dios
007b3f11f9 🐛 Fix pass new icons to radio buttons (#7939) 2025-12-10 12:28:27 +01:00
Elena Torro
a661b2564f 🐛 Fix default case on vertical align 2025-12-10 10:59:27 +01:00
Elena Torro
2c3732f3f4 🔧 Fix line height calculation 2025-12-10 10:59:27 +01:00
Andrey Antukh
e16645227b Merge branch 'staging-render' into develop 2025-12-10 10:10:44 +01:00
Andrey Antukh
45665a3c21 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-10 10:09:58 +01:00
Eva Marco
179e6a195d 🎉 Add test for token creation (#7915) 2025-12-10 09:56:21 +01:00
Stephan Paternotte
b45bdd723f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-09 22:00:21 +00:00
Ingrid Pigueron
8696044620 🌐 Add translations for: French
Currently translated at 97.1% (1937 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-09 22:00:19 +00:00
Andrey Antukh
8a8f360c7f Merge remote-tracking branch 'origin/staging' into develop 2025-12-09 19:53:38 +01:00
Luis de Dios
e35fc85c3d 🎉 Create new empty-state component (#7903) 2025-12-09 16:48:12 +01:00
Elena Torro
81bc1bb0af 🔧 Log performance when building using profile-macros 2025-12-09 15:25:13 +01:00
Yamila Moreno
1798461d21 🐳 Add override for assets (#7926) 2025-12-09 14:55:21 +01:00
Yamila Moreno
dde0fddd6f 🐳 Add missing override to Dockerfile.frontend (#7920) 2025-12-09 12:08:46 +01:00
Aitor Moreno
7d36bc4025 Merge pull request #7907 from penpot/alotor-fix-export-text
🐛 Fix problem when exporting texts
2025-12-09 11:28:47 +01:00
Elena Torro
b8feb6374d 🔧 Rebuild indices on zoom change, not pan 2025-12-09 11:26:03 +01:00
Elena Torro
0889df8e08 🔧 Skip slow operations on fast render 2025-12-09 11:26:03 +01:00
Andrey Antukh
4637aced8c Add support auto decoding and validation syntax for obj/reify 2025-12-09 11:13:06 +01:00
Andrey Antukh
9dfe5b0865 🐛 Fix inconsistencies on using obj/reify on plugins 2025-12-09 11:13:06 +01:00
Andrey Antukh
33bcc9544a Update frontend repl script 2025-12-09 11:13:06 +01:00
Andrey Antukh
babd481b7f Make sm/coercer lazy 2025-12-09 11:13:06 +01:00
Andrey Antukh
a9733c792d Make check-fn completly lazy 2025-12-09 11:13:06 +01:00
Belén Albeza
7be8ac3fd7 🐛 Fix internal error while importing a library 2025-12-09 11:10:32 +01:00
Elena Torro
9216d965ef 🔧 Update rendering settings to smooth render 2025-12-09 10:43:33 +01:00
Andrey Antukh
d04fdb5fbd Make the dist bundle use consistent and cache-aware uris (#7911) 2025-12-09 08:05:28 +01:00
Ingrid Pigueron
4f3ca6422c 🌐 Add translations for: French
Currently translated at 96.8% (1931 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-08 19:40:14 +01:00
Nicola Bortoletto
1c03457fda 🌐 Add translations for: Italian
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-06 07:00:19 +00:00
Eva Marco
81e0e4f222 ♻️ Replace token form files (#7896)
* ♻️ Replace shadow form

* ♻️ Rename files and components

* ♻️ Replace offsetx and offsety names

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

* ♻️ Rename files and props
2025-12-05 17:04:07 +01:00
Yamila Moreno
f13b3c8737 🔧 Fix bug in Github Actions (#7908) 2025-12-04 20:24:33 +01:00
alonso.torres
520e979363 🐛 Fix problem when exporting texts 2025-12-04 17:32:54 +01:00
Yamila Moreno
a0f8559ffc 🔧 Add ci/cd for nitrate-module (#7905) 2025-12-04 16:02:29 +01:00
Andrey Antukh
a38f425dd3 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-04 11:06:48 +01:00
VKing9
74d4b9b045 🌐 Add translations for: Hindi
Currently translated at 100.0% (1994 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-04 11:00:32 +01:00
Andrey Antukh
416980f063 🐛 Fix issue on render template on dist bundle (#7899) 2025-12-03 20:48:02 +01:00
Andrey Antukh
f76710296c Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 18:52:28 +01:00
Andrey Antukh
d1379c55f6 Make i18n translation files load on demand 2025-12-03 16:44:37 +01:00
Andrey Antukh
b125c7b5a3 Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:55:01 +01:00
Andrey Antukh
496d37795b Adapt docker images nginx config template to latest changes (#7891) 2025-12-03 13:45:18 +01:00
Andrey Antukh
9f6899007a Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:10:30 +01:00
Marina López
641df77834 🐛 Fix wrong board size presets in Android (#7888) 2025-12-03 12:52:47 +01:00
Aitor Moreno
4e84deca44 Merge pull request #7879 from penpot/elenatorro-12797-fix-update-spans
🐛 Fix paragraph with text spans with multiple styles
2025-12-03 11:30:17 +01:00
Aitor Moreno
0d21e52068 🐛 Fix applyStylesTo entire selection 2025-12-03 11:07:33 +01:00
alonso.torres
1b29e9a50f 🐛 Fix race condition with fix fonts patch 2025-12-03 10:39:05 +01:00
Elena Torro
9f567c3bf4 🐛 Fix italic variant 2025-12-03 08:59:25 +01:00
Elena Torro
1ba15e5d10 🐛 Do not merge fill styles 2025-12-03 08:55:11 +01:00
Yaron Shahrabani
60df56caa3 🌐 Add translations for: Hebrew
Currently translated at 99.6% (1988 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-02 21:00:32 +00:00
Andrey Antukh
53aad7bc15 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 17:43:34 +01:00
alonso.torres
37e45a8bbf 🐛 Fix race condition with text and type 2025-12-02 17:28:20 +01:00
alonso.torres
3471d40f46 🐛 Fix problem with boolean shapes updates 2025-12-02 17:28:20 +01:00
Elena Torro
c6b64a8e39 🐛 Fix selectAll on mixed span styles 2025-12-02 16:50:48 +01:00
Elena Torro
511e80c948 🐛 Fix merge fill styles when there are multiple fills 2025-12-02 16:50:04 +01:00
Elena Torró
f5a640d104 Merge pull request #7876 from penpot/ladybenko-12805-slow-loading
🐛 Fix viewport not being fully drawn on first load until a mouse …
2025-12-02 15:31:43 +01:00
Belén Albeza
3ae7c514e4 🐛 Fix viewport not being fully drawn on first load until a mouse hover 2025-12-02 15:06:28 +01:00
Andrey Antukh
57297741f5 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 13:28:50 +01:00
Dalai Felinto
d63d692d34 🐛 Fix mask issues with component swap #7675
The logic to swap a component would delete the swapped out component
first before bringing in the new one.

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

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

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-12-02 12:31:44 +01:00
alonso.torres
fad9ed1c48 🐛 Fix problem with reordering layers 2025-12-02 12:27:00 +01:00
alonso.torres
0caaefefea 🐛 Fix outline with single click text creation 2025-12-02 11:08:58 +01:00
Elena Torro
b179aa79b1 🐛 Fix create empty text on click regression 2025-12-02 11:08:58 +01:00
Andrey Antukh
fe72d0af82 Add self-signed cert to caddy (#7872) 2025-12-02 10:45:26 +01:00
Aitor Moreno
405ddb60d8 🐛 Fix letter spacing applied to paragraph 2025-12-02 10:45:19 +01:00
Luis de Dios
ef68081d1d 🎉 Add prototype tab UI tweaks (#7832)
* 🎉 Add prototype tab UI tweaks

* 📎 PR changes
2025-12-02 10:44:16 +01:00
Andrey Antukh
4ed49cdc5d Make devenv https and http2 capable (#7871)
Making it more similar on how it runs on production
environments and improves large amount of files loading
thanks to http2.
2025-12-01 20:43:23 +01:00
Elena Torró
95c0d42d5b Merge pull request #7868 from penpot/alotor-fix-flex-tools
🐛 Fix visual feedback on padding/margin/gaps modified
2025-12-01 17:51:44 +01:00
alonso.torres
721b337511 🐛 Fix visual feedback on padding/margin/gaps modified 2025-12-01 16:31:15 +01:00
Elena Torró
359379be09 Merge pull request #7867 from penpot/azazeln28-add-text-editor-v2-tests-to-staging
 Add text editor v2 integration tests
2025-12-01 16:11:25 +01:00
Aitor Moreno
876d5783cf Add text editor v2 integration tests 2025-12-01 15:56:52 +01:00
Elena Torro
786f73767b 🔧 Normalize font attributes to support old formats 2025-12-01 14:59:24 +01:00
Andrey Antukh
50f9eedcdf Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 14:33:38 +01:00
Eva Marco
efe74e62e8 🎉 Replace font family form (#7825) 2025-12-01 11:17:25 +01:00
Eva Marco
456afe46de 🎉 Replace font family form (#7784) 2025-12-01 10:11:29 +01:00
Andrey Antukh
4282cdcd2c Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-01 10:11:06 +01:00
Andrey Antukh
964ef799c2 🔥 Remove core.spec usage on common and frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
d34b6b88b6 Remove malli dev stuff from cljs build
It only used on backend.
2025-12-01 09:30:21 +01:00
Andrey Antukh
9a58f0e954 🔧 Disable code motion on shadow config 2025-12-01 09:30:21 +01:00
Andrey Antukh
adaf8be56d Use sm/coercer on app.render entry point 2025-12-01 09:30:21 +01:00
Andrey Antukh
2f1b99fa53 ♻️ Use ESM target for build frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
5080fcc594 🔥 Remove unused require of edn reader on loggin ns 2025-12-01 09:30:21 +01:00
Andrey Antukh
ea2d3758f0 Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 09:28:49 +01:00
Alejandro Alonso
e889413f26 🐛 Fix nested shadows clipping 2025-12-01 09:22:23 +01:00
Elena Torró
115273b478 Merge pull request #7852 from penpot/alotor-flex-issues
🐛 Fix flex problems in new render
2025-11-28 14:10:42 +01:00
Elena Torró
fdddd3284a Merge pull request #7859 from penpot/ladybenko-12801-fix-mismatched-fonts
🐛 Fix mismatch between fonts for rendered and selected text when no fallback fonts apply
2025-11-28 14:10:17 +01:00
Belén Albeza
51385a04a0 🐛 Fix mismatch between fonts for rendered and selected text when no fallback fonts apply 2025-11-28 13:54:17 +01:00
Belén Albeza
f96ed8ccd6 Fix playwright tests 2025-11-28 13:25:13 +01:00
Belén Albeza
bda5de5c1b 🔧 Update google fonts list 2025-11-28 13:25:13 +01:00
Juanfran
94c15916e2 Merge pull request #7857 from penpot/niwinz-develop-prepare-for-pnpm
 Make automatic workflows not dependent on yarn
2025-11-28 13:07:30 +01:00
Andrey Antukh
ed0f3c3595 Make automatic workflows not dependent on yarn 2025-11-28 12:26:56 +01:00
alonso.torres
59f3b4db4c 🐛 Fix problem with auto-size and element margins 2025-11-28 12:12:19 +01:00
alonso.torres
7ee03ad911 🐛 Fix problem with grid layout editor 2025-11-28 12:12:09 +01:00
alonso.torres
130b8c8214 🐛 Fix problems with flex layout in new render 2025-11-28 10:49:55 +01:00
alonso.torres
0198d41757 🐛 Fix crash when cleanup 2025-11-28 10:44:54 +01:00
alonso.torres
567a955151 🐛 Fix problem with change gap/margin/padding 2025-11-28 10:44:38 +01:00
Nicola Bortoletto
34da754357 🌐 Add translations for: Italian
Currently translated at 98.9% (1973 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-11-28 05:00:28 +00:00
alonso.torres
c2014a37b4 🐛 Fix problem when pasting elements in reverse flex layout 2025-11-27 18:02:34 +01:00
alonso.torres
6611fbd13b 🐛 Fix problem when drag+duplicate a full grid 2025-11-27 18:02:34 +01:00
Andrey Antukh
b5a6867058 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 18:01:08 +01:00
Andrey Antukh
0f88253dd5 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 16:11:36 +01:00
Andrey Antukh
8e3996fbb0 🔧 Change concirrency rules on tests github workflow 2025-11-27 13:16:08 +01:00
Alonso Torres
67762d9450 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 13:02:47 +01:00
Andrey Antukh
7f62652870 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 09:24:40 +01:00
Andrey Antukh
78d31ab11a 🐳 Update devenv docker and compose files
Reuse the already builded imagemagick instead of building
it again on the devenv.
2025-11-26 07:44:56 +01:00
Andrey Antukh
0a80c47901 Merge remote-tracking branch 'origin/staging' into develop 2025-11-26 07:30:42 +01:00
Oğuz Ersen
39eafae251 🌐 Add translations for: Turkish
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-11-25 16:51:25 +00:00
Edgars Andersons
e1e09b7f96 🌐 Add translations for: Latvian
Currently translated at 94.0% (1876 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-11-25 16:51:24 +00:00
Stephan Paternotte
3b39980f2f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-11-25 16:51:23 +00:00
Anton Palmqvist
223b12d2c7 🌐 Add translations for: Swedish
Currently translated at 99.6% (1987 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-25 16:51:21 +00:00
Yamila Moreno
77f1046fc8 🔧 Add MT notification when a docker image with final tag is built (#7824) 2025-11-25 16:39:42 +01:00
Andrey Antukh
553b73a83c ♻️ Replace CircleCI with Github Actions (#7789)
* ♻️ Replace circleci with github actions

* 📎 Add integration test sharding

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,28 @@
# CHANGELOG # 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
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :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) ## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations

View File

@@ -331,6 +331,81 @@
(set/difference cfeat/backend-only-features)) (set/difference cfeat/backend-only-features))
#{})))) #{}))))
(defn check-file-exists
[cfg id & {:keys [include-deleted?]
:or {include-deleted? false}
:as options}]
(db/get-with-sql cfg [sql:get-minimal-file id]
{:db/remove-deleted (not include-deleted?)}))
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn- get-file-permissions*
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-file-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions* conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-file-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(defn get-project (defn get-project
[cfg project-id] [cfg project-id]
(db/get cfg :project {:id project-id})) (db/get cfg :project {:id project-id}))

View File

@@ -30,7 +30,7 @@
(defn- get-file-media-object (defn- get-file-media-object
[pool id] [pool id]
(db/get pool :file-media-object {:id id})) (db/get pool :file-media-object {:id id} {::db/remove-deleted false}))
(defn- serve-object-from-s3 (defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj] [{:keys [::sto/storage] :as cfg} obj]

View File

@@ -309,7 +309,7 @@
(fn [request] (fn [request]
(let [key (yreq/get-header request "x-shared-key")] (let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key) (if (= key shared-key)
(handler request) (handler (assoc request ::http/auth-with-shared-key true))
{::yres/status 403})))) {::yres/status 403}))))
(fn [_ _] (fn [_ _]
{::yres/status 403}))) {::yres/status 403})))

View File

@@ -14,6 +14,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
@@ -92,7 +93,11 @@
(let [handler-name (:type path-params) (let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request)
(if (::http/auth-with-shared-key request)
uuid/zero
nil))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
data (-> params data (-> params

View File

@@ -79,85 +79,14 @@
;; --- FILE PERMISSIONS ;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
and f.deleted_at is null
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
and f.deleted_at is null
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?
and f.deleted_at is null")
(defn get-file-permissions
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(def has-edit-permissions? (def has-edit-permissions?
(perms/make-edition-predicate-fn get-permissions)) (perms/make-edition-predicate-fn bfc/get-file-permissions))
(def has-read-permissions? (def has-read-permissions?
(perms/make-read-predicate-fn get-permissions)) (perms/make-read-predicate-fn bfc/get-file-permissions))
(def has-comment-permissions? (def has-comment-permissions?
(perms/make-comment-predicate-fn get-permissions)) (perms/make-comment-predicate-fn bfc/get-file-permissions))
(def check-edition-permissions! (def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?)) (perms/make-check-fn has-edit-permissions?))
@@ -170,7 +99,7 @@
(defn check-comment-permissions! (defn check-comment-permissions!
[conn profile-id file-id share-id] [conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms) can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)] can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment) (when-not (or can-read can-comment)
@@ -222,7 +151,7 @@
(defn- get-minimal-file-with-perms (defn- get-minimal-file-with-perms
[cfg {:keys [:id ::rpc/profile-id]}] [cfg {:keys [:id ::rpc/profile-id]}]
(let [mfile (get-minimal-file cfg id) (let [mfile (get-minimal-file cfg id)
perms (get-permissions cfg profile-id id)] perms (bfc/get-file-permissions cfg profile-id id)]
(assoc mfile :permissions perms))) (assoc mfile :permissions perms)))
(defn get-file-etag (defn get-file-etag
@@ -248,7 +177,7 @@
;; will be already prefetched and we just reuse them instead ;; will be already prefetched and we just reuse them instead
;; of making an additional database queries. ;; of making an additional database queries.
(let [perms (or (:permissions (::cond/object params)) (let [perms (or (:permissions (::cond/object params))
(get-permissions conn profile-id id))] (bfc/get-file-permissions conn profile-id id))]
(check-read-permissions! perms) (check-read-permissions! perms)
(let [team (teams/get-team conn (let [team (teams/get-team conn
@@ -311,7 +240,7 @@
::sm/result schema:file-fragment} ::sm/result schema:file-fragment}
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}] [cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg] (db/run! cfg (fn [cfg]
(let [perms (get-permissions cfg profile-id file-id share-id)] (let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id) (-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))) (rph/with-http-cache long-cache-duration))))))
@@ -456,8 +385,7 @@
:code :params-validation :code :params-validation
:hint "page-id is required when object-id is provided")) :hint "page-id is required when object-id is provided"))
(let [perms (get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true) file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)}) proj (db/get conn :project {:id (:project-id file)})
@@ -688,11 +616,10 @@
"Get libraries used by the specified file." "Get libraries used by the specified file."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:get-file-libraries} ::sm/params schema:get-file-libraries}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] [cfg {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)] (bfc/check-file-exists cfg file-id)
(check-read-permissions! conn profile-id file-id) (check-read-permissions! cfg profile-id file-id)
(bfc/get-file-libraries conn file-id))) (bfc/get-file-libraries cfg file-id))
;; --- COMMAND QUERY: Files that use this File library ;; --- COMMAND QUERY: Files that use this File library
@@ -785,8 +712,7 @@
FROM file AS f FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id) INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn AND ft.revn = f.revn)
AND ft.deleted_at is null)
WHERE p.team_id = ? WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz) f.deleted_at > ?::timestamptz)

View File

@@ -199,15 +199,13 @@
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}] [cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team conn (let [team (teams/get-team conn
:profile-id profile-id :profile-id profile-id
:file-id file-id) :file-id file-id)
file (bfc/get-file cfg file-id file (bfc/get-file cfg file-id
:include-deleted? true
:realize? true :realize? true
:read-only? true) :read-only? true)
strip-frames-with-thumbnails strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true (or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))] (true? strip-frames-with-thumbnails))]
@@ -333,12 +331,16 @@
;; --- MUTATION COMMAND: create-file-thumbnail ;; --- MUTATION COMMAND: create-file-thumbnail
(defn- create-file-thumbnail! (defn- create-file-thumbnail
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}] [{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media) (media/validate-media-type! media)
(media/validate-media-size! media) (media/validate-media-size! media)
(let [props (db/tjson (or props {})) (let [file (bfc/get-file cfg file-id
:include-deleted? true
:load-data? false)
props (db/tjson (or props {}))
path (:path media) path (:path media)
mtype (:mtype media) mtype (:mtype media)
hash (sto/calculate-hash path) hash (sto/calculate-hash path)
@@ -367,7 +369,7 @@
(db/update! conn :file-thumbnail (db/update! conn :file-thumbnail
{:media-id (:id media) {:media-id (:id media)
:deleted-at nil :deleted-at (:deleted-at file)
:updated-at tnow :updated-at tnow
:props props} :props props}
{:file-id file-id {:file-id file-id
@@ -378,6 +380,7 @@
:revn revn :revn revn
:created-at tnow :created-at tnow
:updated-at tnow :updated-at tnow
:deleted-at (:deleted-at file)
:props props :props props
:media-id (:id media)})) :media-id (:id media)}))
@@ -402,6 +405,8 @@
::rtry/when rtry/conflict-exception? ::rtry/when rtry/conflict-exception?
::sm/params schema:create-file-thumbnail} ::sm/params schema:create-file-thumbnail}
;; FIXME: do not run the thumbnail upload inside a transaction
[cfg {:keys [::rpc/profile-id file-id] :as params}] [cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; TODO For now we check read permissions instead of write, ;; TODO For now we check read permissions instead of write,
@@ -409,6 +414,6 @@
;; review this approach on the future. ;; review this approach on the future.
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [media (create-file-thumbnail! cfg params)] (let [media (create-file-thumbnail cfg params)]
{:uri (files/resolve-public-uri (:id media)) {:uri (files/resolve-public-uri (:id media))
:id (:id media)}))))) :id (:id media)})))))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.fonts (ns app.rpc.commands.fonts
(:require (:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
@@ -66,7 +67,7 @@
(uuid? file-id) (uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]}) (let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]}) project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (files/get-permissions conn profile-id file-id share-id)] perms (bfc/get-file-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms) (files/check-read-permissions! perms)
(db/query conn :team-font-variant (db/query conn :team-font-variant
{:team-id (:team-id project) {:team-id (:team-id project)

View File

@@ -13,7 +13,6 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -121,7 +120,7 @@
[system {:keys [::rpc/profile-id file-id share-id] :as params}] [system {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/run! system (db/run! system
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(let [perms (files/get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
params (-> params params (-> params
(assoc ::perms perms) (assoc ::perms perms)
(assoc :profile-id profile-id))] (assoc :profile-id profile-id))]

View File

@@ -29,7 +29,6 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"} integrant/integrant {:mvn/version "1.0.0"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2026.415"} funcool/cuerdas {:mvn/version "2026.415"}
funcool/promesa funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8" {:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"

View File

@@ -14,8 +14,7 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[clojure.core :as c] [clojure.core :as c]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str])
[expound.alpha :as expound])
#?(:clj #?(:clj
(:import (:import
clojure.lang.IPersistentMap))) clojure.lang.IPersistentMap)))
@@ -110,13 +109,6 @@
(contains? data :explain)) (contains? data :explain))
(explain (:explain data) opts) (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) (contains? data ::sm/explain)
(sm/humanize-explain (::sm/explain data) opts))) (sm/humanize-explain (::sm/explain data) opts)))

View File

@@ -82,6 +82,113 @@
(declare create-svg-children) (declare create-svg-children)
(declare parse-svg-element) (declare parse-svg-element)
(defn- process-gradient-stops
"Processes gradient stops to extract stop-color and stop-opacity from style attributes
and convert them to direct attributes. This ensures stops with style='stop-color:#...;stop-opacity:1'
are properly converted to stop-color and stop-opacity attributes."
[stops]
(mapv (fn [stop]
(let [stop-attrs (:attrs stop)
stop-style (get stop-attrs :style)
;; Parse style if it's a string using csvg/parse-style utility
parsed-style (when (and (string? stop-style) (seq stop-style))
(csvg/parse-style stop-style))
;; Extract stop-color and stop-opacity from style
style-stop-color (when parsed-style (:stop-color parsed-style))
style-stop-opacity (when parsed-style (:stop-opacity parsed-style))
;; Merge: use direct attributes first, then style values as fallback
final-attrs (cond-> stop-attrs
(and style-stop-color (not (contains? stop-attrs :stop-color)))
(assoc :stop-color style-stop-color)
(and style-stop-opacity (not (contains? stop-attrs :stop-opacity)))
(assoc :stop-opacity style-stop-opacity)
;; Remove style attribute if we've extracted its values
(or style-stop-color style-stop-opacity)
(dissoc :style))]
(assoc stop :attrs final-attrs)))
stops))
(defn- resolve-gradient-href
"Resolves xlink:href references in gradients by merging the referenced gradient's
stops and attributes with the referencing gradient. This ensures gradients that
reference other gradients (like linearGradient3550 referencing linearGradient3536)
inherit the stops from the base gradient.
According to SVG spec, when a gradient has xlink:href:
- It inherits all attributes from the referenced gradient
- It inherits all stops from the referenced gradient
- The referencing gradient's attributes override the base ones
- If the referencing gradient has stops, they replace the base stops
Returns the defs map with all gradient href references resolved."
[defs]
(letfn [(resolve-gradient [gradient-id gradient-node defs visited]
(if (contains? visited gradient-id)
(do
#?(:cljs (js/console.warn "[resolve-gradient] Circular reference detected for" gradient-id)
:clj nil)
gradient-node) ;; Avoid circular references
(let [attrs (:attrs gradient-node)
href-id (or (:href attrs) (:xlink:href attrs))
href-id (when (and (string? href-id) (pos? (count href-id)))
(subs href-id 1)) ;; Remove leading #
base-gradient (when (and href-id (contains? defs href-id))
(get defs href-id))
resolved-base (when base-gradient (resolve-gradient href-id base-gradient defs (conj visited gradient-id)))]
(if resolved-base
;; Merge: base gradient attributes + referencing gradient attributes
;; Use referencing gradient's stops if present, otherwise use base stops
(let [base-attrs (:attrs resolved-base)
ref-attrs (:attrs gradient-node)
;; Start with base attributes (without id), then merge with ref attributes
;; This ensures ref attributes override base ones
base-attrs-clean (dissoc base-attrs :id)
ref-attrs-clean (dissoc ref-attrs :href :xlink:href :id)
;; Special handling for gradientTransform: if both have it, combine them
base-transform (get base-attrs :gradientTransform)
ref-transform (get ref-attrs :gradientTransform)
combined-transform (cond
(and base-transform ref-transform)
(str base-transform " " ref-transform) ;; Apply base first, then ref
:else (or ref-transform base-transform))
;; Merge attributes: base first, then ref (ref overrides)
merged-attrs (-> (d/deep-merge base-attrs-clean ref-attrs-clean)
(cond-> combined-transform
(assoc :gradientTransform combined-transform)))
;; If referencing gradient has content (stops), use it; otherwise use base content
final-content (if (seq (:content gradient-node))
(:content gradient-node)
(:content resolved-base))
;; Process stops to extract stop-color and stop-opacity from style attributes
processed-content (process-gradient-stops final-content)
result {:tag (:tag gradient-node)
:attrs (assoc merged-attrs :id gradient-id)
:content processed-content}]
result)
;; Process stops even for gradients without references to extract style attributes
(let [processed-content (process-gradient-stops (:content gradient-node))]
(assoc gradient-node :content processed-content))))))]
(let [gradient-tags #{:linearGradient :radialGradient}
result (reduce-kv
(fn [acc id node]
(if (contains? gradient-tags (:tag node))
(assoc acc id (resolve-gradient id node defs #{}))
(assoc acc id node)))
{}
defs)]
result)))
(defn create-svg-shapes (defn create-svg-shapes
([svg-data pos objects frame-id parent-id selected center?] ([svg-data pos objects frame-id parent-id selected center?]
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?)) (create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
@@ -112,6 +219,9 @@
(csvg/fix-percents) (csvg/fix-percents)
(csvg/extract-defs)) (csvg/extract-defs))
;; Resolve gradient href references in all defs before processing shapes
def-nodes (resolve-gradient-href def-nodes)
;; In penpot groups have the size of their children. To ;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create ;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported ;; a transparent shape as background to respect the imported
@@ -142,12 +252,23 @@
(reduce (partial create-svg-children objects selected frame-id root-id svg-data) (reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []] [unames []]
(d/enumerate (->> (:content svg-data) (d/enumerate (->> (:content svg-data)
(mapv #(csvg/inherit-attributes root-attrs %)))))] (mapv #(csvg/inherit-attributes root-attrs %)))))
[root-shape children]))) ;; Collect all defs from children and merge into root shape
all-defs-from-children (reduce (fn [acc child]
(if-let [child-defs (:svg-defs child)]
(merge acc child-defs)
acc))
{}
children)
;; Merge defs from svg-data and children into root shape
root-shape-with-defs (assoc root-shape :svg-defs (merge def-nodes all-defs-from-children))]
[root-shape-with-defs children])))
(defn create-raw-svg (defn create-raw-svg
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}] [name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs] :as data}]
(let [props (csvg/attrs->props attrs) (let [props (csvg/attrs->props attrs)
vbox (grc/make-rect offset-x offset-y width height)] vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape (cts/setup-shape
@@ -160,10 +281,11 @@
:y y :y y
:content data :content data
:svg-attrs props :svg-attrs props
:svg-viewbox vbox}))) :svg-viewbox vbox
:svg-defs defs})))
(defn create-svg-root (defn create-svg-root
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}] [id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs defs] :as svg-data}]
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns) (let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
(d/without-keys csvg/inheritable-props) (d/without-keys csvg/inheritable-props)
(csvg/attrs->props))] (csvg/attrs->props))]
@@ -177,7 +299,8 @@
:height height :height height
:x (+ x offset-x) :x (+ x offset-x)
:y (+ y offset-y) :y (+ y offset-y)
:svg-attrs props}))) :svg-attrs props
:svg-defs defs})))
(defn create-svg-children (defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]] [objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
@@ -198,7 +321,7 @@
(defn create-group (defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}] [name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs]}]
(let [transform (csvg/parse-transform (:transform attrs)) (let [transform (csvg/parse-transform (:transform attrs))
attrs (-> attrs attrs (-> attrs
(d/without-keys csvg/inheritable-props) (d/without-keys csvg/inheritable-props)
@@ -214,7 +337,8 @@
:height height :height height
:svg-transform transform :svg-transform transform
:svg-attrs attrs :svg-attrs attrs
:svg-viewbox vbox}))) :svg-viewbox vbox
:svg-defs defs})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs))) (when (and (contains? attrs :d) (seq (:d attrs)))
@@ -523,6 +647,21 @@
:else (dm/str tag))] :else (dm/str tag))]
(dm/str "svg-" suffix))) (dm/str "svg-" suffix)))
(defn- filter-valid-def-references
"Filters out false positive references that are not valid def IDs.
Filters out:
- Colors in style attributes (hex colors like #f9dd67)
- Style fragments that contain CSS keywords (like stop-opacity)
- References that don't exist in defs"
[ref-ids defs]
(let [is-style-fragment? (fn [ref-id]
(or (clr/hex-color-string? (str "#" ref-id))
(str/includes? ref-id ";") ;; Contains CSS separator
(str/includes? ref-id "stop-opacity") ;; CSS keyword
(str/includes? ref-id "stop-color")))] ;; CSS keyword
(->> ref-ids
(remove is-style-fragment?) ;; Filter style fragments and hex colors
(filter #(contains? defs %))))) ;; Only existing defs
(defn parse-svg-element (defn parse-svg-element
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames] [frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
@@ -534,7 +673,11 @@
(let [name (or (:id attrs) (tag->name tag)) (let [name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs) att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs) defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs) valid-refs (filter-valid-def-references att-refs defs)
all-refs (csvg/find-def-references defs valid-refs)
;; Filter the final result to ensure all references are valid defs
;; This prevents false positives from style attributes in gradient stops
references (filter-valid-def-references all-refs defs)
href-id (or (:href attrs) (:xlink:href attrs) " ") href-id (or (:href attrs) (:xlink:href attrs) " ")
href-id (if (and (string? href-id) href-id (if (and (string? href-id)

View File

@@ -169,6 +169,7 @@
:enable-component-thumbnails :enable-component-thumbnails
:enable-render-wasm-dpr :enable-render-wasm-dpr
:enable-token-color :enable-token-color
:enable-token-shadow
:enable-inspect-styles :enable-inspect-styles
:enable-feature-fdata-objects-map]) :enable-feature-fdata-objects-map])

View File

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

View File

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

View File

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

View File

@@ -132,3 +132,94 @@ Some naming conventions:
(if-let [last-period (str/last-index-of s ".")] (if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))] [(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""])) [s ""]))
;; Tree building functions --------------------------------------------------
"Build tree structure from flat list of paths"
"`build-tree-root` is the main function to build the tree."
"Receives a list of segments with 'name' properties representing paths,
and a separator string."
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
"Transforms into a tree structure like:
[{:name 'one'
:path 'one'
:depth 0
:leaf nil
:children-fn (fn [] [{:name 'two'
:path 'one.two'
:depth 1
:leaf nil
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
{:name 'five'
:path 'one.five'
:depth 1
:leaf {... :name 'five'}
...}])}]"
(defn- sort-by-children
"Sorts segments so that those with children come first."
[segments separator]
(sort-by (fn [segment]
(let [path (split-path (:name segment) :separator separator)
path-length (count path)]
(if (= path-length 1)
1
0)))
segments))
(defn- group-by-first-segment
"Groups segments by their first path segment and update segment name."
[segments separator]
(reduce (fn [acc segment]
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
(update acc first-segment (fnil conj [])
(if rest-path
(assoc segment :name rest-path)
segment))))
{}
segments))
(defn- sort-and-group-segments
"Sorts elements and groups them by their first path segment."
[segments separator]
(let [sorted (sort-by-children segments separator)
grouped (group-by-first-segment sorted separator)]
grouped))
(defn- build-tree-node
"Builds a single tree node with lazy children."
[segment-name remaining-segments separator parent-path depth]
(let [current-path (if parent-path
(str parent-path "." segment-name)
segment-name)
is-leaf? (and (seq remaining-segments)
(every? (fn [segment]
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
(= segment-name remaining-segment-name)))
remaining-segments))
leaf-segment (when is-leaf? (first remaining-segments))
node {:name segment-name
:path current-path
:depth depth
:leaf leaf-segment
:children-fn (when-not is-leaf?
(fn []
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
(mapv (fn [[child-segment-name remaining-child-segments]]
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
grouped-elements))))}]
node))
(defn build-tree-root
"Builds the root level of the tree."
[segments separator]
(let [grouped-elements (sort-and-group-segments segments separator)]
(mapv (fn [[segment-name remaining-segments]]
(build-tree-node segment-name remaining-segments separator nil 0))
grouped-elements)))

View File

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

View File

@@ -546,9 +546,19 @@
filter-values))) filter-values)))
(defn extract-ids [val] (defn extract-ids [val]
(when (some? val) ;; Extract referenced ids from string values like "url(#myId)".
;; Non-string values (maps, numbers, nil, etc.) return an empty seq
;; to avoid re-seq type errors when attributes carry nested structures.
(cond
(string? val)
(->> (re-seq xml-id-regex val) (->> (re-seq xml-id-regex val)
(mapv second)))) (mapv second))
(sequential? val)
(mapcat extract-ids val)
:else
[]))
(defn fix-dot-number (defn fix-dot-number
"Fixes decimal numbers starting in dot but without leading 0" "Fixes decimal numbers starting in dot but without leading 0"

View File

@@ -340,7 +340,7 @@
(dfn-diff t2 t1))) (dfn-diff t2 t1)))
#?(:cljs #?(:cljs
(defn set-default-locale! (defn set-default-locale
[locale] [locale]
(when-let [locale (unchecked-get locales locale)] (when-let [locale (unchecked-get locales locale)]
(dfn-set-default-options #js {:locale locale})))) (dfn-set-default-options #js {:locale locale}))))

View File

@@ -234,16 +234,15 @@
"Calculate the boolean content from shape and objects. Returns a "Calculate the boolean content from shape and objects. Returns a
packed PathData instance" packed PathData instance"
[shape objects] [shape objects]
(let [content (if (fn? wasm:calc-bool-content) (let [content (calc-bool-content* shape objects)]
(wasm:calc-bool-content (get shape :bool-type)
(get shape :shapes))
(calc-bool-content* shape objects))]
(impl/path-data content))) (impl/path-data content)))
(defn update-bool-shape (defn update-bool-shape
"Calculates the selrect+points for the boolean shape" "Calculates the selrect+points for the boolean shape"
[shape objects] [shape objects]
(let [content (calc-bool-content shape objects) (let [content (if (fn? wasm:calc-bool-content)
(wasm:calc-bool-content shape objects)
(calc-bool-content shape objects))
shape (assoc shape :content content)] shape (assoc shape :content content)]
(update-geometry shape))) (update-geometry shape)))

View File

@@ -59,6 +59,7 @@
:dimensions "dimension" :dimensions "dimension"
:font-family "fontFamilies" :font-family "fontFamilies"
:font-size "fontSizes" :font-size "fontSizes"
:font-weight "fontWeights"
:letter-spacing "letterSpacing" :letter-spacing "letterSpacing"
:number "number" :number "number"
:opacity "opacity" :opacity "opacity"
@@ -70,7 +71,6 @@
:stroke-width "borderWidth" :stroke-width "borderWidth"
:text-case "textCase" :text-case "textCase"
:text-decoration "textDecoration" :text-decoration "textDecoration"
:font-weight "fontWeights"
:typography "typography"}) :typography "typography"})
(def dtcg-token-type->token-type (def dtcg-token-type->token-type

View File

@@ -1545,7 +1545,7 @@ Will return a value that matches this schema:
(and (not (contains? decoded-json "$metadata")) (and (not (contains? decoded-json "$metadata"))
(not (contains? decoded-json "$themes")))) (not (contains? decoded-json "$themes"))))
(defn- convert-dtcg-font-family (defn convert-dtcg-font-family
"Convert font-family token value from DTCG format to internal format. "Convert font-family token value from DTCG format to internal format.
- If value is a string, split it into a collection of font families - If value is a string, split it into a collection of font families
- If value is already an array, keep it as is - If value is already an array, keep it as is
@@ -1556,7 +1556,7 @@ Will return a value that matches this schema:
(sequential? value) value (sequential? value) value
:else value)) :else value))
(defn- convert-dtcg-typography-composite (defn convert-dtcg-typography-composite
"Convert typography token value keys from DTCG format to internal format." "Convert typography token value keys from DTCG format to internal format."
[value] [value]
(if (map? value) (if (map? value)
@@ -1568,17 +1568,17 @@ Will return a value that matches this schema:
;; Reference value ;; Reference value
value)) value))
(defn- convert-dtcg-shadow-composite (defn convert-dtcg-shadow-composite
"Convert shadow token value from DTCG format to internal format." "Convert shadow token value from DTCG format to internal format."
[value] [value]
(let [process-shadow (fn [shadow] (let [process-shadow (fn [shadow]
(if (map? shadow) (if (map? shadow)
(let [legacy-shadow-type (get "type" shadow)] (let [legacy-shadow-type (get "type" shadow)]
(-> shadow (-> shadow
(set/rename-keys {"x" :offsetX (set/rename-keys {"x" :offset-x
"offsetX" :offsetX "offsetX" :offset-x
"y" :offsetY "y" :offset-y
"offsetY" :offsetY "offsetY" :offset-y
"blur" :blur "blur" :blur
"spread" :spread "spread" :spread
"color" :color "color" :color
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
(= "false" %) false (= "false" %) false
(= legacy-shadow-type "innerShadow") true (= legacy-shadow-type "innerShadow") true
:else false)) :else false))
(select-keys [:offsetX :offsetY :blur :spread :color :inset]))) (select-keys [:offset-x :offset-y :blur :spread :color :inset])))
shadow))] shadow))]
(cond (cond
;; Reference value - keep as string ;; Reference value - keep as string
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
(mapv (fn [shadow] (mapv (fn [shadow]
(if (map? shadow) (if (map? shadow)
(-> shadow (-> shadow
(set/rename-keys {:offsetX "offsetX" (set/rename-keys {:offset-x "offsetX"
:offsetY "offsetY" :offset-y "offsetY"
:blur "blur" :blur "blur"
:spread "spread" :spread "spread"
:color "color" :color "color"

View File

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

View File

@@ -1897,15 +1897,15 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")] (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
(t/is (some? token)) (t/is (some? token))
(t/is (= :shadow (:type token))) (t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}] (t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(:value token))))) (:value token)))))
(t/testing "multiple shadow token" (t/testing "multiple shadow token"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")] (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
(t/is (some? token)) (t/is (some? token))
(t/is (= :shadow (:type token))) (t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true} (t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}] {:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(:value token))))) (:value token)))))
(t/testing "shadow token with reference" (t/testing "shadow token with reference"
@@ -1918,7 +1918,7 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")] (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
(t/is (some? token)) (t/is (some? token))
(t/is (= :shadow (:type token))) (t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}] (t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(:value token))))) (:value token)))))
(t/testing "shadow token with description" (t/testing "shadow token with description"
@@ -1937,14 +1937,14 @@
(ctob/make-token (ctob/make-token
{:name "shadow.single" {:name "shadow.single"
:type :shadow :type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}] :value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:description "A single shadow"}) :description "A single shadow"})
"shadow.multiple" "shadow.multiple"
(ctob/make-token (ctob/make-token
{:name "shadow.multiple" {:name "shadow.multiple"
:type :shadow :type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"} :value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]}) {:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
"shadow.ref" "shadow.ref"
(ctob/make-token (ctob/make-token
{:name "shadow.ref" {:name "shadow.ref"
@@ -1991,7 +1991,7 @@
(ctob/make-token (ctob/make-token
{:name "shadow.test" {:name "shadow.test"
:type :shadow :type :shadow
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}] :value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
:description "Round trip test"}) :description "Round trip test"})
"shadow.ref" "shadow.ref"
(ctob/make-token (ctob/make-token

View File

@@ -25,48 +25,6 @@ RUN set -ex; \
binutils \ binutils \
build-essential autoconf libtool pkg-config 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 ## NODE SETUP
################################################################################ ################################################################################
@@ -417,7 +375,7 @@ ENV LANG='C.UTF-8' \
RUSTUP_HOME="/opt/rustup" \ RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH" PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=build-imagemagick /opt/imagick /opt/imagick COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --from=setup-jvm /opt/jdk /opt/jdk COPY --from=setup-jvm /opt/jdk /opt/jdk
COPY --from=setup-jvm /opt/clojure /opt/clojure COPY --from=setup-jvm /opt/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node COPY --from=setup-node /opt/node /opt/node

View File

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

View File

@@ -10,3 +10,7 @@ localhost:3449 {
http://localhost:3450 { http://localhost:3450 {
reverse_proxy localhost:4449 reverse_proxy localhost:4449
} }
http://penpot-devenv-main:3450 {
reverse_proxy localhost:4449
}

View File

@@ -38,11 +38,11 @@ http {
gzip_vary on; gzip_vary on;
gzip_proxied any; gzip_proxied any;
gzip_comp_level 3; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.1; gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml; gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default upgrade; default upgrade;

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f", "packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"
@@ -16,9 +16,9 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"generic-pool": "^3.9.0", "generic-pool": "^3.9.0",
"inflation": "^2.1.0", "inflation": "^2.1.0",
"ioredis": "^5.8.1", "ioredis": "^5.8.2",
"playwright": "^1.55.1", "playwright": "^1.57.0",
"raw-body": "^3.0.1", "raw-body": "^3.0.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1", "svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0", "undici": "^7.16.0",

View File

@@ -243,7 +243,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"bytes@npm:3.1.2": "bytes@npm:~3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "bytes@npm:3.1.2" resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -442,7 +442,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"depd@npm:2.0.0, depd@npm:~2.0.0": "depd@npm:~2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "depd@npm:2.0.0" resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -577,9 +577,9 @@ __metadata:
date-fns: "npm:^4.1.0" date-fns: "npm:^4.1.0"
generic-pool: "npm:^3.9.0" generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0" inflation: "npm:^2.1.0"
ioredis: "npm:^5.8.1" ioredis: "npm:^5.8.2"
playwright: "npm:^1.55.1" playwright: "npm:^1.57.0"
raw-body: "npm:^3.0.1" raw-body: "npm:^3.0.2"
source-map-support: "npm:^0.5.21" source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1" svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0" undici: "npm:^7.16.0"
@@ -683,16 +683,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-errors@npm:2.0.0": "http-errors@npm:~2.0.1":
version: 2.0.0 version: 2.0.1
resolution: "http-errors@npm:2.0.0" resolution: "http-errors@npm:2.0.1"
dependencies: dependencies:
depd: "npm:2.0.0" depd: "npm:~2.0.0"
inherits: "npm:2.0.4" inherits: "npm:~2.0.4"
setprototypeof: "npm:1.2.0" setprototypeof: "npm:~1.2.0"
statuses: "npm:2.0.1" statuses: "npm:~2.0.2"
toidentifier: "npm:1.0.1" toidentifier: "npm:~1.0.1"
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -716,15 +716,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iconv-lite@npm:0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2": "iconv-lite@npm:^0.6.2":
version: 0.6.3 version: 0.6.3
resolution: "iconv-lite@npm:0.6.3" resolution: "iconv-lite@npm:0.6.3"
@@ -734,6 +725,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iconv-lite@npm:~0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"ieee754@npm:^1.2.1": "ieee754@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
@@ -755,16 +755,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"inherits@npm:2.0.4, inherits@npm:~2.0.3": "inherits@npm:~2.0.3, inherits@npm:~2.0.4":
version: 2.0.4 version: 2.0.4
resolution: "inherits@npm:2.0.4" resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node languageName: node
linkType: hard linkType: hard
"ioredis@npm:^5.8.1": "ioredis@npm:^5.8.2":
version: 5.8.1 version: 5.8.2
resolution: "ioredis@npm:5.8.1" resolution: "ioredis@npm:5.8.2"
dependencies: dependencies:
"@ioredis/commands": "npm:1.4.0" "@ioredis/commands": "npm:1.4.0"
cluster-key-slot: "npm:^1.1.0" cluster-key-slot: "npm:^1.1.0"
@@ -775,7 +775,7 @@ __metadata:
redis-errors: "npm:^1.2.0" redis-errors: "npm:^1.2.0"
redis-parser: "npm:^3.0.0" redis-parser: "npm:^3.0.0"
standard-as-callback: "npm:^2.1.0" standard-as-callback: "npm:^2.1.0"
checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465 checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1106,27 +1106,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"playwright-core@npm:1.55.1": "playwright-core@npm:1.57.0":
version: 1.55.1 version: 1.57.0
resolution: "playwright-core@npm:1.55.1" resolution: "playwright-core@npm:1.57.0"
bin: bin:
playwright-core: cli.js playwright-core: cli.js
checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
languageName: node languageName: node
linkType: hard linkType: hard
"playwright@npm:^1.55.1": "playwright@npm:^1.57.0":
version: 1.55.1 version: 1.57.0
resolution: "playwright@npm:1.55.1" resolution: "playwright@npm:1.57.0"
dependencies: dependencies:
fsevents: "npm:2.3.2" fsevents: "npm:2.3.2"
playwright-core: "npm:1.55.1" playwright-core: "npm:1.57.0"
dependenciesMeta: dependenciesMeta:
fsevents: fsevents:
optional: true optional: true
bin: bin:
playwright: cli.js playwright: cli.js
checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771 checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1161,15 +1161,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"raw-body@npm:^3.0.1": "raw-body@npm:^3.0.2":
version: 3.0.1 version: 3.0.2
resolution: "raw-body@npm:3.0.1" resolution: "raw-body@npm:3.0.2"
dependencies: dependencies:
bytes: "npm:3.1.2" bytes: "npm:~3.1.2"
http-errors: "npm:2.0.0" http-errors: "npm:~2.0.1"
iconv-lite: "npm:0.7.0" iconv-lite: "npm:~0.7.0"
unpipe: "npm:1.0.0" unpipe: "npm:~1.0.0"
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1270,7 +1270,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"setprototypeof@npm:1.2.0": "setprototypeof@npm:~1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "setprototypeof@npm:1.2.0" resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -1368,10 +1368,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"statuses@npm:2.0.1": "statuses@npm:~2.0.2":
version: 2.0.1 version: 2.0.2
resolution: "statuses@npm:2.0.1" resolution: "statuses@npm:2.0.2"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1500,7 +1500,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"toidentifier@npm:1.0.1": "toidentifier@npm:~1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "toidentifier@npm:1.0.1" resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -1539,7 +1539,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unpipe@npm:1.0.0": "unpipe@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "unpipe@npm:1.0.0" resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c

View File

@@ -1,5 +1,10 @@
import { withThemeByClassName } from "@storybook/addon-themes"; 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'; import '../resources/public/css/ds.css';
export const decorators = [ export const decorators = [

View File

@@ -8,6 +8,11 @@
metosin/reitit-core {:mvn/version "0.9.1"} metosin/reitit-core {:mvn/version "0.9.1"}
funcool/okulary {:mvn/version "2022.04.11-16"} funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/tubax
{:git/tag "v2025.11.28"
:git/sha "2d9a986"
:git/url "https://github.com/funcool/tubax.git"}
funcool/potok2 funcool/potok2
{:git/tag "v2.2" {:git/tag "v2.2"
:git/sha "0f7e15a" :git/sha "0f7e15a"
@@ -20,8 +25,8 @@
:git/url "https://github.com/funcool/beicon.git"} :git/url "https://github.com/funcool/beicon.git"}
funcool/rumext funcool/rumext
{:git/tag "v2.24" {:git/tag "v2.25"
:git/sha "17a0c94" :git/sha "27e5a1a"
:git/url "https://github.com/funcool/rumext.git"} :git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"} instaparse/instaparse {:mvn/version "1.5.0"}
@@ -42,10 +47,10 @@
:dev :dev
{:extra-paths ["dev"] {:extra-paths ["dev"]
:extra-deps :extra-deps
{thheller/shadow-cljs {:mvn/version "3.2.0"} {thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "0.4.6"}
cider/cider-nrepl {:mvn/version "0.57.0"}}} cider/cider-nrepl {:mvn/version "0.57.0"}}}
:shadow-cljs :shadow-cljs

View File

@@ -32,8 +32,8 @@
"e2e:server": "node ./scripts/e2e-server.js", "e2e:server": "node ./scripts/e2e-server.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/", "fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false 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 -w", "fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js", "fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
"lint:clj": "clj-kondo --parallel --lint src/", "lint:clj": "clj-kondo --parallel --lint src/",
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss", "lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w", "lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
@@ -53,83 +53,74 @@
"watch:storybook:assets": "node ./scripts/watch-storybook.js" "watch:storybook:assets": "node ./scripts/watch-storybook.js"
}, },
"devDependencies": { "devDependencies": {
"@penpot/draft-js": "portal:./packages/draft-js",
"@penpot/mousetrap": "portal:./packages/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@playwright/test": "1.52.0", "@playwright/test": "1.52.0",
"@storybook/addon-docs": "10.0.4", "@storybook/addon-docs": "10.0.4",
"@storybook/addon-themes": "10.0.4", "@storybook/addon-themes": "10.0.4",
"@storybook/addon-vitest": "10.0.4", "@storybook/addon-vitest": "10.0.4",
"@storybook/react-vite": "10.0.4", "@storybook/react-vite": "10.0.4",
"@tokens-studio/sd-transforms": "1.2.11",
"@types/node": "^22.15.21", "@types/node": "^22.15.21",
"@vitest/browser": "3.2.4", "@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@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",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"compression": "^1.8.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"date-fns": "^4.1.0",
"esbuild": "^0.25.9", "esbuild": "^0.25.9",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0", "express": "^5.1.0",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"getopts": "^2.3.0", "getopts": "^2.3.0",
"gettext-parser": "^8.0.0", "gettext-parser": "^8.0.0",
"gulp-concat": "^2.6.1", "highlight.js": "^11.10.0",
"gulp-gzip": "^1.4.2", "js-beautify": "^1.15.4",
"gulp-mustache": "^5.0.0",
"gulp-postcss": "^10.0.0",
"gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^2.0.3",
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"marked": "^15.0.12", "marked": "^15.0.12",
"mkdirp": "^3.0.1", "mkdirp": "^3.0.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"opentype.js": "^1.3.4",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"playwright": "1.56.1", "playwright": "1.56.1",
"postcss": "^8.5.4", "postcss": "^8.5.4",
"postcss-clean": "^1.2.2", "postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
"prettier": "3.5.3", "prettier": "3.5.3",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rimraf": "^6.0.1",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"storybook": "10.0.4",
"svg-sprite": "^2.0.4",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2"
},
"dependencies": {
"@penpot/draft-js": "portal:./vendor/draft-js",
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",
"@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",
"eventsource-parser": "^3.0.6",
"js-beautify": "^1.15.4",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"opentype.js": "^1.3.4",
"postcss-modules": "^6.0.1",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-virtualized": "^9.22.6", "react-virtualized": "^9.22.6",
"rimraf": "^6.0.1",
"rxjs": "8.0.0-alpha.14", "rxjs": "8.0.0-alpha.14",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.1", "sax": "^1.4.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"storybook": "10.0.4",
"style-dictionary": "5.0.0-rc.1", "style-dictionary": "5.0.0-rc.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2", "tdigest": "^0.1.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"typescript": "^5.9.2",
"ua-parser-js": "2.0.5", "ua-parser-js": "2.0.5",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2",
"xregexp": "^5.1.2" "xregexp": "^5.1.2"
} }
} }

View File

@@ -16,7 +16,9 @@ export const {
RichTextEditorUtil, RichTextEditorUtil,
SelectionState, SelectionState,
convertFromRaw, convertFromRaw,
convertToRaw convertToRaw,
EditorBlock,
Editor
} = pkg; } = pkg;
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js'; import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';

View File

@@ -8,7 +8,8 @@
"author": "Andrey Antukh", "author": "Andrey Antukh",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0" "draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
"immutable": "^5.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=0.17.0", "react": ">=0.17.0",

View File

@@ -173,12 +173,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@penpot/draft-js-wrapper@workspace:.": "@penpot/draft-js@workspace:.":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@penpot/draft-js-wrapper@workspace:." resolution: "@penpot/draft-js@workspace:."
dependencies: dependencies:
draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0" draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
esbuild: "npm:^0.24.0" esbuild: "npm:^0.24.0"
immutable: "npm:^5.1.4"
peerDependencies: peerDependencies:
react: ">=0.17.0" react: ">=0.17.0"
react-dom: ">=0.17.0" react-dom: ">=0.17.0"
@@ -320,6 +321,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"immutable@npm:^5.1.4":
version: 5.1.4
resolution: "immutable@npm:5.1.4"
checksum: 10c0/f1c98382e4cde14a0b218be3b9b2f8441888da8df3b8c064aa756071da55fbed6ad696e5959982508456332419be9fdeaf29b2e58d0eadc45483cc16963c0446
languageName: node
linkType: hard
"immutable@npm:~3.7.4": "immutable@npm:~3.7.4":
version: 3.7.6 version: 3.7.6
resolution: "immutable@npm:3.7.6" resolution: "immutable@npm:3.7.6"

View File

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

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,47 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41234",
"~:revn": 1,
"~:vern": 1,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1705307400000",
"~:modified-at": "~m1732111500000",
"~:deleted-at": "~m1732111500000",
"~:name": "Deleted Design File 1",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732716300000",
"~:thumbnail-id": null,
"~:row-num": 1,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41235",
"~:revn": 2,
"~:vern": 2,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1704875700000",
"~:modified-at": "~m1732025400000",
"~:deleted-at": "~m1732025400000",
"~:name": "Deleted Design File 2",
"~:is-shared": true,
"~:will-be-deleted-at": "~m1732630200000",
"~:thumbnail-id": null,
"~:row-num": 2,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41236",
"~:revn": 3,
"~:vern": 3,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
"~:created-at": "~m1706792400000",
"~:modified-at": "~m1731939600000",
"~:deleted-at": "~m1731939600000",
"~:name": "Old Project Design",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732544400000",
"~:thumbnail-id": null,
"~:row-num": 3,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
}
]

View File

@@ -0,0 +1,58 @@
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 1",
"~:revn": 11,
"~:modified-at": "~m1713873823633",
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:is-shared": false,
"~:version": 46,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1713536343369",
"~:data": {
"~:pages": [
"~u66697432-c33d-8055-8006-2c62cc084cad"
],
"~:pages-index": {
"~u66697432-c33d-8055-8006-2c62cc084cad": {
"~#penpot/pointer": [
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
{
"~:created-at": "~m1713873823636"
}
]
}
},
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:options": {
"~:components-v2": true
},
"~:recent-colors": [
{
"~:color": "#0000ff",
"~:opacity": 1,
"~:id": null,
"~:file-id": null,
"~:image": null
}
]
}
}

View File

@@ -0,0 +1,345 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type",
"text-editor/v2"
]
},
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Bug 11552",
"~:revn": 3,
"~:modified-at": "~m1753957736516",
"~:vern": 0,
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0004-clean-shadow-color",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
"~:created-at": "~m1753957644225",
"~:data": {
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
"~:pages-index": {
"~u238a17e0-75ff-8075-8006-934586ea2231": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0.0,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0.0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
}
},
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
"~#shape": {
"~:y": 438,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "1s4am1jl24s",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "13p0zwl2yhc",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "Lorem ipsum"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "20hf3kmyoub",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Lorem ipsum",
"~:width": 77,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 404,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 455
}
},
{
"~#point": {
"~:x": 404,
"~:y": 455
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 404,
"~:selrect": {
"~#rect": {
"~:x": 404,
"~:y": 438,
"~:width": 77,
"~:height": 17,
"~:x1": 404,
"~:y1": 438,
"~:x2": 481,
"~:y2": 455
}
},
"~:flip-x": null,
"~:height": 17,
"~:flip-y": null
}
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
"~:name": "Page 1"
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,5 +1 @@
{ w
"~:revn": 2,
"~:lagged": []
}

View File

@@ -0,0 +1,4 @@
{
"~:revn": 2,
"~:lagged": []
}

View File

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

View File

@@ -0,0 +1,9 @@
[
{
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
"~:revn": 21,
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
"~:changes": []
}
]

View File

@@ -0,0 +1,36 @@
export class Clipboard {
static Permission = {
ONLY_READ: ["clipboard-read"],
ONLY_WRITE: ["clipboard-write"],
ALL: ["clipboard-read", "clipboard-write"],
};
static enable(context, permissions) {
return context.grantPermissions(permissions);
}
static writeText(page, text) {
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
}
static readText(page) {
return page.evaluate(() => navigator.clipboard.readText());
}
constructor(page, context) {
this.page = page;
this.context = context;
}
enable(permissions) {
return Clipboard.enable(this.context, permissions);
}
writeText(text) {
return Clipboard.writeText(this.page, text);
}
readText() {
return Clipboard.readText(this.page);
}
}

View File

@@ -0,0 +1,28 @@
export class Transit {
static parse(value) {
if (typeof value !== "string") return value;
if (value.startsWith("~")) return value.slice(2);
return value;
}
static get(object, ...path) {
let aux = object;
for (const name of path) {
if (typeof name !== "string") {
if (!(name in aux)) {
return undefined;
}
aux = aux[name];
} else {
const transitName = `~:${name}`;
if (!(transitName in aux)) {
return undefined;
}
aux = aux[transitName];
}
}
return this.parse(aux);
}
}

View File

@@ -1,4 +1,27 @@
export class BasePage { export class BasePage {
/**
* Mocks multiple RPC calls in a single call.
*
* @param {Page} page
* @param {object<string, string>} paths
* @param {*} options
* @returns {Promise<void>}
*/
static async mockRPCs(page, paths, options) {
for (const [path, jsonFilename] of Object.entries(paths)) {
await this.mockRPC(page, path, jsonFilename, options);
}
}
/**
* Mocks an RPC call using a file.
*
* @param {Page} page
* @param {string} path
* @param {string} jsonFilename
* @param {*} options
* @returns {Promise<void>}
*/
static async mockRPC(page, path, jsonFilename, options) { static async mockRPC(page, path, jsonFilename, options) {
if (!page) { if (!page) {
throw new TypeError("Invalid page argument. Must be a Playwright page."); throw new TypeError("Invalid page argument. Must be a Playwright page.");
@@ -73,7 +96,7 @@ export class BasePage {
} }
static async mockConfigFlags(page, flags) { static async mockConfigFlags(page, flags) {
const url = "**/js/config.js?ts=*"; const url = "**/js/config.js*";
return await page.route(url, (route) => return await page.route(url, (route) =>
route.fulfill({ route.fulfill({
status: 200, status: 200,
@@ -93,6 +116,10 @@ export class BasePage {
return this.#page; return this.#page;
} }
async mockRPCs(paths, options) {
return BasePage.mockRPCs(this.page, paths, options);
}
async mockRPC(path, jsonFilename, options) { async mockRPC(path, jsonFilename, options) {
return BasePage.mockRPC(this.page, path, jsonFilename, options); return BasePage.mockRPC(this.page, path, jsonFilename, options);
} }

View File

@@ -106,6 +106,13 @@ export class DashboardPage extends BaseWebSocketPage {
); );
} }
async setupDeletedFiles() {
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
}
async setupDrafts() { async setupDrafts() {
await this.mockRPC( await this.mockRPC(
"get-project-files?project-id=*", "get-project-files?project-id=*",
@@ -160,6 +167,10 @@ export class DashboardPage extends BaseWebSocketPage {
}); });
await this.mockRPC("search-files", "dashboard/search-files.json"); await this.mockRPC("search-files", "dashboard/search-files.json");
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json"); await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
} }
async setupAccessTokensEmpty() { async setupAccessTokensEmpty() {
@@ -289,6 +300,13 @@ export class DashboardPage extends BaseWebSocketPage {
await expect(this.mainHeading).toHaveText("Libraries"); await expect(this.mainHeading).toHaveText("Libraries");
} }
async goToDeleted() {
await this.page.goto(
`#/dashboard/deleted?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Projects");
}
async openProfileMenu() { async openProfileMenu() {
await this.userAccount.click(); await this.userAccount.click();
} }

View File

@@ -1,7 +1,146 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { BaseWebSocketPage } from "./BaseWebSocketPage"; import { BaseWebSocketPage } from "./BaseWebSocketPage";
import { Transit } from "../../helpers/Transit";
export class WorkspacePage extends BaseWebSocketPage { export class WorkspacePage extends BaseWebSocketPage {
static TextEditor = class TextEditor {
constructor(workspacePage) {
this.workspacePage = workspacePage;
// locators.
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
name: "Line Height",
});
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
"textbox",
{
name: "Letter Spacing",
},
);
}
get page() {
return this.workspacePage.page;
}
async waitForStyle(locator, styleName) {
return locator.evaluate(
(element, styleName) => element.style.getPropertyValue(styleName),
styleName,
);
}
async waitForEditor() {
return this.page.waitForSelector('[data-itype="editor"]');
}
async waitForRoot() {
return this.page.waitForSelector('[data-itype="root"]');
}
async waitForParagraph(nth) {
if (!nth) {
return this.page.waitForSelector('[data-itype="paragraph"]');
}
return this.page.waitForSelector(
`[data-itype="paragraph"]:nth-child(${nth})`,
);
}
async waitForParagraphStyle(nth, styleName) {
const paragraph = await this.waitForParagraph(nth);
return this.waitForStyle(paragraph, styleName);
}
async waitForTextSpan(nth = 0) {
if (!nth) {
return this.page.waitForSelector('[data-itype="inline"]');
}
return this.page.waitForSelector(
`[data-itype="inline"]:nth-child(${nth})`,
);
}
async waitForTextSpanContent(nth = 0) {
const textSpan = await this.waitForTextSpan(nth);
const textContent = await textSpan.textContent();
return textContent;
}
async waitForTextSpanStyle(nth, styleName) {
const textSpan = await this.waitForTextSpan(nth);
return this.waitForStyle(textSpan, styleName);
}
async startEditing() {
await this.page.keyboard.press("Enter");
return this.waitForEditor();
}
stopEditing() {
return this.page.keyboard.press("Escape");
}
async moveToLeft(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowLeft");
}
}
async moveToRight(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowRight");
}
}
async moveFromStart(offset = 0) {
await this.page.keyboard.press("ArrowLeft");
await this.moveToRight(offset);
}
async moveFromEnd(offset = 0) {
await this.page.keyboard.press("ArrowRight");
await this.moveToLeft(offset);
}
async selectFromStart(length, offset = 0) {
await this.moveFromStart(offset);
await this.page.keyboard.down("Shift");
await this.moveToRight(length);
await this.page.keyboard.up("Shift");
}
async selectFromEnd(length, offset = 0) {
await this.moveFromEnd(offset);
await this.page.keyboard.down("Shift");
await this.moveToLeft(length);
await this.page.keyboard.up("Shift");
}
async changeNumericInput(locator, newValue) {
await expect(locator).toBeVisible();
await locator.focus();
await locator.fill(`${newValue}`);
await locator.blur();
}
changeFontSize(newValue) {
return this.changeNumericInput(this.fontSize, newValue);
}
changeLineHeight(newValue) {
return this.changeNumericInput(this.lineHeight, newValue);
}
changeLetterSpacing(newValue) {
return this.changeNumericInput(this.letterSpacing, newValue);
}
};
/** /**
* This should be called on `test.beforeEach`. * This should be called on `test.beforeEach`.
* *
@@ -11,50 +150,21 @@ export class WorkspacePage extends BaseWebSocketPage {
static async init(page) { static async init(page) {
await BaseWebSocketPage.initWebSockets(page); await BaseWebSocketPage.initWebSockets(page);
await BaseWebSocketPage.mockRPC( await BaseWebSocketPage.mockRPCs(page, {
page, "get-profile": "logged-in-user/get-profile-logged-in.json",
"get-profile", "get-team-users?file-id=*":
"logged-in-user/get-profile-logged-in.json", "logged-in-user/get-team-users-single-user.json",
); "get-comment-threads?file-id=*":
await BaseWebSocketPage.mockRPC( "workspace/get-comment-threads-empty.json",
page, "get-project?id=*": "workspace/get-project-default.json",
"get-team-users?file-id=*", "get-team?id=*": "workspace/get-team-default.json",
"logged-in-user/get-team-users-single-user.json", "get-teams": "get-teams.json",
); "get-team-members?team-id=*":
await BaseWebSocketPage.mockRPC( "logged-in-user/get-team-members-your-penpot.json",
page, "get-profiles-for-file-comments?file-id=*":
"get-comment-threads?file-id=*", "workspace/get-profile-for-file-comments.json",
"workspace/get-comment-threads-empty.json", "update-profile-props": "workspace/update-profile-empty.json",
); });
await BaseWebSocketPage.mockRPC(
page,
"get-project?id=*",
"workspace/get-project-default.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-team?id=*",
"workspace/get-team-default.json",
);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
await BaseWebSocketPage.mockRPC(
page,
"get-team-members?team-id=*",
"logged-in-user/get-team-members-your-penpot.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-profiles-for-file-comments?file-id=*",
"workspace/get-profile-for-file-comments.json",
);
await BaseWebSocketPage.mockRPC(
page,
"update-profile-props",
"workspace/update-profile-empty.json",
);
} }
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a"; static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
@@ -62,9 +172,20 @@ export class WorkspacePage extends BaseWebSocketPage {
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
/**
* WebSocket mock
*
* @type {MockWebSocketHelper}
*/
#ws = null; #ws = null;
constructor(page) { /**
* Constructor
*
* @param {Page} page
* @param {} [options]
*/
constructor(page, options) {
super(page); super(page);
this.pageName = page.getByTestId("page-name"); this.pageName = page.getByTestId("page-name");
@@ -112,11 +233,14 @@ export class WorkspacePage extends BaseWebSocketPage {
"tokens-context-menu-for-set", "tokens-context-menu-for-set",
); );
this.contextMenuForShape = page.getByTestId("context-menu"); this.contextMenuForShape = page.getByTestId("context-menu");
if (options?.textEditor) {
this.textEditor = new WorkspacePage.TextEditor(this);
}
} }
async goToWorkspace({ async goToWorkspace({
fileId = WorkspacePage.anyFileId, fileId = this.fileId ?? WorkspacePage.anyFileId,
pageId = WorkspacePage.anyPageId, pageId = this.pageId ?? WorkspacePage.anyPageId,
} = {}) { } = {}) {
await this.page.goto( await this.page.goto(
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`, `/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
@@ -141,48 +265,59 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
async setupEmptyFile() { async setupEmptyFile() {
await this.mockRPC( await this.mockRPCs({
"get-profile", "get-profile": "logged-in-user/get-profile-logged-in.json",
"logged-in-user/get-profile-logged-in.json", "get-team-users?file-id=*":
); "logged-in-user/get-team-users-single-user.json ",
await this.mockRPC( "get-comment-threads?file-id=*":
"get-team-users?file-id=*", "workspace/get-comment-threads-empty.json",
"logged-in-user/get-team-users-single-user.json", "get-project?id=*": "workspace/get-project-default.json",
); "get-team?id=*": "workspace/get-team-default.json",
await this.mockRPC( "get-profiles-for-file-comments?file-id=*":
"get-comment-threads?file-id=*", "workspace/get-profile-for-file-comments.json",
"workspace/get-comment-threads-empty.json", "get-file-object-thumbnails?file-id=*":
); "workspace/get-file-object-thumbnails-blank.json",
await this.mockRPC( "get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
"get-project?id=*", "get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
"workspace/get-project-default.json", "get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
); });
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
await this.mockRPC( if (this.textEditor) {
"get-profiles-for-file-comments?file-id=*", await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
"workspace/get-profile-for-file-comments.json", }
);
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); // by default we mock the blank file.
await this.mockRPC( await this.mockGetFile("workspace/get-file-blank.json");
"get-file-object-thumbnails?file-id=*",
"workspace/get-file-object-thumbnails-blank.json",
);
await this.mockRPC(
"get-font-variants?team-id=*",
"workspace/get-font-variants-empty.json",
);
await this.mockRPC(
"get-file-fragment?file-id=*",
"workspace/get-file-fragment-blank.json",
);
await this.mockRPC(
"get-file-libraries?file-id=*",
"workspace/get-file-libraries-empty.json",
);
} }
async mockGetFile(jsonFile) { async mockGetFile(jsonFilename, options) {
await this.mockRPC(/get\-file\?/, jsonFile); const page = this.page;
const jsonPath = `playwright/data/${jsonFilename}`;
const body = await readFile(jsonPath, "utf-8");
const payload = JSON.parse(body);
const fileId = Transit.get(payload, "id");
const pageId = Transit.get(payload, "data", "pages", 0);
const teamId = Transit.get(payload, "team-id");
this.fileId = fileId ?? this.anyFileId;
this.pageId = pageId ?? this.anyPageId;
this.teamId = teamId ?? this.anyTeamId;
const path = /get\-file\?/;
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
const interceptConfig = {
status: 200,
contentType: "application/transit+json",
...options,
};
return page.route(url, (route) =>
route.fulfill({
...interceptConfig,
body,
}),
);
// await this.mockRPC(/get\-file\?/, jsonFile);
} }
async mockGetAsset(regex, asset) { async mockGetAsset(regex, asset) {
@@ -190,22 +325,15 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
async setupFileWithComments() { async setupFileWithComments() {
await this.mockRPC( await this.mockRPCs({
"get-comment-threads?file-id=*", "get-comment-threads?file-id=*":
"workspace/get-comment-threads-unread.json", "workspace/get-comment-threads-unread.json",
); "get-file-fragment?file-id=*&fragment-id=*":
await this.mockRPC( "viewer/get-file-fragment-single-board.json",
"get-file-fragment?file-id=*&fragment-id=*", "get-comments?thread-id=*": "workspace/get-thread-comments.json",
"viewer/get-file-fragment-single-board.json", "update-comment-thread-status":
); "workspace/update-comment-thread-status.json",
await this.mockRPC( });
"get-comments?thread-id=*",
"workspace/get-thread-comments.json",
);
await this.mockRPC(
"update-comment-thread-status",
"workspace/update-comment-thread-status.json",
);
} }
async clickWithDragViewportAt(x, y, width, height) { async clickWithDragViewportAt(x, y, width, height) {
@@ -223,6 +351,67 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.mouse.up(); await this.page.mouse.up();
} }
/**
* Clicks and moves from the coordinates x1,y1 to x2,y2
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
async clickAndMove(x1, y1, x2, y2) {
await this.page.waitForTimeout(100);
await this.viewport.hover({ position: { x: x1, y: y1 } });
await this.page.mouse.down();
await this.viewport.hover({ position: { x: x2, y: y2 } });
await this.page.mouse.up();
}
/**
* Creates a new Text Shape in the specified coordinates
* with an initial text.
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {string} initialText
* @param {*} [options]
*/
async createTextShape(x1, y1, x2, y2, initialText, options) {
const timeToWait = options?.timeToWait ?? 100;
await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait);
await this.clickAndMove(x1, y1, x2, y2);
await this.page.waitForTimeout(timeToWait);
if (initialText) {
await this.page.keyboard.type(initialText);
}
}
/**
* Copies the selected element into the clipboard.
*
* @returns {Promise<void>}
*/
async copy() {
return this.page.keyboard.press("Control+C");
}
/**
* Pastes something from the clipboard.
*
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
* @returns {Promise<void>}
*/
async paste(kind = "keyboard") {
if (kind === "context-menu") {
await this.viewport.click({ button: "right" });
return this.page.getByText("PasteCtrlV").click();
}
return this.page.keyboard.press("Control+V");
}
async panOnViewportAt(x, y, width, height) { async panOnViewportAt(x, y, width, height) {
await this.page.waitForTimeout(100); await this.page.waitForTimeout(100);
await this.viewport.hover({ position: { x, y } }); await this.viewport.hover({ position: { x, y } });
@@ -250,10 +439,15 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.waitForTimeout(500); await this.page.waitForTimeout(500);
} }
async doubleClickLeafLayer(name, clickOptions = {}) {
await this.clickLeafLayer(name, clickOptions);
await this.clickLeafLayer(name, clickOptions);
}
async clickToggableLayer(name, clickOptions = {}) { async clickToggableLayer(name, clickOptions = {}) {
const layer = this.layers const layer = this.layers
.getByTestId("layer-row") .getByTestId("layer-row")
.filter({ hasText: name }); .filter({ hasText: name });
const button = layer.getByRole("button"); const button = layer.getByRole("button");
await button.waitFor(); await button.waitFor();

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05", id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06", pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
}); });
await workspace.waitForFirstRender(); await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot(); await expect(workspace.canvas).toHaveScreenshot();
}); });

View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test.describe("Dashboard Deleted Page", () => {
test("User can navigate to deleted page", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
// Setup mock for deleted files API
await dashboardPage.setupDeletedFiles();
// Navigate directly to deleted page
await dashboardPage.goToDeleted();
// Check for the restore all and clear trash buttons
await expect(
page.getByRole("button", { name: "Restore All" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Clear trash" }),
).toBeVisible();
});
});

View File

@@ -18,6 +18,10 @@ const setupFile = async (workspacePage) => {
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d", fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
pageId: "ce79274b-11ab-8088-8007-0487ad43f789", pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
}); });
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-empty.json",
);
}; };
const shapeToLayerName = { const shapeToLayerName = {

View File

@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d", pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
}); });
await workspacePage.page.waitForTimeout(1000) await workspacePage.page.waitForTimeout(1000);
await workspacePage.waitForFirstRender(); await workspacePage.waitForFirstRender();
await expect( await expect(

View File

@@ -1,12 +1,323 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { Clipboard } from "../../helpers/Clipboard";
import { WorkspacePage } from "../pages/WorkspacePage"; import { WorkspacePage } from "../pages/WorkspacePage";
test.beforeEach(async ({ page }) => { const timeToWait = 100;
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
await WorkspacePage.init(page); await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]); await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
}); });
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => { test.afterEach(async ({ context }) => {
context.clearPermissions();
});
test("Create a new text shape", async ({ page }) => {
const initialText = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.createTextShape(190, 150, 300, 200, initialText);
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(initialText);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text", async ({ page, context }) => {
const textToPaste = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("keyboard");
await page.waitForTimeout(timeToWait);
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text using context menu", async ({
page,
context,
}) => {
const textToPaste = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("context-menu");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by appending text", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd(0);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by prepending text", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(0);
await page.keyboard.type("Dolor sit amet ");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by inserting text in between", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(5);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape appending text by pasting text", async ({
page,
context,
}) => {
const textToPaste = " dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape prepending text by pasting text", async ({
page,
context,
}) => {
const textToPaste = "Dolor sit amet ";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (starting) text with pasted text", async ({
page,
}) => {
const textToPaste = "Dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (ending) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromEnd(5);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (in between) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5, 3);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lordolor sit ametsum");
await workspace.textEditor.stopEditing();
});
test("Update text font size selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeFontSize(36);
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test.skip("Update text line height selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLineHeight(1.4);
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
1,
"line-height",
);
expect(lineHeight).toBe("1.4");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLetterSpacing(10);
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
const workspace = new WorkspacePage(page); const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-11552.json"); await workspace.mockGetFile("text-editor/get-file-11552.json");
@@ -14,21 +325,16 @@ test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
"update-file?id=*", "update-file?id=*",
"text-editor/update-file-11552.json", "text-editor/update-file-11552.json",
); );
await workspace.goToWorkspace();
await workspace.goToWorkspace({ await workspace.doubleClickLeafLayer("Lorem ipsum");
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
});
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.clickLeafLayer("Lorem ipsum");
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", { const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
name: "Font Size", name: "Font Size",
}); });
await expect(fontSizeInput).toBeVisible(); await expect(fontSizeInput).toBeVisible();
await workspace.page.keyboard.press("Enter"); await page.keyboard.press("Enter");
await workspace.page.keyboard.press("ArrowRight"); await page.keyboard.press("ArrowRight");
await fontSizeInput.fill("36"); await fontSizeInput.fill("36");

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,8 @@ export function startWorker() {
} }
export const isDebug = process.env.NODE_ENV !== "production"; 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 = {}) { async function findFiles(basePath, predicate, options = {}) {
predicate = predicate =
@@ -47,8 +49,7 @@ async function findFiles(basePath, predicate, options = {}) {
function syncDirs(originPath, destPath) { function syncDirs(originPath, destPath) {
const command = `rsync -ar --delete ${originPath} ${destPath}`; const command = `rsync -ar --delete ${originPath} ${destPath}`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
proc.exec(command, (cause, stdout) => {
if (cause) { if (cause) {
reject(cause); reject(cause);
} else { } else {
@@ -186,38 +187,36 @@ async function readManifestFile(resource) {
return JSON.parse(content); return JSON.parse(content);
} }
async function readShadowManifest() { async function generateManifest() {
const ts = Date.now(); const index = {
try { app_main: "./js/main.js",
const content = await readManifestFile("js/manifest.json"); render_main: "./js/render.js",
rasterizer_main: "./js/rasterizer.js",
const index = { config: "./js/config.js?version=" + CURRENT_VERSION,
ts: ts, polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
config: "js/config.js?ts=" + ts, libs: "./js/libs.js?version=" + CURRENT_VERSION,
polyfills: "js/polyfills.js?ts=" + ts, worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
}; default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
for (let item of content) { importmap: JSON.stringify({
index[item.name] = "js/" + item["output-name"]; "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
}
})
};
const content2 = await readManifestFile("js/worker/manifest.json"); return index;
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 = {}) { async function renderTemplate(path, context = {}, partials = {}) {
@@ -257,7 +256,7 @@ const markedOptions = {
marked.use(markedOptions); marked.use(markedOptions);
async function readTranslations() { export async function compileTranslations() {
const langs = [ const langs = [
"ar", "ar",
"ca", "ca",
@@ -295,9 +294,10 @@ async function readTranslations() {
["uk", "ukr_UA"], ["uk", "ukr_UA"],
"ha", "ha",
]; ];
const result = {};
for (let lang of langs) { for (let lang of langs) {
const result = {};
let filename = `${lang}.po`; let filename = `${lang}.po`;
if (l.isArray(lang)) { if (l.isArray(lang)) {
filename = `${lang[1]}.po`; filename = `${lang[1]}.po`;
@@ -316,11 +316,6 @@ async function readTranslations() {
for (let key of Object.keys(trdata)) { for (let key of Object.keys(trdata)) {
if (key === "") continue; if (key === "") continue;
const comments = trdata[key].comments || {}; const comments = trdata[key].comments || {};
if (l.isNil(result[key])) {
result[key] = {};
}
const isMarkdown = l.includes(comments.flag, "markdown"); const isMarkdown = l.includes(comments.flag, "markdown");
const msgs = trdata[key].msgstr; const msgs = trdata[key].msgstr;
@@ -330,9 +325,9 @@ async function readTranslations() {
message = marked.parseInline(message); message = marked.parseInline(message);
} }
result[key][lang] = message; result[key] = message;
} else { } else {
result[key][lang] = msgs.map((item) => { result[key] = msgs.map((item) => {
if (isMarkdown) { if (isMarkdown) {
return marked.parseInline(item); return marked.parseInline(item);
} else { } else {
@@ -341,22 +336,12 @@ async function readTranslations() {
}); });
} }
} }
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
const outputDir = "resources/public/js/";
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
await fs.writeFile(outputFile, esm);
} }
return result;
}
function filterTranslations(translations, langs = [], keyFilter) {
const filteredEntries = Object.entries(translations)
.filter(([translationKey, _]) => keyFilter(translationKey))
.map(([translationKey, value]) => {
const langEntries = Object.entries(value).filter(([lang, _]) =>
langs.includes(lang),
);
return [translationKey, Object.fromEntries(langEntries)];
});
return Object.fromEntries(filteredEntries);
} }
async function generateSvgSprite(files, prefix) { async function generateSvgSprite(files, prefix) {
@@ -408,15 +393,7 @@ async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production"; const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true }); await fs.mkdir("./resources/public/", { recursive: true });
let translations = await readTranslations(); const manifest = await generateManifest();
const storybookTranslations = JSON.stringify(
filterTranslations(translations, ["en"], (key) =>
key.startsWith("labels."),
),
);
translations = JSON.stringify(translations);
const manifest = await readShadowManifest();
let content; let content;
const iconsSprite = await fs.readFile( const iconsSprite = await fs.readFile(
@@ -437,13 +414,16 @@ async function generateTemplates() {
"../public/images/sprites/assets.svg": assetsSprite, "../public/images/sprites/assets.svg": assetsSprite,
}; };
const context = {
manifest: manifest,
version: CURRENT_VERSION,
build_date: BUILD_DATE,
isDebug,
};
content = await renderTemplate( content = await renderTemplate(
"resources/templates/index.mustache", "resources/templates/index.mustache",
{ context,
manifest: manifest,
translations: JSON.stringify(translations),
isDebug,
},
partials, partials,
); );
@@ -451,41 +431,30 @@ async function generateTemplates() {
content = await renderTemplate( content = await renderTemplate(
"resources/templates/challenge.mustache", "resources/templates/challenge.mustache",
{}, context,
partials, partials,
); );
await fs.writeFile("./resources/public/challenge.html", content); await fs.writeFile("./resources/public/challenge.html", content);
content = await renderTemplate( content = await renderTemplate(
"resources/templates/preview-body.mustache", "resources/templates/preview-body.mustache",
{ context,
manifest: manifest,
},
partials, partials,
); );
await fs.writeFile("./.storybook/preview-body.html", content); await fs.writeFile("./.storybook/preview-body.html", content);
content = await renderTemplate( content = await renderTemplate(
"resources/templates/preview-head.mustache", "resources/templates/preview-head.mustache",
{ context,
manifest: manifest,
translations: JSON.stringify(storybookTranslations),
},
partials, partials,
); );
await fs.writeFile("./.storybook/preview-head.html", content); await fs.writeFile("./.storybook/preview-head.html", content);
content = await renderTemplate("resources/templates/render.mustache", { content = await renderTemplate("resources/templates/render.mustache", context);
manifest: manifest,
translations: JSON.stringify(translations),
});
await fs.writeFile("./resources/public/render.html", content); await fs.writeFile("./resources/public/render.html", content);
content = await renderTemplate("resources/templates/rasterizer.mustache", { content = await renderTemplate("resources/templates/rasterizer.mustache", context);
manifest: manifest,
translations: JSON.stringify(translations),
});
await fs.writeFile("./resources/public/rasterizer.html", content); await fs.writeFile("./resources/public/rasterizer.html", content);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,7 +86,6 @@
(def default-theme "default") (def default-theme "default")
(def default-language "en") (def default-language "en")
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
(def build-date (parse-build-date global)) (def build-date (parse-build-date global))

View File

@@ -90,9 +90,12 @@
(rx/map #(ws/initialize))))))) (rx/map #(ws/initialize)))))))
(defn ^:export init (defn ^:export init
[] [options]
(some-> (unchecked-get options "defaultTranslations")
(i18n/set-default-translations))
(mw/init!) (mw/init!)
(i18n/init! cf/translations) (i18n/init)
(cur/init-styles) (cur/init-styles)
(thr/init!) (thr/init!)
(init-ui) (init-ui)
@@ -114,11 +117,4 @@
[] []
(reinit)) (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) (set! (.-stackTraceLimit js/Error) 50)

View File

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

View File

@@ -76,7 +76,7 @@
(map :page-id)) (map :page-id))
(defn- apply-changes-localy (defn- apply-changes-localy
[{:keys [file-id redo-changes] :as commit} pending] [{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
(ptk/reify ::apply-changes-localy (ptk/reify ::apply-changes-localy
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@@ -103,7 +103,7 @@
pids (into #{} xf:map-page-id redo-changes)] pids (into #{} xf:map-page-id redo-changes)]
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))] (reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
(if (features/active-feature? state "render-wasm/v1") (if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
;; Update the wasm model ;; Update the wasm model
(let [shape-changes (volatile! {}) (let [shape-changes (volatile! {})
@@ -122,7 +122,7 @@
(defn commit (defn commit
"Create a commit event instance" "Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features [{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn file-vern undo-group tags stack-undo? source]}] file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
(assert (cpc/check-changes redo-changes) (assert (cpc/check-changes redo-changes)
"expect valid vector of changes for redo-changes") "expect valid vector of changes for redo-changes")
@@ -147,7 +147,8 @@
:save-undo? save-undo? :save-undo? save-undo?
:undo-group undo-group :undo-group undo-group
:tags tags :tags tags
:stack-undo? stack-undo?}] :stack-undo? stack-undo?
:ignore-wasm? ignore-wasm?}]
(ptk/reify ::commit (ptk/reify ::commit
cljs.core/IDeref cljs.core/IDeref

View File

@@ -386,3 +386,21 @@
(rx/of ::dps/force-persist (rx/of ::dps/force-persist
(rt/nav :viewer params options)))))) (rt/nav :viewer params options))))))
(defn go-to-dashboard-deleted
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-deleted
ptk/WatchEvent
(watch [_ state _]
(let [profile (get state :profile)
team-id (cond
(= :default team-id)
(:default-team-id profile)
(uuid? team-id)
team-id
:else
(:current-team-id state))
params {:team-id team-id}]
(rx/of (modal/hide)
(rt/nav :dashboard-deleted params options))))))

View File

@@ -21,6 +21,7 @@
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.websocket :as dws] [app.main.data.websocket :as dws]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.sse :as sse] [app.util.sse :as sse]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@@ -76,7 +77,8 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(reduce (fn [state {:keys [id] :as project}] (reduce (fn [state {:keys [id] :as project}]
(update-in state [:projects id] merge project)) ;; Replace completely instead of merge to ensure deleted-at is removed
(assoc-in state [:projects id] project))
state state
projects)))) projects))))
@@ -152,6 +154,34 @@
(->> (rp/cmd! :get-builtin-templates) (->> (rp/cmd! :get-builtin-templates)
(rx/map builtin-templates-fetched))))) (rx/map builtin-templates-fetched)))))
;; --- EVENT: deleted-files
(defn- deleted-files-fetched
[files]
(ptk/reify ::deleted-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [now (ct/now)
filtered-files (filterv (fn [file]
(let [will-be-deleted-at (:will-be-deleted-at file)]
(or (nil? will-be-deleted-at)
(ct/is-after? will-be-deleted-at now))))
files)
files (d/index-by :id filtered-files)]
(-> state
(assoc :deleted-files files)
(update :files d/merge files))))))
(defn fetch-deleted-files
([] (fetch-deleted-files nil))
([team-id]
(ptk/reify ::fetch-deleted-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-deleted-files {:team-id team-id})
(rx/map deleted-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection ;; Data Selection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -460,6 +490,7 @@
(-> state (-> state
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id) (d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id) (d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:deleted-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files)))))) (d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file ;; --- EVENT: create-file
@@ -656,3 +687,156 @@
:team-role-change (handle-change-team-role msg) :team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg) :team-membership-change (dcm/team-membership-change msg)
nil)) nil))
;; --- Delete files immediately
(defn delete-files-immediately
[{:keys [team-id ids] :as params}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(ptk/reify ::delete-files-immediately
ev/Event
(-data [_]
{:team-id team-id
:num-files (count ids)})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :permanently-delete-team-files {:team-id team-id :ids ids})
(rx/tap on-success)
(rx/catch on-error))))))
;; --- Restore deleted files immediately
(defn- initialize-restore-status
[files]
(ptk/reify ::init-restore-status
ptk/UpdateEvent
(update [_ state]
(let [restore-state {:in-progress true
:healthy? true
:error false
:progress 0
:widget-visible true
:detail-visible true
:files files
:last-update (ct/now)
:cmd :restore-files}]
(assoc state :restore restore-state)))))
(defn- update-restore-status
[{:keys [index total] :as data}]
(ptk/reify ::upd-restore-status
ptk/UpdateEvent
(update [_ state]
(let [time-diff (ct/diff-ms (get-in state [:restore :last-update]) (ct/now))
healthy? (< time-diff 6000)]
(update state :restore assoc
:progress index
:total total
:last-update (ct/now)
:healthy? healthy?)))))
(defn- complete-restore-status
[]
(ptk/reify ::comp-restore-status
ptk/UpdateEvent
(update [_ state]
(let [total (get-in state [:restore :total])]
(update state :restore assoc
:in-progress false
:progress total ; Ensure progress equals total on completion
:last-update (ct/now))))))
(defn- error-restore-status
[error]
(ptk/reify ::err-restore-status
ptk/UpdateEvent
(update [_ state]
(update state :restore assoc
:in-progress false
:error error
:last-update (ct/now)
:healthy? false))))
(defn toggle-restore-detail-visibility
[]
(ptk/reify ::toggle-restore-detail
ptk/UpdateEvent
(update [_ state]
(update-in state [:restore :detail-visible] not))))
(defn retry-last-restore
[]
(ptk/reify ::retry-restore
ptk/UpdateEvent
(update [_ state]
;; Reset restore state for retry - actual retry will be handled by UI
(if (get state :restore)
(update state :restore assoc :error false :in-progress false)
state))))
(defn clear-restore-state
[]
(ptk/reify ::clear-restore
ptk/UpdateEvent
(update [_ state]
(dissoc state :restore))))
(defn- projects-restored
[team-id]
(ptk/reify ::projects-restored
ptk/WatchEvent
(watch [_ _ _]
;; Refetch projects to get the updated state without deleted-at
(rx/of (fetch-projects team-id)))))
(defn restore-files-immediately
[{:keys [team-id ids] :as params}]
(dm/assert! (uuid? team-id))
(dm/assert! (set? ids))
(dm/assert! (every? uuid? ids))
(ptk/reify ::restore-files-immediately
ev/Event
(-data [_]
{:team-id team-id
:num-files (count ids)})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
files (mapv #(hash-map :id %) ids)]
(rx/merge
(rx/of (initialize-restore-status files))
(->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
(rx/tap (fn [event]
(let [payload (sse/get-payload event)
type (sse/get-type event)]
(when (and payload (= type "progress"))
(let [{:keys [index total]} payload]
(when (and index total)
;; Dispatch progress update
(st/emit! (update-restore-status {:index index :total total}))))))))
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/tap on-success)
(rx/mapcat (fn [_]
(rx/of (complete-restore-status)
(projects-restored team-id))))
(rx/catch (fn [error]
(rx/concat
(rx/of (error-restore-status (ex-message error)))
(on-error error)))))
(rx/of (ptk/data-event ::restore-start {:total (count ids)})))))))

View File

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

View File

@@ -24,6 +24,8 @@
(def revn-data (atom {})) (def revn-data (atom {}))
(def queue-conj (fnil conj #queue [])) (def queue-conj (fnil conj #queue []))
(def force-persist? #(= % ::force-persist))
(defn- update-status (defn- update-status
[status] [status]
(ptk/reify ::update-status (ptk/reify ::update-status

View File

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

View File

@@ -59,9 +59,15 @@
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`. "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`." If the value is not parseable and/or has missing references returns a map with `:errors`."
[value] [value]
(if-let [tc (tinycolor/valid-color value)] (let [missing-references (seq (cto/find-token-value-references value))]
{:value value :unit (tinycolor/color-format tc)} (if-let [tc (tinycolor/valid-color value)]
{:errors [(wte/error-with-value :error.token/invalid-color value)]})) {:value value :unit (tinycolor/color-format tc)}
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
:else
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
(defn- numeric-string? [s] (defn- numeric-string? [s]
(and (string? s) (and (string? s)
@@ -120,7 +126,7 @@
If the `value` is not parseable and/or has missing references returns a map with `:errors`. 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`." If the `value` is parseable but is out of range returns a map with `warnings`."
[value] [value]
(let [missing-references? (seq (cto/find-token-value-references value)) (let [missing-references? (seq (seq (cto/find-token-value-references value)))
parsed-value (cft/parse-token-value value) parsed-value (cft/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1)) out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))] references (seq (cto/find-token-value-references value))]
@@ -373,8 +379,8 @@
(let [add-keyed-errors (fn [shadow-result k errors] (let [add-keyed-errors (fn [shadow-result k errors]
(update shadow-result :errors concat (update shadow-result :errors concat
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors))) (map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
parsers {:offsetX parse-sd-token-general-value parsers {:offset-x parse-sd-token-general-value
:offsetY parse-sd-token-general-value :offset-y parse-sd-token-general-value
:blur parse-sd-token-shadow-blur :blur parse-sd-token-shadow-blur
:spread parse-sd-token-shadow-spread :spread parse-sd-token-shadow-spread
:color parse-sd-token-color-value :color parse-sd-token-color-value
@@ -394,35 +400,42 @@
(defn- parse-sd-token-shadow-value (defn- parse-sd-token-shadow-value
"Parses shadow value and validates it." "Parses shadow value and validates it."
[value] [value]
(cond (let [missing-references
;; Reference value (string) (when (string? value)
(string? value) {:value value} (seq (cto/find-token-value-references value)))]
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
(string? value)
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
;; Empty value ;; 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 ;; 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 ;; Array of shadows
:else :else
(let [converted (js->clj value :keywordize-keys true) (let [converted (js->clj value :keywordize-keys true)
;; Parse each shadow with its index ;; Parse each shadow with its index
parsed-shadows (map-indexed parsed-shadows (map-indexed
(fn [idx shadow-map] (fn [idx shadow-map]
(parse-single-shadow shadow-map idx)) (parse-single-shadow shadow-map idx))
converted) converted)
;; Collect all errors from all shadows ;; 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 ;; 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) (if (seq all-errors)
{:errors all-errors {:errors all-errors
:value all-values} :value all-values}
{:value all-values})))) {:value all-values})))))
(defn collect-shadow-errors [token shadow-index] (defn collect-shadow-errors [token shadow-index]
(group-by :shadow-key (group-by :shadow-key

View File

@@ -32,7 +32,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps] [app.main.data.persistence :as dps]
[app.main.data.plugins :as dp] [app.main.data.plugins :as dp]
[app.main.data.profile :as du] [app.main.data.profile :as du]
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
@@ -67,6 +67,7 @@
[app.main.errors] [app.main.errors]
[app.main.features :as features] [app.main.features :as features]
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.render-wasm :as wasm] [app.render-wasm :as wasm]
@@ -269,8 +270,12 @@
(ptk/reify ::process-wasm-object (ptk/reify ::process-wasm-object
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [objects (dsh/lookup-page-objects state)] (let [objects (dsh/lookup-page-objects state)
(wasm.api/process-object (get objects id)))))) shape (get objects id)]
;; Only process objects that exist in the current page
;; This prevents errors when processing changes from other pages
(when shape
(wasm.api/process-object shape))))))
(defn initialize-workspace (defn initialize-workspace
[team-id file-id] [team-id file-id]
@@ -379,6 +384,59 @@
(->> (rx/from added) (->> (rx/from added)
(rx/map process-wasm-object))))))) (rx/map process-wasm-object)))))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)

View File

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

View File

@@ -102,7 +102,8 @@
{:origin it {:origin it
:redo-changes changes :redo-changes changes
:undo-changes [] :undo-changes []
:save-undo? false}))))))) :save-undo? false
:ignore-wasm? true})))))))
;; FIXME: would be nice to not execute this code twice per page in the ;; FIXME: would be nice to not execute this code twice per page in the
;; same working session, maybe some local memoization can improve that ;; same working session, maybe some local memoization can improve that
@@ -119,4 +120,5 @@
{:origin it {:origin it
:redo-changes changes :redo-changes changes
:undo-changes [] :undo-changes []
:save-undo? false}))))))) :save-undo? false
:ignore-wasm? true})))))))

View File

@@ -119,21 +119,6 @@
(let [page (dsh/lookup-page state)] (let [page (dsh/lookup-page state)]
(rx/of (update-flow (:id page) flow-id #(assoc % :name name))))))) (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 ;; --- Interactions
(defn- connected-frame? (defn- connected-frame?

View File

@@ -649,7 +649,7 @@
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state)) (propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
ids ids
(into [] xf:without-uuid-zero (keys transforms)) (into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
update-shape update-shape
(fn [shape] (fn [shape]

View File

@@ -831,7 +831,8 @@
(effect [_ state _] (effect [_ state _]
(when (features/active-feature? state "text-editor/v2") (when (features/active-feature? state "text-editor/v2")
(let [instance (:workspace-editor state) (let [instance (:workspace-editor state)
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration)) attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration))
overriden-attrs (merge attrs-to-override attrs) overriden-attrs (merge attrs-to-override attrs)
styles (styles/attrs->styles overriden-attrs)] styles (styles/attrs->styles overriden-attrs)]
(editor.v2/applyStylesToSelection instance styles)))))) (editor.v2/applyStylesToSelection instance styles))))))

View File

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

View File

@@ -112,6 +112,10 @@
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread {:error/code :error.style-dictionary/invalid-token-value-shadow-spread
:error/fn #(tr "workspace.tokens.shadow-spread-range")} :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/unknown
{:error/code :error/unknown {:error/code :error/unknown
:error/fn #(tr "labels.unknown-error")}}) :error/fn #(tr "labels.unknown-error")}})

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