Compare commits

...

198 Commits

Author SHA1 Message Date
Andrey Antukh
e2c5a1378e Merge pull request #6724 from penpot/elenatorro-improve-create-profile-command
🔧 Add option to skip tutorial/walkthrough when creating a profile from the script
2025-07-11 14:15:49 +02:00
Xavier Julian
81f99458e5 📎 Add new font-size token type to CHANGELOG 2025-07-11 14:09:39 +02:00
Florian Schroedl
9c5a13c4ac Enable font-size token 2025-07-11 10:37:17 +02:00
Xavier Julian
02ae934e25 📎 Add import tokens from zip file to CHANGELOG 2025-07-10 13:40:42 +02:00
Andrey Antukh
95cfb26b38 Merge pull request #6882 from penpot/xaviju-11283-info-tab-visibility-attrs-review
♻️ Fix tab info not updating and suggested code refactor
2025-07-10 11:56:38 +02:00
Andrey Antukh
935c22d124 Merge pull request #6885 from penpot/marina-change-text-capitalize
🐛 Fix title button from Title case to Capitalize
2025-07-10 11:55:50 +02:00
Marina López
ba6a02d1d9 🐛 Add fixes from subscription design review (#6870)
* 🐛 Fixes from subscription design review

* 🐛 Fix to consider professional plan the unpaid and canceled status

* 📎 Fixes PR feedback
2025-07-10 11:55:16 +02:00
Xavier Julian
0b681effe7 ♻️ Fix tab info not updating and suggested code refactor 2025-07-10 11:38:53 +02:00
Marina López
6826db8498 🐛 Fix title button from Title case to Capitalize 2025-07-10 11:29:48 +02:00
Andrey Antukh
66c5841d48 Merge pull request #6886 from penpot/alotor-fix-create-layout
🐛 Fix problem when creating a layout from an existing layout
2025-07-10 11:28:19 +02:00
Xavier Julian
af10705b4c ♻️ Review import message text 2025-07-10 10:18:59 +02:00
Pablo Alba
41146ef71d 🐛 Fix text overrides when there are structure changes 2025-07-09 21:58:01 +02:00
Pablo Alba
abb6aee57d 🐛 On texts overrides, keep also vertical-align property 2025-07-09 21:58:01 +02:00
alonso.torres
aa01d3b707 🐛 Fix problem when creating a layout from an existing layout 2025-07-09 15:44:15 +02:00
alonso.torres
a003687256 🐛 Fix problem with grid assignments 2025-07-09 14:55:14 +02:00
Andrey Antukh
51a6d61be6 Merge pull request #6865 from penpot/xaviju-11283-info-tab-visibility-attrs
 Add visibility group and attributes to info tab
2025-07-09 12:18:10 +02:00
Xavier Julian
0daa8be0b5 Add visibility group and attributes to info tab 2025-07-09 11:19:30 +02:00
Andrey Antukh
00599f76d0 Merge pull request #6875 from penpot/ladybenko-fix-devenv-mac-ubuntu
🔧 Fix building and running devenv (Mac / Linux)
2025-07-09 08:28:49 +02:00
Belén Albeza
cb8aae4d5f 🔧 Drop the -R in chown (dockerfile mac) 2025-07-08 15:45:34 +02:00
Belén Albeza
927228fc8f 🔧 Remove COPY of apt.sources (linux issue) 2025-07-08 15:44:42 +02:00
Andrés Moya
88bb9bfe52 🐛 Detach styles from assets when applying tokens 2025-07-08 13:15:45 +02:00
Andrey Antukh
e554b9fcb7 Merge remote-tracking branch 'origin/staging' into develop 2025-07-08 11:04:29 +02:00
Aitor Moreno
4548310235 Merge pull request #6867 from penpot/azazeln28-fix-missing-solid-color
🐛 Fix missing required SolidColor
2025-07-08 09:11:12 +02:00
Aitor Moreno
ea9261b0b2 🐛 Fix missing required SolidColor 2025-07-08 08:45:03 +02:00
Aitor Moreno
6ffcd58368 Merge pull request #6846 from penpot/alotor-wasm-refactor-mut-2
♻️ Refactor wasm shapes state management
2025-07-08 08:31:15 +02:00
alonso.torres
69135ef8c7 ♻️ Refactor wasm shapes state management 2025-07-08 08:30:40 +02:00
Aitor Moreno
747427daa4 Merge pull request #6841 from penpot/superalex-fix-frame-clipping
🐛 Fix frame clipping
2025-07-08 08:26:48 +02:00
Pablo Alba
cfec023585 ♻️ Rename flag :component-swap to :allow-altering-copies 2025-07-07 12:07:36 +02:00
Pablo Alba
469d47eaf3 🐛 Fix variants combobox and select to new options format 2025-07-07 11:46:50 +02:00
Alejandro Alonso
51bb6583d2 🐛 Fix frame clipping 2025-07-07 11:09:29 +02:00
Pablo Alba
a44c70ef69 Keep the swapped childs if the copies when doing a variant switch 2025-07-07 10:50:49 +02:00
Andrés Moya
4fddf34a73 🐛 Fix error when there exists a tokens lib with no sets 2025-07-07 10:02:49 +02:00
Xavier Julian
8f840daa91 Improve token import error copy 2025-07-07 09:59:57 +02:00
Juanfran
0a7d6d98e1 Integrate plugin runtime as npm library (#6852) 2025-07-07 09:46:07 +02:00
Álvaro Tejero-Cantero
bcb69b6227 🐛 Restore viewport and selection when exiting focus mode (#6827)
* 📚 Provide guidance on how to exit focus mode

* 🐛 Restore viewport & selection post focus mode

* 📎 Update changelog
2025-07-07 09:44:06 +02:00
Andrey Antukh
92d708d52c Merge remote-tracking branch 'origin/staging' into develop 2025-07-07 09:37:55 +02:00
Andrey Antukh
0374e4f3eb Merge remote-tracking branch 'origin/staging' into develop 2025-07-04 12:02:12 +02:00
David Barragán Merino
528c819323 🔧 Add Github Action to build and upload artifact (#6840)
Co-authored-by: Francis Santiago <francis.santiago@kaleidos.net>
2025-07-04 08:25:23 +02:00
Florian Schrödl
21746144b7 Add letter spacing token (#6814)
* 🐛 Fix merge schema not working with key generation

*  Add letter-spacing token

* ♻️ Remove comments

* ♻️ Inline line-height for now
2025-07-03 16:00:58 +02:00
Andrey Antukh
3165761bac Merge remote-tracking branch 'origin/staging' into develop 2025-07-03 15:32:30 +02:00
Andrés Moya
c09f72c3d5 🐛 Sanitize wrong ids in token themes (#6843) 2025-07-03 15:31:45 +02:00
Florian Schrödl
7dd61968b5 Implement object type specific tokens (#6816)
*  Allow token applying for supported shape types only

* 🐛 Remove x/y attribute keys from spacing token

*  Shape specific context-menu

*  Only apply tokens to supported shapes when doing multi selection apply

*  Handle groups not supported by tokens yet

* 🐛 Fix outdated tests

* ♻️ Commentary

*  Add helper functions for attribute applicability checks

* ♻️ Groups don't have own attributes

* ♻️ Remove unused function

* ♻️ Move attribute logic to common.types.token
2025-07-03 12:22:04 +02:00
Juanfran
669d6d9ae2 Merge pull request #6837 from penpot/juanfran-us-11186-rules-help
 Add in-app help to guide users about variant rules
2025-07-03 11:30:16 +02:00
Miguel de Benito Delgado
b931547300 🐳 Add "postgres" network alias to default docker network in devenv (#6823) 2025-07-03 10:28:53 +02:00
Belén Albeza
30274c4f5c 🔧 Restore arm64 build of devenv (#6826) 2025-07-03 08:28:07 +02:00
Andrés Moya
0a71134652 🔧 Sanitize and check tokens when deserializing from db (#6838) 2025-07-02 17:01:10 +02:00
Juanfran
72b1919e29 Add in-app help to guide users about variant rules 2025-07-02 14:46:36 +02:00
Xavier Julian
be43365909 🐛 Fix broken import file type drodown options 2025-07-02 14:35:01 +02:00
Andrey Antukh
41994703a9 ♻️ Refactor tab-switcher* component (#6815)
* 💄 Add minor style adjustments to workspace sidebar

* 💄 Add style enhacement to sitemap component

* ♻️ Refactor tab-switcher* component
2025-07-02 14:08:47 +02:00
Marina López
3d45080e3c 🐛 Fixes from subscription design review (#6812) 2025-07-02 10:49:16 +02:00
Miguel de Benito Delgado
28c055e3f9 📚 Fix and extend backend repl doc (#6819) 2025-07-02 10:38:35 +02:00
Prithvi Tharun
4f993bf4ae 💄 Replace 'Verify new email' label with 'Confirm new email' (#6831)
Improves clarity by using more accurate and familiar terminology.

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2025-07-02 10:32:09 +02:00
Alejandro Alonso
3cb0e1b6ee 🐛 Fix exif rotation detection when auto-rotation isn't supported (#6818) 2025-07-02 10:31:05 +02:00
Andrey Antukh
1432b211a6 Merge remote-tracking branch 'origin/staging' into develop 2025-07-02 10:13:30 +02:00
Miguel de Benito Delgado
3e45e4fb25 🐛 Fix internal error on missing theme setting in profile (#6822) 2025-07-02 09:57:56 +02:00
Andrés Moya
953287ea33 🐛 Avoid crash in combobox with empty options 2025-07-02 08:57:25 +02:00
Elena Torró
493831f110 Merge pull request #6821 from penpot/alotor-refactor-mutability
♻️ Refactor mutability modifiers in wasm
2025-07-01 13:52:39 +02:00
alonso.torres
3d374e8e97 ♻️ Refactor mutability modifiers in wasm 2025-07-01 12:47:31 +02:00
Andrés Moya
f0f01af55c 🔧 Make TokenSet an abstract data type 2025-06-30 16:59:00 +02:00
Xavier Julian
6de9de9e38 Add new metric for token update and provide token type 2025-06-30 13:21:49 +02:00
Kelp
b893a62e40 Add new typography icon to the DS (#6808)
Signed-off-by: Kelp <5446186+NatachaMenjibar@users.noreply.github.com>
2025-06-30 11:06:54 +02:00
alonso.torres
8dcb376b18 Add drop grid cells in wasm 2025-06-30 10:28:59 +02:00
alonso.torres
52a4fc6030 🐛 Fix drop index on flex layout wasm 2025-06-30 10:28:59 +02:00
Andrey Antukh
403d92838a ♻️ Add minor refactor to options dropdown options handling and validation (#6739)
* ♻️ Refactor options-dropdown* and related components

* 🐛 Fix props error

* 🐛 Fix test

* 📎 Update rumext

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2025-06-29 11:52:29 +02:00
Xavier Julian
6bd3253e5e ♻️ Restructure UI files for tokens editor 2025-06-27 13:23:42 +02:00
Pablo Alba
20b5b7f6e4 🐛 Fix variant switch in another page (#6802) 2025-06-27 12:23:54 +02:00
Pablo Alba
804146ae9a 🐛 Fix text partial change doesn't show up on another page (#6799) 2025-06-27 10:21:21 +02:00
Juanfran
24e78e6a10 🐛 Display error message on register form (#6797) 2025-06-27 10:01:54 +02:00
Pablo Alba
daca26e54f 🐛 On variants override use the component name instead of the copy name 2025-06-26 17:37:21 +02:00
Aitor Moreno
29016cef49 Merge pull request #6794 from penpot/alotor-wasm-fix-grid-fr
🐛 Fix problem with fr allocation
2025-06-26 14:39:42 +02:00
alonso.torres
fb07788e8f 🐛 Fix problem with fr allocation 2025-06-26 13:17:26 +02:00
Andrey Antukh
c75a617d26 Merge remote-tracking branch 'origin/staging' into develop 2025-06-26 11:19:29 +02:00
Andrey Antukh
f2c4a1eb1f Merge pull request #6674 from penpot/niwinz-develop-enhacements-3
 Refactor fills-menu and related components
2025-06-26 11:09:30 +02:00
Marina López
62371fded0 🐛 Fix libraries position in dashboard sidebar (#6791) 2025-06-26 11:08:18 +02:00
Andrey Antukh
e72d31a082 🔥 Remove unused and commented code 2025-06-26 10:50:38 +02:00
Andrey Antukh
6b4a85cd15 🐛 Fix issue on changing from gradient to solid color on colorpicker 2025-06-26 10:50:38 +02:00
Andrey Antukh
027a7a457d Add minor style improvements for reorder-handler component 2025-06-26 10:50:38 +02:00
Andrey Antukh
20d2d22f39 Add performance oriented refactor to fill-menu component 2025-06-26 10:50:36 +02:00
Andrey Antukh
a191fe63a1 Merge remote-tracking branch 'origin/staging' into develop 2025-06-26 09:18:23 +02:00
luisδμ
2de0c90fc7 🐛 Remove empty properties starting with the last one (#6757)
* 🐛 Remove empty properties starting with the last one

*  MR changes

---------

Co-authored-by: Pablo Alba <pablo.alba@kaleidos.net>
2025-06-26 09:16:59 +02:00
Luis de Dios
7cd0e28c3b Allow variants with no properties 2025-06-26 08:42:34 +02:00
Andrey Antukh
25ef1800d0 Merge remote-tracking branch 'origin/staging' into develop 2025-06-25 19:30:57 +02:00
Andrey Antukh
9760911fce Merge remote-tracking branch 'origin/staging' into develop 2025-06-25 14:24:26 +02:00
Marina López
f81a973a4d 🐛 Fix text decoration line through value in inspect tab (#6778) 2025-06-25 14:11:58 +02:00
Alejandro Alonso
ca99671d3c 📚 Update CHANGES with support for exif rotated images (#6782) 2025-06-25 14:10:13 +02:00
Marina López
1f42f032fc 🐛 Add fixes for subscription design review (#6751)
* 🐛 Fix from subscription design review

* 📎 Fixes PR feedback
2025-06-25 13:41:45 +02:00
Marina López
67ca8ccb22 🐛 Fix copy font-size doesn't copy the unit (#6776) 2025-06-25 12:14:33 +02:00
Xavier Julian
ce59070fd1 ♻️ Restructure UI files for token sets 2025-06-25 11:27:13 +02:00
Marina López
e258030bc0 💄 Change 'save color' button (#6774) 2025-06-25 10:21:22 +02:00
Alejandro Alonso
8f00292f8f 🎉 Support for exim rotated images (#6767) 2025-06-25 10:20:37 +02:00
Florian Schroedl
fe91201431 Keep warning for unsupported token types when FF is disabled 2025-06-24 15:41:24 +02:00
Florian Schroedl
00c7411f92 🐛 Fix dtcg token type name 2025-06-24 15:41:24 +02:00
Xavier Julian
e585cbd673 ♻️ Restructure UI files for import/export and common files 2025-06-24 13:58:54 +02:00
Alejandro Alonso
bdc10ac173 Merge pull request #6754 from penpot/azazeln28-issue-11401-fix-wrong-aspect-ratio
🐛 Fix image aspect ratio rendering on oriented images
2025-06-24 13:23:35 +02:00
Elena Torró
9f5cb61a19 Merge pull request #6766 from penpot/elenatorro-fix-text-auto-height
🐛 Fix text auto height
2025-06-24 13:18:28 +02:00
Alejandro Alonso
e442d8adad Add tests for exif rotated images 2025-06-24 13:08:18 +02:00
Elena Torro
925af4e1e9 🐛 Fix text auto height 2025-06-24 12:36:12 +02:00
alonso.torres
a45886c86c Small cosmetic change 2025-06-24 10:26:37 +02:00
alonso.torres
36b6f6323a ♻️ Refactor modifiers methods 2025-06-24 10:26:37 +02:00
alonso.torres
afec3b9bc1 🐛 Fix problem with margin in flex layout 2025-06-24 10:26:37 +02:00
alonso.torres
ac6a814026 🐛 Fix problem with flex layout in wasm 2025-06-24 10:26:37 +02:00
Alejandro Alonso
89fb802362 Merge pull request #6764 from penpot/alotor-fix-problem-lines
🐛 Fix wasm problem with horizontal/vertical lines
2025-06-24 09:43:11 +02:00
alonso.torres
b0d858df2b 🐛 Fix wasm problem with horizontal/vertical lines 2025-06-24 09:24:00 +02:00
Aitor Moreno
f54497194a Merge pull request #6762 from penpot/elenatorro-10901-add-text-vertical-alignment
🔧 Add vertical alignment for text shapes
2025-06-23 17:05:47 +02:00
Elena Torro
134fb1ab4c 🔧 Add vertical alignment for text shapes 2025-06-23 16:45:25 +02:00
Aitor Moreno
833546d754 🐛 Fix wrong aspect ratio on oriented image 2025-06-23 15:30:01 +02:00
Elena Torró
0010d61ae2 Merge pull request #6758 from penpot/elenatorro-text-rendering-fixes-and-tests
🔧 Add tests to cover text styles
2025-06-23 14:06:19 +02:00
Elena Torro
747f72be47 🔧 Add tests to cover text styles 2025-06-23 13:43:09 +02:00
Alejandro Alonso
1882efe3f7 🐛 Fix paths rendered initially ony in tile 0 0 2025-06-23 12:23:49 +02:00
Florian Schrödl
580bb46a05 Implement font-size token type (#6708)
*  Implement font-size token type

*  Hide typography tokens behind FF

* 💄 Update icon

* 🔧 Add font-size to unapply check

* ♻️ Generalize status-icon logic and remove icon for font-size
2025-06-23 12:12:40 +02:00
Alejandro Alonso
9ea0875e65 Merge pull request #6742 from penpot/ladybenko-fix-wasm-debug-text-hi-dpr
 Fix size of 'wasm renderer' debug text on dpr > 1
2025-06-23 11:47:20 +02:00
Alejandro Alonso
43b19ba33e Merge pull request #6738 from penpot/ladybenko-11247-enable-dpr-when-render-wasm
🔧 Enable render-wasm-dpr by default
2025-06-23 11:46:24 +02:00
Andrey Fedorov
130cd52f79 Notify user if imported file or directory doesn't contain token files 2025-06-23 11:44:00 +02:00
Aitor Moreno
21fd56076c Merge pull request #6756 from penpot/superalex-fix-empty-fills
🐛 Fix empty fills
2025-06-23 11:31:34 +02:00
Alejandro Alonso
c97314ddb5 🐛 Fix empty fills 2025-06-23 11:14:58 +02:00
Andrey Antukh
34bbce5089 Merge remote-tracking branch 'origin/staging' into develop 2025-06-23 10:06:05 +02:00
ºelhombretecla
9a0538e5e3 Add visual indicator for new comments in the workspace (#6728)
* 🎉 Add comment notification to workspace

* 💄 Add info to changelog

*  Add minor efficiency improvements

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-06-23 09:51:41 +02:00
Belén Albeza
56d96aaf07 🔧 Enable render-wasm-dpr by default 2025-06-20 12:48:49 +02:00
luisδμ
70f3988046 Show warning when selecting a copy of conflicted variant (#6743) 2025-06-20 11:52:07 +02:00
Andrey Antukh
ec021d944d Merge remote-tracking branch 'origin/staging' into develop 2025-06-20 11:37:50 +02:00
Elena Torró
f1a6b46165 Merge pull request #6745 from penpot/superalex-bug-fix-text-image-strokes
🐛 Fix image fill strokes are not rendered correctly for texts
2025-06-20 11:14:24 +02:00
Belén Albeza
2412402a23 Fix size of 'wasm renderer' debug text on dpr > 1 2025-06-20 10:55:33 +02:00
Alejandro Alonso
5375029497 🐛 Fix image fill strokes are not rendered correctly for texts 2025-06-20 10:52:26 +02:00
Aitor Moreno
8bfc314b17 Merge pull request #6700 from penpot/superalex-fix-async-content-rendering
🐛 Fix asynchronous content rendering
2025-06-20 10:11:28 +02:00
Elena Torró
38112e287a Merge pull request #6734 from penpot/ladybenko-11392-no-ui-in-tests
🔧 Make visual regression tests to hide the UI when taking a screenshot (render-wasm tests only)
2025-06-20 09:05:30 +02:00
Alejandro Alonso
5c7a4ce5b7 🐛 Fix fill images for text 2025-06-20 07:45:28 +02:00
Andrey Antukh
7e909dfbe8 Merge remote-tracking branch 'origin/staging' into develop 2025-06-19 15:35:19 +02:00
Alonso Torres
7f7f0893d0 🐛 Fix sidebar width in localhost (#6732) 2025-06-19 15:31:27 +02:00
María Valderrama
22fbc3fa5f 💄 Improve dashboard's sidebar (#6736) 2025-06-19 15:28:32 +02:00
Alejandro Alonso
d71fa659d5 🐛 Fix asynchronous content rendering 2025-06-19 14:03:40 +02:00
Alejandro Alonso
d0425cabda Merge pull request #6721 from penpot/ladybenko-11276-fix-modifiers-dpr
🐛 Fix panning and scroll when dpr > 1 (render wasm)
2025-06-19 14:01:47 +02:00
Belén Albeza
9852d24b83 🔧 Make visual regression tests to hide the UI when taking a screenshot (render-wasm tests only) 2025-06-19 13:37:52 +02:00
Alejandro Alonso
2239482049 Merge pull request #6717 from penpot/alotor-grid-editor
 Support grid editor with wasm modifiers
2025-06-19 13:32:27 +02:00
Xavier Julian
4ea4a1e130 ♻️ Restructure UI files for token settings 2025-06-19 13:10:09 +02:00
alonso.torres
11467e26a2 🐛 Fix problem with flex wrap in wasm 2025-06-19 13:03:25 +02:00
alonso.torres
b997d5a320 🐛 Fix problem with grid layout wasm 2025-06-19 13:03:25 +02:00
alonso.torres
5b4cd9f4f1 🐛 Fix problem when moving masks, bools, groups with wasm 2025-06-19 13:03:25 +02:00
alonso.torres
58e5748b4f 🐛 Fix wasm layout problems 2025-06-19 13:03:25 +02:00
alonso.torres
b2647f30c2 Support grid editor with wasm modifiers 2025-06-19 13:03:25 +02:00
luisδμ
72f2a409f9 Show warning when duplicated variant prop name and value (#6639)
*  Show warning when duplicated variant prop name and value

* 📎 PR changes
2025-06-19 12:34:28 +02:00
Xavier Julian
62a6f2c2f1 ♻️ Restructure UI files for theme creation modal 2025-06-19 11:59:25 +02:00
Xavier Julian
105e0ba75f ♻️ Create themes folder and themes root file 2025-06-19 10:53:31 +02:00
Belén Albeza
4a9f6ea04e 🐛 Fix panning and scroll when dpr > 1 (render wasm) 2025-06-19 10:42:19 +02:00
luisδμ
e7e39a5521 Avoid duplicated property names adding a number (#6681)
*  Avoid repeated property names appending a number

* 📎 PR changes

* 🐛 Adjust rules for incrementing numbers in prop names
2025-06-19 09:11:41 +02:00
Belén Albeza
70a29c43ec 🔧 Fix race condition in colorpicket gradient tests (#6722) 2025-06-19 08:57:38 +02:00
Andrey Antukh
386c729507 Merge remote-tracking branch 'origin/staging' into develop 2025-06-19 08:49:11 +02:00
Alejandro Alonso
219dca3ab8 Merge pull request #6723 from penpot/elenatorro-11385-fix-text-gradients
🐛 Fix text fill gradients and add visual regression test for text…
2025-06-19 07:03:19 +02:00
Elena Torro
5c120b601c 🐛 Fix text fill gradients and add visual regression test for text styles 2025-06-18 18:02:28 +02:00
Elena Torro
cf8006ce9c 🔧 Add option to skip tutorial/walkthrough when creating profiles for dev purposes 2025-06-18 17:00:46 +02:00
Kelp
71afccbeb5 Adds new font-size icon to the DS
Signed-off-by: Kelp <5446186+NatachaMenjibar@users.noreply.github.com>
2025-06-18 15:55:13 +02:00
Andrey Antukh
bbb9713f97 Merge remote-tracking branch 'origin/staging' into develop 2025-06-18 14:09:49 +02:00
Andrey Antukh
063c6e7771 Merge remote-tracking branch 'origin/staging' into develop 2025-06-18 13:34:47 +02:00
Andrey Antukh
b8b56d5aa4 Merge remote-tracking branch 'origin/staging' into develop 2025-06-18 10:54:17 +02:00
Andrey Antukh
402508a710 Merge remote-tracking branch 'origin/staging' into develop 2025-06-18 10:41:30 +02:00
Andrey Antukh
88ed08916e Merge remote-tracking branch 'origin/staging' into develop 2025-06-18 10:39:23 +02:00
Andrey Antukh
a9a0970001 Merge pull request #6679 from penpot/niwinz-develop-enhacements-4
 Add editors count to get-owned-teams rpc method response
2025-06-18 10:38:48 +02:00
Andrey Antukh
5ea515cc9f Merge pull request #6713 from mdbenito/doc/undo-ns
📚 Document app.main.data.workspace.undo
2025-06-18 10:37:42 +02:00
Miguel de Benito Delgado
c0df527b3d 📚 Document app.main.data.workspace.undo 2025-06-18 09:52:15 +02:00
Alejandro Alonso
6a46110f80 Merge pull request #6672 from penpot/superalex-fix-focus-mode-wasm
🐛 Fix focus mode for wasm render
2025-06-17 16:55:47 +02:00
Alejandro Alonso
1c7aea4b84 🐛 Fix focus mode for wasm render 2025-06-17 16:42:45 +02:00
Andrey Antukh
90116c207f Merge remote-tracking branch 'origin/staging' into develop 2025-06-17 16:23:35 +02:00
Pablo Alba
46fe3a6239 📚 Add comments on convoluted variants code (#6704) 2025-06-17 16:17:56 +02:00
Elena Torró
01311225c7 Merge pull request #6695 from penpot/superalex-fix-allocate-0-bytes-path-attrs
🐛 Fix wasm render path issues
2025-06-17 14:44:15 +02:00
Elena Torró
717f3e1b32 Merge pull request #6703 from penpot/elenatorro-update-render-wasm-docs
📚 Add schemas and links to render-wasm README
2025-06-17 14:24:47 +02:00
Elena Torro
9a44bd0967 📚 Add schemas and links to render-wasm README 2025-06-17 14:06:46 +02:00
Belén Albeza
b92e108205 Are more visual regression tests for wasm (#6702) 2025-06-17 12:39:38 +02:00
Alejandro Alonso
8c6a80829f Merge pull request #6671 from penpot/azazeln28-refactor-minor-perf-issues
♻️ Refactor some minor perf issues
2025-06-17 11:30:43 +02:00
Elena Torró
039a544990 Merge pull request #6675 from penpot/alotor-grid-helpers
 Add grid helpers to wasm
2025-06-17 11:14:18 +02:00
Alejandro Alonso
60dbf02896 Merge pull request #6701 from penpot/elenatorro-fix-custom-font-load
🐛 Fix storing custom fonts
2025-06-17 10:06:02 +02:00
Elena Torro
d248dd09bc 🐛 Fix storing custom fonts 2025-06-17 09:38:17 +02:00
Alejandro Alonso
81d2b9a82e 🐛 Fix group fills propagation when fill is none 2025-06-17 09:17:54 +02:00
Alejandro Alonso
1bb6f2754c 🐛 Fix allocate 0 bytes for path attrs 2025-06-17 08:43:00 +02:00
Andrey Antukh
df84396fea Merge remote-tracking branch 'origin/staging' into develop 2025-06-16 16:51:14 +02:00
mirakernel
a56822ad99 🐛 Avoid crash on empty string in interaction prototype (#6687)
Fixes #6469

Using `uuid/parse` caused a crash when "Relative to" field was set to "auto",
producing an empty string. This change uses `uuid/parse*` instead, which safely
returns nil for invalid or empty inputs, preventing the crash.

Signed-off-by: Dmitriy Mikheev <mirakernel.disroot.org>
Co-authored-by: kira <kira@kira.kira>
2025-06-16 15:18:59 +02:00
Elena Torró
4869373a43 🔧 Add methods to render text as path (#6624)
* 🔧 Refactor text strokes drawing

* 🔧 Add text to path methods for future usage

* 📚 Add text as paths internal documentation
2025-06-16 13:37:29 +02:00
Pablo Alba
2d36a1f3e0 🐛 Fix when retrieving a variant from several with same props, it get the last one 2025-06-16 12:23:40 +02:00
Alejandro Alonso
38941d4811 Merge pull request #6676 from penpot/elenatorro-fix-load-pending-single-attr
🐛 Fix parsing pending callback on setting single shape attr
2025-06-16 11:50:57 +02:00
alonso.torres
0be8a6e0e6 Add grid helpers to wasm 2025-06-16 09:55:35 +02:00
Marina López
3624a14141 Subscription tests (#6669)
*  Subscription tests

*  Subscription tests
2025-06-16 09:31:50 +02:00
Peter Kahoun
141431bb9e Update cs.po - inflection fixes (#6677)
Signed-off-by: Peter Kahoun <kahi.cz@gmail.com>
2025-06-16 09:29:47 +02:00
Elena Torro
f58ee2c89f 🔧 Add visual regression tests for font load 2025-06-11 13:22:23 +02:00
Pablo Alba
925b6c02d6 🎉 Separate the content of the text of the rest of properties on variants 2025-06-11 11:22:43 +02:00
Pablo Alba
9761cba337 ♻️ Restore separate the content of the text of the rest of properties on components updates
This reverts commit b2aaa5f0df.
2025-06-11 11:21:54 +02:00
Andrey Antukh
71f5806e23 Add editors count to get-owned-teams rpc method response 2025-06-11 08:40:58 +02:00
Elena Torro
330bee7839 🐛 Fix parsing pending callback on setting single shape attr 2025-06-10 21:34:41 +02:00
Andrey Fedorov
d44e4e5275 Add zip file format import for tokens 2025-06-10 17:32:06 +02:00
Aitor Moreno
369e134bed ♻️ Refactor some minor perf issues 2025-06-10 16:00:10 +02:00
Elena Torró
f02dd9f8dc Merge pull request #6651 from penpot/superalex-path-fixes
🐛 Path fixes
2025-06-10 12:11:25 +02:00
Alejandro Alonso
e91550cd9d Merge pull request #6646 from penpot/ladybenko-10904-playwright-wasm
🔧 Set up visual regression tests for wasm renderer
2025-06-10 09:24:59 +02:00
Alejandro Alonso
ed76b1b1ee 🎉 Support for webp images (#6665) 2025-06-10 08:40:30 +02:00
Belén Albeza
afdbb5cf2f 📚 Add documentation specific for wasm visual regression tests 2025-06-09 17:46:18 +02:00
Belén Albeza
971b92a75b 🔧 Make mockAsset to accept an array of asset ids too 2025-06-09 17:46:18 +02:00
Belén Albeza
479406b884 🔧 Add initial snapshots 2025-06-09 17:46:18 +02:00
Belén Albeza
1a10b7ebfd 🔧 Wait for first render using a custom event (visual regression tests) 2025-06-09 17:46:18 +02:00
Belén Albeza
59a4b51d2c 🔧 Set up playwright project for render wasm 2025-06-09 17:01:29 +02:00
Alejandro Alonso
78d6166bac 🐛 Fix caps for rounded paths 2025-06-09 14:40:54 +02:00
Alejandro Alonso
8db910baee 🐛 Fix rendering vertical and horizontal paths 2025-06-09 13:04:18 +02:00
Alejandro Alonso
a9702f104d 🐛 Fix shapes without fills contained in a group with fills 2025-06-09 13:04:18 +02:00
351 changed files with 33800 additions and 13049 deletions

View File

@@ -1,5 +1,31 @@
# CHANGELOG
## 2.9.0 (Unreleased)
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Add visual indicator for new comments in the workspace [Taiga #11328](https://tree.taiga.io/project/penpot/issue/11328)
- On components overrides, separate the content of the text from the rest of properties [Taiga #7434](https://tree.taiga.io/project/penpot/us/7434)
- Improve dashboard's sidebar [Taiga #10700](https://tree.taiga.io/project/penpot/us/10700)
- Change "Save color" button to primary button [Taiga #9410](https://tree.taiga.io/project/penpot/issue/9410)
- Support for exif rotated images [GitHub #6767](https://github.com/penpot/penpot/issues/6767)
- Display Blend Mode and Layer Opacity properties in the Inspect tab [Taiga #11283](https://tree.taiga.io/project/penpot/issue/11283)
- Add the option to import tokens in a .zip file. [Taiga #11378](https://tree.taiga.io/project/penpot/us/11378)
- New typography token type - font size token [Taiga #10938](https://tree.taiga.io/project/penpot/us/10938)
### :bug: Bugs fixed
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)
- Fix text-decoration line-through that displays a wrong property value [Taiga #11145](https://tree.taiga.io/project/penpot/issue/11145)
- Fix display error message on register form [Taiga #11444](https://tree.taiga.io/project/penpot/issue/11444)
- Fix toggle focus mode did not restore viewport and selection upon exit [GitHub #6280](https://github.com/penpot/penpot/issues/6820)
- Fix problem when creating a layout from an existing layout [Taiga #11554](https://tree.taiga.io/project/penpot/issue/11554)
- Fix title button from Title Case to Capitalize [Taiga #11476](https://tree.taiga.io/project/penpot/issue/11476)
## 2.8.0
### :rocket: Epics and highlights

View File

@@ -71,19 +71,27 @@ def run_cmd(params):
print("EXC:", str(cause))
sys.exit(-2)
def create_profile(fullname, email, password):
def create_profile(fullname, email, password, skip_tutorial=False, skip_walkthrough=False):
props = {}
if skip_tutorial:
props["viewed-tutorial?"] = True
if skip_walkthrough:
props["viewed-walkthrough?"] = True
params = {
"cmd": "create-profile",
"params": {
"fullname": fullname,
"email": email,
"password": password
"password": password,
**props
}
}
res = run_cmd(params)
print(f"Created: {res['email']} / {res['id']}")
def update_profile(email, fullname, password, is_active):
params = {
"cmd": "update-profile",
@@ -170,6 +178,8 @@ parser.add_argument("-n", "--fullname", help="fullname", action="store")
parser.add_argument("-e", "--email", help="email", action="store")
parser.add_argument("-p", "--password", help="password", action="store")
parser.add_argument("-c", "--connect", help="connect to PREPL", action="store", default="tcp://localhost:6063")
parser.add_argument("--skip-tutorial", help="mark tutorial as viewed", action="store_true")
parser.add_argument("--skip-walkthrough", help="mark walkthrough as viewed", action="store_true")
args = parser.parse_args()

View File

@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.schema.openapi :as-alias oapi]
@@ -21,6 +22,7 @@
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
[clojure.java.shell :as sh]
[clojure.string]
[clojure.xml :as xml]
[cuerdas.core :as str]
[datoteka.fs :as fs]
@@ -215,6 +217,23 @@
{:width (int width)
:height (int height)})))]))
(defn- get-dimensions-with-orientation [^String path]
;; Image magick doesn't give info about exif rotation so we use the identify command
;; If we are processing an animated gif we use the first frame with -scene 0
(let [dim-result (sh/sh "identify" "-format" "%w %h\n" path)
orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)]
(if (and (= 0 (:exit dim-result))
(= 0 (:exit orient-result)))
(let [[w h] (-> (:out dim-result)
str/trim
(clojure.string/split #"\s+")
(->> (mapv #(Integer/parseInt %))))
orientation (-> orient-result :out str/trim)]
(case orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
{:width w :height h})) ; Normal or unknown orientation
nil)))
(defmethod process :info
[{:keys [input] :as params}]
(let [{:keys [path mtype] :as input} (check-input input)]
@@ -234,13 +253,17 @@
:code :media-type-mismatch
:hint (str "Seems like you are uploading a file whose content does not match the extension."
"Expected: " mtype ". Got: " mtype')))
;; For an animated GIF, getImageWidth/Height returns the delta size of one frame (if no frame given
;; it returns size of the last one), whereas getPageWidth/Height always return the full size of
;; any frame.
(assoc input
:width (.getPageWidth instance)
:height (.getPageHeight instance)
:ts (dt/now))))))
(let [{:keys [width height]}
(or (get-dimensions-with-orientation (str path))
(do
(l/warn "Failed to read image dimensions with orientation; falling back to im4java"
{:path path})
{:width (.getPageWidth instance)
:height (.getPageHeight instance)}))]
(assoc input
:width width
:height height
:ts (dt/now)))))))
(defmethod process-error org.im4java.core.InfoException
[error]

View File

@@ -140,7 +140,7 @@
WHEN 'professional' THEN 'active'
ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete')
END,
'~:seats', p.props->'~:quantity'
'~:seats', p.props->'~:subscription'->'~:quantity'
) AS subscription
FROM team_profile_rel AS tp
JOIN team AS t ON (t.id = tp.team_id)
@@ -193,7 +193,8 @@
(def ^:private sql:get-owned-teams
"SELECT t.id, t.name,
(SELECT count(*) FROM team_profile_rel WHERE team_id=t.id) AS total_members
(SELECT count(*) FROM team_profile_rel WHERE team_id=t.id) AS total_members,
(SELECT count(*) FROM team_profile_rel WHERE team_id=t.id AND can_edit=true) AS total_editors
FROM team AS t
JOIN team_profile_rel AS tpr ON (tpr.team_id = t.id)
WHERE t.is_default IS false

View File

@@ -460,11 +460,14 @@
::rpc/profile-id (:id profile1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(let [[item1 :as result] (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:id team1) (-> result first :id)))
(t/is (not= (:default-team-id profile1) (-> result first :id))))))
(t/is (= (:id team1) (:id item1)))
(t/is (= 1 (:total-members item1)))
(t/is (= 1 (:total-editors item1)))
(t/is (not= (:default-team-id profile1) (:id item1))))))
(t/deftest team-deletion-1

View File

@@ -241,7 +241,7 @@
[:shapes ::sm/any]
[:index {:optional true} [:maybe :int]]
[:after-shape {:optional true} ::sm/any]
[:component-swap {:optional true} :boolean]]]
[:allow-altering-copies {:optional true} :boolean]]]
[:reorder-children
[:map {:title "ReorderChildrenChange"}
@@ -418,7 +418,7 @@
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
[:token-set [:maybe ctob/schema:token-set-attrs]]]]
[:token-set [:maybe [:fn ctob/token-set?]]]]]
[:set-token
[:map {:title "SetTokenChange"}
@@ -761,7 +761,7 @@
(d/update-in-when data [:components component-id :objects] reg-objects))))
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape component-swap syncing]}]
[data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape allow-altering-copies syncing]}]
(letfn [(calculate-invalid-targets [objects shape-id]
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
(->> (get-in objects [shape-id :shapes])
@@ -776,7 +776,7 @@
(and shape
(not (invalid-targets parent-id))
(not (cfh/components-nesting-loop? objects shape-id parent-id))
(or component-swap ;; On a component swap it's allowed to change the structure of a copy
(or allow-altering-copies ;; In some cases (like a component swap) it's allowed to change the structure of a copy
syncing ;; If we are syncing the changes of a main component, it's allowed to change the structure of a copy
(and
(not (ctk/in-component-copy? (get objects (:parent-id shape)))) ;; We don't want to change the structure of component copies
@@ -1027,11 +1027,10 @@
(ctob/delete-set lib' set-name))
(not (ctob/get-set lib' set-name))
(ctob/add-set lib' (ctob/make-token-set token-set))
(ctob/add-set lib' token-set)
:else
(ctob/update-set lib' set-name (fn [prev-token-set]
(ctob/make-token-set (merge prev-token-set token-set)))))))))
(ctob/update-set lib' set-name (fn [_] token-set)))))))
(defmethod process-change :set-token-theme
[data {:keys [group theme-name theme]}]

View File

@@ -464,8 +464,8 @@
(some? index)
(assoc :index index)
(:component-swap options)
(assoc :component-swap true)
(:allow-altering-copies options)
(assoc :allow-altering-copies true)
(:ignore-touched options)
(assoc :ignore-touched true))
@@ -473,12 +473,14 @@
(fn [undo-changes shape]
(let [prev-sibling (cfh/get-prev-sibling objects (:id shape))]
(conj undo-changes
{:type :mov-objects
:page-id (::page-id (meta changes))
:parent-id (:parent-id shape)
:shapes [(:id shape)]
:after-shape prev-sibling
:index 0}))) ; index is used in case there is no after-shape (moving bottom shapes)
(cond-> {:type :mov-objects
:page-id (::page-id (meta changes))
:parent-id (:parent-id shape)
:shapes [(:id shape)]
:after-shape prev-sibling
:index 0} ; index is used in case there is no after-shape (moving bottom shapes)
(:allow-altering-copies options)
(assoc :allow-altering-copies true)))))
restore-touched-change
{:type :mod-obj
@@ -916,7 +918,7 @@
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name name
:token-set (assoc prev-token-set :name new-name)
:token-set (ctob/rename prev-token-set new-name)
:group? false})
(update :undo-changes conj {:type :set-token-set
:set-name new-name
@@ -937,11 +939,11 @@
:group? group?})
(update :undo-changes conj (if prev-token-set
{:type :set-token-set
:set-name (or
;; Undo of edit
(:name token-set)
;; Undo of delete
set-name)
:set-name (if token-set
;; Undo of edit
(ctob/get-name token-set)
;; Undo of delete
set-name)
:token-set prev-token-set
:group? group?}
;; Undo of create

View File

@@ -152,12 +152,22 @@
(dm/get-prop shape :type))))
(defn get-children-ids
[objects id]
(letfn [(get-children-ids-rec [id processed]
(when (not (contains? processed id))
(when-let [shapes (-> (get objects id) :shapes (some-> vec))]
(into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))]
(get-children-ids-rec id #{})))
"Returns the ids of all the descendants of the shape identified
by the id. Optionally, you can pass an ignore function to indicate
when to ignore a descendant (and all its descendants)"
([objects id]
(get-children-ids objects id {}))
([objects id {:keys [ignore-children-fn]
;;ignore-children-fn should receive a shape and return a boolean
:or {ignore-children-fn (constantly false)}}]
(letfn [(get-children-ids-rec [id processed]
(when-not (contains? processed id)
(when-let [shapes (as-> (get objects id) $
(:shapes $)
(remove ignore-children-fn $)
(some-> $ vec))]
(into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))]
(get-children-ids-rec id #{}))))
(defn get-children-ids-with-self
[objects id]

View File

@@ -32,6 +32,7 @@
[app.common.types.shape :as cts]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.shadow :as ctss]
[app.common.types.text :as cttx]
[app.common.uuid :as uuid]
[clojure.set :as set]
[cuerdas.core :as str]))
@@ -1527,6 +1528,31 @@
colors
colors))))
(defmethod migrate-data "0009-add-partial-text-touched-flags"
[data _]
(letfn [(update-object [page object]
(if (and (cfh/text-shape? object)
(ctk/in-component-copy? object))
(let [file {:id (:id data) :data data}
libs (when (:libs data)
(deref (:libs data)))
ref-shape (ctf/find-ref-shape file page libs object
{:include-deleted? true :with-context? true})
partial-touched (when ref-shape
(cttx/get-diff-type (:content object) (:content ref-shape)))]
(if (seq partial-touched)
(update object :touched (fn [touched]
(reduce #(ctk/set-touched-group %1 %2)
touched
partial-touched)))
object))
object))
(update-page [page]
(d/update-when page :objects d/update-vals (partial update-object page)))]
(update data :pages-index d/update-vals update-page)))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@@ -1591,4 +1617,5 @@
"0006-fix-old-texts-fills"
"0007-clear-invalid-strokes-and-fills-v2"
"0008-fix-library-colors-v4"
"0009-clean-library-colors"]))
"0009-clean-library-colors"
"0009-add-partial-text-touched-flags"]))

View File

@@ -96,7 +96,7 @@
(log/dbg :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/change-parent (:parent-id args) [shape] nil {:component-swap true})))
(pcb/change-parent (:parent-id args) [shape] nil {:allow-altering-copies true})))
(defmethod repair-error :frame-not-found
[_ {:keys [shape page-id] :as error} file-data _]
@@ -387,7 +387,7 @@
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape)
(pcb/change-parent uuid/zero [shape] nil {:component-swap true}))))
(pcb/change-parent uuid/zero [shape] nil {:allow-altering-copies true}))))
(defmethod repair-error :root-copy-not-allowed
[_ {:keys [shape page-id] :as error} file-data _]
@@ -602,11 +602,6 @@
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(defmethod repair-error :variant-no-properties
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(defmethod repair-error :variant-bad-variant-name
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))

View File

@@ -68,7 +68,6 @@
:variant-bad-name
:variant-bad-variant-name
:variant-component-bad-name
:variant-no-properties
:variant-component-bad-id})
(def ^:private schema:error
@@ -589,11 +588,7 @@
(when-not (ctk/is-variant? main-component)
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))
(when (< (count (:variant-properties component)) 1)
(report-error :variant-no-properties
(str/ffmt "Component variant % should have properties" (:id main-component))
main-component file nil))))
main-component file component-page))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."

View File

@@ -117,6 +117,7 @@
;; Only for developtment.
:tiered-file-data-storage
:token-units
:token-typography-types
:transit-readable-response
:user-feedback
;; TODO: remove this flag.
@@ -149,7 +150,8 @@
:enable-onboarding
:enable-dashboard-templates-section
:enable-google-fonts-provider
:enable-component-thumbnails])
:enable-component-thumbnails
:enable-render-wasm-dpr])
(defn parse
[& flags]

View File

@@ -467,15 +467,15 @@
row-tracks (set-flex-multi-span parent row-tracks children-map shape-cells bounds objects :row)
;; Once auto sizes have been calculated we get calculate the `fr` unit with the remainining size and adjust the size
free-column-space (max 0 (- bound-width (+ column-total-size-nofr column-total-gap)))
free-row-space (max 0 (- bound-height (+ row-total-size-nofr row-total-gap)))
fr-column-space (max 0 (- bound-width (+ column-total-size-nofr column-total-gap)))
fr-row-space (max 0 (- bound-height (+ row-total-size-nofr row-total-gap)))
;; Get the minimum values for fr's
min-column-fr (min-fr-value column-tracks)
min-row-fr (min-fr-value row-tracks)
column-fr (if auto-width? min-column-fr (mth/finite (/ free-column-space column-frs) 0))
row-fr (if auto-height? min-row-fr (mth/finite (/ free-row-space row-frs) 0))
column-fr (if auto-width? min-column-fr (mth/finite (/ fr-column-space column-frs) 0))
row-fr (if auto-height? min-row-fr (mth/finite (/ fr-row-space row-frs) 0))
column-tracks (set-fr-value column-tracks column-fr auto-width?)
row-tracks (set-fr-value row-tracks row-fr auto-height?)
@@ -484,13 +484,13 @@
column-total-size (tracks-total-size column-tracks)
row-total-size (tracks-total-size row-tracks)
free-column-space (max 0 (if auto-width? 0 (- bound-width (+ column-total-size column-total-gap))))
free-row-space (max 0 (if auto-height? 0 (- bound-height (+ row-total-size row-total-gap))))
auto-column-space (max 0 (if auto-width? 0 (- bound-width (+ column-total-size column-total-gap))))
auto-row-space (max 0 (if auto-height? 0 (- bound-height (+ row-total-size row-total-gap))))
column-autos (tracks-total-autos column-tracks)
row-autos (tracks-total-autos row-tracks)
column-add-auto (/ free-column-space column-autos)
row-add-auto (/ free-row-space row-autos)
column-add-auto (/ auto-column-space column-autos)
row-add-auto (/ auto-row-space row-autos)
column-tracks (cond-> column-tracks
(= :stretch (:layout-justify-content parent))

View File

@@ -29,6 +29,7 @@
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as cttx]
[app.common.types.token :as cto]
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
@@ -1664,28 +1665,33 @@
:shapes all-parents})]))))
(defn- text-partial-change-value
[touched-content untouched-content touched]
(cond
(touched :text-content-structure-same-attrs)
(if (touched :text-content-attribute)
;; Both structure and attrs has been touched, keep the
;; touched-content
touched-content
;; Keep the touched-content structure and texts, update
;; its attrs to make them like the untouched-content
(cttx/copy-attrs-keys touched-content (cttx/get-first-paragraph-text-attrs untouched-content)))
(touched :text-content-text)
;; Keep the texts touched in touched-content, so copy the
;; texts from touched-content into untouched-content
(cttx/copy-text-keys touched-content untouched-content)
(touched :text-content-attribute)
;; Keep the attrs touched in touched-content, so copy the
;; texts from untouched-content into touched-content
(cttx/copy-text-keys untouched-content touched-content)))
(defn- add-update-attr-operations
[attr dest-shape origin-shape roperations uoperations touched]
(let [orig-value (get origin-shape attr)
dest-value (get dest-shape attr)
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data?
(and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= orig-value dest-value)
(touched :geometry-group))
val (cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data? nil
:else orig-value)
roperation {:type :set
[attr dest-shape roperations uoperations attr-val]
(let [roperation {:type :set
:attr attr
:val val
:val attr-val
:ignore-touched true}
uoperation {:type :set
:attr attr
@@ -1694,6 +1700,33 @@
[(conj roperations roperation)
(conj uoperations uoperation)]))
(defn- is-text-partial-change?
"Check if the attr update is a text partial change"
[untouched-shape touched-shape]
(let [touched (get touched-shape :touched #{})
partial-text-keys [:text-content-attribute :text-content-text]
active-keys (filter touched partial-text-keys)
untouched-content (:content untouched-shape)
untouched-attrs (cttx/get-first-paragraph-text-attrs untouched-content)
eq-untouched-attrs? (cttx/equal-attrs? untouched-content untouched-attrs)]
(and
(or
;; One and only one of the keys is pressent
(= 1 (count active-keys))
(and
(not (touched :text-content-attribute))
(touched :text-content-structure-same-attrs)))
(or
;; Both has the same structure
(cttx/equal-structure? untouched-content (:content touched-shape))
;; The origin and destiny have different structures, but each have the same attrs
;; for all the items on its content tree
(and
eq-untouched-attrs?
(touched :text-content-structure-same-attrs))))))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1737,58 +1770,166 @@
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(let [attr-group (get ctk/sync-attrs attr)
skip-operations? (or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
omit-touched?))
;; position-data is a special case because can be affected by
;; :geometry-group and :content-group so, if the position-data
;; changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data? (and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= (:position-data origin-shape) (:position-data dest-shape))
(touched :geometry-group))
;; On texts, when we want to omit the touched attrs, both text (the actual letters)
;; and attrs (bold, font, etc) are in the same attr :content.
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-partial-change?
(when (and
omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched attr-group))
(is-text-partial-change? origin-shape dest-shape))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
omit-touched?
;; When it is a text-partial-change, we should generate operations
;; even when omit-touched? is true, but updating only the text or
;; the attributes, omiting the other part
(not text-partial-change?)))
attr-val (when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
text-partial-change?
(text-partial-change-value (:content dest-shape)
(:content origin-shape)
touched)
:else
(get origin-shape attr)))
;; On a text-partial-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
(if text-partial-change?
(add-update-attr-operations :position-data dest-shape roperations uoperations nil)
[roperations uoperations])
[roperations' uoperations']
(if skip-operations?
[roperations uoperations]
(add-update-attr-operations attr dest-shape origin-shape roperations uoperations touched))]
(add-update-attr-operations attr dest-shape roperations uoperations attr-val))]
(recur (next attrs)
roperations'
uoperations')))))))
(defn update-attrs-on-switch
"Copy attributes that have changed in the origin shape to the dest shape. Used on variants switch"
[changes dest-shape origin-shape dest-root origin-root origin-ref-shape container]
"Copy attributes that have changed in the shape previous to the switch
to the current shape (post switch). Used only on variants switch"
;; NOTE: This function have similitudes but is very different to
;; update-attrs:
;; In components (update-attrs), the source shape is "clean", and the destination
;; shape may have touched elements that shouldn't be overwritten.
;; In variants (update-attrs-on-switch), the destination shape is "clean",
;; and it's the source shape that may have touched elements, and we only want
;; to copy those touched elements.
[changes current-shape previous-shape current-root prev-root origin-ref-shape container]
(let [;; We need to sync only the position relative to the origin of the component.
;; (see update-attrs for a full explanation)
origin-shape (reposition-shape origin-shape origin-root dest-root)
touched (get dest-shape :touched #{})
touched-origin (get origin-shape :touched #{})]
previous-shape (reposition-shape previous-shape prev-root current-root)
touched (get previous-shape :touched #{})]
(loop [attrs updatable-attrs
roperations [{:type :set-touched :touched (:touched origin-shape)}]
uoperations (list {:type :set-touched :touched (:touched dest-shape)})]
roperations [{:type :set-touched :touched (:touched previous-shape)}]
uoperations (list {:type :set-touched :touched (:touched current-shape)})]
(if-let [attr (first attrs)]
(let [attr-group (get ctk/sync-attrs attr)
skip-operations?
(or
;; If the attribute is not valid for the destiny, don't copy it
(not (cts/is-allowed-attr? attr (:type current-shape)))
;; If the values are already equal, don't copy them
(= (get previous-shape attr) (get current-shape attr))
;; If both variants (origin and destiny) don't have the same value
;; for that attribute, don't copy it.
;; Exceptions: :points :selrect and :content can be different
;;
;; Sample:
;; 1. We have a variant with C1 (bg red) and C2 (bg blue).
;; 2. We make a copy of C1 called Copy.
;; 3. We set Copys bg to green (so it it has an override on the bg).
;; 4. We switch Copy to use C2 as base.
;; 5. The bg of Copy now is blue (we ignore the override)
(and
(not (contains? #{:points :selrect :content} attr))
(not= (get origin-ref-shape attr) (get current-shape attr)))
;; The :content attr cant't be copied to elements of different type
(and (= attr :content) (not= (:type previous-shape) (:type current-shape)))
;; If the attr is not touched, don't copy it
(not (touched attr-group)))
;; On texts, both text (the actual letters)
;; and attrs (bold, font, etc) are in the same attr :content.
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-partial-change?
(when (and
(not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched attr-group))
(is-text-partial-change? current-shape previous-shape))
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data? (and
(not skip-operations?)
(cfh/text-shape? previous-shape)
(= attr :position-data)
(not= (:position-data previous-shape) (:position-data current-shape))
(touched :geometry-group))
attr-val (when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
text-partial-change?
(text-partial-change-value (:content previous-shape)
(:content current-shape)
touched)
:else
(get previous-shape attr)))
[roperations' uoperations']
(if (or
;; If the attribute is not valid for the destiny, don't copy it
(not (cts/is-allowed-attr? attr (:type dest-shape)))
;; If the values are already equal, don't copy it
(= (get origin-shape attr) (get dest-shape attr))
;; If the referenced shape on the original component doesn't have the same value, don't copy it
;; Exceptions: :points :selrect and :content can be different
(and
(not (contains? #{:points :selrect :content} attr))
(not= (get origin-ref-shape attr) (get dest-shape attr)))
;; The :content attr cant't be copied to elements of different type
(and (= attr :content) (not= (:type origin-shape) (:type dest-shape)))
;; If the attr is not touched in the origin shape, don't copy it
(not (touched-origin attr-group)))
(if skip-operations?
[roperations uoperations]
(add-update-attr-operations attr dest-shape origin-shape roperations uoperations touched))]
(add-update-attr-operations attr current-shape roperations uoperations attr-val))]
(recur (next attrs)
roperations'
uoperations'))
(cond-> changes
(> (count roperations) 1)
(add-update-attr-changes dest-shape container roperations uoperations)
(add-update-attr-changes current-shape container roperations uoperations)
:always
(generate-update-tokens container dest-shape origin-shape touched false))))))
(generate-update-tokens container current-shape previous-shape touched false))))))
(defn- propagate-attrs
"Helper that puts the origin attributes (attrs) into dest but only if
@@ -2115,7 +2256,7 @@
(pcb/update-shapes [(:id new-shape)] #(d/patch-object % keep-props-values))
;; We need to set the same index as the original shape
(pcb/change-parent (:parent-id shape) [new-shape] index {:component-swap true
(pcb/change-parent (:parent-id shape) [new-shape] index {:allow-altering-copies true
:ignore-touched true})
(change-touched new-shape
shape
@@ -2123,10 +2264,21 @@
{}))]))
(defn generate-component-swap
[changes objects shape file page libraries id-new-component index target-cell keep-props-values]
(let [[all-parents changes]
[changes objects shape file page libraries id-new-component
index target-cell keep-props-values ignore-swapped?]
(let [;; When we keep the touched properties, we can't delete the
;; swapped children (we will keep them too)
ignore-swapped-fn
(if ignore-swapped?
#(-> (get objects %)
(ctk/get-swap-slot))
(constantly false))
[all-parents changes]
(-> changes
(cls/generate-delete-shapes file page objects (d/ordered-set (:id shape)) {:component-swap true}))
(cls/generate-delete-shapes
file page objects (d/ordered-set (:id shape))
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
[new-shape changes]
(-> changes
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]

View File

@@ -99,7 +99,14 @@
(pcb/with-library-data file))
ids
options))
([changes ids {:keys [ignore-touched component-swap]}]
([changes ids {:keys [ignore-touched
allow-altering-copies
;; We will delete the shapes and its descendants.
;; ignore-children-fn is used to ignore some descendants
;; on the deletion process. It should receive a shape and
;; return a boolean
ignore-children-fn]
:or {ignore-children-fn (constantly false)}}]
(let [objects (pcb/get-objects changes)
data (pcb/get-library-data changes)
page-id (pcb/get-page-id changes)
@@ -112,11 +119,12 @@
;; Look for shapes that are inside a component copy, but are
;; not the root. In this case, they must not be deleted,
;; but hidden (to be able to recover them more easily).
;; Unless we are doing a component swap, in which case we want
;; If we want to specifically allow altering the copies, this is
;; a special case, like a component swap, in which case we want
;; to delete the old shape
(let [shape (get objects shape-id)]
(and (ctn/has-any-copy-parent? objects shape)
(not component-swap))))
(not allow-altering-copies))))
[ids-to-delete ids-to-hide]
(loop [ids-seq (seq ids)
@@ -177,10 +185,15 @@
(d/ordered-set)
(concat ids-to-delete ids-to-hide))
all-children
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
;; Descendants of deleted shapes must be also deleted,
;; except the ignored ones by the function ignore-children-fn
descendants-to-delete
(->> ids-to-delete
(reduce (fn [res id]
(into res (cfh/get-children-ids objects id)))
(into res (cfh/get-children-ids
objects
id
{:ignore-children-fn ignore-children-fn})))
[])
(reverse)
(into (d/ordered-set)))
@@ -200,9 +213,10 @@
empty-parents
;; Any parent whose children are all deleted, must be deleted too.
;; Unless we are during a component swap: in this case we are replacing a shape by
;; If we want to specifically allow altering the copies, this is a special case,
;; for example during a component swap. in this case we are replacing a shape by
;; other one, so must not delete empty parents.
(if-not component-swap
(if-not allow-altering-copies
(into (d/ordered-set) (find-all-empty-parents #{}))
#{})
@@ -214,7 +228,7 @@
(conj components (:component-id shape))
components)))
[]
(into ids-to-delete all-children))
(into ids-to-delete descendants-to-delete))
ids-set (set ids-to-delete)
@@ -241,7 +255,7 @@
changes (-> changes
(generate-update-shape-flags ids-to-hide objects {:hidden true})
(pcb/remove-objects all-children {:ignore-touched true})
(pcb/remove-objects descendants-to-delete {:ignore-touched true})
(pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched})
(pcb/remove-objects empty-parents)
(pcb/resize-parents all-parents)

View File

@@ -41,7 +41,7 @@
[group-path tokens-lib tokens-lib-theme]
(let [deactivate? (contains? #{:all :partial} (ctob/sets-at-path-all-active? tokens-lib group-path))
sets-names (->> (ctob/get-sets-at-path tokens-lib group-path)
(map :name)
(map ctob/get-name)
(into #{}))]
(if deactivate?
(ctob/disable-sets tokens-lib-theme sets-names)

View File

@@ -18,7 +18,12 @@
[changes variant-id pos new-name]
(let [data (pcb/get-library-data changes)
objects (pcb/get-objects changes)
related-components (cfv/find-variant-components data objects variant-id)]
related-components (cfv/find-variant-components data objects variant-id)
props (-> related-components last :variant-properties)
prop-names (mapv :name props)
prop-names (concat (subvec prop-names 0 pos) (subvec prop-names (inc pos)))
new-name (ctv/update-number-in-repeated-item prop-names new-name)]
(reduce (fn [changes component]
(pcb/update-component
changes (:id component)
@@ -81,6 +86,9 @@
next-prop-num (ctv/next-property-number props)
property-name (or property-name (str ctv/property-prefix next-prop-num))
prop-names (mapv :name props)
property-name (ctv/update-number-in-repeated-item prop-names property-name)
[_ changes]
(reduce (fn [[num changes] component]
(let [main-id (:main-instance-id component)

View File

@@ -1,13 +1,17 @@
(ns app.common.logic.variants
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.variant :as ctv]))
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]))
(defn generate-add-new-variant
[changes shape variant-id new-component-id new-shape-id prop-num]
@@ -62,29 +66,142 @@
shapes))))
(defn- keep-swapped-item
"As part of the keep-touched process on a switch, given a child on the original
copy that was swapped (orig-swapped-child), and its related shape on the new copy
(related-shape-in-new), move the orig-swapped-child into the parent of
related-shape-in-new, fix its swap-slot if needed, and then delete
related-shape-in-new"
[changes related-shape-in-new orig-swapped-child ldata page swap-ref-id]
(let [;; Before to the swap, temporary move the previous
;; shape to the root panel to avoid problems when
;; the previous parent is deleted.
before-changes (-> (pcb/empty-changes)
(pcb/with-page page)
(pcb/with-objects (:objects page))
(pcb/change-parent uuid/zero [orig-swapped-child] 0 {:allow-altering-copies true}))
objects (pcb/get-objects changes)
prev-swap-slot (ctk/get-swap-slot orig-swapped-child)
current-parent (get objects (:parent-id related-shape-in-new))
pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))]
(-> (pcb/concat-changes before-changes changes)
;; Move the previous shape to the new parent
(pcb/change-parent (:parent-id related-shape-in-new) [orig-swapped-child] pos {:allow-altering-copies true})
;; We need to update the swap slot only when it pointed
;; to the swap-ref-id. Oterwise this is a swapped item
;; inside a nested copy, so we need to keep it.
(cond->
(= prev-swap-slot swap-ref-id)
(pcb/update-shapes
[(:id orig-swapped-child)]
#(ctk/set-swap-slot % (:shape-ref related-shape-in-new))))
;; Delete new non-swapped item
(cls/generate-delete-shapes ldata page objects (d/ordered-set (:id related-shape-in-new)) {:allow-altering-copies true})
second)))
(defn- child-of-swapped?
"Check if any ancestor of a shape (between base-parent-id and shape) was swapped"
[shape objects base-parent-id]
(let [ancestors (->> (ctn/get-parent-heads objects shape)
;; Ignore ancestors ahead of base-parent
(drop-while #(not= base-parent-id (:id %)))
seq)
num-ancestors (count ancestors)
;; Ignore first and last (base-parent and shape)
ancestors (when (and ancestors (<= 3 num-ancestors))
(subvec (vec ancestors) 1 (dec num-ancestors)))]
(some ctk/get-swap-slot ancestors)))
(defn generate-keep-touched
[changes new-shape original-shape original-shapes page libraries]
"This is used as part of the switch process, when you switch from
an original-shape to a new-shape. It generate changes to
copy the touched attributes on the shapes children of the original-shape
into the related children of the new-shape.
This relation is tricky. The shapes are related if:
* On the main components, both have the same name (the name on the copies are ignored)
* Both has the same type of ancestors, on the same order (see generate-path for the
translation of the types)"
[changes new-shape original-shape original-shapes page libraries ldata]
(let [objects (pcb/get-objects changes)
orig-objects (into {} (map (juxt :id identity) original-shapes))
orig-shapes-w-path (add-unique-path
(reverse original-shapes)
orig-objects
(:id original-shape))
container (ctn/make-container page :page)
page-objects (:objects page)
;; Get the touched children of the original-shape
;; Ignore children of swapped items, because
;; they will be moved without change when
;; managing their swapped ancestor
orig-touched (->> (filter (comp seq :touched) original-shapes)
(remove
#(child-of-swapped? %
page-objects
(:id original-shape))))
;; Adds a :shape-path attribute to the children of the new-shape,
;; that contains the type of its ancestors and its name
new-shapes-w-path (add-unique-path
(reverse (cfh/get-children-with-self objects (:id new-shape)))
objects
(:id new-shape))
new-shapes-map (into {} (map (juxt :shape-path identity) new-shapes-w-path))
orig-touched (filter (comp seq :touched) orig-shapes-w-path)
;; Creates a map to quickly find a child of the new-shape by its shape-path
new-shapes-map (into {} (map (juxt :shape-path identity)) new-shapes-w-path)
container (ctn/make-container page :page)]
;; The original-shape is in a copy. For the relation rules, we need the referenced
;; shape on the main component
orig-ref-shape (ctf/find-ref-shape nil container libraries original-shape)
orig-ref-objects (-> (ctf/get-component-container-from-head orig-ref-shape libraries)
:objects)
;; Adds a :shape-path attribute to the children of the orig-ref-shape,
;; that contains the type of its ancestors and its name
o-ref-shapes-wp (add-unique-path
(reverse (cfh/get-children-with-self orig-ref-objects (:id orig-ref-shape)))
orig-ref-objects
(:id orig-ref-shape))
;; Creates a map to quickly find a child of the orig-ref-shape by its shape-path
o-ref-shapes-p-map (into {} (map (juxt :id :shape-path)) o-ref-shapes-wp)]
;; Process each touched children of the original-shape
(reduce
(fn [changes touched-shape]
(let [related-shape (get new-shapes-map (:shape-path touched-shape))
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
(if related-shape
(cll/update-attrs-on-switch
changes related-shape touched-shape new-shape original-shape orig-ref-shape container)
(fn [changes orig-child-touched]
(let [;; If the orig-child-touched was swapped, get its swap-slot
swap-slot (ctk/get-swap-slot orig-child-touched)
;; orig-child-touched is in a copy. Get the referenced shape on the main component
;; If there is a swap slot, we will get the referenced shape in another way
orig-ref-shape (when-not swap-slot
;; TODO Maybe just get it from o-ref-shapes-wp
(ctf/find-ref-shape nil container libraries orig-child-touched))
orig-ref-id (if swap-slot
;; If there is a swap slot, find the referenced shape id
(ctf/find-ref-id-for-swapped orig-child-touched container libraries)
;; If there is not a swap slot, get the id from the orig-ref-shape
(:id orig-ref-shape))
;; Get the shape path of the referenced main
shape-path (get o-ref-shapes-p-map orig-ref-id)
;; Get its related shape in the children of new-shape: the one that
;; has the same shape-path
related-shape-in-new (get new-shapes-map shape-path)]
;; If there is a related shape, keep its data
(if related-shape-in-new
(if swap-slot
;; If the orig-child-touched was swapped, keep it
(keep-swapped-item changes related-shape-in-new orig-child-touched
ldata page orig-ref-id)
;; If the orig-child-touched wasn't swapped, copy
;; the touched attributes into it
(cll/update-attrs-on-switch
changes related-shape-in-new orig-child-touched
new-shape original-shape orig-ref-shape container))
changes)))
changes
orig-touched)))

View File

@@ -156,7 +156,7 @@
[new_shape _ changes]
(-> (pcb/empty-changes nil (:id page))
(cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values))
(cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values false))
file' (thf/apply-changes file changes)]

View File

@@ -291,7 +291,8 @@
:id)
0
nil
{})
{}
false)
file' (thf/apply-changes file changes)]
(if propagate-fn

View File

@@ -18,6 +18,7 @@
[app.common.types.plugins :as ctpg]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as cttx]
[app.common.types.token :as ctt]
[app.common.uuid :as uuid]
[clojure.set :as set]))
@@ -569,13 +570,16 @@
(not equal?)
(not (and ignore-geometry is-geometry?)))
content-diff-type (when (and (= (:type shape) :text) (= attr :content))
(cttx/get-diff-type (:content shape) val))
token-groups (if (= attr :applied-tokens)
(get-token-groups shape val)
#{})
groups (cond-> token-groups
(and group (not equal?))
(set/union #{group}))]
(set/union #{group} content-diff-type))]
(cond-> shape
;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to.

View File

@@ -242,6 +242,13 @@
(cfh/make-container component-page :page))
(cfh/make-container component :component)))
(defn get-component-container-from-head
[instance-head libraries & {:keys [include-deleted?] :or {include-deleted? true}}]
(let [library-data (-> (get-component-library libraries instance-head)
:data)
component (ctkl/get-component library-data (:component-id instance-head) include-deleted?)]
(get-component-container library-data component)))
(defn get-component-root
"Retrieve the root shape of the component."
[file-data component]
@@ -390,6 +397,47 @@
(or (= slot-main slot-inst)
(= (:id shape-main) slot-inst)))))
(defn- find-next-related-swap-shape-id
"Go up from the chain of references shapes that will eventually lead to the shape
with swap-slot-id as id. Returns the next shape on the chain"
[parent swap-slot-id libraries]
(let [container (get-component-container-from-head parent libraries)
objects (:objects container)
children (cfh/get-children objects (:id parent))
original-shape-id (->> children
(filter #(= swap-slot-id (:id %)))
first
:id)]
(if original-shape-id
;; Return the children which id is the swap-slot-id
original-shape-id
;; No children with swap-slot-id as id, go up
(let [referenced-shape (find-ref-shape nil container libraries parent)
;; Recursive call that will get the id of the next shape on
;; the chain that ends on a shape with swap-slot-id as id
next-shape-id (when referenced-shape
(find-next-related-swap-shape-id referenced-shape swap-slot-id libraries))]
;; Return the children which shape-ref points to the next-shape-id
(->> children
(filter #(= next-shape-id (:shape-ref %)))
first
:id)))))
(defn find-ref-id-for-swapped
"When a shape has been swapped, find the original ref-id that the shape had
before the swap"
[shape container libraries]
(let [swap-slot (ctk/get-swap-slot shape)
objects (:objects container)
parent (get objects (:parent-id shape))
parent-head (ctn/get-head-shape objects parent)
parent-ref (find-ref-shape nil container libraries parent-head)]
(when (and swap-slot parent-ref)
(find-next-related-swap-shape-id parent-ref swap-slot libraries))))
(defn get-component-shapes
"Retrieve all shapes of the component"
[file-data component]

View File

@@ -148,7 +148,7 @@
(cond
(map? origin)
(into {}
(for [k (keys origin) :when (not= k :key)] ;; We ignore :key because it is a draft artifact
(for [k (keys destiny) :when (not= k :key)] ;; We ignore :key because it is a draft artifact
(cond
(= :children k)
[k (vec (map #(copy-text-keys %1 %2) (get origin k) (get destiny k)))]

View File

@@ -33,6 +33,8 @@
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-size "fontSizes"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
:other "other"
@@ -101,18 +103,15 @@
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
[:m4 {:optional true} token-name-ref]])
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions
[:merge
schema:sizing
schema:spacing
schema:stroke-width
schema:border-radius])
(reduce mu/union [schema:sizing
schema:spacing
schema:stroke-width
schema:border-radius]))
(def dimensions-keys (schema-keys schema:dimensions))
@@ -122,10 +121,25 @@
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:number
(def ^:private schema:font-size
[:map
[:rotation {:optional true} token-name-ref]
[:line-height {:optional true} token-name-ref]])
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def typography-keys (set/union font-size-keys letter-spacing-keys))
(def ff-typography-keys (set/difference typography-keys font-size-keys))
(def ^:private schema:number
(reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation]))
(def number-keys (schema-keys schema:number))
@@ -137,6 +151,7 @@
spacing-keys
dimensions-keys
rotation-keys
typography-keys
number-keys))
(def ^:private schema:tokens
@@ -150,6 +165,8 @@
schema:spacing
schema:rotation
schema:number
schema:font-size
schema:letter-spacing
schema:dimensions])
(defn shape-attr->token-attrs
@@ -177,6 +194,8 @@
changed-sub-attr
#{:m1 :m2 :m3 :m4})
(font-size-keys shape-attr) #{shape-attr}
(letter-spacing-keys shape-attr) #{shape-attr}
(border-radius-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr}
@@ -192,6 +211,56 @@
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN SHAPE ATTRIBUTES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def position-attributes #{:x :y})
(def generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
sizing-keys
opacity-keys
position-attributes))
(def rect-attributes
(set/union generic-attributes
border-radius-keys))
(def frame-attributes
(set/union rect-attributes
spacing-keys))
(def text-attributes
(set/union generic-attributes
typography-keys
number-keys))
(defn shape-type->attributes
[type]
(case type
:bool generic-attributes
:circle generic-attributes
:rect rect-attributes
:frame frame-attributes
:image rect-attributes
:path generic-attributes
:svg-raw generic-attributes
:text text-attributes
nil))
(defn appliable-attrs
"Returns intersection of shape `attributes` for `token-type`."
[attributes token-type]
(set/intersection attributes (shape-type->attributes token-type)))
(defn any-appliable-attr?
"Checks if `token-type` supports given shape `attributes`."
[attributes token-type]
(seq (appliable-attrs attributes token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -218,13 +287,5 @@
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn maybe-apply-token-to-shape
"When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape."
[{:keys [shape token _attributes] :as props}]
(if token
(apply-token-to-shape props)
shape))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))

View File

@@ -17,6 +17,7 @@
[app.common.transit :as t]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.core.protocols :as protocols]
[clojure.set :as set]
[clojure.walk :as walk]
[cuerdas.core :as str]))
@@ -25,13 +26,6 @@
;; TODO: add again the removed functions and refactor the rest of the module to use them
(def ^:private schema:groupable-item
[:map {:title "Groupable item"}
[:name :string]])
(def ^:private valid-groupable-item?
(sm/validator schema:groupable-item))
(def ^:private xf-map-trim
(comp
(map str/trim)
@@ -60,14 +54,38 @@
(defn get-path
"Get the path of object by specified separator (E.g. with '.' separator, the
'group.subgroup.name' -> ['group' 'subgroup'])"
[item separator]
(assert (valid-groupable-item? item) "expected groupable item")
(->> (split-path (:name item) separator)
[name separator]
(->> (split-path name separator)
(not-empty)))
;; === Common
(defprotocol INamedItem
"Protocol for items that have a name, a description and a modified date."
(get-name [_] "Get the name of the item.")
(get-description [_] "Get the description of the item.")
(get-modified-at [_] "Get the description of the item.")
(rename [_ new-name] "Set the name of the item.")
(set-description [_ new-description] "Set the description of the item."))
;; === Token
(defrecord Token [id name type value description modified-at])
(defrecord Token [id name type value description modified-at]
INamedItem
(get-name [_]
name)
(get-description [_]
description)
(get-modified-at [_]
modified-at)
(rename [this new-name]
(assoc this :name new-name))
(set-description [this new-description]
(assoc this :description new-description)))
(defn token?
[o]
@@ -109,7 +127,7 @@
(defn get-token-path
[token]
(get-path token token-separator))
(get-path (:name token) token-separator))
(defn find-token-value-references
"Returns set of token references found in `token-value`.
@@ -146,9 +164,53 @@
(update-token [_ token-name f] "update a token in the list")
(delete-token [_ token-name] "delete a token from the list")
(get-token [_ token-name] "return token by token-name")
(get-tokens [_] "return an ordered sequence of all tokens in the set"))
(get-tokens [_] "return an ordered sequence of all tokens in the set")
(get-tokens-map [_] "return a map of tokens in the set, indexed by token-name"))
(deftype TokenSet [id name description modified-at tokens]
#?@(:clj [clojure.lang.IDeref
(deref [_] {:id id
:name name
:description description
:modified-at modified-at
:tokens tokens})]
:cljs [cljs.core/IDeref
(-deref [_] {:id id
:name name
:description description
:modified-at modified-at
:tokens tokens})])
#?@(:cljs [cljs.core/IEncodeJS
(-clj->js [_] (js-obj "id" (clj->js id)
"name" (clj->js name)
"description" (clj->js description)
"modified-at" (clj->js modified-at)
"tokens" (clj->js tokens)))])
INamedItem
(get-name [_]
name)
(get-description [_]
description)
(get-modified-at [_]
modified-at)
(rename [_ new-name]
(TokenSet. id
new-name
description
(dt/now)
tokens))
(set-description [_ new-description]
(TokenSet. id
name
(d/nilv new-description "")
(dt/now)
tokens))
(defrecord TokenSet [id name description modified-at tokens]
ITokenSet
(add-token [_ token]
(let [token (check-token token)]
@@ -184,7 +246,10 @@
(get tokens token-name))
(get-tokens [_]
(vals tokens)))
(vals tokens))
(get-tokens-map [_]
tokens))
(defn token-set?
[o]
@@ -218,10 +283,7 @@
(declare make-token-set)
(def schema:token-set
[:and {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
(sm/required-keys schema:token-set-attrs)
[:fn token-set?]])
(sm/required-keys schema:token-set-attrs))
(sm/register! ::token-set schema:token-set) ;; need to register for the recursive schema of token-sets
@@ -233,13 +295,17 @@
(defn make-token-set
[& {:as attrs}]
(-> attrs
(update :id #(or % (uuid/next)))
(update :modified-at #(or % (dt/now)))
(update :tokens #(into (d/ordered-map) %))
(update :description d/nilv "")
(check-token-set-attrs)
(map->TokenSet)))
(let [attrs (-> attrs
(update :id #(or % (uuid/next)))
(update :modified-at #(or % (dt/now)))
(update :tokens #(into (d/ordered-map) %))
(update :description d/nilv "")
(check-token-set-attrs))]
(TokenSet. (:id attrs)
(:name attrs)
(:description attrs)
(:modified-at attrs)
(:tokens attrs))))
(def ^:private set-prefix "S-")
@@ -291,7 +357,7 @@
(defn get-set-path
[token-set]
(get-path token-set set-separator))
(get-path (get-name token-set) set-separator))
(defn split-set-name
[name]
@@ -315,7 +381,7 @@
(set-full-path->set-prefixed-full-path)))
(defn get-set-prefixed-path [token-set]
(let [path (get-path token-set set-separator)]
(let [path (get-path (get-name token-set) set-separator)]
(set-full-path->set-prefixed-full-path path)))
(defn prefixed-set-path-string->set-name-string [path-str]
@@ -333,7 +399,7 @@
(conj name)))
(defn tokens-tree
"Convert tokens into a nested tree with their `:name` as the path.
"Convert tokens into a nested tree with their name as the path.
Optionally use `update-token-fn` option to transform the token."
[tokens & {:keys [update-token-fn]
:or {update-token-fn identity}}]
@@ -343,7 +409,7 @@
{} tokens))
(defn backtrace-tokens-tree
"Convert tokens into a nested tree with their `:name` as the path.
"Convert tokens into a nested tree with their name as the path.
Generates a uuid per token to backtrace a token from an external source (StyleDictionary).
The backtrace can't be the name as the name might not exist when the user is creating a token."
[tokens]
@@ -392,7 +458,7 @@
(get-set [_ set-name] "get one set looking for name"))
(def schema:token-set-node
[:schema {:registry {::node [:or ::token-set
[:schema {:registry {::node [:or [:fn token-set?]
[:and
[:map-of {:gen/max 5} :string [:ref ::node]]
[:fn d/ordered-map?]]]}}
@@ -443,6 +509,22 @@
(hidden-theme? [_] "if a theme is the (from the user ui) hidden temporary theme"))
(defrecord TokenTheme [id name group description is-source external-id modified-at sets]
INamedItem
(get-name [_]
name)
(get-description [_]
description)
(get-modified-at [_]
modified-at)
(rename [this new-name]
(assoc this :name new-name))
(set-description [this new-description]
(assoc this :description new-description))
ITokenTheme
(set-sets [_ set-names]
(TokenTheme. id
@@ -528,13 +610,17 @@
(defn make-token-theme
[& {:as attrs}]
(let [id (uuid/next)]
(let [new-id (uuid/next)]
(-> attrs
(update :id d/nilv id)
(update :id (fn [id]
(-> (if (string? id) ;; TODO: probably this may be deleted in some time, when we may be sure
(uuid/parse* id) ;; that no file exists that has not been correctly migrated to
id) ;; convert :id into :external-id
(d/nilv new-id))))
(update :group d/nilv top-level-theme-group-name)
(update :description d/nilv "")
(update :is-source d/nilv false)
(update :external-id #(or % (str id)))
(update :external-id #(or % (str new-id)))
(update :modified-at #(or % (dt/now)))
(update :sets set)
(check-token-theme-attrs)
@@ -618,7 +704,7 @@
;; Set
(and v (instance? TokenSet v))
[{:group? false
:path (split-set-name (:name v))
:path (split-set-name (get-name v))
:parent-path parent
:depth depth
:set v}]
@@ -664,7 +750,7 @@
;; Set
(and v (instance? TokenSet v))
(let [name (:name v)]
(let [name (get-name v)]
[{:is-group false
:path (split-set-name name)
:id name
@@ -725,8 +811,14 @@ Will return a value that matches this schema:
(declare export-dtcg-json)
(deftype TokensLib [sets themes active-themes]
;; NOTE: This is only for debug purposes, pending to properly
;; implement the toString and alternative printing.
;; This is to convert the TokensLib to a plain map, for debugging or unit tests.
protocols/Datafiable
(datafy [_]
{:sets (d/update-vals sets deref)
:themes themes
:active-themes active-themes})
;; TODO: this is used in serialization, but there should be a better way to do it
#?@(:clj [clojure.lang.IDeref
(deref [_] {:sets sets
:themes themes
@@ -746,8 +838,8 @@ Will return a value that matches this schema:
ITokenSets
(add-set [_ token-set]
(let [path (get-set-prefixed-path token-set)
token-set (check-token-set token-set)]
(assert (token-set? token-set) "expected valid token-set")
(let [path (get-set-prefixed-path token-set)]
(TokensLib. (d/oassoc-in sets path token-set)
themes
active-themes)))
@@ -756,10 +848,9 @@ Will return a value that matches this schema:
(let [prefixed-full-path (set-name->prefixed-full-path set-name)
set (get-in sets prefixed-full-path)]
(if set
(let [set' (-> (make-token-set (f set))
(assoc :modified-at (dt/now)))
(let [set' (f set)
prefixed-full-path' (get-set-prefixed-path set')
name-changed? (not= (:name set) (:name set'))]
name-changed? (not= (get-name set) (get-name set'))]
(if name-changed?
(TokensLib. (-> sets
(d/oassoc-in-before prefixed-full-path prefixed-full-path' set')
@@ -767,7 +858,7 @@ Will return a value that matches this schema:
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
(update-set-name form (:name set) (:name set'))
(update-set-name form (get-name set) (get-name set'))
form))
themes)
active-themes)
@@ -791,7 +882,7 @@ Will return a value that matches this schema:
(let [path (split-set-name set-group-name)
prefixed-path (map add-set-group-prefix path)
child-set-names (->> (get-sets-at-path this path)
(map :name)
(map get-name)
(into #{}))]
(TokensLib. (d/dissoc-in sets prefixed-path)
(walk/postwalk
@@ -833,7 +924,7 @@ Will return a value that matches this schema:
(set-full-path->set-prefixed-full-path before-path)))
set
(assoc prev-set :name (join-set-path to-path))
(rename prev-set (join-set-path to-path))
reorder?
(= prefixed-from-path prefixed-to-path)
@@ -856,7 +947,7 @@ Will return a value that matches this schema:
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
(update-set-name form (:name prev-set) (:name set))
(update-set-name form (get-name prev-set) (get-name set))
form))
themes))
active-themes))
@@ -888,15 +979,15 @@ Will return a value that matches this schema:
(d/oupdate-in prefixed-to-path (fn [sets]
(walk/prewalk
(fn [form]
(if (instance? TokenSet form)
(update form :name #(str to-path-str (str/strip-prefix % from-path-str)))
(if (token-set? form)
(rename form (str to-path-str (str/strip-prefix (get-name form) from-path-str)))
form))
sets)))))
themes' (if reorder?
themes
(let [rename-sets-map (->> (get-sets-at-path this from-path)
(map (fn [set]
[(:name set) (str to-path-str (str/strip-prefix (:name set) from-path-str))]))
[(get-name set) (str to-path-str (str/strip-prefix (get-name set) from-path-str))]))
(into {}))]
(walk/postwalk
(fn [form]
@@ -934,12 +1025,12 @@ Will return a value that matches this schema:
sets (get-sets-at-path this path)]
(reduce
(fn [lib set]
(update-set lib (:name set) (fn [set']
(update set' :name #(str to-path-str (str/strip-prefix % from-path-str))))))
(update-set lib (get-name set) (fn [set']
(rename set' (str to-path-str (str/strip-prefix (get-name set') from-path-str))))))
this sets)))
(get-ordered-set-names [this]
(map :name (get-sets this)))
(map get-name (get-sets this)))
(set-count [this]
(count (get-sets this)))
@@ -1080,7 +1171,7 @@ Will return a value that matches this schema:
prefixed-path-str (set-group-path->set-group-prefixed-path-str group-path)]
(if (seq active-set-names)
(let [path-active-set-names (->> (get-sets-at-prefix-path this prefixed-path-str)
(map :name)
(map get-name)
(into #{}))
difference (set/difference path-active-set-names active-set-names)]
(cond
@@ -1095,7 +1186,7 @@ Will return a value that matches this schema:
active-set-names (filter theme-set-names all-set-names)
tokens (reduce (fn [tokens set-name]
(let [set (get-set this set-name)]
(merge tokens (:tokens set))))
(merge tokens (get-tokens-map set))))
(d/ordered-map)
active-set-names)]
tokens))
@@ -1160,11 +1251,10 @@ Will return a value that matches this schema:
(defn duplicate-set [set-name lib & {:keys [suffix]}]
(let [sets (get-sets lib)
unames (map :name sets)
unames (map get-name sets)
copy-name (cfh/generate-unique-name set-name unames :suffix suffix)]
(some-> (get-set lib set-name)
(assoc :name copy-name)
(assoc :modified-at (dt/now)))))
(rename copy-name))))
;; === Import / Export from JSON format
@@ -1473,8 +1563,10 @@ Will return a value that matches this schema:
[tokens-lib]
(let [{:keys [themes active-themes]} (dtcg-export-themes tokens-lib)
sets (->> (get-sets tokens-lib)
(map (fn [{:keys [name tokens]}]
[(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)]))
(map (fn [token-set]
(let [name (get-name token-set)
tokens (get-tokens-map token-set)]
[(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)])))
(into {}))]
(-> sets
(assoc "$themes.json" themes)
@@ -1491,8 +1583,9 @@ Will return a value that matches this schema:
(->> (get-set-tree tokens-lib)
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet))
(map (fn [{:keys [name tokens]}]
[name (tokens-tree tokens :update-token-fn token->dtcg-token)])))
(map (fn [set]
[(get-name set)
(tokens-tree (get-tokens-map set) :update-token-fn token->dtcg-token)])))
ordered-set-names
(mapv first name-set-tuples)
@@ -1512,28 +1605,31 @@ Will return a value that matches this schema:
(defn get-tokens-of-unknown-type
"Search for all tokens in the decoded json file that have a type that is not currently
supported by Penpot. Returns a map token-path -> token type."
([decoded-json]
(get-tokens-of-unknown-type decoded-json "" (get-json-format decoded-json)))
([decoded-json parent-path json-format]
(let [type-key (if (= json-format :json-format/dtcg) "$type" "type")]
(reduce-kv
(fn [unknown-tokens k v]
(let [child-path (if (empty? parent-path)
(name k)
(str parent-path "." k))]
(if (and (map? v)
(not (contains? v type-key)))
(let [nested-unknown-tokens (get-tokens-of-unknown-type v child-path json-format)]
(merge unknown-tokens nested-unknown-tokens))
(let [token-type-str (get v type-key)
token-type (cto/dtcg-token-type->token-type token-type-str)]
(if (and (not (some? token-type)) (some? token-type-str))
(assoc unknown-tokens child-path token-type-str)
unknown-tokens)))))
nil
decoded-json))))
[decoded-json {:keys [json-format parent-path process-token-type]
:or {json-format (get-json-format decoded-json)
parent-path ""
process-token-type identity}
:as opts}]
(let [type-key (if (= json-format :json-format/dtcg) "$type" "type")]
(reduce-kv
(fn [unknown-tokens k v]
(let [child-path (if (empty? parent-path)
(name k)
(str parent-path "." k))]
(if (and (map? v)
(not (contains? v type-key)))
(let [nested-unknown-tokens (get-tokens-of-unknown-type v (assoc opts :parent-path child-path))]
(merge unknown-tokens nested-unknown-tokens))
(let [token-type-str (get v type-key)
token-type (-> (cto/dtcg-token-type->token-type token-type-str)
(process-token-type))]
(if (and (not (some? token-type)) (some? token-type-str))
(assoc unknown-tokens child-path token-type-str)
unknown-tokens)))))
nil
decoded-json)))
;; === Serialization handlers for RPC API (transit) and database (fressian)
;; === Serialization handlers for RPC API (transit)
(t/add-handlers!
{:id "penpot/tokens-lib"
@@ -1543,8 +1639,8 @@ Will return a value that matches this schema:
{:id "penpot/token-set"
:class TokenSet
:wfn #(into {} %)
:rfn #(map->TokenSet %)}
:wfn deref
:rfn #(make-token-set %)}
{:id "penpot/token-theme"
:class TokenTheme
@@ -1556,6 +1652,8 @@ Will return a value that matches this schema:
:wfn #(into {} %)
:rfn #(map->Token %)})
;; === Serialization handlers for database (fressian)
#?(:clj
(defn- read-tokens-lib-v1-0
"Reads the first version of tokens lib, now completly obsolete"
@@ -1675,16 +1773,16 @@ Will return a value that matches this schema:
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->Token obj)))}
(make-token obj)))}
{:name "penpot/token-set/v1"
:class TokenSet
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (into {} o)))
(fres/write-object! w (into {} (deref o))))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenSet obj)))}
(make-token-set obj)))}
{:name "penpot/token-theme/v1"
:class TokenTheme
@@ -1693,7 +1791,7 @@ Will return a value that matches this schema:
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenTheme obj)))}
(make-token-theme obj)))}
;; LEGACY TOKENS LIB READERS (with migrations)
{:name "penpot/tokens-lib/v1"

View File

@@ -93,13 +93,16 @@
remap-typography
content)))))
(defn remove-typography-from-node
"Remove the typography reference from a node."
[node]
(dissoc node :typography-ref-file :typography-ref-id))
(defn remove-external-typographies
"Change the shape so that any use of an external typography now is removed"
[shape file-id]
(let [remove-ref-file #(dissoc % :typography-ref-file :typography-ref-id)]
(update shape :content
(fn [content]
(txt/transform-nodes #(not= (:typography-ref-file %) file-id)
remove-ref-file
content)))))
(update shape :content
(fn [content]
(txt/transform-nodes #(not= (:typography-ref-file %) file-id)
remove-typography-from-node
content))))

View File

@@ -139,7 +139,6 @@
(< (count (first %)) property-max-length)
(< (count (second %)) property-max-length)))))
(defn find-properties-to-remove
"Compares two property maps to find which properties should be removed"
[prev-props upd-props]
@@ -161,6 +160,46 @@
(filterv #(not (contains? prev-names (:name %))) upd-props)))
(defn- split-base-name-and-number
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
[item]
(let [pattern-num-parens #"\(\d+\)$"
pattern-num #"\d+"
base (-> item (str/replace pattern-num-parens "") (str/trim))
num (some->> item (re-find pattern-num-parens) (re-find pattern-num) (d/parse-integer))]
[base (d/nilv num 0)]))
(defn- group-numbers-by-base-name
"Return a map with a set of numbers associated to each base name"
[items]
(reduce (fn [acc item]
(let [[base num] (split-base-name-and-number item)]
(update acc base (fnil conj #{}) num)))
{}
items))
(defn update-number-in-repeated-item
"Add, keep or update a number in parentheses for a given item, if necessary, depending on the items
already present in a list, to avoid repetitions"
[items item]
(let [names (group-numbers-by-base-name items)
[base num] (split-base-name-and-number item)
nums-taken (get names base #{})]
(loop [n num]
(if (nums-taken n)
(recur (inc n))
(str base (when (pos? n) (str " (" n ")")))))))
(defn update-number-in-repeated-prop-names
"Add, keep or update a number for each prop name depending on the previous ones"
[props]
(->> props
(reduce (fn [acc prop]
(conj acc {:name (update-number-in-repeated-item (mapv :name acc) (:name prop))
:value (:value prop)}))
[])))
(defn find-index-for-property-name
"Finds the index of a name in a property map"
[props name]

View File

@@ -0,0 +1,881 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.logic.text-sync-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-sync-unchanged-copy-when-changed-attribute
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-unchanged-copy-when-changed-text
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-unchanged-copy-when-changed-both
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "32" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because it was touched
(t/is (= "14" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because it was touched
(t/is (= "14" (:font-size line)))
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because only text were touched
(t/is (= "32" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because only text were touched
(t/is (= "32" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because it was touched
(t/is (= "14" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because it was touched
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-structure-same-attrs-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because all the attrs on the structure are equal
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-same-attrs-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because the structure was touched
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-same-attrs-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because all the attrs on the structure are equal
(t/is (= "32" (:font-size line)))
;; The text doesn't change, because the structure was touched
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-diff-attrs-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because not all the attrs on the structure are equal
(t/is (= "14" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-diff-attrs-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because the structure was touched
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-diff-attrs-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr doesn't change, because not all the attrs on the structure are equal
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because the structure was touched
(t/is (= "hello world" (:text line)))))

View File

@@ -0,0 +1,132 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.logic.text-touched-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-text-copy-changed-attribute
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-attribute} (:touched copy-child')))))
(t/deftest test-text-copy-changed-text
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-text} (:touched copy-child')))))
(t/deftest test-text-copy-changed-both
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-attribute :text-content-text} (:touched copy-child')))))
(t/deftest test-text-copy-changed-structure-same-attrs
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0])]
(update-in shape [:content :children 0 :children]
#(conj % line))))
(:objects page)
{})
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-structure :text-content-structure-same-attrs} (:touched copy-child')))))
(t/deftest test-text-copy-changed-structure-diff-attrs
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
(thc/make-component :component1 :main-root)
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(let [line (-> shape
(get-in [:content :children 0 :children 0])
(assoc :font-size "32"))]
(update-in shape [:content :children 0 :children]
#(conj % line))))
(:objects page)
{})
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-structure} (:touched copy-child')))))

View File

@@ -54,9 +54,17 @@
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-dimensions"
:type :dimensions
:value 100))))
:value 100))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-font-size"
:type :font-size
:value 24))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-letter-spacing"
:type :letter-spacing
:value 2))))
(tho/add-frame :frame1)
(tho/add-text :text1 "Hello World")))
(tho/add-text :text1 "Hello World!")))
(defn- apply-all-tokens
[file]
@@ -68,19 +76,23 @@
(tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
(tht/apply-token-to-shape :text1 "token-color" [:fill] [:fill] "#00ff00")))
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] 24)
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)))
(t/deftest apply-tokens-to-shape
(let [;; ==== Setup
file (setup-file)
page (thf/current-page file)
frame1 (ths/get-shape file :frame1)
text1 (ths/get-shape file :text1)
token-radius (tht/get-token file "test-token-set" "token-radius")
token-rotation (tht/get-token file "test-token-set" "token-rotation")
token-opacity (tht/get-token file "test-token-set" "token-opacity")
token-stroke-width (tht/get-token file "test-token-set" "token-stroke-width")
token-color (tht/get-token file "test-token-set" "token-color")
token-dimensions (tht/get-token file "test-token-set" "token-dimensions")
token-font-size (tht/get-token file "test-token-set" "token-font-size")
token-letter-spacing (tht/get-token file "test-token-set" "token-letter-spacing")
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)
@@ -89,38 +101,48 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(as-> shape $
(cto/maybe-apply-token-to-shape {:token nil ; test nil case
:shape $
:attributes []})
(cto/maybe-apply-token-to-shape {:token token-radius
:shape $
:attributes [:r1 :r2 :r3 :r4]})
(cto/maybe-apply-token-to-shape {:token token-rotation
:shape $
:attributes [:rotation]})
(cto/maybe-apply-token-to-shape {:token token-opacity
:shape $
:attributes [:opacity]})
(cto/maybe-apply-token-to-shape {:token token-stroke-width
:shape $
:attributes [:stroke-width]})
(cto/maybe-apply-token-to-shape {:token token-color
:shape $
:attributes [:stroke-color]})
(cto/maybe-apply-token-to-shape {:token token-color
:shape $
:attributes [:fill]})
(cto/maybe-apply-token-to-shape {:token token-dimensions
:shape $
:attributes [:width :height]})))
(cto/apply-token-to-shape {:token token-radius
:shape $
:attributes [:r1 :r2 :r3 :r4]})
(cto/apply-token-to-shape {:token token-rotation
:shape $
:attributes [:rotation]})
(cto/apply-token-to-shape {:token token-opacity
:shape $
:attributes [:opacity]})
(cto/apply-token-to-shape {:token token-stroke-width
:shape $
:attributes [:stroke-width]})
(cto/apply-token-to-shape {:token token-color
:shape $
:attributes [:stroke-color]})
(cto/apply-token-to-shape {:token token-color
:shape $
:attributes [:fill]})
(cto/apply-token-to-shape {:token token-dimensions
:shape $
:attributes [:width :height]})))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(as-> shape $
(cto/apply-token-to-shape {:token token-font-size
:shape $
:attributes [:font-size]})
(cto/apply-token-to-shape {:token token-letter-spacing
:shape $
:attributes [:letter-spacing]})))
(:objects page)
{}))
file' (thf/apply-changes file changes)
;; ==== Get
frame1' (ths/get-shape file' :frame1)
applied-tokens' (:applied-tokens frame1')]
frame1' (ths/get-shape file' :frame1)
applied-tokens' (:applied-tokens frame1')
text1' (ths/get-shape file' :text1)
text1-applied-tokens (:applied-tokens text1')]
;; ==== Check
(t/is (= (count applied-tokens') 11))
@@ -134,7 +156,10 @@
(t/is (= (:stroke-color applied-tokens') "token-color"))
(t/is (= (:fill applied-tokens') "token-color"))
(t/is (= (:width applied-tokens') "token-dimensions"))
(t/is (= (:height applied-tokens') "token-dimensions"))))
(t/is (= (:height applied-tokens') "token-dimensions"))
(t/is (= (count text1-applied-tokens) 2))
(t/is (= (:font-size text1-applied-tokens) "token-font-size"))
(t/is (= (:letter-spacing text1-applied-tokens) "token-letter-spacing"))))
(t/deftest unapply-tokens-from-shape
(let [;; ==== Setup
@@ -142,6 +167,7 @@
(apply-all-tokens))
page (thf/current-page file)
frame1 (ths/get-shape file :frame1)
text1 (ths/get-shape file :text1)
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)
@@ -158,16 +184,26 @@
(cto/unapply-token-id [:fill])
(cto/unapply-token-id [:width :height])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])))
(:objects page)
{}))
file' (thf/apply-changes file changes)
;; ==== Get
frame1' (ths/get-shape file' :frame1)
applied-tokens' (:applied-tokens frame1')]
frame1' (ths/get-shape file' :frame1)
applied-tokens' (:applied-tokens frame1')
text1' (ths/get-shape file' :text1)
text1-applied-tokens (:applied-tokens text1')]
;; ==== Check
(t/is (= (count applied-tokens') 0))))
(t/is (= (count applied-tokens') 0))
(t/is (= (count text1-applied-tokens) 0))))
(t/deftest unapply-tokens-automatic
(let [;; ==== Setup
@@ -202,7 +238,9 @@
shape
txt/is-content-node?
d/txt-merge
{:fills (ths/sample-fills-color :fill-color "#fabada")}))
{:fills (ths/sample-fills-color :fill-color "#fabada")
:font-size "1"
:letter-spacing "0"}))
(:objects page)
{}))
@@ -216,4 +254,4 @@
;; ==== Check
(t/is (= (count applied-tokens-frame') 0))
(t/is (= (count applied-tokens-text') 0))))
(t/is (= (count applied-tokens-text') 0))))

View File

@@ -269,8 +269,7 @@
new-set-name "foo1"
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-name false (assoc prev-token-set
:name new-set-name)))
(pcb/set-token-set set-name false (ctob/rename prev-token-set new-set-name)))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)

View File

@@ -14,6 +14,7 @@
[app.common.time :as dt]
[app.common.transit :as tr]
[app.common.types.tokens-lib :as ctob]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(defn setup-virtual-time
@@ -72,14 +73,14 @@
:modified-at now
:tokens [])]
(t/is (= (:name token-set1) "test-token-set-1"))
(t/is (= (:description token-set1) ""))
(t/is (some? (:modified-at token-set1)))
(t/is (empty? (:tokens token-set1)))
(t/is (= (:name token-set2) "test-token-set-2"))
(t/is (= (:description token-set2) "test description"))
(t/is (= (:modified-at token-set2) now))
(t/is (empty? (:tokens token-set2)))))
(t/is (= (ctob/get-name token-set1) "test-token-set-1"))
(t/is (= (ctob/get-description token-set1) ""))
(t/is (some? (ctob/get-modified-at token-set1)))
(t/is (empty? (ctob/get-tokens-map token-set1)))
(t/is (= (ctob/get-name token-set2) "test-token-set-2"))
(t/is (= (ctob/get-description token-set2) "test description"))
(t/is (= (ctob/get-modified-at token-set2) now))
(t/is (empty? (ctob/get-tokens-map token-set2)))))
(t/deftest make-invalid-token-set
(let [params {:name 777 :description 999}]
@@ -183,7 +184,7 @@
:type :boolean
:value true)})))
expected (-> (ctob/get-set tokens-lib "A")
(get :tokens)
(ctob/get-tokens-map)
(ctob/tokens-tree))]
(t/is (= (get-in expected ["foo" "bar" "baz" :name]) "foo.bar.baz"))
(t/is (= (get-in expected ["foo" "bar" "bam" :name]) "foo.bar.bam"))
@@ -249,20 +250,18 @@
tokens-lib' (-> tokens-lib
(ctob/update-set "test-token-set"
(fn [token-set]
(assoc token-set
:description "some description")))
(ctob/set-description token-set "some description")))
(ctob/update-set "not-existing-set"
(fn [token-set]
(assoc token-set
:description "no-effect"))))
(ctob/set-description token-set "no-effect"))))
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (:name token-set') "test-token-set"))
(t/is (= (:description token-set') "some description"))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (= (ctob/get-name token-set') "test-token-set"))
(t/is (= (ctob/get-description token-set') "some description"))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest rename-token-set
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -271,15 +270,14 @@
tokens-lib' (-> tokens-lib
(ctob/update-set "test-token-set"
(fn [token-set]
(assoc token-set
:name "updated-name"))))
(ctob/rename token-set "updated-name"))))
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "updated-name")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (:name token-set') "updated-name"))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (= (ctob/get-name token-set') "updated-name"))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest rename-token-set-group
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -323,11 +321,11 @@
:type :boolean
:value true)})))
token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"})
token (get-in token-set-copy [:tokens "test-token"])]
token (ctob/get-token token-set-copy "test-token")]
(t/is (some? token-set-copy))
(t/is (= (:name token-set-copy) "test-token-set-copy"))
(t/is (= (count (:tokens token-set-copy)) 1))
(t/is (= (ctob/get-name token-set-copy) "test-token-set-copy"))
(t/is (= (count (ctob/get-tokens-map token-set-copy)) 1))
(t/is (= (:name token) "test-token"))))
(t/deftest duplicate-token-set-twice
@@ -341,11 +339,11 @@
tokens-lib (ctob/add-set tokens-lib (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"}))
token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"})
token (get-in token-set-copy [:tokens "test-token"])]
token (ctob/get-token token-set-copy "test-token")]
(t/is (some? token-set-copy))
(t/is (= (:name token-set-copy) "test-token-set-copy-2"))
(t/is (= (count (:tokens token-set-copy)) 1))
(t/is (= (ctob/get-name token-set-copy) "test-token-set-copy-2"))
(t/is (= (count (ctob/get-tokens-map token-set-copy)) 1))
(t/is (= (:name token) "test-token"))))
(t/deftest duplicate-empty-token-set
@@ -353,11 +351,11 @@
(ctob/add-set (ctob/make-token-set :name "test-token-set")))
token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"})
tokens (get token-set-copy :tokens)]
tokens (ctob/get-tokens-map token-set-copy)]
(t/is (some? token-set-copy))
(t/is (= (:name token-set-copy) "test-token-set-copy"))
(t/is (= (count (:tokens token-set-copy)) 0))
(t/is (= (ctob/get-name token-set-copy) "test-token-set-copy"))
(t/is (= (count (ctob/get-tokens-map token-set-copy)) 0))
(t/is (= (count tokens) 0))))
(t/deftest duplicate-not-existing-token-set
@@ -392,12 +390,12 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token' (get-in token-set' [:tokens "test-token"])]
token' (ctob/get-token token-set' "test-token")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (count (:tokens token-set')) 1))
(t/is (= (count (ctob/get-tokens-map token-set')) 1))
(t/is (= (:name token') "test-token"))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest update-token
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -428,16 +426,16 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token (get-in token-set [:tokens "test-token-1"])
token' (get-in token-set' [:tokens "test-token-1"])]
token (ctob/get-token token-set "test-token-1")
token' (ctob/get-token token-set' "test-token-1")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (count (:tokens token-set')) 2))
(t/is (= (d/index-of (keys (:tokens token-set')) "test-token-1") 0))
(t/is (= (count (ctob/get-tokens-map token-set')) 2))
(t/is (= (d/index-of (keys (ctob/get-tokens-map token-set')) "test-token-1") 0))
(t/is (= (:name token') "test-token-1"))
(t/is (= (:description token') "some description"))
(t/is (= (:value token') false))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest rename-token
@@ -460,16 +458,16 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token (get-in token-set [:tokens "test-token-1"])
token' (get-in token-set' [:tokens "updated-name"])]
token (ctob/get-token token-set "test-token-1")
token' (ctob/get-token token-set' "updated-name")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (count (:tokens token-set')) 2))
(t/is (= (d/index-of (keys (:tokens token-set')) "updated-name") 0))
(t/is (= (count (ctob/get-tokens-map token-set')) 2))
(t/is (= (d/index-of (keys (ctob/get-tokens-map token-set')) "updated-name") 0))
(t/is (= (:name token') "updated-name"))
(t/is (= (:description token') ""))
(t/is (= (:value token') true))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest delete-token
@@ -486,12 +484,12 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token' (get-in token-set' [:tokens "test-token"])]
token' (ctob/get-token token-set' "test-token")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (count (:tokens token-set')) 0))
(t/is (= (count (ctob/get-tokens-map token-set')) 0))
(t/is (nil? token'))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest get-ordered-sets
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -897,7 +895,7 @@
:value true)))
set (ctob/get-set tokens-lib "test-token-set")
tokens-list (vals (:tokens set))]
tokens-list (ctob/get-tokens set)]
(t/is (= (count tokens-list) 5))
(t/is (= (:name (nth tokens-list 0)) "token1"))
@@ -931,14 +929,14 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token (get-in token-set [:tokens "group1.test-token-2"])
token' (get-in token-set' [:tokens "group1.test-token-2"])]
token (ctob/get-token token-set "group1.test-token-2")
token' (ctob/get-token token-set' "group1.test-token-2")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (:name token') "group1.test-token-2"))
(t/is (= (:description token') "some description"))
(t/is (= (:value token') false))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest update-token-in-sets-rename
@@ -965,14 +963,14 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token (get-in token-set [:tokens "group1.test-token-2"])
token' (get-in token-set' [:tokens "group1.updated-name"])]
token (ctob/get-token token-set "group1.test-token-2")
token' (ctob/get-token token-set' "group1.updated-name")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (:name token') "group1.updated-name"))
(t/is (= (:description token') ""))
(t/is (= (:value token') true))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (:ctob/get-modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest move-token-of-group
@@ -999,15 +997,15 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token (get-in token-set [:tokens "group1.test-token-2"])
token' (get-in token-set' [:tokens "group2.updated-name"])]
token (ctob/get-token token-set "group1.test-token-2")
token' (ctob/get-token token-set' "group2.updated-name")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (d/index-of (keys (:tokens token-set')) "group2.updated-name") 1))
(t/is (= (d/index-of (keys (ctob/get-tokens-map token-set')) "group2.updated-name") 1))
(t/is (= (:name token') "group2.updated-name"))
(t/is (= (:description token') ""))
(t/is (= (:value token') true))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest delete-token-in-group
@@ -1026,12 +1024,12 @@
token-set (ctob/get-set tokens-lib "test-token-set")
token-set' (ctob/get-set tokens-lib' "test-token-set")
token' (get-in token-set' [:tokens "group1.test-token-2"])]
token' (ctob/get-token token-set' "group1.test-token-2")]
(t/is (= (ctob/set-count tokens-lib') 1))
(t/is (= (count (:tokens token-set')) 1))
(t/is (= (count (ctob/get-tokens-map token-set')) 1))
(t/is (nil? token'))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest update-token-set-in-groups
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -1044,7 +1042,7 @@
tokens-lib' (-> tokens-lib
(ctob/update-set "group1/token-set-2"
(fn [token-set]
(assoc token-set :description "some description"))))
(ctob/set-description token-set "some description"))))
sets-tree (ctob/get-set-tree tokens-lib)
sets-tree' (ctob/get-set-tree tokens-lib')
@@ -1055,9 +1053,9 @@
(t/is (= (ctob/set-count tokens-lib') 5))
(t/is (= (count group1') 3))
(t/is (= (d/index-of (keys group1') "S-token-set-2") 0))
(t/is (= (:name token-set') "group1/token-set-2"))
(t/is (= (:description token-set') "some description"))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (= (ctob/get-name token-set') "group1/token-set-2"))
(t/is (= (ctob/get-description token-set') "some description"))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest rename-token-set-in-groups
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -1070,8 +1068,7 @@
tokens-lib' (-> tokens-lib
(ctob/update-set "group1/token-set-2"
(fn [token-set]
(assoc token-set
:name "group1/updated-name"))))
(ctob/rename token-set "group1/updated-name"))))
sets-tree (ctob/get-set-tree tokens-lib)
sets-tree' (ctob/get-set-tree tokens-lib')
@@ -1082,9 +1079,9 @@
(t/is (= (ctob/set-count tokens-lib') 5))
(t/is (= (count group1') 3))
(t/is (= (d/index-of (keys group1') "S-updated-name") 0))
(t/is (= (:name token-set') "group1/updated-name"))
(t/is (= (:description token-set') ""))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (= (ctob/get-name token-set') "group1/updated-name"))
(t/is (= (ctob/get-description token-set') ""))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest move-token-set-of-group
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -1097,8 +1094,7 @@
tokens-lib' (-> tokens-lib
(ctob/update-set "group1/token-set-2"
(fn [token-set]
(assoc token-set
:name "group2/updated-name"))))
(ctob/rename token-set "group2/updated-name"))))
sets-tree (ctob/get-set-tree tokens-lib)
sets-tree' (ctob/get-set-tree tokens-lib')
@@ -1111,9 +1107,9 @@
(t/is (= (count group1') 2))
(t/is (= (count group2') 1))
(t/is (nil? (get group1' "S-updated-name")))
(t/is (= (:name token-set') "group2/updated-name"))
(t/is (= (:description token-set') ""))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/is (= (ctob/get-name token-set') "group2/updated-name"))
(t/is (= (ctob/get-description token-set') ""))
(t/is (dt/is-after? (ctob/get-modified-at token-set') (ctob/get-modified-at token-set)))))
(t/deftest delete-token-set-in-group
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -1413,7 +1409,7 @@
tokens-lib' (ctob/parse-decoded-json encoded "")]
(t/testing "library got updated but data is equal"
(t/is (not= tokens-lib' tokens-lib))
(t/is (= @tokens-lib' @tokens-lib)))))))
(t/is (= (datafy tokens-lib') (datafy tokens-lib))))))))
#?(:clj
(t/deftest export-dtcg-json-with-default-theme

View File

@@ -15,13 +15,13 @@
map-with-two-props-dashes [{:name "border" :value "no"} {:name "color" :value "--"}]
map-with-one-prop [{:name "border" :value "no"}]
map-with-equal [{:name "border" :value "yes color=yes"}]
map-with-spaces [{:name "border 1" :value "of course"}
{:name "color 2" :value "dark gray"}
{:name "background 3" :value "anoth€r co-lor"}]
map-with-spaces [{:name "border (1)" :value "of course"}
{:name "color (2)" :value "dark gray"}
{:name "background (3)" :value "anoth€r co-lor"}]
string-valid-with-two-props "border=yes, color=gray"
string-valid-with-one-prop "border=no"
string-valid-with-spaces "border 1=of course, color 2=dark gray, background 3=anoth€r co-lor"
string-valid-with-spaces "border (1)=of course, color (2)=dark gray, background (3)=anoth€r co-lor"
string-valid-with-no-value "border=no, color="
string-valid-with-dashes "border=no, color=--"
string-valid-with-equal "border=yes color=yes"
@@ -131,3 +131,31 @@
(t/is (= (ctv/same-variant? components-2) false))
(t/is (= (ctv/same-variant? components-3) false))
(t/is (= (ctv/same-variant? components-4) false)))))
(t/deftest update-number-in-repeated-item
(let [names ["border" "color" "color 1" "color 2" "color (1)" "color (7)" "area 51"]]
(t/testing "update-number-in-repeated-item"
(t/is (= (ctv/update-number-in-repeated-item names "background") "background"))
(t/is (= (ctv/update-number-in-repeated-item names "border") "border (1)"))
(t/is (= (ctv/update-number-in-repeated-item names "color") "color (2)"))
(t/is (= (ctv/update-number-in-repeated-item names "color 1") "color 1 (1)"))
(t/is (= (ctv/update-number-in-repeated-item names "color (1)") "color (2)"))
(t/is (= (ctv/update-number-in-repeated-item names "area 51") "area 51 (1)")))))
(t/deftest update-number-in-repeated-prop-names
(let [props [{:name "color" :value "yellow"}
{:name "color" :value "blue"}
{:name "color" :value "red"}
{:name "border (1)" :value "no"}
{:name "border (1)" :value "yes"}]
numbered-props [{:name "color" :value "yellow"}
{:name "color (1)" :value "blue"}
{:name "color (2)" :value "red"}
{:name "border (1)" :value "no"}
{:name "border (2)" :value "yes"}]]
(t/testing "update-number-in-repeated-prop-names"
(t/is (= (ctv/update-number-in-repeated-prop-names props) numbered-props)))))

View File

@@ -26,8 +26,6 @@ RUN set -ex; \
build-essential autoconf libtool pkg-config
COPY files/apt.sources /etc/apt/sources.list.d/ubuntu.sources
################################################################################
## IMAGE MAGICK
################################################################################

View File

@@ -96,6 +96,10 @@ services:
- ./files/postgresql.conf:/etc/postgresql.conf:z
- ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql:z
- postgres_data_pg16:/var/lib/postgresql/data
networks:
default:
aliases:
- postgres
redis:
image: valkey/valkey:8.1

View File

@@ -10,7 +10,7 @@ cp /root/.bashrc /home/penpot/.bashrc
cp /root/.vimrc /home/penpot/.vimrc
cp /root/.tmux.conf /home/penpot/.tmux.conf
chown -R penpot:users /home/penpot
chown penpot:users /home/penpot
rsync -ar --chown=penpot:users /opt/cargo/ /home/penpot/.cargo/
export PATH="/home/penpot/.cargo/bin:$PATH"

View File

@@ -4,14 +4,22 @@ title: 3.06. Backend Guide
# Backend guide #
This guide intends to explain the essential details of the backend
application.
This guide collects some basic information on the backend application.
## REPL ##
In the devenv environment you can execute <code class="language-clojure">scripts/repl</code> to open a
Clojure interactive shell ([REPL](https://codewith.mu/en/tutorials/1.0/repl)).
_Note:_ When in development mode, the backend spins up a traditional nREPL socket on port 6064.
If you are experimenting locally, you can connect to it using your Clojure editor or
with `backend/scripts/nrepl`, which starts a [REPLy client](https://github.com/trptcolin/reply),
[see here][1] for more information.
[1]: /technical-guide/developer/devenv/#backend
In the devenv environment you can execute `backend/scripts/repl` to open a
Clojure interactive shell ([REPL](https://codewith.mu/en/tutorials/1.0/repl)) (this is not a socket-based
REPL, but a local, in-process console (over stdin/stdout) with some fancy line-editing and colors). Note
that the backend must be stopped before executing this script, otherwise it will fail with `Port already
in use: 9090`.
Once there, you can execute <code class="language-clojure">(restart)</code> to load and execute the backend
process, or to reload it after making changes to the source code.
@@ -39,11 +47,11 @@ For example:
## Fixtures ##
This is a development feature that allows populate the database with a
good amount of content (usually used for just test the application or
perform performance tweaks on queries).
This is a development feature that allows populating the database with a
good amount of content (typically used to test the application or to run
performance tweaks on queries).
In order to load fixtures, enter to the REPL environment with the <code class="language-clojure">scripts/repl</code>
In order to load fixtures, enter the REPL environment with the <code class="language-clojure">backend/scripts/repl</code>
script, and then execute <code class="language-clojure">(app.cli.fixtures/run {:preset :small})</code>.
You also can execute this as a standalone script with:
@@ -52,11 +60,11 @@ You also can execute this as a standalone script with:
clojure -Adev -X:fn-fixtures
```
NOTE: It is an optional step because the application can start with an
_NOTE:_ This is an optional step because the application can start with an
empty database.
This by default will create a bunch of users that can be used to login
in the application. All users uses the following pattern:
The above will create several users that can be used to login
into the application. All of them follow the pattern:
- Username: <code class="language-text">profileN@example.com</code>
- Password: <code class="language-text">123123</code>

View File

@@ -170,6 +170,23 @@ similar to a webmail client. Simply navigate to:
[http://localhost:1080](http://localhost:1080)
## Create user
You can register a new user manually, or create new users automatically with this script. From your tmux instance, run:
```sh
cd penpot/backend/scripts
python3 manage.py create-profile
```
You can also skip tutorial and walkthrough steps:
```sh
python3 manage.py create-profile --skip-tutorial --skip-walkthrough
python3 manage.py create-profile -n "Jane Doe" -e jane@example.com -p secretpassword --skip-tutorial --skip-walkthrough
```
## Team Feature Flags
To test a Feature Flag, you can enable or disable them by team through the `dbg` page:

View File

@@ -89,6 +89,13 @@ For instance, if the registration is disabled, the only way to create a new use
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
```
or
```bash
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile --skip-tutorial --skip-walkthrough
```
**NOTE:** the exact container name depends on your docker version and platform.
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.
You can check the correct name executing <code class="language-bash">docker ps</code>.

View File

@@ -281,12 +281,15 @@ press <kbd>Shift/⇧</kbd> + left click over the right arrow of a group or a boa
<h2 id="focus-mode">Focus mode</h2>
<p>Select the elements of a page you want to work with in a specific moment hiding the rest so they dont get in the way of your attention. This option is also useful to improve the performance in cases where the page has a large number of elements.</p>
<p>Focus mode zooms into the elements of a page you want to work with in a specific moment, and hides the rest so that they dont get in the way. When the page has many elements, focus mode can also improve performance.</p>
<p>To activate focus mode:</p>
<ol>
<ol>
<li>Select one or more elements.</li>
<li>Right click to show the menu and select the option "Focus on" or press <kbd>F</kbd>.</li>
<li>Right click on the selection to show the menu and select the option Focus on or press <kbd>F</kbd>.</li>
</ol>
<p>Notice that the layer panel will now only show the focused layers. A focus mode status line will also appear at the top.</p>
<p>To exit focus mode and return to the original viewport and selection, right click anywhere and select “Focus off” or just press <kbd>F</kbd> again. You can also click anywhere on the focus mode status line at the top of the layer panel.
</p>
<figure>
<video title="Focus mode" muted="" playsinline="" controls="" width="100%" poster="/img/layers/layers-focus.webp" height="auto">
<source src="/img/layers/layers-focus.mp4" type="video/mp4">

1
frontend/.gitignore vendored
View File

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

View File

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

View File

@@ -103,6 +103,7 @@
"@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.1",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",

View File

@@ -53,6 +53,21 @@ export default defineConfig({
toHaveScreenshot: { maxDiffPixelRatio: 0.005 },
},
},
{
name: "render-wasm",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
deviceScaleFactor: 2,
},
testDir: "./playwright/ui/render-wasm-specs",
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.001,
},
},
},
],
/* Run your local dev server before starting the tests */

View File

@@ -0,0 +1,5 @@
{
"~:type": "~:restriction",
"~:code": "~:email-does-not-match-invitation",
"~:hint": "email should match the invitation"
}

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,633 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~: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": "Multiple fills",
"~:revn": 19,
"~:modified-at": "~m1749564220299",
"~:vern": 0,
"~:id": "~uc0939f58-37bc-805d-8006-51cd3a51c255",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-add-partial-text-touched-flags",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0004-clean-shadow-and-colors",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u53a7ff09-2228-81d3-8006-4b5ea964593b",
"~:created-at": "~m1749564032332",
"~:data": {
"~:pages": [
"~uc0939f58-37bc-805d-8006-51cd3a51c256"
],
"~:pages-index": {
"~uc0939f58-37bc-805d-8006-51cd3a51c256": {
"~: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": [
"~ub688a894-3697-80d3-8006-51cd477981bc",
"~ub688a894-3697-80d3-8006-51cd5504e381",
"~ub688a894-3697-80d3-8006-51cd5de7c5f3",
"~ub688a894-3697-80d3-8006-51cd67bc1de9"
]
}
},
"~ub688a894-3697-80d3-8006-51cd477981bc": {
"~#shape": {
"~:y": 297,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 153,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 239,
"~:y": 297
}
},
{
"~#point": {
"~:x": 392,
"~:y": 297
}
},
{
"~#point": {
"~:x": 392,
"~:y": 441
}
},
{
"~#point": {
"~:x": 239,
"~:y": 441
}
}
],
"~: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": "~ub688a894-3697-80d3-8006-51cd477981bc",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 239,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 239,
"~:y": 297,
"~:width": 153,
"~:height": 144,
"~:x1": 239,
"~:y1": 297,
"~:x2": 392,
"~:y2": 441
}
},
"~:fills": [
{
"~:fill-color": "#ff0000",
"~:fill-opacity": 1
},
{
"~:fill-color": "#003fff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 144,
"~:flip-y": null
}
},
"~ub688a894-3697-80d3-8006-51cd5504e381": {
"~#shape": {
"~:y": 297,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 153,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 442,
"~:y": 297
}
},
{
"~#point": {
"~:x": 595,
"~:y": 297
}
},
{
"~#point": {
"~:x": 595,
"~:y": 441
}
},
{
"~#point": {
"~:x": 442,
"~:y": 441
}
}
],
"~: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": "~ub688a894-3697-80d3-8006-51cd5504e381",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 442,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 442,
"~:y": 297,
"~:width": 153,
"~:height": 144,
"~:x1": 442,
"~:y1": 297,
"~:x2": 595,
"~:y2": 441
}
},
"~:fills": [
{
"~:fill-color": "#ff0000",
"~:fill-opacity": 0.5
},
{
"~:fill-color": "#003fff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 144,
"~:flip-y": null
}
},
"~ub688a894-3697-80d3-8006-51cd5de7c5f3": {
"~#shape": {
"~:y": 476.99998474121094,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 153,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 239,
"~:y": 476.99998474121094
}
},
{
"~#point": {
"~:x": 392,
"~:y": 476.99998474121094
}
},
{
"~#point": {
"~:x": 392,
"~:y": 620.9999847412109
}
},
{
"~#point": {
"~:x": 239,
"~:y": 620.9999847412109
}
}
],
"~: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": "~ub688a894-3697-80d3-8006-51cd5de7c5f3",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 239,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 239,
"~:y": 476.99998474121094,
"~:width": 153,
"~:height": 144,
"~:x1": 239,
"~:y1": 476.99998474121094,
"~:x2": 392,
"~:y2": 620.9999847412109
}
},
"~:fills": [
{
"~:fill-color": "#ff0000",
"~:fill-opacity": 0.5
},
{
"~:fill-color-gradient": {
"~:stops": [
{
"~:color": "#003fff",
"~:offset": 0,
"~:opacity": 1
},
{
"~:color": "#003fff",
"~:offset": 1,
"~:opacity": 0
}
],
"~:width": 1,
"~:type": "~:linear",
"~:start-x": 0.5,
"~:end-y": 1,
"~:end-x": 0.5,
"~:start-y": 0
}
}
],
"~:flip-x": null,
"~:height": 144,
"~:flip-y": null
}
},
"~ub688a894-3697-80d3-8006-51cd67bc1de9": {
"~#shape": {
"~:y": 476.99998474121094,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 153,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 442,
"~:y": 476.99998474121094
}
},
{
"~#point": {
"~:x": 595,
"~:y": 476.99998474121094
}
},
{
"~#point": {
"~:x": 595,
"~:y": 620.9999847412109
}
},
{
"~#point": {
"~:x": 442,
"~:y": 620.9999847412109
}
}
],
"~: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": "~ub688a894-3697-80d3-8006-51cd67bc1de9",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 442,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 442,
"~:y": 476.99998474121094,
"~:width": 153,
"~:height": 144,
"~:x1": 442,
"~:y1": 476.99998474121094,
"~:x2": 595,
"~:y2": 620.9999847412109
}
},
"~:fills": [
{
"~:fill-color-gradient": {
"~:stops": [
{
"~:color": "#010512",
"~:offset": 0,
"~:opacity": 0
},
{
"~:color": "#010512",
"~:offset": 1,
"~:opacity": 1
}
],
"~:width": 1,
"~:type": "~:radial",
"~:start-x": 0.5,
"~:end-y": 1,
"~:end-x": 0.5,
"~:start-y": 0.5
},
"~:fill-opacity": 0.5
},
{
"~:fill-image": {
"~:mtype": "image/jpeg",
"~:name": "Aptenodytes_forsteri_-Snow_Hill_Island,_Antarctica_-adults_and_juvenile-8.jpg",
"~:keep-aspect-ratio": true,
"~:width": 872,
"~:id": "~uc0939f58-37bc-805d-8006-51cda84a405a",
"~:height": 1400
},
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 144,
"~:flip-y": null
}
}
},
"~:id": "~uc0939f58-37bc-805d-8006-51cd3a51c256",
"~:name": "Page 1"
}
},
"~:id": "~uc0939f58-37bc-805d-8006-51cd3a51c255",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,538 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~: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": "Multiple strokes",
"~:revn": 16,
"~:modified-at": "~m1749564011553",
"~:vern": 0,
"~:id": "~uc0939f58-37bc-805d-8006-51cc78297208",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-add-partial-text-touched-flags",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0004-clean-shadow-and-colors",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u53a7ff09-2228-81d3-8006-4b5ea964593b",
"~:created-at": "~m1749563833517",
"~:data": {
"~:pages": [
"~uc0939f58-37bc-805d-8006-51cc78297209"
],
"~:pages-index": {
"~uc0939f58-37bc-805d-8006-51cc78297209": {
"~: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": [
"~ub688a894-3697-80d3-8006-51cc8a55c2fd",
"~ub688a894-3697-80d3-8006-51ccce062cb3",
"~ub688a894-3697-80d3-8006-51ccfa2e6eeb"
]
}
},
"~ub688a894-3697-80d3-8006-51cc8a55c2fd": {
"~#shape": {
"~:y": 334,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 147,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 199,
"~:y": 334
}
},
{
"~#point": {
"~:x": 346,
"~:y": 334
}
},
{
"~#point": {
"~:x": 346,
"~:y": 464
}
},
{
"~#point": {
"~:x": 199,
"~:y": 464
}
}
],
"~: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": "~ub688a894-3697-80d3-8006-51cc8a55c2fd",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#0000ff",
"~:stroke-opacity": 0.5,
"~:stroke-alignment": "~:outer",
"~:stroke-width": 20
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#ff0000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:center",
"~:stroke-width": 5
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:inner",
"~:stroke-width": 10
}
],
"~:x": 199,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 199,
"~:y": 334,
"~:width": 147,
"~:height": 130,
"~:x1": 199,
"~:y1": 334,
"~:x2": 346,
"~:y2": 464
}
},
"~:fills": [
{
"~:fill-color": "#B1B2B5",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 130,
"~:flip-y": null
}
},
"~ub688a894-3697-80d3-8006-51ccce062cb3": {
"~#shape": {
"~:y": 334,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Ellipse",
"~:width": 130,
"~:type": "~:circle",
"~:points": [
{
"~#point": {
"~:x": 512.9999961853027,
"~:y": 334
}
},
{
"~#point": {
"~:x": 642.9999961853027,
"~:y": 334
}
},
{
"~#point": {
"~:x": 642.9999961853027,
"~:y": 459
}
},
{
"~#point": {
"~:x": 512.9999961853027,
"~:y": 459
}
}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ub688a894-3697-80d3-8006-51ccce062cb3",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#0000ff",
"~:stroke-opacity": 0.5,
"~:stroke-alignment": "~:outer",
"~:stroke-width": 20
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#ff0000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:center",
"~:stroke-width": 5
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:inner",
"~:stroke-width": 10
}
],
"~:x": 512.9999961853027,
"~:proportion": 1,
"~:selrect": {
"~#rect": {
"~:x": 512.9999961853027,
"~:y": 334,
"~:width": 130,
"~:height": 125,
"~:x1": 512.9999961853027,
"~:y1": 334,
"~:x2": 642.9999961853027,
"~:y2": 459
}
},
"~:fills": [
{
"~:fill-color": "#B1B2B5",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 125,
"~:flip-y": null
}
},
"~ub688a894-3697-80d3-8006-51ccfa2e6eeb": {
"~#shape": {
"~:y": null,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAACm6Y9DAAAJRAMAAADAyXlD4wIQRCW0I0NPCiJE9sBWQzqOK0QDAAAA4+aEQyUSNUS++9dDuQojRDj3xUPoxhlEAwAAALLys0MYgxBEpumPQwAACUSm6Y9DAAAJRA=="
},
"~:name": "Path",
"~:width": null,
"~:type": "~:path",
"~:points": [
{
"~#point": {
"~:x": 198.9999999038273,
"~:y": 547.9999675750732
}
},
{
"~#point": {
"~:x": 401.00001319474995,
"~:y": 547.9999675750732
}
},
{
"~#point": {
"~:x": 401.00001319474995,
"~:y": 696.9999543199095
}
},
{
"~#point": {
"~:x": 198.9999999038273,
"~:y": 696.9999543199095
}
}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ub688a894-3697-80d3-8006-51ccfa2e6eeb",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#0000ff",
"~:stroke-opacity": 0.5,
"~:stroke-alignment": "~:outer",
"~:stroke-width": 20
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#ff0000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:center",
"~:stroke-width": 5
},
{
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-alignment": "~:inner",
"~:stroke-width": 10
}
],
"~:x": null,
"~:proportion": 1,
"~:selrect": {
"~#rect": {
"~:x": 198.9999999038273,
"~:y": 547.9999675750732,
"~:width": 202.00001329092265,
"~:height": 148.9999867448363,
"~:x1": 198.9999999038273,
"~:y1": 547.9999675750732,
"~:x2": 401.00001319474995,
"~:y2": 696.9999543199095
}
},
"~:fills": [],
"~:flip-x": null,
"~:height": null,
"~:flip-y": null
}
}
},
"~:id": "~uc0939f58-37bc-805d-8006-51cc78297209",
"~:name": "Page 1"
}
},
"~:id": "~uc0939f58-37bc-805d-8006-51cc78297208",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,779 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u5d1327cf-3054-8111-8005-328a160ff966",
"~: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": "Exif rotated fills",
"~:revn": 17,
"~:modified-at": "~m1750761275050",
"~:vern": 0,
"~:id": "~u27270c45-35b4-80f3-8006-63a3912bdce8",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-clean-shadow-and-colors",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u5d1327cf-3054-8111-8005-340b8ba38a69",
"~:created-at": "~m1750761070908",
"~:data": {
"~:pages": [
"~u27270c45-35b4-80f3-8006-63a3912bdce9"
],
"~:pages-index": {
"~u27270c45-35b4-80f3-8006-63a3912bdce9": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 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,
"~: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": [
"~u8ae169c2-73c6-809f-8006-63a3d429cea3",
"~u8ae169c2-73c6-809f-8006-63a394f96940",
"~u8ae169c2-73c6-809f-8006-63a3ef35c521",
"~u8ae169c2-73c6-809f-8006-63a40defed29"
]
}
},
"~u8ae169c2-73c6-809f-8006-63a394f96940": {
"~#shape": {
"~:y": -119,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 1044,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": -2211,
"~:y": -119
}
},
{
"~#point": {
"~:x": -1167,
"~:y": -119
}
},
{
"~#point": {
"~:x": -1167,
"~:y": 577
}
},
{
"~#point": {
"~:x": -2211,
"~:y": 577
}
}
],
"~:r2": 0,
"~:layout-item-h-sizing": "~:fix",
"~:proportion-lock": true,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:r3": 0,
"~:r1": 0,
"~:id": "~u8ae169c2-73c6-809f-8006-63a394f96940",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": -2211,
"~:proportion": 1.5,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": -2211,
"~:y": -119,
"~:width": 1044,
"~:height": 696,
"~:x1": -2211,
"~:y1": -119,
"~:x2": -1167,
"~:y2": 577
}
},
"~:fills": [
{
"~:fill-opacity": 1,
"~:fill-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a39cf292e7",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:flip-x": null,
"~:height": 696,
"~:flip-y": null
}
},
"~u8ae169c2-73c6-809f-8006-63a3d429cea3": {
"~#shape": {
"~:y": -119,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 1044,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": -1059,
"~:y": -119
}
},
{
"~#point": {
"~:x": -15,
"~:y": -119
}
},
{
"~#point": {
"~:x": -15,
"~:y": 577
}
},
{
"~#point": {
"~:x": -1059,
"~:y": 577
}
}
],
"~:r2": 0,
"~:layout-item-h-sizing": "~:fix",
"~:proportion-lock": true,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:r3": 0,
"~:r1": 0,
"~:id": "~u8ae169c2-73c6-809f-8006-63a3d429cea3",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:inner",
"~:stroke-width": 200,
"~:stroke-opacity": 1,
"~:stroke-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a3ea82557f",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:x": -1059,
"~:proportion": 1.5,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": -1059,
"~:y": -119,
"~:width": 1044,
"~:height": 696,
"~:x1": -1059,
"~:y1": -119,
"~:x2": -15,
"~:y2": 577
}
},
"~:fills": [],
"~:flip-x": null,
"~:height": 696,
"~:flip-y": null
}
},
"~u8ae169c2-73c6-809f-8006-63a3ef35c521": {
"~#shape": {
"~:y": 577,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~:type": "root",
"~: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",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "1500",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-opacity": 1,
"~:fill-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a41d147866",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:font-family": "sourcesanspro",
"~:text": "X"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "9nfs8",
"~:font-size": "1500",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-opacity": 1,
"~:fill-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a41d147866",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:font-family": "sourcesanspro"
}
]
}
]
},
"~:hide-in-viewer": false,
"~:name": "X",
"~:width": 770,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": -2211,
"~:y": 577
}
},
{
"~#point": {
"~:x": -1441,
"~:y": 577
}
},
{
"~#point": {
"~:x": -1441,
"~:y": 2377
}
},
{
"~#point": {
"~:x": -2211,
"~:y": 2377
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:id": "~u8ae169c2-73c6-809f-8006-63a3ef35c521",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~#rect": {
"~:y": 2448,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "1500px",
"~:font-weight": "400",
"~:y1": -71,
"~:width": 769.046875,
"~:text-decoration": "none solid rgb(0, 0, 0)",
"~:letter-spacing": "normal",
"~:x": -2211,
"~:x1": 0,
"~:y2": 1871,
"~:fills": [
{
"~:fill-opacity": 1,
"~:fill-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a41d147866",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:x2": 769.046875,
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 1942,
"~:text": "X"
}
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": -2211,
"~:selrect": {
"~#rect": {
"~:x": -2211,
"~:y": 577,
"~:width": 770,
"~:height": 1800,
"~:x1": -2211,
"~:y1": 577,
"~:x2": -1441,
"~:y2": 2377
}
},
"~:flip-x": null,
"~:height": 1800,
"~:flip-y": null
}
},
"~u8ae169c2-73c6-809f-8006-63a40defed29": {
"~#shape": {
"~:y": 577,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~:type": "root",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "1500",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#B1B2B5",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "X"
}
],
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "9nfs8",
"~:font-size": "1500",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#B1B2B5",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
]
},
"~:hide-in-viewer": false,
"~:name": "X",
"~:width": 770,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": -1059,
"~:y": 577
}
},
{
"~#point": {
"~:x": -289,
"~:y": 577
}
},
{
"~#point": {
"~:x": -289,
"~:y": 2377
}
},
{
"~#point": {
"~:x": -1059,
"~:y": 2377
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:id": "~u8ae169c2-73c6-809f-8006-63a40defed29",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~#rect": {
"~:y": 2448,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "1500px",
"~:font-weight": "400",
"~:y1": -71,
"~:width": 769.046875,
"~:text-decoration": "none solid rgb(177, 178, 181)",
"~:letter-spacing": "normal",
"~:x": -1059,
"~:x1": 0,
"~:y2": 1871,
"~:fills": [
{
"~:fill-color": "#B1B2B5",
"~:fill-opacity": 1
}
],
"~:x2": 769.046875,
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 1942,
"~:text": "X"
}
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:outer",
"~:stroke-width": 100,
"~:stroke-opacity": 1,
"~:stroke-image": {
"~:id": "~u27270c45-35b4-80f3-8006-63a43dc4984b",
"~:width": 1200,
"~:height": 1800,
"~:mtype": "image/jpeg",
"~:name": "Landscape_6.jpg",
"~:keep-aspect-ratio": true
}
}
],
"~:x": -1059,
"~:selrect": {
"~#rect": {
"~:x": -1059,
"~:y": 577,
"~:width": 770,
"~:height": 1800,
"~:x1": -1059,
"~:y1": 577,
"~:x2": -289,
"~:y2": 2377
}
},
"~:flip-x": null,
"~:height": 1800,
"~:flip-y": null
}
}
},
"~:id": "~u27270c45-35b4-80f3-8006-63a3912bdce9",
"~:name": "Page 1"
}
},
"~:id": "~u27270c45-35b4-80f3-8006-63a3912bdce8",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,486 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~: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": "Text: Custom Fonts",
"~:revn": 13,
"~:modified-at": "~m1750151641034",
"~:vern": 0,
"~:id": "~u434b0541-fa2f-802f-8006-59827d964a9b",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-clean-shadow-and-colors",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u53a7ff09-2228-81d3-8006-4b5ea964593b",
"~:created-at": "~m1750081311326",
"~:data": {
"~:pages": [
"~u434b0541-fa2f-802f-8006-59827d964a9c"
],
"~:pages-index": {
"~u434b0541-fa2f-802f-8006-59827d964a9c": {
"~: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": [
"~u7d85a63e-18e7-809f-8006-59827fe8501e",
"~u7d85a63e-18e7-809f-8006-59833ef5fcef"
]
}
},
"~u7d85a63e-18e7-809f-8006-59827fe8501e": {
"~#shape": {
"~:y": 451.9999962296588,
"~: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": "xgmgu1frox",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "custom-7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:key": "ee7vl7klqs",
"~:font-size": "72",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "normal-400",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "\"Nodesto Caps Condensed\"",
"~:text": "Penpot & Dragons"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "center",
"~:font-id": "custom-7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:key": "17bt2f4evfs",
"~:font-size": "72",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "normal-400",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "\"Nodesto Caps Condensed\""
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Penpot & Dragons",
"~:width": 403.99995992417394,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 744.0000211580308,
"~:y": 451.9999962296588
}
},
{
"~#point": {
"~:x": 1147.9999810822046,
"~:y": 451.9999962296588
}
},
{
"~#point": {
"~:x": 1147.9999810822046,
"~:y": 537.9999971833331
}
},
{
"~#point": {
"~:x": 744.0000211580308,
"~:y": 537.9999971833331
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u7d85a63e-18e7-809f-8006-59827fe8501e",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 744.0000211580307,
"~:selrect": {
"~#rect": {
"~:x": 744.0000211580307,
"~:y": 451.9999962296588,
"~:width": 403.99995992417394,
"~:height": 86.00000095367432,
"~:x1": 744.0000211580307,
"~:y1": 451.9999962296588,
"~:x2": 1147.9999810822046,
"~:y2": 537.9999971833331
}
},
"~:flip-x": null,
"~:height": 86.00000095367432,
"~:flip-y": null
}
},
"~u7d85a63e-18e7-809f-8006-59833ef5fcef": {
"~#shape": {
"~:y": 537.9999971833331,
"~: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": "xgmgu1frox",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "custom-7d85a63e-18e7-809f-8006-59832d696634",
"~:key": "ee7vl7klqs",
"~:font-size": "36",
"~:font-weight": "500",
"~:typography-ref-file": null,
"~:font-variant-id": "normal-500",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "\"Mr Eaves SC Remake\"",
"~:text": "Lorem Ipsum Dolors Sit Amet"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "center",
"~:font-id": "custom-7d85a63e-18e7-809f-8006-59832d696634",
"~:key": "17bt2f4evfs",
"~:font-size": "0",
"~:font-weight": "500",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "normal-500",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "\"Mr Eaves SC Remake\""
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Penpot & Dragons",
"~:width": 466.0000131576671,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 712.9999941849438,
"~:y": 537.9999971833331
}
},
{
"~#point": {
"~:x": 1179.0000073426108,
"~:y": 537.9999971833331
}
},
{
"~#point": {
"~:x": 1179.0000073426108,
"~:y": 580.9999976601703
}
},
{
"~#point": {
"~:x": 712.9999941849438,
"~:y": 580.9999976601703
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u7d85a63e-18e7-809f-8006-59833ef5fcef",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 712.9999941849437,
"~:selrect": {
"~#rect": {
"~:x": 712.9999941849437,
"~:y": 537.9999971833331,
"~:width": 466.0000131576671,
"~:height": 43.00000047683716,
"~:x1": 712.9999941849437,
"~:y1": 537.9999971833331,
"~:x2": 1179.0000073426108,
"~:y2": 580.9999976601703
}
},
"~:flip-x": null,
"~:height": 43.00000047683716,
"~:flip-y": null
}
}
},
"~:id": "~u434b0541-fa2f-802f-8006-59827d964a9c",
"~:name": "Page 1"
}
},
"~:id": "~u434b0541-fa2f-802f-8006-59827d964a9b",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,791 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a",
"~: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": "garden",
"~:revn": 26,
"~:modified-at": "~m1750423208667",
"~:vern": 0,
"~:id": "~u6bd7c17d-4f59-815e-8006-5e999f38f210",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-clean-shadow-and-colors",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u6bd7c17d-4f59-815e-8006-5c1f68846e43",
"~:created-at": "~m1750422919396",
"~:data": {
"~:pages": [
"~u6bd7c17d-4f59-815e-8006-5e999f38f211"
],
"~:pages-index": {
"~u6bd7c17d-4f59-815e-8006-5e999f38f211": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 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,
"~: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": [
"~uef609b51-0d34-80f3-8006-5e99c014febd"
]
}
},
"~uef609b51-0d34-80f3-8006-5e99a0e7e241": {
"~#shape": {
"~:y": 224.0000021457672,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "24e85t84f3p",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1vetvwgrfb6",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "▫️▫️🌲▫️🌲🌲🌲▫️🌲🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "r0535lnzdr",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1yug53qv91w",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲🐛🌲🌲▫️🌲🌲🌲🌲🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "2aqkfsbxb5i",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "22yly6s8yv3",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲🌲▫️🌲▫️🌲🌲🌰🌲🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "q9ovldxs6h",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "2e29fo2vfyu",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲🌲▫️🌲▫️🌲🌲▫️🌲▫️"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "1f8krcpsg8l",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1ehkqv5vril",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "▫️▫️▫️🐌🌲🍁🌲▫️🥕🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "kikos098xa",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "2cxzm7orynt",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲🌲🐰🌲▫️▫️🌲🌲🌲▫️"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "so4z3gbyhs",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1ey304k5xqb",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲🌲🌲🥕☁️🌲🐰▫️🌲🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "1orh5xhi3o3",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "8aout8mor6",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "▫️🌲▫️▫️🌲▫️🌲🌲▫️🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "lir8cs117z",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1iqonahtkum",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "🌲▫️🌲▫️🌲▫️▫️🌲▫️🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "2urfb0xejy",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
},
{
"~:line-height": "1.2",
"~:font-style": "",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "",
"~:typography-ref-id": null,
"~:text-transform": "",
"~:font-id": "",
"~:key": "1e06otc9bbq",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": "",
"~:text": "▫️🌲▫️▫️🌲▫️🌲▫️🍃🌲"
}
],
"~:typography-ref-id": null,
"~:text-transform": "",
"~:text-align": "",
"~:font-id": "",
"~:key": "1t55y3u9pg3",
"~:font-size": "16",
"~:font-weight": "",
"~:typography-ref-file": null,
"~:text-direction": "",
"~:type": "paragraph",
"~:font-variant-id": "",
"~:text-decoration": "",
"~:letter-spacing": "",
"~:fills": null,
"~:font-family": ""
}
]
}
],
"~:vertical-align": ""
},
"~:name": "▫️▫️🌲▫️🌲🌲🌲▫️🌲🌲🌲🐛🌲🌲▫️🌲🌲🌲🌲🌲🌲🌲▫️🌲▫️🌲🌲🌰🌲🌲🌲🌲▫️🌲▫️🌲🌲▫️🌲▫️▫️▫️▫️🐌🌲🍁🌲▫️🥕🌲🌲🌲🐰🌲▫️▫️🌲🌲🌲▫️🌲🌲🌲🥕☁️🌲🐰▫️🌲🌲▫️🌲▫️▫️🌲▫️🌲🌲▫️🌲🌲▫️🌲▫️🌲▫️▫️🌲▫️🌲▫️🌲▫️▫️🌲▫️🌲▫️🍃🌲",
"~:width": 200.00000894069672,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 224.99999487400055,
"~:y": 224.0000021457672
}
},
{
"~#point": {
"~:x": 425.00000381469727,
"~:y": 224.0000021457672
}
},
{
"~#point": {
"~:x": 425.00000381469727,
"~:y": 414.0000021457672
}
},
{
"~#point": {
"~:x": 224.99999487400055,
"~:y": 414.0000021457672
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~uef609b51-0d34-80f3-8006-5e99a0e7e241",
"~:parent-id": "~uef609b51-0d34-80f3-8006-5e99c014febd",
"~:frame-id": "~uef609b51-0d34-80f3-8006-5e99c014febd",
"~:x": 224.99999487400055,
"~:selrect": {
"~#rect": {
"~:x": 224.99999487400055,
"~:y": 224.0000021457672,
"~:width": 200.00000894069672,
"~:height": 190,
"~:x1": 224.99999487400055,
"~:y1": 224.0000021457672,
"~:x2": 425.00000381469727,
"~:y2": 414.0000021457672
}
},
"~:flip-x": null,
"~:height": 190,
"~:flip-y": null
}
},
"~uef609b51-0d34-80f3-8006-5e99c014febd": {
"~#shape": {
"~:y": 194.00000454845173,
"~:hide-fill-on-export": false,
"~:layout-gap-type": "~:multiple",
"~:layout-padding": {
"~:p1": 18.999997597315485,
"~:p2": 13.999998715849017,
"~:p3": 18.999997597315485,
"~:p4": 13.999998715849017
},
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:layout-wrap-type": "~:nowrap",
"~:layout": "~:flex",
"~:hide-in-viewer": false,
"~:name": "Garden",
"~:layout-align-items": "~:center",
"~:width": 249.99999881089417,
"~:layout-padding-type": "~:simple",
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 199.99999615815156,
"~:y": 194.00000454845173
}
},
{
"~#point": {
"~:x": 449.99999496904576,
"~:y": 194.00000454845173
}
},
{
"~#point": {
"~:x": 449.99999496904576,
"~:y": 444.00000708125077
}
},
{
"~#point": {
"~:x": 199.99999615815156,
"~:y": 444.00000708125077
}
}
],
"~:r2": 0,
"~:layout-item-h-sizing": "~:fix",
"~:proportion-lock": false,
"~:layout-gap": {
"~:row-gap": 0,
"~:column-gap": 0
},
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:r3": 0,
"~:layout-justify-content": "~:center",
"~:r1": 0,
"~:id": "~uef609b51-0d34-80f3-8006-5e99c014febd",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:layout-flex-dir": "~:row",
"~:layout-align-content": "~:stretch",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 199.99999615815153,
"~:proportion": 1,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 199.99999615815153,
"~:y": 194.00000454845173,
"~:width": 249.99999881089417,
"~:height": 250.00000253279904,
"~:x1": 199.99999615815153,
"~:y1": 194.00000454845173,
"~:x2": 449.9999949690457,
"~:y2": 444.00000708125077
}
},
"~:fills": [
{
"~:fill-color": "#939a85",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 250.00000253279904,
"~:flip-y": null,
"~:shapes": [
"~uef609b51-0d34-80f3-8006-5e99a0e7e241"
]
}
}
},
"~:id": "~u6bd7c17d-4f59-815e-8006-5e999f38f211",
"~:name": "Page 1"
}
},
"~:id": "~u6bd7c17d-4f59-815e-8006-5e999f38f210",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,420 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~: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": "Text: Google Fonts",
"~:revn": 9,
"~:modified-at": "~m1750150559868",
"~:vern": 0,
"~:id": "~u434b0541-fa2f-802f-8006-5981e47bd732",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-clean-shadow-and-colors",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u53a7ff09-2228-81d3-8006-4b5ea964593b",
"~:created-at": "~m1750081154582",
"~:data": {
"~:pages": [
"~u434b0541-fa2f-802f-8006-5981e47bd733"
],
"~:pages-index": {
"~u434b0541-fa2f-802f-8006-5981e47bd733": {
"~: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": [
"~u83b40135-4e19-8020-8006-598226885685"
]
}
},
"~u83b40135-4e19-8020-8006-598226885685": {
"~#shape": {
"~:y": 399.99997901916055,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~:type": "root",
"~:key": "u3udm63y8h",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "gfont-eb-garamond",
"~:key": "hjb7822r9z",
"~:font-size": "36",
"~: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": "\"EB Garamond\"",
"~:text": "This is an example text"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "gfont-eb-garamond",
"~:key": "2dka2139qgl",
"~:font-size": "36",
"~: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": "\"EB Garamond\""
},
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "gfont-eb-garamond",
"~:key": "26bngh5on3d",
"~:font-size": "36",
"~: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": "\"EB Garamond\"",
"~:text": "with "
},
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "gfont-fira-code",
"~:key": "23oemgkf8le",
"~:font-size": "36",
"~: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": "\"Fira Code\"",
"~:text": "Google Fonts"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "gfont-fira-code",
"~:key": "1dxk7oihnqh",
"~:font-size": "36",
"~: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": "\"Fira Code\""
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "This is an example textwith Google Fonts",
"~:width": 325.00011493483566,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 680.9999634764606,
"~:y": 399.9999790191605
}
},
{
"~#point": {
"~:x": 1006.0000784112963,
"~:y": 399.9999790191605
}
},
{
"~#point": {
"~:x": 1006.0000784112963,
"~:y": 658.0000126361805
}
},
{
"~#point": {
"~:x": 680.9999634764606,
"~:y": 658.0000126361805
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u83b40135-4e19-8020-8006-598226885685",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 680.9999634764606,
"~:selrect": {
"~#rect": {
"~:x": 680.9999634764606,
"~:y": 399.99997901916055,
"~:width": 325.00011493483566,
"~:height": 258.00003361702,
"~:x1": 680.9999634764606,
"~:y1": 399.99997901916055,
"~:x2": 1006.0000784112963,
"~:y2": 658.0000126361806
}
},
"~:flip-x": null,
"~:height": 258.00003361702,
"~:flip-y": null
}
}
},
"~:id": "~u434b0541-fa2f-802f-8006-5981e47bd733",
"~:name": "Page 1"
}
},
"~:id": "~u434b0541-fa2f-802f-8006-5981e47bd732",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,349 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u1091e979-bbec-8194-8005-f7aa420b5660",
"~: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": "simple-text",
"~:revn": 7,
"~:modified-at": "~m1749629891313",
"~:vern": 0,
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
"~: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",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content",
"0004-clean-shadow-and-colors",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-opacity"
]
},
"~:version": 67,
"~:project-id": "~u1091e979-bbec-8194-8005-f7aa420b8b07",
"~:created-at": "~m1749629823499",
"~:data": {
"~:pages": [
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73"
],
"~:pages-index": {
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73": {
"~: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": [
"~u7274a6af-66db-8009-8006-52c837bed25d"
]
}
},
"~u7274a6af-66db-8009-8006-52c837bed25d": {
"~#shape": {
"~:y": 368.000005463652,
"~: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": "13hr3ftth2o",
"~: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": "1qm8gi1rphc",
"~:font-size": "48",
"~: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": "this is a text"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "r8gahivbg7",
"~:font-size": "48",
"~: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": "this is a text",
"~:width": 237.0000390021974,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 414.9999714372273,
"~:y": 368.000005463652
}
},
{
"~#point": {
"~:x": 652.0000104394247,
"~:y": 368.000005463652
}
},
{
"~#point": {
"~:x": 652.0000104394247,
"~:y": 426.0000039162686
}
},
{
"~#point": {
"~:x": 414.9999714372273,
"~:y": 426.0000039162686
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:layout-item-v-sizing": "~:fix",
"~:id": "~u7274a6af-66db-8009-8006-52c837bed25d",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 414.9999714372274,
"~:selrect": {
"~#rect": {
"~:x": 414.9999714372274,
"~:y": 368.000005463652,
"~:width": 237.0000390021974,
"~:height": 57.99999845261664,
"~:x1": 414.9999714372274,
"~:y1": 368.000005463652,
"~:x2": 652.0000104394248,
"~:y2": 426.0000039162686
}
},
"~:flip-x": null,
"~:height": 57.99999845261664,
"~:flip-y": null
}
}
},
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73",
"~:name": "Page 1"
}
},
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,106 @@
[
{
"~:font-style": "normal",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:font-weight": 400,
"~:ttf-file-id": "~u69e76833-0816-49fa-8c7b-4b97c71c6f1a",
"~:modified-at": "~m1750081452108",
"~:otf-file-id": "~uf7ea405b-73be-40d7-8ce4-fbf734696997",
"~:id": "~u434b0541-fa2f-802f-8006-5983078ad50e",
"~:woff1-file-id": "~uaef0b0c5-56de-47c7-bd99-fb5e249ccef9",
"~:created-at": "~m1750081452108",
"~:font-family": "Nodesto Caps Condensed"
},
{
"~:font-style": "italic",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:font-weight": 400,
"~:ttf-file-id": "~uc45955ee-8c16-47ce-a89e-1a2faadb4178",
"~:modified-at": "~m1750081452589",
"~:otf-file-id": "~ucc0a799d-34ed-4829-a0de-6d7efec8202c",
"~:id": "~u434b0541-fa2f-802f-8006-59830795b436",
"~:woff1-file-id": "~u9940a178-833e-4a4c-8bc3-64ee09472ea6",
"~:created-at": "~m1750081452589",
"~:font-family": "Nodesto Caps Condensed"
},
{
"~:font-style": "normal",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:font-weight": 700,
"~:ttf-file-id": "~u5c59fbdc-46de-456f-8da6-0dcd916e95c1",
"~:modified-at": "~m1750081452631",
"~:otf-file-id": "~uadde63b1-c9f4-484a-bf1c-1121b77d751d",
"~:id": "~u434b0541-fa2f-802f-8006-598307a05c77",
"~:woff1-file-id": "~ucea20394-48af-41a0-8d0f-ada497a5ffe3",
"~:created-at": "~m1750081452631",
"~:font-family": "Nodesto Caps Condensed"
},
{
"~:font-style": "italic",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-5983057a9b7c",
"~:font-weight": 700,
"~:ttf-file-id": "~u5060a5c4-c7c3-44ea-aa0e-5797d6647ec0",
"~:modified-at": "~m1750081452674",
"~:otf-file-id": "~u1646d986-88d1-4904-910d-663da9e35eef",
"~:id": "~u434b0541-fa2f-802f-8006-598307a94233",
"~:woff1-file-id": "~ue7691712-70d1-49d9-a596-7b4ac495ff15",
"~:created-at": "~m1750081452674",
"~:font-family": "Nodesto Caps Condensed"
},
{
"~:font-style": "normal",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-59831351af03",
"~:font-weight": 700,
"~:ttf-file-id": "~u3ccf58ee-731c-402f-8b26-5b5049abbe7f",
"~:modified-at": "~m1750081469706",
"~:otf-file-id": "~u96293106-78ff-4683-b819-c1c4c1e44705",
"~:id": "~u434b0541-fa2f-802f-8006-5983185366a8",
"~:woff1-file-id": "~uaf7a782a-c96b-4926-a08a-c6a81a1726dc",
"~:created-at": "~m1750081469706",
"~:font-family": "Bookinsanity Remake"
},
{
"~:font-style": "normal",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-59831351af03",
"~:font-weight": 400,
"~:ttf-file-id": "~u78583622-dc5d-4780-990f-85afb4ad59e9",
"~:modified-at": "~m1750081469774",
"~:otf-file-id": "~ua5eb4175-91d0-4c61-a471-2d5d0648f8a6",
"~:id": "~u434b0541-fa2f-802f-8006-5983185cde2d",
"~:woff1-file-id": "~u0fcf15c2-59c5-4566-9dc9-9acaf280a518",
"~:created-at": "~m1750081469774",
"~:font-family": "Bookinsanity Remake"
},
{
"~:font-style": "italic",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-59831351af03",
"~:font-weight": 400,
"~:ttf-file-id": "~u517f4e27-b83f-4a6f-8753-8acaabc49ee9",
"~:modified-at": "~m1750081469812",
"~:otf-file-id": "~u12fe845f-afd2-453d-9ac6-6a899487ba9f",
"~:id": "~u434b0541-fa2f-802f-8006-598318671138",
"~:woff1-file-id": "~u53e0cb17-788e-4dae-aa7c-28f090f87968",
"~:created-at": "~m1750081469812",
"~:font-family": "Bookinsanity Remake"
},
{
"~:font-style": "normal",
"~:team-id": "~u04868522-3ebf-81e8-8006-306b0c9b5f59",
"~:font-id": "~u7d85a63e-18e7-809f-8006-59832d696634",
"~:font-weight": 500,
"~:ttf-file-id": "~u2d1ffeb6-e70b-4027-bbcc-910248ba45f8",
"~:modified-at": "~m1750081492609",
"~:otf-file-id": "~ubcc136ac-da48-4335-af5c-52abc4613490",
"~:id": "~u434b0541-fa2f-802f-8006-59832eb23bb4",
"~:woff1-file-id": "~u01338648-afcc-47aa-8aa7-600c9022bd9f",
"~:created-at": "~m1750081492609",
"~:font-family": "Mr Eaves SC Remake"
}
]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
[
{
"~:id": "~uf88e52d7-2b77-81fd-8006-23413fafe56c",
"~:name": "The Alpaca team",
"~:total-editors": 3,
"~:total-members": 3
},
{
"~:id": "~u81be1d05-a07b-81d5-8006-3e728bea76fb",
"~:name": "The Quokka team",
"~:total-editors": 1,
"~:total-members": 1
}
]

View File

@@ -0,0 +1,25 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:subscription": {
"~:quantity": 2,
"~:status": "canceled",
"~:type": "enterprise",
"~start-date": "~m1746444667"
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false
}
}

View File

@@ -0,0 +1,25 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:subscription": {
"~:quantity": 2,
"~:status": "trialing",
"~:type": "enterprise",
"~start-date": "~m1746444667"
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false
}
}

View File

@@ -0,0 +1,25 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:subscription": {
"~:quantity": 2,
"~:status": "trialing",
"~:type": "unlimited",
"~start-date": "~m1746444667"
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false
}
}

View File

@@ -0,0 +1,25 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:subscription": {
"~:quantity": 2,
"~:status": "unpaid",
"~:type": "unlimited",
"~start-date": "~m1746444667"
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false
}
}

View File

@@ -0,0 +1,4 @@
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:is-default": false
}

View File

@@ -0,0 +1,128 @@
[
{
"~:is-admin": true,
"~:email": "bar@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Han Solo",
"~:fullname": "Han Solo",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:created-at": "~m1733324626956"
},
{
"~:is-admin": true,
"~:email": "foo@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-owner": true,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "luke@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Luke Skywalker",
"~:fullname": "Luke Skywalker",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u3a1b2c3d-1234-5678-8001-abcdefabcdef",
"~:profile-id": "~u3a1b2c3d-1234-5678-8001-abcdefabcdef",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "chewie@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Chewbacca",
"~:fullname": "Chewbacca",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u4b2c3d4e-2345-6789-8002-bcdefabcdefa",
"~:profile-id": "~u4b2c3d4e-2345-6789-8002-bcdefabcdefa",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "lando@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Lando Calrissian",
"~:fullname": "Lando Calrissian",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u5c3d4e5f-3456-7890-8003-cdefabcdefab",
"~:profile-id": "~u5c3d4e5f-3456-7890-8003-cdefabcdefab",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "r2d2@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "R2-D2",
"~:fullname": "R2-D2",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u6d4e5f6a-4567-8901-8004-defabcdefabc",
"~:profile-id": "~u6d4e5f6a-4567-8901-8004-defabcdefabc",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "c3po@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "C-3PO",
"~:fullname": "C-3PO",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u7e5f6a7b-5678-9012-8005-efabcdefabcd",
"~:profile-id": "~u7e5f6a7b-5678-9012-8005-efabcdefabcd",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "ben@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Ben Kenobi",
"~:fullname": "Ben Kenobi",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u8f6a7b8c-6789-0123-8006-fabcdefabcde",
"~:profile-id": "~u8f6a7b8c-6789-0123-8006-fabcdefabcde",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "yoda@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Yoda",
"~:fullname": "Yoda",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u9a7b8c9d-7890-1234-8007-abcdefabcdef",
"~:profile-id": "~u9a7b8c9d-7890-1234-8007-abcdefabcdef",
"~:created-at": "~m1713533116365"
}
]

View File

@@ -0,0 +1,44 @@
[
{
"~:is-admin": true,
"~:email": "bar@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Han Solo",
"~:fullname": "Han Solo",
"~:is-owner": true,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:created-at": "~m1733324626956"
},
{
"~:is-admin": true,
"~:email": "foo@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "luke@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Luke Skywalker",
"~:fullname": "Luke Skywalker",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u123456789-0000-0000-0000-abcdefabcdef",
"~:profile-id": "~u123456789-0000-0000-0000-abcdefabcdef",
"~:created-at": "~m1713533116365"
}
]

View File

@@ -0,0 +1,44 @@
[
{
"~:is-admin": true,
"~:email": "bar@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Han Solo",
"~:fullname": "Han Solo",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:created-at": "~m1733324626956"
},
{
"~:is-admin": true,
"~:email": "foo@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-owner": true,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
"~:created-at": "~m1713533116365"
},
{
"~:is-admin": false,
"~:email": "luke@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Luke Skywalker",
"~:fullname": "Luke Skywalker",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u123456789-0000-0000-0000-abcdefabcdef",
"~:profile-id": "~u123456789-0000-0000-0000-abcdefabcdef",
"~:created-at": "~m1713533116365"
}
]

View File

@@ -0,0 +1,28 @@
[{
"~: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
},
"~:subscription": {
"~:type": "enterprise",
"~:status": "trialing",
"~:seats": null
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:created-at": "~m1713533116375",
"~:is-default": true
}]

View File

@@ -0,0 +1,53 @@
[
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:owner",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
},
{
"~: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
},
"~:subscription": {
"~:type": "professional",
"~:status": "active",
"~:seats": null
},
"~:name": "Second team",
"~:modified-at": "~m1701164272671",
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:created-at": "~m1701164272671",
"~:is-default": false
}
]

View File

@@ -0,0 +1,28 @@
[{
"~: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
},
"~:subscription": {
"~:type": "unlimited",
"~:status": "trialing",
"~:seats": 2
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:created-at": "~m1713533116375",
"~:is-default": true
}]

View File

@@ -0,0 +1,53 @@
[
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:owner",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
},
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": false,
"~:is-admin": true,
"~:can-edit": true
},
"~:subscription": {
"~:type": "unlimited",
"~:status": "trialing",
"~:seats": 2
},
"~:name": "Second team",
"~:modified-at": "~m1701164272671",
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:created-at": "~m1701164272671",
"~:is-default": false
}
]

View File

@@ -0,0 +1,53 @@
[
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:owner",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
},
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:owner",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:subscription": {
"~:type": "unlimited",
"~:status": "trialing",
"~:seats": 2
},
"~:name": "Second team",
"~:modified-at": "~m1701164272671",
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:created-at": "~m1701164272671",
"~:is-default": false
}
]

View File

@@ -23,6 +23,65 @@ export class BasePage {
);
}
static async mockFileMediaAsset(page, assetId, assetFilename, options) {
const ids = Array.isArray(assetId) ? assetId : [assetId];
for (const id of ids) {
const url = `**/assets/by-file-media-id/${id}`;
await page.route(url, (route) =>
route.fulfill({
path: `playwright/data/${assetFilename}`,
status: 200,
...options,
}),
);
}
}
static async mockAsset(page, assetId, assetFilename, options) {
const ids = Array.isArray(assetId) ? assetId : [assetId];
for (const id of ids) {
const url = `**/assets/by-id/${id}`;
await page.route(url, (route) =>
route.fulfill({
path: `playwright/data/${assetFilename}`,
status: 200,
...options,
}),
);
}
}
static async mockFileMediaAsset(page, assetId, assetFilename, options) {
const ids = Array.isArray(assetId) ? assetId : [assetId];
for (const id of ids) {
const url = `**/assets/by-file-media-id/${id}`;
await page.route(url, (route) =>
route.fulfill({
path: `playwright/data/${assetFilename}`,
status: 200,
...options,
}),
);
}
}
static async mockConfigFlags(page, flags) {
const url = "**/js/config.js?ts=*";
return await page.route(url, (route) =>
route.fulfill({
status: 200,
contentType: "application/javascript",
body: `var penpotFlags = "${flags.join(" ")}";`,
}),
);
}
#page = null;
constructor(page) {
@@ -38,15 +97,21 @@ export class BasePage {
}
async mockConfigFlags(flags) {
const url = "**/js/config.js?ts=*";
return await this.page.route(url, (route) =>
route.fulfill({
status: 200,
contentType: "application/javascript",
body: `var penpotFlags = "${flags.join(" ")}";`,
}),
return BasePage.mockConfigFlags(this.page, flags);
}
async mockFileMediaAsset(assetId, assetFilename, options) {
return BasePage.mockFileMediaAsset(
this.page,
assetId,
assetFilename,
options,
);
}
async mockAsset(assetId, assetFilename, options) {
return BasePage.mockAsset(this.page, assetId, assetFilename, options);
}
}
export default BasePage;

View File

@@ -0,0 +1,35 @@
import { BasePage } from "./BasePage";
export class RegisterPage extends BasePage {
constructor(page) {
super(page);
this.registerButton = page.getByRole("button", { name: "Create an account" });
this.password = page.getByLabel("Password");
this.email = page.getByLabel("Work email");
this.fullName = page.getByLabel("Full name");
}
async fillRegisterFormInputs(name, email, password) {
await this.fullName.fill(name);
await this.email.fill(email);
await this.password.fill(password);
}
async clickRegisterButton() {
await this.registerButton.click();
}
async setupMismatchedEmailError() {
await this.mockRPC(
"prepare-register-profile",
"register/prepare-register-profile-email-mismatch.json",
{ status: 400 },
);
}
static async initWithLoggedOutUser(page) {
await this.mockRPC(page, "get-profile", "get-profile-anonymous.json");
}
}
export default RegisterPage;

View File

@@ -0,0 +1,30 @@
import { expect } from "@playwright/test";
import { DashboardPage } from "./DashboardPage";
export class SubscriptionProfilePage extends DashboardPage {
static async init(page) {
await DashboardPage.initWebSockets(page);
await DashboardPage.mockRPC(
page,
"get-owned-teams",
"subscription/get-owned-teams.json",
);
}
constructor(page) {
super(page);
this.mainHeading = page.getByRole("heading", {
name: "Subscription",
level: 2,
});
}
async goToSubscriptions() {
await this.page.goto(`#/settings/subscriptions`);
await expect(this.mainHeading).toBeVisible();
}
}
export default SubscriptionProfilePage;

View File

@@ -0,0 +1,62 @@
import { expect } from "@playwright/test";
import { WorkspacePage } from "./WorkspacePage";
export class WasmWorkspacePage extends WorkspacePage {
static async init(page) {
await super.init(page);
await WorkspacePage.mockConfigFlags(page, [
"enable-feature-render-wasm",
"enable-render-wasm-dpr",
]);
await page.addInitScript(() => {
document.addEventListener("wasm:set-objects-finished", () => {
window.wasmSetObjectsFinished = true;
});
});
}
constructor(page) {
super(page);
this.canvas = page.getByTestId("canvas-wasm-shapes");
}
async waitForFirstRender(config = {}) {
const options = { hideUI: true, ...config };
await expect(this.pageName).toHaveText("Page 1");
if (options.hideUI) {
await this.hideUI();
}
await this.canvas.waitFor({ state: "visible" });
await this.page.waitForFunction(() => {
return window.wasmSetObjectsFinished;
});
}
async hideUI() {
await this.page.keyboard.press("\\");
await expect(this.pageName).not.toBeVisible();
}
static async mockGoogleFont(page, fontSlug, assetFilename, options = {}) {
const url = new RegExp(`/internal/gfonts/font/${fontSlug}`);
return await page.route(url, (route) =>
route.fulfill({
status: 200,
path: `playwright/data/${assetFilename}`,
contentType: "application/font-ttf",
...options,
}),
);
}
async mockGoogleFont(fontSlug, assetFilename, options) {
return WasmWorkspacePage.mockGoogleFont(
this.page,
fontSlug,
assetFilename,
options,
);
}
}

View File

@@ -99,7 +99,7 @@ export class WorkspacePage extends BaseWebSocketPage {
this.tokenThemeUpdateCreateModal = page.getByTestId(
"token-theme-update-create-modal",
);
this.tokenThemesSetsSidebar = page.getByTestId("token-themes-sets-sidebar");
this.tokenThemesSetsSidebar = page.getByTestId("token-management-sidebar");
this.tokensSidebar = page.getByTestId("tokens-sidebar");
this.tokenSetItems = page.getByTestId("tokens-set-item");
this.tokenSetGroupItems = page.getByTestId("tokens-set-group-item");
@@ -178,6 +178,14 @@ export class WorkspacePage extends BaseWebSocketPage {
);
}
async mockGetFile(jsonFile) {
await this.mockRPC(/get\-file\?/, jsonFile);
}
async mockGetAsset(regex, asset) {
await this.mockRPC(new RegExp(regex), asset);
}
async setupFileWithComments() {
await this.mockRPC(
"get-comment-threads?file-id=*",
@@ -309,3 +317,5 @@ export class WorkspacePage extends BaseWebSocketPage {
.click(clickOptions);
}
}
export default WorkspacePage;

View File

@@ -0,0 +1,140 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, [
"enable-feature-render-wasm",
"enable-render-wasm-dpr",
]);
});
test("Renders a file with basic shapes, boards and groups", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-shapes-groups-boards.json");
await workspace.goToWorkspace({
id: "53a7ff09-2228-81d3-8006-4b5eac177245",
pageId: "53a7ff09-2228-81d3-8006-4b5eac177246",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with solid, gradient and image fills", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"1ebcea38-f1bf-8101-8006-4c8fd68e7c84",
"1ebcea38-f1bf-8101-8006-4c8f579da49c",
],
"render-wasm/assets/penguins.jpg",
);
await workspace.mockGetFile("render-wasm/get-file-shapes-fills.json");
await workspace.goToWorkspace({
id: "1ebcea38-f1bf-8101-8006-4c8ec4a9bffe",
pageId: "1ebcea38-f1bf-8101-8006-4c8ec4a9bfff",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with strokes", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"202c1104-9385-81d3-8006-5074e4682cac",
"202c1104-9385-81d3-8006-5074c50339b6",
"202c1104-9385-81d3-8006-507560ce29e3",
],
"render-wasm/assets/penguins.jpg",
);
await workspace.mockGetFile("render-wasm/get-file-shapes-strokes.json");
await workspace.goToWorkspace({
id: "202c1104-9385-81d3-8006-507413ff2c99",
pageId: "202c1104-9385-81d3-8006-507413ff2c9a",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with mutliple strokes", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-multiple-strokes.json");
await workspace.goToWorkspace({
id: "c0939f58-37bc-805d-8006-51cc78297208",
pageId: "c0939f58-37bc-805d-8006-51cc78297209",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with shapes with multiple fills", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-multiple-fills.json");
await workspace.goToWorkspace({
id: "c0939f58-37bc-805d-8006-51cd3a51c255",
pageId: "c0939f58-37bc-805d-8006-51cd3a51c256",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
// TODO: update the screenshots for this test once Taiga #11325 is fixed
// https://tree.taiga.io/project/penpot/task/11325
test("Renders shapes taking into account blend modes", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-blend-modes.json");
await workspace.goToWorkspace({
id: "c0939f58-37bc-805d-8006-51cdf8e18e76",
pageId: "c0939f58-37bc-805d-8006-51cdf8e18e77",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders shapes with exif rotated images fills and strokes", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"27270c45-35b4-80f3-8006-63a39cf292e7",
"27270c45-35b4-80f3-8006-63a41d147866",
"27270c45-35b4-80f3-8006-63a43dc4984b",
"27270c45-35b4-80f3-8006-63a3ea82557f"
],
"render-wasm/assets/landscape.jpg",
);
await workspace.mockGetFile("render-wasm/get-file-shapes-exif-rotated-fills.json");
await workspace.goToWorkspace({
id: "27270c45-35b4-80f3-8006-63a3912bdce8",
pageId: "27270c45-35b4-80f3-8006-63a3912bdce9",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

View File

@@ -0,0 +1,315 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, [
"enable-feature-render-wasm",
"enable-render-wasm-dpr",
]);
});
async function mockGetEmojiFont(workspace) {
await workspace.mockGetAsset(
/notocoloremoji.*\.ttf$/,
"render-wasm/assets/notocoloremojisubset.ttf"
);
}
async function mockGetJapaneseFont(workspace) {
await workspace.mockGetAsset(
/notosansjp.*\.ttf$/,
"render-wasm/assets/notosansjpsubset.ttf"
);
await workspace.mockGetAsset(
/notosanssc.*\.ttf$/,
"render-wasm/assets/notosansjpsubset.ttf"
);
}
test("Renders a file with texts", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text.json");
await workspace.goToWorkspace({
id: "3b0d758a-8c9d-8013-8006-52c8337e5c72",
pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Updates a text font", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text.json");
await workspace.goToWorkspace({
id: "3b0d758a-8c9d-8013-8006-52c8337e5c72",
pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73",
});
await workspace.waitForFirstRender({ hideUI: false });
await workspace.clickLeafLayer("this is a text");
const fontStyle = workspace.page.getByTitle("Font Style");
await fontStyle.click();
const boldOption = fontStyle.getByText("bold").first();
await boldOption.click();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts that use google fonts", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text-google-fonts.json");
await workspace.mockGoogleFont(
"ebgaramond",
"render-wasm/assets/ebgaramond.ttf",
);
await workspace.mockGoogleFont("firacode", "render-wasm/assets/firacode.ttf");
await workspace.goToWorkspace({
id: "434b0541-fa2f-802f-8006-5981e47bd732",
pageId: "434b0541-fa2f-802f-8006-5981e47bd733",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts that use custom fonts", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text-custom-fonts.json");
await workspace.mockRPC(
"get-font-variants?team-id=*",
"render-wasm/get-font-variants-custom-fonts.json",
);
await workspace.mockAsset(
"2d1ffeb6-e70b-4027-bbcc-910248ba45f8",
"render-wasm/assets/mreaves.ttf",
);
await workspace.mockAsset(
"69e76833-0816-49fa-8c7b-4b97c71c6f1a",
"render-wasm/assets/nodesto-condensed.ttf",
);
await workspace.goToWorkspace({
id: "434b0541-fa2f-802f-8006-59827d964a9b",
pageId: "434b0541-fa2f-802f-8006-59827d964a9c",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with styled texts", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text-styles.json");
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5c2559af4939",
pageId: "6bd7c17d-4f59-815e-8006-5c2559af493a",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts with images", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"6bd7c17d-4f59-815e-8006-5e9765e0fabd",
"6bd7c17d-4f59-815e-8006-5e97441071cc"
],
"render-wasm/assets/pattern.png",
);
await mockGetEmojiFont(workspace);
await mockGetJapaneseFont(workspace);
await workspace.mockGetFile("render-wasm/get-file-text-images.json");
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5e96453952b0",
pageId: "6bd7c17d-4f59-815e-8006-5e96453952b1",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with multiple emoji", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text-emoji-board.json");
await mockGetEmojiFont(workspace);
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5e999f38f210",
pageId: "6bd7c17d-4f59-815e-8006-5e999f38f211",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts with different alignments", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-text-align.json");
await workspace.goToWorkspace({
id: "692f368b-63ca-8141-8006-62925640b827",
pageId: "692f368b-63ca-8141-8006-62925640b828",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Updates text alignment edition - part 1", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-multiple-texts-base.json");
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5c1f68846e43",
pageId: "f8b42814-8653-81cf-8006-638aacdc3ffb",
});
await workspace.waitForFirstRender({ hideUI: false });
await workspace.clickLeafLayer("Text 1");
const textOptionsButton = workspace.page.getByTestId("text-align-options-button");
const autoWidthButton = workspace.page.getByTitle("Auto width");
const autoHeightButton = workspace.page.getByTitle("Auto height");
const alignMiddleButton = workspace.page.getByTitle("Align middle");
const alignBottomButton = workspace.page.getByTitle("Align bottom");
const alignRightButton = workspace.page.getByTitle("Align right (Ctrl+Alt+R)");
await textOptionsButton.click();
await workspace.clickLeafLayer("Text 1");
await autoWidthButton.click();
await workspace.clickLeafLayer("Text 2");
await autoHeightButton.click();
await workspace.clickLeafLayer("Text 3");
await alignMiddleButton.click();
await alignRightButton.click();
await workspace.clickLeafLayer("Text 4");
await alignBottomButton.click();
await workspace.page.keyboard.press("Escape");
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot({timeout: 10000});
});
test("Updates text alignment edition - part 2", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-multiple-texts-base.json");
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5c1f68846e43",
pageId: "f8b42814-8653-81cf-8006-638aacdc3ffb",
});
await workspace.waitForFirstRender({ hideUI: false });
await workspace.clickLeafLayer("Text 1");
const textOptionsButton = workspace.page.getByTestId("text-align-options-button");
const alignTopButton = workspace.page.getByTitle("Align top");
const alignMiddleButton = workspace.page.getByTitle("Align middle");
const alignBottomButton = workspace.page.getByTitle("Align bottom");
const alignCenterButton = workspace.page.getByTitle("Align center (Ctrl+Alt+T)");
const alignJustifyButton = workspace.page.getByTitle("Justify (Ctrl+Alt+J)");
const LTRButton = workspace.page.getByTitle("LTR");
const RTLButton = workspace.page.getByTitle("RTL");
await textOptionsButton.click();
await workspace.clickLeafLayer("Text 5");
await alignBottomButton.click();
await alignTopButton.click();
await alignCenterButton.click();
await workspace.clickLeafLayer("Text 6");
await alignJustifyButton.click();
await RTLButton.click();
await workspace.clickLeafLayer("Text 7");
await alignJustifyButton.click();
await RTLButton.click();
await LTRButton.click();
await workspace.clickLeafLayer("Text 8");
await alignMiddleButton.click();
await alignJustifyButton.click();
await RTLButton.click();
await workspace.page.keyboard.press("Escape");
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot({timeout: 10000});
});
test("Updates text alignment edition - part 3", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-multiple-texts-base.json");
await workspace.goToWorkspace({
id: "6bd7c17d-4f59-815e-8006-5c1f68846e43",
pageId: "f8b42814-8653-81cf-8006-638aacdc3ffb",
});
await workspace.waitForFirstRender({ hideUI: false });
await workspace.clickLeafLayer("Text 1");
const textOptionsButton = workspace.page.getByTestId("text-align-options-button");
const autoWidthButton = workspace.page.getByTitle("Auto width");
const autoHeightButton = workspace.page.getByTitle("Auto height");
const alignMiddleButton = workspace.page.getByTitle("Align middle");
const alignBottomButton = workspace.page.getByTitle("Align bottom");
const alignLeftButton = workspace.page.getByTitle("Align left (Ctrl+Alt+L)");
const alignCenterButton = workspace.page.getByTitle("Align center (Ctrl+Alt+T)");
const alignJustifyButton = workspace.page.getByTitle("Justify (Ctrl+Alt+J)");
const RTLButton = workspace.page.getByTitle("RTL");
await textOptionsButton.click();
await workspace.clickLeafLayer("Text 9");
await autoHeightButton.click();
await alignBottomButton.click();
await alignJustifyButton.click();
await RTLButton.click();
await workspace.clickLeafLayer("Text 10");
await alignBottomButton.click();
await alignJustifyButton.click();
await RTLButton.click();
await autoWidthButton.click();
await workspace.clickLeafLayer("Text 11");
await alignCenterButton.click();
await alignBottomButton.click();
await workspace.clickLeafLayer("Text 12");
await alignCenterButton.click();
await alignLeftButton.click();
await alignMiddleButton.click();
await workspace.page.keyboard.press("Escape");
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot({timeout: 10000});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

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