Compare commits

...

118 Commits

Author SHA1 Message Date
Aitor Moreno
dbbf3ab41d WIP 2025-07-24 10:16:48 +02:00
Aitor Moreno
5b26a9a269 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
f56420d327 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
462ac77fcd WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
fd3c3d720e WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
a20e0c5733 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
26ca7ed566 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
2a58387b02 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
84ad14183e WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
0704fa5df6 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
b438f4dfa1 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
4a85a9ac46 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
a77ca32bf8 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
2a7ab93892 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
03e4ff828b WIP 2025-07-24 09:23:19 +02:00
Alejandro Alonso
2ab5487a84 WIP fix partial 2025-07-24 09:23:19 +02:00
Aitor Moreno
f60254e3eb WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
4274db11e3 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
ee3bf72034 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
446fc53f00 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
3af70a965d WIP 2025-07-24 09:23:19 +02:00
Andrey Antukh
9a6989d2ca 📎 Fix linter issues introduced on merging staging into develop 2025-07-23 12:27:04 +02:00
Andrey Antukh
8aebe1a41e Merge remote-tracking branch 'origin/staging' into develop 2025-07-23 12:26:09 +02:00
Andrey Antukh
1acf78d57c Merge branch 'main' into staging 2025-07-23 12:09:37 +02:00
Andrey Antukh
523373dfa2 📎 Update .gitignore file 2025-07-23 12:09:15 +02:00
Andrés Moya
f55e7d8165 🐛 Keep shape level groups for token sync later 2025-07-23 12:04:31 +02:00
Andrés Moya
9fdc6be465 🐛 Fix bad touched attributes when applying tokens to text shapes 2025-07-23 12:04:31 +02:00
Alejandro Alonso
9390c1e7be 🐛 Fix "Copy as SVG" generates different code from the Inspect panel (#6945) 2025-07-23 11:46:58 +02:00
Florian Schroedl (aider)
d788a4d252 Implement new token-type :font-families 2025-07-23 11:26:28 +02:00
Eva Marco
b20b272eae 📚 Update changelog 2025-07-23 09:53:49 +02:00
Aitor Moreno
2cddc6fb5b Merge pull request #6583 from penpot/niwinz-fills-binary-type
🎉 Fills as binary type
2025-07-23 09:26:26 +02:00
Alejandro Alonso
d46b519524 🐛 Fix remove color button in the gradient editor (#6942) 2025-07-23 09:04:54 +02:00
Andrey Antukh
4effd375a9 Add several improvements to admin pannel 2025-07-23 08:33:33 +02:00
Andrey Antukh
4e753dc474 💄 Use resolved schemas instead of references
For several schemas on common types
2025-07-23 08:33:28 +02:00
Andrey Antukh
fbf63b98c3 Reuse file data checkers on file validate ns 2025-07-23 08:33:23 +02:00
Marina López
3df557b370 ♻️ Remove the workaround for updating the subscription after subscribing (#6938) 2025-07-23 08:10:20 +02:00
Aitor Moreno
cdb600b081 Remove unused code 2025-07-23 08:03:23 +02:00
Aitor Moreno
ffb688696b 🎉 Add keep-aspect-ratio integration 2025-07-23 08:03:23 +02:00
Andrey Antukh
8bb210e7b6 🎉 Add binary fills integration 2025-07-23 08:03:23 +02:00
Andrey Antukh
9ee488009f ♻️ Add substantial refactor on how types are organized
This mainly affects types related to colors, fills and texts, moving library
based operations from color namespace.
2025-07-23 08:03:23 +02:00
Andrey Antukh
96d9b102b6 Add type hints on config ns 2025-07-23 07:32:11 +02:00
Andrey Antukh
16fba49937 Expose flags for common submodule 2025-07-23 07:32:11 +02:00
Andrey Antukh
af99bd620c Use binary fills to write data to wasm memory 2025-07-23 07:32:11 +02:00
Andrey Antukh
8a58b9d459 Use new write-bool helper on fills metadata 2025-07-23 07:32:11 +02:00
Andrey Antukh
e3c62075b8 Write keep-aspect-ration on fill binary format 2025-07-23 07:32:11 +02:00
Andrey Antukh
22a70eb5b2 🎉 Add write-bool helper to buffer ns helpers 2025-07-23 07:32:11 +02:00
Andrey Antukh
4e2998a366 ♻️ Rename fill to fills namespace 2025-07-23 07:32:11 +02:00
Andrey Antukh
158f759cde Add binary fills initialization on workspace fetch 2025-07-23 07:32:11 +02:00
Aitor Moreno
3e3be95420 Merge pull request #6927 from penpot/elenatorro-test-fix-text-shadows
🐛 Fix text shadows apply text opacity
2025-07-23 06:59:28 +02:00
Elena Torró
b5808701ec Merge pull request #6873 from penpot/niwinz-develop-enhancements-1
 Add improvements for backend admin/debug page
2025-07-22 15:14:08 +02:00
Xaviju
35f3125fff 🐛 Fix null when copying shadow color on inspect tab (#6923)
Co-authored-by: Xavier Julian <xaviju@proton.me>
2025-07-22 14:49:36 +02:00
Elena Torro
5427d207cd 🐛 Fix text shadows apply text opacity 2025-07-22 14:34:10 +02:00
Xaviju
ee23d72d13 🐛 Fix null when copying shadow color on inspect tab (#6923)
Co-authored-by: Xavier Julian <xaviju@proton.me>
2025-07-22 14:06:06 +02:00
Francis Santiago
f22aa606ce 📚 Clarify OpenShift requirements (#6937)
* 📚 Clarify OpenShift requirements

* 📚 Remove the click for expanding
2025-07-22 14:05:02 +02:00
Andrey Antukh
d914314c1c Merge remote-tracking branch 'origin/staging' into develop 2025-07-22 13:04:57 +02:00
Pablo Alba
4aa9f1f62b 🐛 On component swap do not show secondary variants (#6928) 2025-07-22 12:33:37 +02:00
David Barragán Merino
9d288486d7 🐛 Subscription current period dates could be null (#6931)
`current-period-start` and `current-period-end` can be null if the invoice has not yet been created in stripe. This happens after the subscription is created, before the webhook is sent.
2025-07-22 12:32:42 +02:00
Pablo Alba
ea5521485a ♻️ Remove redundant flag on text overrides (#6933) 2025-07-22 12:32:24 +02:00
Marina López
f768ffbdad 🐛 Fix wrong behaviour for unpaid or canceled subscriptions (#6932) 2025-07-22 12:31:45 +02:00
Andrey Antukh
4f0d3660de 🎉 Add imagemagick docker image build scripts (#6925)
* 🎉 Add imagemagick docker image build scripts

* 📎 Add PR feedback changes
2025-07-22 11:51:13 +02:00
Andrey Antukh
fa72bb4adf Add several improvements to admin pannel 2025-07-22 10:06:29 +02:00
Andrey Antukh
ea0044f69a 💄 Use resolved schemas instead of references
For several schemas on common types
2025-07-22 10:06:29 +02:00
Andrey Antukh
7e493376a4 Reuse file data checkers on file validate ns 2025-07-22 10:06:29 +02:00
Andrey Antukh
8c5afe5ab3 📎 Add next release entries to the changelog 2025-07-21 21:20:46 +02:00
Andrey Antukh
7ccb742ef3 Merge remote-tracking branch 'origin/develop' into staging 2025-07-21 21:15:54 +02:00
Andrey Antukh
7bc29c22ed Merge remote-tracking branch 'origin/develop' into staging 2025-07-21 21:07:24 +02:00
Andrey Antukh
1d550eaa18 Merge remote-tracking branch 'origin/staging' into develop 2025-07-21 21:03:19 +02:00
Andrey Antukh
b71ec4bfe0 Simplify docker build script (#6924) 2025-07-21 20:45:03 +02:00
Andrey Antukh
827bbf6a7f Merge pull request #6926 from penpot/juanfran-close-libraries-modal-on-esc
🐛 Fix ESC key not closing Add/Manage Libraries modal
2025-07-21 15:48:40 +02:00
Juanfran
2db0cc0cbf 🐛 Fix ESC key not closing Add/Manage Libraries modal 2025-07-21 15:23:54 +02:00
Andrey Antukh
42ef01b339 Merge pull request #6871 from penpot/niwinz-develop-login-enhancements
 Allow login dialog on settings
2025-07-21 15:19:06 +02:00
Aitor Moreno
fdaef2be69 Merge pull request #6891 from penpot/elenatorro-test-style-decoration-blending
🔧 Add text decoration styles
2025-07-21 15:18:18 +02:00
Pablo Alba
ae3213f5d4 🐛 Fix text override corner case 2025-07-21 12:40:03 +02:00
Andrey Antukh
6dfd05fdd1 Merge remote-tracking branch 'origin/staging' into develop 2025-07-21 12:05:24 +02:00
Andrey Antukh
51107c3fc9 🐛 Fix incorrect event name on event constructor 2025-07-21 11:58:48 +02:00
Andrey Antukh
b6863efb3a Merge pull request #6874 from penpot/xaviju-11355-tokens-import-details-layout
 Improve legibility on import token notification details
2025-07-21 11:54:08 +02:00
Andrey Antukh
799bceb8b7 🐛 Check if profile is logged-in on subscriptions internal redirects 2025-07-21 11:40:31 +02:00
Andrey Antukh
9e573128c1 🐛 Fix incorrect event name on event constructor 2025-07-21 11:40:31 +02:00
Andrey Antukh
1f05511add Allow login dialog on settings 2025-07-21 11:40:30 +02:00
Elena Torro
eeee52a738 🐛 Ensure line height is properly handled on line breaks 2025-07-21 11:37:56 +02:00
Xavier Julian
7f53860296 📎 Add warning on feature flag temporary fix for font-size tokens 2025-07-21 11:23:27 +02:00
Andrey Antukh
16d0077393 Merge pull request #6920 from penpot/mdbenito-feature/wheel-scrolling-for-templates
 Enable wheel scrolling over templates-section in the dashboard
2025-07-21 11:22:47 +02:00
Andrey Antukh
622fed2f0d 💄 Add minor formating enhancements to dashboard templates ui code 2025-07-21 10:39:50 +02:00
Andrey Antukh
d22ade3289 Remove duplicated code 2025-07-21 10:38:18 +02:00
Miguel de Benito Delgado
7febf330ac Enable wheel scrolling over templates-section in the dashboard 2025-07-21 10:34:50 +02:00
Andrey Antukh
75a50ac1ac Merge pull request #6912 from penpot/andy-highlight-font-selector
 Highlight first font in font selector search, apply on Enter/click
2025-07-21 10:33:53 +02:00
Andres Gonzalez
e62567d09e Highlight first font in font selector search, apply on Enter/click
[Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579)

 Highlight first font in font selector search, apply on Enter/click
2025-07-21 10:13:36 +02:00
Andrey Antukh
8d80eebeb1 Merge pull request #6906 from penpot/andy-enhance-text-auto-resize
 Switch auto-width to auto-height on horizontal resize on text shapes
2025-07-21 10:11:35 +02:00
Andres Gonzalez
ee9a42238d Switch auto-width to auto-height on horizontal resize on text shapes 2025-07-21 09:56:45 +02:00
Andrey Antukh
758c76d661 Merge pull request #6905 from penpot/andy-enhance-text-resize-behavior
 Allow double-click on text bounding box to set auto-width/auto-height
2025-07-21 09:55:18 +02:00
Andrey Antukh
1dec46cbfa Merge pull request #6903 from penpot/superalex-fix-page-duplication
🐛 Fix error on validating file referential integrity when duplicating a page
2025-07-21 09:46:12 +02:00
Andrey Antukh
ae25d704c1 📎 Add missing use-fn hook 2025-07-21 09:32:44 +02:00
Andres Gonzalez
e05f8c0329 Improve text layer resize behavior
Text layers now only switch to fixed grow-type on vertical resize, not on horizontal resize, for a more intuitive UX. See #4602.
2025-07-21 09:27:42 +02:00
Alejandro Alonso
ce62e11626 🐛 Fix error on validating file referential integrity when duplicating a page 2025-07-21 09:26:23 +02:00
Andrey Antukh
9f04c2fc1d Merge pull request #6901 from penpot/superalex-hide-bb-when-editing-effects
 Hide bounding box while editing visual effects
2025-07-21 09:23:18 +02:00
Andrey Antukh
05a405a82d Merge pull request #6893 from penpot/xaviju-11144-copy-color-attr
 Keep color data when copying from info tab into CSS
2025-07-21 09:22:57 +02:00
Andrey Antukh
3c8c21c378 Merge pull request #6899 from abedef/patch-1
📚 Fix broken link in self-hosting docs
2025-07-21 09:19:39 +02:00
Xavier Julian
2dbeb884a5 Keep color data when copying from info tab into CSS 2025-07-21 09:07:20 +02:00
Andrey Antukh
931d72b41f Merge pull request #6887 from dfelinto/fix-trackpad-swipe
🐛 Fix touchpad swipe back/forward #4246
2025-07-21 08:58:32 +02:00
Alejandro Alonso
2e3cdd872c Revert " Highlight first found font in font list when searching [Taiga #3204]"
This reverts commit 55a13c3139.
2025-07-17 13:01:24 +02:00
Andres Gonzalez
55a13c3139 Highlight first found font in font list when searching [Taiga #3204]
This enhancement highlights the first found font in the font list when searching, and allows pressing Enter to select it, for a more intuitive font selection experience.

See [Taiga #3204](https://tree.taiga.io/project/penpot/issue/3204).
2025-07-17 12:09:50 +02:00
Andrey Antukh
f63d1c87e3 Merge pull request #6904 from penpot/andy-fix-email-change-message
 Update email change confirmation message for clarity
2025-07-17 11:31:21 +02:00
Alejandro Alonso
abbfd44534 Hide bounding box while editing visual effects 2025-07-17 09:33:10 +02:00
Andres Gonzalez
f772724f9a Update email change confirmation message for clarity 2025-07-16 10:22:53 +02:00
Andrey Antukh
f3abd0f190 Merge pull request #6902 from penpot/andy-clarify-invite-member-message
 Clarify invite member message for existing team members
2025-07-15 15:48:45 +02:00
Andres Gonzalez
5d4042c861 Clarify invite member message for existing team members
Update the English message shown when inviting team members whose emails are already part of the team, as suggested in issue #6785.
2025-07-15 14:05:20 +02:00
Dalai Felinto
1fbcec98fb 🐛 Fix touchpad swipe back/forward #4246
This prevents the browser to take over the trackbad swipe gesture both
for the dashboard and the workspace.

At an early attempt I did get the code to work only for the workspace,
but it is too unreliable and I could every now and then get it to misbehave.

I believe it is better to be safe and always prevent the browser from
going back/forth, regardless of workspace/dashboard.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-07-15 00:24:20 +02:00
Alejandro Alonso
abef9f3cf7 Merge pull request #6889 from penpot/niwinz-staging-bugfix
🐛 Fix unexpected exception on processing old texts
2025-07-14 07:08:31 +02:00
Abed Fayyad
6f1958f9f2 📚 Fix broken link in self-hosting docs
Replaced broken Markdown link to the unofficial self-hosting section.

Signed-off-by: Abed Fayyad <yo@abedef.ca>
2025-07-13 09:16:13 -04:00
Andrey Antukh
6b2ce86d5f Merge pull request #6896 from penpot/juanfran-issue-show-main-component-focus
🐛 Fix initialize-page namespace when showing main component
2025-07-11 18:31:37 +02:00
Pablo Alba
0cfd70da2e 🐛 Fix corner cases on variants text overrides 2025-07-11 15:28:55 +02:00
Xavier Julian
4167faf39d 📎 Add blend-mode in code editor feature to CHANGELOG 2025-07-11 15:14:29 +02:00
Pablo Alba
90e6e8c5eb 🐛 Fix double undo on text partial overrides 2025-07-11 15:05:30 +02:00
Elena Torro
b40b1fa2e4 🔧 Refactor ParagraphBuilder and fix auto height 2025-07-11 13:29:22 +02:00
Juanfran
bb1ec109d8 🐛 Fix initialize-page namespace when showing main component 2025-07-11 13:09:20 +02:00
Elena Torro
4c21468850 🔧 Add text decoration styles 2025-07-10 14:26:41 +02:00
Andrey Antukh
40c300fa1a 🐛 Fix unexpected exception on processing old texts 2025-07-10 09:22:00 +02:00
Xavier Julian
77a47e4b2b Improve legibility on import token notification details 2025-07-08 15:09:50 +02:00
201 changed files with 10149 additions and 3064 deletions

6
.gitignore vendored
View File

@@ -41,6 +41,7 @@
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
/backend/experiments
/bundle*
/cd.md
/clj-profiler/
@@ -51,9 +52,6 @@
/exporter/target
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/cypress/fixtures/validuser.json
/frontend/cypress/videos/*/
/frontend/cypress/videos/*/
/frontend/dist/
/frontend/npm-debug.log
/frontend/out/
@@ -70,6 +68,8 @@
/vendor/svgclean/bundle*.js
/web
/library/target/
/library/*.zip
/external
clj-profiler/
node_modules

View File

@@ -1,5 +1,18 @@
# CHANGELOG
## 2.10.0 (Unreleased)
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
## 2.9.0 (Unreleased)
### :rocket: Epics and highlights
@@ -8,23 +21,51 @@
### :heart: Community contributions (Thank you!)
- Clarify message when inviting existing team members to make it more user-friendly and clear which invitations will be sent. [Taiga #11441](https://tree.taiga.io/project/penpot/issue/11441) by [@iprithvitharun](https://github.com/iprithvitharun)
- Update email change confirmation message for clarity and correct grammar. [GitHub #6786](https://github.com/penpot/penpot/issues/6786) by [@iprithvitharun](https://github.com/iprithvitharun)
### :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)
- Provide CSS `mix-blend-mode` property in code editor when present on shape [Taiga #11282](https://tree.taiga.io/project/penpot/issue/11282)
- 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)
- Hide bounding box while editing visual effects [Taiga #11576](https://tree.taiga.io/project/penpot/issue/11576)
- Improved text layer resizing: Allow double-click on text bounding box to set auto-width/auto-height [Taiga #11577](https://tree.taiga.io/project/penpot/issue/11577)
- Improve text layer auto-resize: auto-width switches to auto-height on horizontal resize, and only switches to fixed on vertical resize [Taiga #11578](https://tree.taiga.io/project/penpot/issue/11578)
- Highlight first font in font selector search. Apply only on Enter or click. [Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579)
- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871)
- Improve the application of tokens with object specific tokens [Taiga #10209](https://tree.taiga.io/project/penpot/us/10209)
### :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)
- Fix touchpad swipe leading to navigating back/forth [GitHub #4246](https://github.com/penpot/penpot/issues/4246)
- Keep color data when copying from info tab into CSS [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Update HSL values to modern syntax as defined in W3C CSS Color Module Level 4 [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Fix main component receives focus and is selected when using 'Show Main Component' [Taiga #11402](https://tree.taiga.io/project/penpot/issue/11402)
- Fix duplicating pages with mainInstance shapes nested inside groups [Taiga #10774](https://tree.taiga.io/project/penpot/issue/10774)
- Fix ESC key not closing Add/Manage Libraries modal [Taiga #11523](https://tree.taiga.io/project/penpot/issue/11523)
- Fix copying a shadow color from info tab [Taiga #11211](https://tree.taiga.io/project/penpot/issue/11211)
- Fix remove color button in the gradient editor [Taiga #11623](https://tree.taiga.io/project/penpot/issue/11623)
- Fix "Copy as SVG" generates different code from the Inspect panel [Taiga #11519](https://tree.taiga.io/project/penpot/issue/11519)
- Fix overriden tokens in text copies are not preserved [Taiga #11486](https://tree.taiga.io/project/penpot/issue/11486)
## 2.8.1 (Unreleased)
### :bug: Bugs fixed
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
## 2.8.0
@@ -46,6 +87,7 @@ in future versions. Therefore, **migration from Redis to ValKey is recommended f
on-premises instances** that want to keep up to date.
### :heart: Community contributions (Thank you!)
- Add Serbian language [GitHub #5002](https://github.com/penpot/penpot/issues/5002) by [crnobog69](https://github.com/crnobog69)
### :sparkles: New features & Enhancements
@@ -101,7 +143,6 @@ on-premises instances** that want to keep up to date.
- Fix copy in error message [GitHub #6615](https://github.com/penpot/penpot/pull/6615)
- Fix url on invitation link [Taiga #11284](https://tree.taiga.io/project/penpot/issue/11284)
## 2.7.1
### :bug: Bugs fixed
@@ -109,7 +150,6 @@ on-premises instances** that want to keep up to date.
- Fix incorrect handling of strokes with images on importing files
- Fix tokens disappearing after manual additions [Taiga #11063](https://tree.taiga.io/project/penpot/issue/11063)
## 2.7.0
### :rocket: Epics and highlights
@@ -241,7 +281,6 @@ on-premises instances** that want to keep up to date.
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
## 2.5.4
### :heart: Community contributions (Thank you!)
@@ -286,7 +325,7 @@ on-premises instances** that want to keep up to date.
### :boom: Breaking changes & Deprecations
Although this is not a breaking change, we believe its important to highlight it in this
Although this is not a breaking change, we believe it's important to highlight it in this
section:
This release includes a fix for an internal bug in Penpot that caused incorrect handling
@@ -294,9 +333,9 @@ of media assets (e.g., fill images). The issue has been resolved since version 2
no new incorrect references will be generated. However, existing files may still contain
incorrect references.
To address this, weve provided a script to correct these references in existing files.
To address this, we've provided a script to correct these references in existing files.
While having incorrect references generally doesnt result in visible issues, there are
While having incorrect references generally doesn't result in visible issues, there are
rare cases where it can cause problems. For example, if a component library (containing
images) is deleted, and that library is being used in other files, running the FileGC task
(responsible for freeing up space and performing logical deletions) could leave those
@@ -371,7 +410,6 @@ is a number of cores)
- Fix missing methods reference on API Docs
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
## 2.4.1
### :bug: Bugs fixed
@@ -379,7 +417,6 @@ is a number of cores)
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
## 2.4.0
### :rocket: Epics and highlights
@@ -433,7 +470,6 @@ is a number of cores)
- Add initial documentation for Kubernetes
## 2.3.1
### :bug: Bugs fixed
@@ -441,7 +477,6 @@ is a number of cores)
- Fix unexpected issue on interaction between plugins sandbox and
internal impl of promise
## 2.3.0
### :rocket: Epics and highlights
@@ -467,7 +502,6 @@ is a number of cores)
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
### :bug: Bugs fixed
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
@@ -507,8 +541,8 @@ is a number of cores)
### :boom: Breaking changes & Deprecations
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
### :heart: Community contributions (Thank you!)
@@ -524,7 +558,7 @@ time being.
freeing up space in the database. It can be enabled with the
`enable-enable-tiered-file-data-storage` flag.
*(On-Premise feature, EXPERIMENTAL).*
_(On-Premise feature, EXPERIMENTAL)._
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
@@ -567,7 +601,7 @@ time being.
- **Design System**
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
@@ -622,11 +656,11 @@ time being.
### :sparkles: New features
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
### :bug: Bugs fixed
- Fix the search label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix the "search" label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
- Fix several issues on the OIDC.
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
@@ -706,22 +740,21 @@ time being.
- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458)
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
## 2.0.1
### :bug: Bugs fixed
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
## 2.0.0 - I Just Can't Get Enough
### :rocket: Epics and highlights
- Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915)
- UI redesign [Taiga #4958](https://tree.taiga.io/project/penpot/epic/4958)
- New components System [Taiga #2662](https://tree.taiga.io/project/penpot/epic/2662)
- Swap components [Taiga #1331](https://tree.taiga.io/project/penpot/us/1331)
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
- HTML code generation [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277)
- Light and dark themes [Taiga #2287](https://tree.taiga.io/project/penpot/us/2287)
@@ -730,9 +763,9 @@ time being.
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
### :heart: Community contributions (Thank you!)
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
- Hide bounding-box when editing shape (by @VasilevsVV) [#3930](https://github.com/penpot/penpot/pull/3930)
- CTRL + "+" to zoom into canvas instead of browser (by @audriu) [#3848](https://github.com/penpot/penpot/pull/3848)
- Add dev deps.edn in the project root (by @PEZ) [#3794](https://github.com/penpot/penpot/pull/3794)
@@ -741,6 +774,7 @@ time being.
- Typo (by StephanEggermont) [#157](https://github.com/penpot/penpot-docs/pull/157)
### :sparkles: New features
- Send comments with Ctrl+Enter / Cmd + Enter [Taiga #6085](https://tree.taiga.io/project/penpot/issue/6085)
- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
- Stroke default position [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
@@ -808,6 +842,7 @@ time being.
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
### :bug: Bugs fixed
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
@@ -816,7 +851,7 @@ time being.
- Selecting from Color Palette does not work for board when there is no existing fill [Taiga #6464](https://tree.taiga.io/project/penpot/issue/6464)
- Color thumbnails are consistently rounded in the inspect code mode [Taiga #5886](https://tree.taiga.io/project/penpot/issue/5886)
- Adding vector path points before first point of existing open path not working [Taiga #6593](https://tree.taiga.io/project/penpot/issue/6593)
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
- Gradient color tool doesn't work properly with flipped items [Taiga #6485](https://tree.taiga.io/project/penpot/issue/6485)
- [TEXT] Align options are not shown when several text are selected [Taiga #5948](https://tree.taiga.io/project/penpot/issue/5948)
- [VIEW MODE] Comments not working properly on multiple pages [Taiga #6281](https://tree.taiga.io/project/penpot/issue/6281)
@@ -860,7 +895,7 @@ time being.
### :sparkles: New features
- Improve selected colors [Taiga #5805]( https://tree.taiga.io/project/penpot/us/5805)
- Improve selected colors [Taiga #5805](https://tree.taiga.io/project/penpot/us/5805)
### :bug: Bugs fixed
@@ -895,7 +930,6 @@ time being.
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
## 1.19.1
### :bug: Bugs fixed
@@ -1009,7 +1043,6 @@ time being.
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
### :heart: Community contributions by (Thank you!)
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
@@ -1163,12 +1196,14 @@ time being.
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
### :heart: Community contributions by (Thank you!)
- To @ondrejkonec: for contributing to the code with:
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
## 1.17.3
### :bug: Bugs fixed
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
- Fix problem with shadows and blur on multiple selection
@@ -1201,6 +1236,7 @@ time being.
## 1.17.1
### :bug: Bugs fixed
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
@@ -1315,7 +1351,7 @@ time being.
### :boom: Breaking changes & Deprecations
- Removed the support for v2 internal file data blob format. This
- Removed the support for v2 internal file data blob format. This
version has never been documented nor set as default value so
technically this is not a breaking change because we are removing
a "private API".
@@ -1420,7 +1456,6 @@ time being.
- Fix when ungrouping, the items previously grouped should ALWAYS remain selected [Taiga #4064](https://tree.taiga.io/project/penpot/issue/4064)
- Change shortcut for "Clear undo" [#2219](https://github.com/penpot/penpot/issues/2219)
## 1.15.2-beta
### :bug: Bugs fixed
@@ -1504,6 +1539,7 @@ time being.
- Fix bringing complete file data when launching the export dialog [Taiga #4006](https://tree.taiga.io/project/penpot/issue/4006)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.14.2-beta
@@ -1544,10 +1580,10 @@ time being.
- Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384)
- Fix problem with empty text boxes events [Taiga #3627](https://tree.taiga.io/project/penpot/issue/3627)
## 1.13.5-beta
### :bug: Bugs fixed
- Fix orientation artboard preset not working with differently sized artboards [Taiga #3548](https://tree.taiga.io/project/penpot/issue/3548)
- Fix background on export arboards [Taiga #1991](https://tree.taiga.io/project/penpot/issue/1991)
@@ -1691,6 +1727,7 @@ time being.
- Fix problem when resizing a group with texts with auto-width/height [#3171](https://tree.taiga.io/project/penpot/issue/3171)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.12.4-beta
@@ -1708,7 +1745,7 @@ time being.
### :bug: Bugs fixed
- Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
- Fix issue on password persistence after registration process on private instances
## 1.12.2-beta
@@ -1726,7 +1763,6 @@ time being.
- Fix length of names in sidebar [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
- Fix issues on loki integration
## 1.12.0-beta
### :boom: Breaking changes

View File

@@ -193,7 +193,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Click to the link below to confirm the change:</div>
Click the link below to confirm the change.</div>
</td>
</tr>
<tr>
@@ -217,8 +217,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you received this email by mistake, please consider changing your password for security
reasons.</div>
If you did not request this change, consider changing your password for security reasons.</div>
</td>
</tr>
<tr>

View File

@@ -2,12 +2,11 @@ Hello {{name|abbreviate:25}}!
We received a request to change your current email to {{ pending-email }}.
Click to the link below to confirm the change:
Click the link below to confirm the change.
{{ public-uri }}/#/auth/verify-token?token={{token}}
If you received this email by mistake, please consider changing your password
for security reasons.
If you did not request this change, consider changing your password for security reasons.
Enjoy!
The Penpot team.

View File

@@ -17,38 +17,6 @@ Debug Main Page
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
<fieldset>
<legend>Download file data:</legend>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
@@ -81,6 +49,50 @@ Debug Main Page
</section>
<section class="widget">
<fieldset>
<legend>Download RAW file data:</legend>
<desc>Given an FILE-ID, downloads the file AS-IS (no validation
checks, just exports the file data and related objects in raw)
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="get" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
<legend>Export binfile:</legend>
@@ -88,7 +100,7 @@ Debug Main Page
the related libraries in a single custom formatted binary
file.</desc>
<form method="get" action="/dbg/file/export">
<form method="get" action="/dbg/actions/file-export">
<div class="row set-of-inputs">
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
@@ -116,7 +128,7 @@ Debug Main Page
<legend>Import binfile:</legend>
<desc>Import penpot file in binary format.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/import">
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-import">
<div class="row">
<input type="file" name="file" value="" />
</div>
@@ -130,79 +142,27 @@ Debug Main Page
<section class="widget">
<fieldset>
<legend>Reset file version</legend>
<desc>Allows reset file data version to a specific number/</desc>
<form method="post" action="/dbg/actions/reset-file-version">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="number" style="width:100px" name="version" placeholder="version" value="32" />
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<h2>Feature Flags</h2>
<fieldset>
<legend>Enable</legend>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/add-team-feature">
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Disable</legend>
<desc>Remove a feature flag from a team</desc>
<form method="post" action="/dbg/actions/remove-team-feature">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">

View File

@@ -7,7 +7,9 @@ penpot - error list
{% block content %}
<nav>
<div class="title">
<h1>Error reports (last 200)</h1>
<h1>Error reports (last 200)
<a href="/dbg">[GO BACK]</a>
</h1>
</div>
</nav>
<main class="horizontal-list">

View File

@@ -155,7 +155,7 @@
(defn decode-file
"A general purpose file decoding function that resolves all external
pointers, run migrations and return plain vanilla file map"
[cfg {:keys [id] :as file}]
[cfg {:keys [id] :as file} & {:keys [migrate?] :or {migrate? true}}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [file (->> file
(feat.fmigr/resolve-applied-migrations cfg)
@@ -168,7 +168,7 @@
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(update :data assoc :id id)
(fmg/migrate-file libs)))))
(cond-> migrate? (fmg/migrate-file libs))))))
(defn get-file
"Get file, resolve all features and apply migrations.

View File

@@ -37,3 +37,9 @@
{::db/return-keys false
::sql/on-conflict-do-nothing true})
(db/get-update-count))))
(defn reset-migrations!
"Replace file migrations"
[conn {:keys [id] :as file}]
(db/delete! conn :file-migration {:file-id id})
(upsert-migrations! conn file))

View File

@@ -15,9 +15,11 @@
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.file-migrations :as feat.fmig]
[app.http.session :as session]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.files-create :refer [create-file]]
@@ -50,26 +52,26 @@
{::yres/status 200
::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version)}))})
(tmpl/render {:version (:full cf/version)
:supported-features cfeat/supported-features}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn prepare-response
[body]
(let [headers {"content-type" "application/transit+json"}]
{::yres/status 200
::yres/body body
::yres/headers headers}))
(defn- get-resolved-file
[cfg file-id]
(some-> (bfc/get-file cfg file-id :migrate? false)
(update :data blob/encode)))
(defn prepare-download-response
[body filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}]
{::yres/status 200
::yres/body body
::yres/headers headers}))
(defn prepare-download
[file filename]
{::yres/status 200
::yres/headers
{"content-disposition" (str "attachment; filename=" filename ".json")
"content-type" "application/octet-stream"}
::yres/body
(t/encode file {:type :json-verbose})})
(def sql:retrieve-range-of-changes
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
@@ -77,45 +79,51 @@
(def sql:retrieve-single-change
"select revn, changes, data from file_change where file_id=? and revn = ?")
(defn- retrieve-file-data
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}]
(defn- download-file-data
[cfg {:keys [params ::session/profile-id] :as request}]
(let [file-id (some-> params :file-id parse-uuid)
revn (some-> params :revn parse-long)
filename (str file-id)]
(when-not file-id
(ex/raise :type :validation
:code :missing-arguments))
(let [data (if (integer? revn)
(some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data)
(some-> (db/get-by-id pool :file file-id) :data))]
(when-not data
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))
(if-let [file (get-resolved-file cfg file-id)]
(cond
(contains? params :download)
(prepare-download-response data filename)
(prepare-download file filename)
(contains? params :clone)
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [profile (profile/get-profile conn profile-id)
project-id (:default-project-id profile)
file (-> (create-file cfg {:id (uuid/next)
:name (str "Cloned: " (:name file))
:features (:features file)
:project-id project-id
:profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file cfg {:id file-id
:name (str "Cloned file: " filename)
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{::yres/status 201
::yres/body "OK CREATED"})))
(feat.fmig/reset-migrations! conn file)
(db/update! conn :file
{:data (:data file)}
{:id (:id file)}
{::db/return-keys false})
{::yres/status 201
::yres/body "OK CLONED"})))
:else
(prepare-response (blob/decode data))))))
(ex/raise :type :validation
:code :invalid-params
:hint "invalid button"))
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))))
(defn- is-file-exists?
[pool id]
@@ -123,81 +131,61 @@
(-> (db/exec-one! pool [sql id]) :exists)))
(defn- upload-file-data
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
[{:keys [::db/pool] :as cfg} {:keys [::session/profile-id params] :as request}]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (some-> params :file :path io/read*)]
file (some-> params :file :path io/read* t/decode)]
(if (and data project-id)
(let [fname (str "Imported file *: " (dt/now))
(if (and file project-id)
(let [fname (str "Imported: " (:name file) "(" (dt/now) ")")
reuse-id? (contains? params :reuseid)
file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid)))
(uuid/next))]
(if (and reuse-id? file-id
(is-file-exists? pool file-id))
(do
(db/update! pool :file
{:data data
:deleted-at nil}
{:id file-id})
{::yres/status 200
::yres/body "OK UPDATED"})
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(db/update! conn :file
{:data (:data file)
:features (into-array (:features file))
:deleted-at nil}
{:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 200
::yres/body "OK UPDATED"}))
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [file (-> (create-file cfg {:id file-id
:name fname
:features (:features file)
:project-id project-id
:profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file cfg {:id file-id
:name fname
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{:data (:data file)}
{:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 201
::yres/body "OK CREATED"}))))
::yres/body "OK CREATED"})))))
{::yres/status 500
::yres/body "ERROR"})))
(ex/raise :type :validation
:code :invalid-params
:hint "invalid file uploaded"))))
(defn file-data-handler
(defn raw-export-import-handler
[cfg request]
(case (yreq/method request)
:get (retrieve-file-data cfg request)
:get (download-file-data cfg request)
:post (upload-file-data cfg request)
(ex/raise :type :http
:code :method-not-found)))
(defn file-changes-handler
[{:keys [::db/pool]} {:keys [params] :as request}]
(letfn [(retrieve-changes [file-id revn]
(if (str/includes? revn ":")
(let [[start end] (->> (str/split revn #":")
(map str/trim)
(map parse-long))]
(some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end])
(map :changes)
(map blob/decode)
(mapcat identity)
(vec)))
(if-let [revn (parse-long revn)]
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])]
(some-> item :changes blob/decode vec))
(ex/raise :type :validation :code :invalid-arguments))))]
(let [file-id (some-> params :id parse-uuid)
revn (or (some-> params :revn parse-long) "latest")
filename (str file-id)]
(when (or (not file-id) (not revn))
(ex/raise :type :validation
:code :invalid-arguments
:hint "missing arguments"))
(let [data (retrieve-changes file-id revn)]
(if (contains? params :download)
(prepare-download-response data filename)
(prepare-response data))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ERROR BROWSER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -430,49 +418,49 @@
::yres/body "OK"}))
(defn- add-team-feature
[{:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
(defn- handle-team-features
[cfg {:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
action (some-> params :action)
skip-check (contains? params :skip-check)]
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(when (nil? team-id)
(ex/raise :type :validation
:code :invalid-team-id
:hint "provided invalid team id"))
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
(if (= action "show")
(let [team (db/run! cfg teams/get-team-info {:id team-id})]
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body (apply str "Team features:\n"
(->> (:features team)
(map (fn [feature]
(str "- " feature "\n")))))})
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
(do
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(defn- remove-team-feature
[{:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
skip-check (contains? params :skip-check)]
(cond
(= action "enable")
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(= action "disable")
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
(when (nil? team-id)
(ex/raise :type :validation
:code :invalid-team-id
:hint "provided invalid team id"))
:else
(ex/raise :type :validation
:code :invalid-action
:hint (str "invalid action: " action)))
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS
@@ -525,6 +513,25 @@
(ex/raise :type :authentication
:code :only-admins-allowed)))))})
(def errors
(letfn [(handle-error [cause]
(when-let [data (ex-data cause)]
(when (= :validation (:type data))
(str "Error: " (or (:hint data) (ex-message cause)) "\n"))))]
{:name ::errors
:compile
(fn [& _params]
(fn [handler]
(fn [request]
(try
(handler request)
(catch Throwable cause
(let [body (or (handle-error cause)
(ex/format-throwable cause))]
{::yres/status 400
::yres/headers {"content-type" "text/plain"}
::yres/body body}))))))}))
(defmethod ig/assert-key ::routes
[_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool")
@@ -540,15 +547,14 @@
["/changelog" {:handler (partial changelog-handler cfg)}]
["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}]
["/actions/resend-email-verification"
{:handler (partial resend-email-notification cfg)}]
["/actions/reset-file-version"
{:handler (partial reset-file-version cfg)}]
["/actions/add-team-feature"
{:handler (partial add-team-feature)}]
["/actions/remove-team-feature"
{:handler (partial remove-team-feature)}]
["/file/export" {:handler (partial export-handler cfg)}]
["/file/import" {:handler (partial import-handler cfg)}]
["/file/data" {:handler (partial file-data-handler cfg)}]
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
["/actions" {:middleware [[errors]]}
["/resend-email-verification"
{:handler (partial resend-email-notification cfg)}]
["/reset-file-version"
{:handler (partial reset-file-version cfg)}]
["/handle-team-features"
{:handler (partial handle-team-features cfg)}]
["/file-export" {:handler (partial export-handler cfg)}]
["/file-import" {:handler (partial import-handler cfg)}]
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]])

View File

@@ -7,7 +7,6 @@
(ns app.rpc.commands.files-create
(:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.file :as ctf]
@@ -41,9 +40,7 @@
:or {is-shared false revn 0 create-page true}
:as params}]
(dm/assert!
"expected a valid connection"
(db/connection? conn))
(assert (db/connection? conn) "expected a valid connection")
(binding [pmap/*tracked* (pmap/create-tracked)
cfeat/*current* features]

View File

@@ -78,9 +78,10 @@
(defn decode-row
[{:keys [features subscription] :as row}]
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
(when row
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))))
;; FIXME: move
@@ -461,11 +462,12 @@
;; --- COMMAND QUERY: get-team-info
(defn- get-team-info
(defn get-team-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :team
{:id id}
{::sql/columns [:id :is-default]}))
(-> (db/get* conn :team
{:id id}
{::sql/columns [:id :is-default :features]})
(decode-row)))
(sv/defmethod ::get-team-info
"Retrieve minimal team info by its ID."

View File

@@ -205,9 +205,8 @@
[:trial-start [:maybe ::sm/timestamp]]
[:cancel-at [:maybe ::sm/timestamp]]
[:canceled-at [:maybe ::sm/timestamp]]
[:current-period-end ::sm/timestamp]
[:current-period-start ::sm/timestamp]
[:current-period-end [:maybe ::sm/timestamp]]
[:current-period-start [:maybe ::sm/timestamp]]
[:cancel-at-period-end :boolean]
[:cancellation-details

View File

@@ -8,7 +8,7 @@
(:require
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.text :as txt]))
[app.common.types.text :as txt]))
(defn- get-attr
[obj attr]

View File

@@ -74,6 +74,13 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.put ~target ~offset (unchecked-byte ~value)))))
(defmacro write-bool
[target offset value]
(if (:ns &env)
`(.setInt8 ~target ~offset (if ~value 0x01 0x00) true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.put ~target ~offset (unchecked-byte (if ~value 0x01 0x00))))))
(defmacro write-short
[target offset value]
(if (:ns &env)

View File

@@ -349,7 +349,7 @@
rounded-s (d/format-number (* 100 s) precision)
rounded-l (d/format-number (* 100 l) precision)
rounded-a (d/format-number a precision)]
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
(str/concat "" rounded-h " " rounded-s "% " rounded-l "% / " rounded-a)))
(defn format-rgba
[[r g b a]]

View File

@@ -9,17 +9,16 @@
data resources."
(:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration concat mapcat
parse-uuid max min regexp?])
parse-uuid max min regexp? array?])
#?(:cljs
(:require-macros [app.common.data]))
(:require
#?(:cljs [cljs.core :as c]
:clj [clojure.core :as c])
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
#?(:cljs [goog.array :as garray])
[app.common.math :as mth]
[clojure.core :as c]
[clojure.set :as set]
[cuerdas.core :as str]
[linked.map :as lkm]
@@ -167,6 +166,15 @@
;; Data Structures Access & Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn array?
[o]
#?(:cljs
(c/array? o)
:clj
(if (some? o)
(.isArray (class o))
false)))
(defn not-empty?
[coll]
(boolean (seq coll)))

View File

@@ -356,7 +356,7 @@
(first children)
(last children))
fills (if (and (contains? head :svg-attrs) (empty? (:fills head)))
types.path/default-bool-fills
(types.path/get-default-bool-fills)
(get head :fills))]
(-> bool-shape
(assoc :fills fills)

View File

@@ -21,6 +21,7 @@
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.grid :as ctg]
[app.common.types.library :as ctl]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.path :as path]
@@ -927,15 +928,15 @@
(defmethod process-change :add-color
[data {:keys [color]}]
(ctc/add-color data color))
(ctl/add-color data color))
(defmethod process-change :mod-color
[data {:keys [color]}]
(ctc/set-color data color))
(ctl/set-color data color))
(defmethod process-change :del-color
[data {:keys [id]}]
(ctc/delete-color data id))
(ctl/delete-color data id))
;; DEPRECATED: remove before 2.3
(defmethod process-change :add-recent-color

View File

@@ -21,18 +21,17 @@
[app.common.math :as mth]
[app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.text :as txt]
[app.common.types.color :as types.color]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.fill :as types.fill]
[app.common.types.fills :as types.fills]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segment]
[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.types.text :as types.text]
[app.common.uuid :as uuid]
[clojure.set :as set]
[cuerdas.core :as str]))
@@ -623,7 +622,7 @@
(let [invalid-node? (complement valid-node?)]
(cond-> object
(cfh/text-shape? object)
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
(update :content #(types.text/transform-nodes invalid-node? fix-node %)))))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
@@ -730,7 +729,7 @@
(let [shape (update-object shape)]
(if (cfh/text-shape? shape)
(-> shape
(update :content (partial txt/transform-nodes identity update-fill))
(update :content (partial types.text/transform-nodes identity update-fill))
(d/update-when :position-data #(mapv update-object %)))
shape)))
@@ -838,7 +837,7 @@
(d/update-when :components d/update-vals update-container))))
(def ^:private valid-fill?
(sm/lazy-validator types.fill/schema:fill))
(sm/lazy-validator types.fills/schema:fill))
(defmethod migrate-data "legacy-43"
[data _]
@@ -856,7 +855,7 @@
(update-object [object]
(if (cfh/text-shape? object)
(update object :content #(txt/transform-nodes txt/is-content-node? update-text-node %))
(update object :content #(types.text/transform-nodes types.text/is-content-node? update-text-node %))
object))
(update-container [container]
@@ -1105,7 +1104,7 @@
;; The text shape also can has fills on the text
;; fragments so we need to fix fills there
(cond-> (cfh/text-shape? object)
(update :content (partial txt/transform-nodes txt/is-content-node? fix-fills)))))
(update :content (partial types.text/transform-nodes types.text/is-content-node? fix-fills)))))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
@@ -1423,7 +1422,7 @@
(update-object [object]
(if (cfh/text-shape? object)
(update object :content (partial txt/transform-nodes txt/is-content-node? fix-fills))
(update object :content (partial types.text/transform-nodes types.text/is-content-node? fix-fills))
object))
(update-container [container]
@@ -1457,7 +1456,7 @@
;; Fixes shapes with nested :fills in the :fills attribute
;; introduced in a migration `0006-fix-old-texts-fills` when
;; txt/transform-nodes with identity pred was broken
;; types.text/transform-nodes with identity pred was broken
(remove-nested-fills [[fill :as fills]]
(if (and (= 1 (count fills))
(contains? fill :fills))
@@ -1466,7 +1465,7 @@
(clear-fill [fill]
(-> fill
(select-keys types.fill/fill-attrs)
(select-keys types.fills/fill-attrs)
(d/update-when :fill-image clear-color-image)
(d/update-when :fill-color-gradient clear-color-gradient)))
@@ -1483,8 +1482,8 @@
(fix-text-content [content]
(->> content
(txt/transform-nodes txt/is-content-node? fix-object)
(txt/transform-nodes txt/is-paragraph-set-node? #(dissoc % :fills))))
(types.text/transform-nodes types.text/is-content-node? fix-object)
(types.text/transform-nodes types.text/is-paragraph-set-node? #(dissoc % :fills))))
(update-shape [object]
(-> object
@@ -1539,7 +1538,7 @@
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)))]
(types.text/get-diff-type (:content object) (:content ref-shape)))]
(if (seq partial-touched)
(update object :touched (fn [touched]
(reduce #(ctk/set-touched-group %1 %2)

View File

@@ -7,7 +7,6 @@
(ns app.common.files.shapes-builder
"A SVG to Shapes builder."
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -21,6 +20,7 @@
[app.common.math :as mth]
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
[app.common.svg :as csvg]
[app.common.types.color :as clr]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segm]
[app.common.types.shape :as cts]

View File

@@ -650,26 +650,12 @@
(check-component component file)
(deref *errors*)))
(def ^:private valid-fdata?
"Structural validation of file data using defined schema"
(sm/lazy-validator ::ctf/data))
(def ^:private get-fdata-explain
"Get schema explain data for file data"
(sm/lazy-explainer ::ctf/data))
(defn validate-file-schema!
"Validates the file itself, without external dependencies, it
performs the schema checking and some semantical validation of the
content."
[{:keys [id data] :as file}]
(when-not (valid-fdata? data)
(ex/raise :type :validation
:code :schema-validation
:hint (str/ffmt "invalid file data structure found on file '%'" id)
:file-id id
::sm/explain (get-fdata-explain data)))
file)
[file]
(update file :data ctf/check-file-data))
(defn validate-file!
"Validate full referential integrity and semantic coherence on file data.
@@ -683,7 +669,6 @@
:file-id (:id file)
:details errors)))
(declare compare-slots)
;; Optional check to look for missing swap slots.

View File

@@ -10,6 +10,8 @@
[clojure.set :as set]
[cuerdas.core :as str]))
(def ^:dynamic *current* #{})
(def login
"Flags related to login features"
#{;; Allows registration with login / password

View File

@@ -17,19 +17,18 @@
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.spec :as us]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.library :as ctl]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[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.shape.layout :as ctsl]
[app.common.types.text :as txt]
[app.common.types.token :as cto]
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
@@ -215,8 +214,8 @@
[(:frame-id new-main-instance-shape)]
(fn [shape objects]
(cond-> shape
(ctl/grid-layout? shape)
(ctl/assign-cells objects)))
(ctsl/grid-layout? shape)
(ctsl/assign-cells objects)))
{:with-objects? true}))]))
@@ -277,7 +276,7 @@
(->> ids-map vals (some #(= % (:parent-id first-shape))))
changes
(if (and (ctl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
(if (and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
(let [target-cell (-> position meta :cell)
[row column]
@@ -288,10 +287,10 @@
[(:parent-id first-shape)]
(fn [shape objects]
(-> shape
(ctl/assign-cells objects)
(ctsl/assign-cells objects)
(cond-> (and (some? row) (some? column))
(-> (ctl/push-into-cell [(:id first-shape)] row column)
(ctl/assign-cells objects)))))
(-> (ctsl/push-into-cell [(:id first-shape)] row column)
(ctsl/assign-cells objects)))))
{:with-objects? true})
(pcb/reorder-grid-children [(:parent-id first-shape)])))
changes)
@@ -576,8 +575,8 @@
(defmethod uses-assets? :colors
[_ color-id shape library-id]
(if (nil? color-id)
(ctc/uses-library-colors? shape library-id)
(ctc/uses-library-color? shape library-id color-id)))
(cts/uses-library-colors? shape library-id)
(cts/uses-library-color? shape library-id color-id)))
(defmethod uses-assets? :typographies
[_ typography-id shape library-id]
@@ -605,7 +604,7 @@
(let [library-colors (get-in libraries [library-id :data :colors])]
(pcb/update-shapes changes
[(:id shape)]
#(ctc/sync-shape-colors % library-id library-colors))))
#(ctl/sync-colors % library-id library-colors))))
(defmethod generate-sync-shape :typographies
[_ changes library-id container shape libraries _]
@@ -854,7 +853,7 @@
container
omit-touched?)
(ctl/flex-layout? shape-main)
(ctsl/flex-layout? shape-main)
(update-flex-child-copy-attrs shape-main
shape-inst
library
@@ -973,7 +972,7 @@
changes
(cond-> changes
(ctl/grid-layout? shape-inst)
(ctsl/grid-layout? shape-inst)
(update-grid-copy-attrs
(:id shape-inst)
shape-main
@@ -1063,14 +1062,14 @@
component-container
{:copy-touched? true}))
(ctl/flex-layout? shape-main)
(ctsl/flex-layout? shape-main)
(update-flex-child-main-attrs shape-main
shape-inst
component-container
container
omit-touched?)
(ctl/grid-layout? shape-main)
(ctsl/grid-layout? shape-main)
(update-grid-main-attrs shape-main
shape-inst
component-container
@@ -1665,27 +1664,61 @@
: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
(defn- text-change-value
[touched-content ;; The :content of the copy text before updating
untouched-content ;; The :content of the main component
touched]
(let [main-comps-diff (txt/get-diff-type touched-content untouched-content)
diff-structure? (contains? main-comps-diff :text-content-structure)
touched-attrs (txt/get-first-paragraph-text-attrs touched-content)
;; Have touched content an uniform style?
thed-unif-style? (txt/equal-attrs? touched-content touched-attrs)
untouched-attrs (txt/get-first-paragraph-text-attrs untouched-content)
;; Have untouched content an uniform style?
untched-unif-style? (txt/equal-attrs? untouched-content untouched-attrs)]
(cond
;; Both text and attrs has been touched, keep the
;; touched-content
(and (touched :text-content-text) (touched :text-content-attribute))
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-structure)
;; Special case for adding or removing paragraphs:
;; If the structure has been touched, but the attrs don't,
;; and both have uniform attributes, we keep the touched-content structure and
;; texts, updating its attrs to make them like the untouched-content
(if (and (not (touched :text-content-attribute)) thed-unif-style? untched-unif-style?)
(txt/copy-attrs-keys touched-content untouched-attrs)
;; In other case, we keep the touched content
touched-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)))
(touched :text-content-text)
;; Keep the texts touched in touched-content, so copy the
;; texts from touched-content into untouched-content
(txt/copy-text-keys touched-content untouched-content)
(touched :text-content-attribute)
;; The untouched content has a different structure, but the touched content had't
;; touched the structure
(if diff-structure?
;; If both have uniform attributes, we keep the untouched-content structure and
;; texts, updating its attrs to make them like the touched-content
(if (and thed-unif-style? untched-unif-style?)
(txt/copy-attrs-keys untouched-content touched-attrs)
;; In other case, we keep the touched content
touched-content)
;; Keep the attrs touched in touched-content, so copy the
;; texts from untouched-content into touched-content
(txt/copy-text-keys untouched-content touched-content))
;; Nothing is touched
:else
untouched-content)))
(defn- add-update-attr-operations
[attr dest-shape roperations uoperations attr-val]
@@ -1700,33 +1733,6 @@
[(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.
@@ -1783,13 +1789,13 @@
;; 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))
text-content-change?
(and
omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched attr-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
@@ -1798,7 +1804,7 @@
;; 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?)))
(not text-content-change?)))
attr-val (when-not skip-operations?
(cond
@@ -1807,18 +1813,23 @@
reset-pos-data?
nil
text-partial-change?
(text-partial-change-value (:content dest-shape)
(:content origin-shape)
touched)
text-content-change?
(text-change-value (:content dest-shape)
(:content origin-shape)
touched)
:else
(get origin-shape attr)))
;; If the final attr-value is the actual value, skip
skip-operations? (or skip-operations?
(= attr-val (get dest-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?
(if (and text-content-change? (not skip-operations?))
(add-update-attr-operations :position-data dest-shape roperations uoperations nil)
[roperations uoperations])
@@ -1830,6 +1841,93 @@
roperations'
uoperations')))))))
(defn- switch-text-change-value
[prev-content ;; The :content of the text before the switch
current-content ;; The :content of the text after the switch (a clean copy)
ref-content touched] ;; The :content of the referenced text on the main component
;; before the switch
(let [;; We need the differences between the contents on the main
;; components. current-content is the content of a clean copy,
;; so for all effects its the same as the content on its main
main-comps-diff (txt/get-diff-type ref-content current-content)
can-keep-text? (not (contains? main-comps-diff :text-content-text))
can-keep-attr? (not (contains? main-comps-diff :text-content-attribute))
main-diff-structure? (contains? main-comps-diff :text-content-structure)
current-attrs (txt/get-first-paragraph-text-attrs current-content)
;; Have current content an uniform style?
curr-unif-style? (txt/equal-attrs? current-content current-attrs)
prev-attrs (txt/get-first-paragraph-text-attrs prev-content)
;; Have prev content an uniform style?
prev-unif-style? (txt/equal-attrs? prev-content prev-attrs)
ref-attrs (txt/get-first-paragraph-text-attrs ref-content)
;; Have ref content an uniform style?
ref-unif-style? (txt/equal-attrs? ref-content ref-attrs)]
(cond
;; When the main components have a difference in structure
;; (different number of paragraph or text entries)
main-diff-structure?
;; Special case for adding or removing paragraphs:
;; If the structure has changed between ref-content and current-content,
;; but each one have uniform attributes, and the attrs on the main
;; components were equal, we keep the touched-content structure and
;; texts, updating its attrs to make them like the current-content
(if (and curr-unif-style?
ref-unif-style?
prev-unif-style?
(= ref-attrs current-attrs))
(txt/copy-attrs-keys current-content prev-attrs)
;; In any other case of structure change, we discard all
;; the overrides and keep the content of the current-shape
current-content)
;; When the main components are equal, we keep the updated
;; content from previous-shape as is
(and can-keep-text? can-keep-attr?)
prev-content
;; When we can't keep anything, we discard all the
;; overrides and keep the content of the current-shape
(and (not can-keep-text?) (not can-keep-attr?))
current-content
;; Special case for added or removed paragraphs:
;; If the structure has changed on current-content, but it has uniform attributes
;; and the previous-content also has uniform attributes, and we can keep the changes
;; on the text, we keep the touched-content structure and texts, updating
;; its attrs to make them like the current-content
(and (touched :text-content-structure)
curr-unif-style?
prev-unif-style?)
(if can-keep-text?
(txt/copy-attrs-keys prev-content current-attrs)
(txt/copy-attrs-keys current-content prev-attrs))
;; In any other case of structure change, we discard all
;; the overrides and keep the content of the current-shape
(touched :text-content-structure)
current-content
;; When there is a change on :text-content-text,
;; and and we can keep it, we copy the texts from
;; previous-shape over the attrs of current-shape
(and
(touched :text-content-text) can-keep-text?)
(txt/copy-text-keys prev-content current-content)
;; When there is a change on :text-content-attribute,
;; and we can keep it, we copy the texts from current-shape
;; over the attrs of previous-shape
(and
(touched :text-content-attribute) can-keep-attr?)
(txt/copy-text-keys current-content prev-content)
;; In any other case, we discard all the overrides
;; and keep the content of the current-shape
:else
current-content)))
(defn update-attrs-on-switch
"Copy attributes that have changed in the shape previous to the switch
to the current shape (post switch). Used only on variants switch"
@@ -1883,14 +1981,13 @@
;; 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))
text-change?
(and
(not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched attr-group))
;; 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
@@ -1902,20 +1999,34 @@
(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
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)
text-change?
(switch-text-change-value (:content previous-shape)
(:content current-shape)
(:content origin-ref-shape)
touched)
:else
(get previous-shape attr)))
:else
(get previous-shape attr)))
;; If the final attr-value is the actual value, skip
skip-operations? (or skip-operations?
(= attr-val (get current-shape attr)))
;; On a text-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
(if (and (not skip-operations?) text-change?)
(add-update-attr-operations :position-data current-shape roperations uoperations nil)
[roperations uoperations])
[roperations' uoperations']
(if skip-operations?
@@ -2046,11 +2157,11 @@
(update cell :shapes #(filterv child? %)))))))
;; Take cells from main and remap the shapes to assign it to the copy
copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy))
main-cells (-> shape-main (ctl/remap-grid-cells ids-map) :layout-grid-cells)]
main-cells (-> shape-main (ctsl/remap-grid-cells ids-map) :layout-grid-cells)]
(-> shape-copy
(assoc :layout-grid-cells
(ctl/merge-cells main-cells copy-cells omit-touched?))
(ctl/assign-cells objects))))
(ctsl/merge-cells main-cells copy-cells omit-touched?))
(ctsl/assign-cells objects))))
{:ignore-touched true :with-objects? true})))
(defn- update-grid-main-attrs
@@ -2074,7 +2185,7 @@
[(:id shape-main)]
(fn [shape-main]
;; Take cells from copy and remap the shapes to assign it to the copy
(let [new-cells (-> (ctl/remap-grid-cells shape-copy ids-map) :layout-grid-cells)]
(let [new-cells (-> (ctsl/remap-grid-cells shape-copy ids-map) :layout-grid-cells)]
(assoc shape-main :layout-grid-cells new-cells)))
{:ignore-touched true}))]
(pcb/concat-changes changes new-changes)))
@@ -2187,8 +2298,8 @@
parent-id (:parent-id shape)
insert-before?
(and (ctl/flex-layout? objects parent-id)
(not (ctl/reverse? objects parent-id)))
(and (ctsl/flex-layout? objects parent-id)
(not (ctsl/reverse? objects parent-id)))
objects
(-> objects
@@ -2204,7 +2315,7 @@
(pcb/with-objects objects)
(pcb/resize-parents new-objects-ids)
;; Fix the order of the children inside the parent
(cond-> (ctl/any-layout? objects parent-id)
(cond-> (ctsl/any-layout? objects parent-id)
(pcb/reorder-children parent-id (get-in objects [parent-id :shapes]))))]
(assoc changes :file-id library-id)))
@@ -2541,8 +2652,8 @@
(gsh/move delta)
(d/update-when :interactions #(ctsi/remap-interactions % ids-map objects))
(cond-> (ctl/grid-layout? obj)
(ctl/remap-grid-cells ids-map))
(cond-> (ctsl/grid-layout? obj)
(ctsl/remap-grid-cells ids-map))
(cond-> (ctk/is-variant-container? parent)
(assoc :variant-id parent-id))
@@ -2558,8 +2669,8 @@
;; We want the first added object to touch it's parent, but not subsequent children
changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)})
(pcb/amend-last-change #(assoc % :old-id (:id obj)))
(cond-> (ctl/grid-layout? objects (:parent-id obj))
(-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true})
(cond-> (ctsl/grid-layout? objects (:parent-id obj))
(-> (pcb/update-shapes [(:parent-id obj)] ctsl/assign-cells {:with-objects? true})
(pcb/reorder-grid-children [(:parent-id obj)]))))
changes (cond-> changes

View File

@@ -8,15 +8,18 @@
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.logic.variants :as clv]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.text :as txt]
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]))
[app.common.types.shape :as cts]
[app.common.types.text :as txt]))
;; ----- File building
@@ -32,7 +35,7 @@
(defn add-text
[file text-label content & {:keys [text-params] :as text}]
(let [shape (-> (cts/setup-shape {:type :text :x 0 :y 0})
(txt/change-text content))]
(update :content txt/change-text content))]
(ths/add-sample-shape file text-label
(merge shape
text-params))))
@@ -71,7 +74,7 @@
(defn add-frame-with-text
[file frame-label child-label text & {:keys [frame-params child-params]}]
(let [shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text text)
(update :content txt/change-text text)
(assoc :position-data nil
:parent-label frame-label))]
(-> file
@@ -275,26 +278,36 @@
(defn swap-component
"Swap the specified shape by the component specified by component-tag"
[file shape component-tag & {:keys [page-label propagate-fn]}]
[file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}]
(let [page (if page-label
(thf/get-page file page-label)
(thf/current-page file))
libraries {(:id file) file}
[_ _all-parents changes]
orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape)))
[new-shape _all-parents changes]
(cll/generate-component-swap (pcb/empty-changes)
(:objects page)
shape
(:data file)
page
{(:id file) file}
libraries
(-> (thc/get-component file component-tag)
:id)
0
nil
{}
false)
(true? keep-touched?))
changes (if keep-touched?
(clv/generate-keep-touched changes new-shape shape orig-shapes page libraries (:data file))
changes)
file' (thf/apply-changes file changes)]
(when new-shape-label
(thi/set-id! new-shape-label (:id new-shape)))
(if propagate-fn
(propagate-fn file')
file')))

View File

@@ -6,18 +6,18 @@
(ns app.common.test-helpers.shapes
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.color :as clr]
[app.common.types.container :as ctn]
[app.common.types.library :as ctl]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.text :as txt]
[app.common.types.typographies-list :as cttl]
[app.common.types.typography :as ctt]))
@@ -125,7 +125,7 @@
(defn add-sample-library-color
[file label & {:keys [] :as params}]
(let [color (sample-library-color label params)]
(update file :data ctc/add-color color)))
(update file :data ctl/add-color color)))
(defn sample-typography
[label & {:keys [] :as params}]
@@ -149,4 +149,4 @@
(fn [file-data]
(ctpl/update-page file-data
(:id page)
#(ctst/set-shape % (assoc origin :interactions interactions)))))))
#(ctst/set-shape % (assoc origin :interactions interactions)))))))

View File

@@ -8,7 +8,9 @@
(:require
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]))
[app.common.test-helpers.shapes :as ths]
[app.common.types.shape :as cts]
[app.common.types.text :as txt]))
(defn add-variant
[file variant-label component1-label root1-label component2-label root2-label
@@ -37,3 +39,48 @@
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v1"} {:name "Property2" :value "p2v1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v2"} {:name "Property2" :value "p2v2"}]}))))
(defn add-variant-with-child
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label
& {:keys [child1-params child2-params]}]
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(ths/add-sample-shape child1-label (assoc child1-params :parent-label root1-label))
(ths/add-sample-shape child2-label (assoc child2-params :parent-label root2-label))
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))
(defn add-variant-with-text
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label text1 text2
& {:keys [text1-params text2-params]}]
(let [text1 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(update :content txt/change-text text1)
(assoc :position-data nil
:parent-label root1-label))
text2 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(update :content txt/change-text text2)
(assoc :position-data nil
:parent-label root2-label))
file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(ths/add-sample-shape child1-label
(merge text1
text1-params))
(ths/add-sample-shape child2-label
(merge text2
text2-params))
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))

View File

@@ -5,186 +5,16 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.text
"Legacy editor helpers (draftjs).
NOTE: this namespace should be not used for new code related to texts"
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.transit :as t]
[clojure.walk :as walk]
[app.common.types.text :as types.text]
[cuerdas.core :as str]))
;; -- Attrs
(def text-typography-attrs
[:typography-ref-id
:typography-ref-file])
(def text-fill-attrs
[:fill-color
:fill-opacity
:fill-color-ref-id
:fill-color-ref-file
:fill-color-gradient])
(def text-font-attrs
[:font-id
:font-family
:font-variant-id
:font-size
:font-weight
:font-style])
(def text-align-attrs
[:text-align])
(def text-direction-attrs
[:text-direction])
(def text-spacing-attrs
[:line-height
:letter-spacing])
(def text-valign-attrs
[:vertical-align])
(def text-decoration-attrs
[:text-decoration])
(def text-transform-attrs
[:text-transform])
(def text-fills
[:fills])
(def shape-attrs
[:grow-type])
(def root-attrs
text-valign-attrs)
(def paragraph-attrs
(d/concat-vec
text-align-attrs
text-direction-attrs))
(def text-node-attrs
(d/concat-vec
text-typography-attrs
text-font-attrs
text-spacing-attrs
text-decoration-attrs
text-transform-attrs
text-fills))
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
(def text-style-attrs
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
(def default-root-attrs
{:vertical-align "top"})
(def default-text-attrs
{:typography-ref-file nil
:typography-ref-id nil
:font-id "sourcesanspro"
:font-family "sourcesanspro"
:font-variant-id "regular"
:font-size "14"
:font-weight "400"
:font-style "normal"
:line-height "1.2"
:letter-spacing "0"
:text-transform "none"
:text-align "left"
:text-decoration "none"
:text-direction "ltr"
:fills [{:fill-color clr/black
:fill-opacity 1}]})
(def default-attrs
(merge default-root-attrs default-text-attrs))
(def typography-fields
[:font-id
:font-family
:font-variant-id
:font-size
:font-weight
:font-style
:line-height
:letter-spacing
:text-transform])
(def default-typography
(merge
{:name "Source Sans Pro Regular"}
(select-keys default-text-attrs typography-fields)))
(defn node-seq
([root] (node-seq identity root))
([match? root]
(->> (tree-seq map? :children root)
(filter match?)
(seq))))
(defn is-text-node?
[node]
(and (nil? (:type node))
(string? (:text node))))
(defn is-paragraph-set-node?
[node]
(= "paragraph-set" (:type node)))
(defn is-paragraph-node?
[node]
(= "paragraph" (:type node)))
(defn is-root-node?
[node]
(= "root" (:type node)))
(defn is-node?
[node]
(or ^boolean (is-text-node? node)
^boolean (is-paragraph-node? node)
^boolean (is-paragraph-set-node? node)
^boolean (is-root-node? node)))
(defn is-content-node?
"Only matches content nodes, ignoring the paragraph-set nodes."
[node]
(or ^boolean (is-text-node? node)
^boolean (is-paragraph-node? node)
^boolean (is-root-node? node)))
(defn transform-nodes
([transform root]
(transform-nodes identity transform root))
([pred transform root]
(walk/postwalk
(fn [item]
(if (and (is-node? item) (pred item))
(transform item)
item))
root)))
(defn update-text-content
[shape pred-fn update-fn attrs]
(let [update-attrs-fn #(update-fn % attrs)
transform #(transform-nodes pred-fn update-attrs-fn %)]
(-> shape
(update :content transform))))
(defn generate-shape-name
[text]
(subs text 0 (min 280 (count text))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DraftJS <-> Penpot Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn encode-style-value
[v]
(t/encode-str v))
@@ -369,7 +199,7 @@
:entityRanges []
:inlineStyleRanges (calc-ranges paragraph)})]
{:blocks (reduce #(conj %1 (build-block %2)) [] (node-seq #(= (:type %) "paragraph") root))
{:blocks (reduce #(conj %1 (build-block %2)) [] (types.text/node-seq #(= (:type %) "paragraph") root))
:entityMap {}}))
(defn content->text+styles
@@ -377,13 +207,13 @@
[node]
(letfn
[(rec-style-text-map [acc node style]
(let [node-style (merge style (select-keys node text-all-attrs))
(let [node-style (merge style (select-keys node types.text/text-all-attrs))
head (or (-> acc first) [{} ""])
[head-style head-text] head
new-acc
(cond
(not (is-text-node? node))
(not (types.text/is-text-node? node))
(reduce #(rec-style-text-map %1 %2 node-style) acc (:children node))
(not= head-style node-style)
@@ -403,82 +233,6 @@
(-> (rec-style-text-map [] node {})
reverse)))
(defn content-range->text+styles
"Given a root node of a text content extracts the texts with its associated styles"
[node start end]
(let [sss (content->text+styles node)]
(loop [styles (seq sss)
taking? false
acc 0
result []]
(if styles
(let [[node-style text] (first styles)
from acc
to (+ acc (count text))
taking? (or taking? (and (<= from start) (< start to)))
text (subs text (max 0 (- start acc)) (- end acc))
result (cond-> result
(and taking? (d/not-empty? text))
(conj (assoc node-style :text text)))
continue? (or (> from end) (>= end to))]
(recur (when continue? (rest styles)) taking? to result))
result))))
(defn content->text
"Given a root node of a text content extracts the texts with its associated styles"
[content]
(letfn [(add-node [acc node]
(cond
(is-paragraph-node? node)
(conj acc [])
(is-text-node? node)
(let [i (dec (count acc))]
(update acc i conj (:text node)))
:else
acc))]
(->> (node-seq content)
(reduce add-node [])
(map #(str/join "" %))
(str/join "\n"))))
(defn change-text
"Changes the content of the text shape to use the text as argument. Will use the styles of the
first paragraph and text that is present in the shape (and override the rest)"
[shape text]
(let [content (:content shape)
root-styles (select-keys content root-attrs)
paragraph-style (merge
default-text-attrs
(select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs))
text-style (merge
default-text-attrs
(select-keys (->> content (node-seq is-text-node?) first) text-all-attrs))
paragraph-texts (str/split text "\n")
paragraphs
(->> paragraph-texts
(mapv
(fn [pt]
(merge
paragraph-style
{:type "paragraph"
:children [(merge {:text pt} text-style)]}))))
new-content
(d/patch-object
{:type "root"
:children
[{:type "paragraph-set"
:children paragraphs}]}
root-styles)]
(assoc shape :content new-content)))
(defn index-content
"Adds a property `$id` that identifies the current node inside"
([content]

View File

@@ -5,15 +5,15 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.types.color
(:refer-clojure :exclude [test])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi]
[app.common.text :as txt]
[app.common.time :as dt]
[app.common.types.plugins :as ctpg]
[clojure.set :as set]
[cuerdas.core :as str]))
@@ -163,11 +163,183 @@
(def check-color
(sm/check-fn schema:color :hint "expected valid color"))
;: FIXME: maybe declare it under types.library ?
(def check-library-color
(sm/check-fn schema:library-color :hint "expected valid color"))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;; CONSTANTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:const black "#000000")
(def ^:const default-layout "#DE4762")
(def ^:const gray-20 "#B1B2B5")
(def ^:const info "#59B9E2")
(def ^:const test "#fabada")
(def ^:const white "#FFFFFF")
(def ^:const warning "#FC8802")
;; new-css-system colors
(def ^:const new-primary "#7efff5")
(def ^:const new-danger "#ff3277")
(def ^:const new-warning "#fe4811")
(def ^:const new-primary-light "#6911d4")
(def ^:const background-quaternary "#2e3434")
(def ^:const background-quaternary-light "#eef0f2")
(def ^:const canvas "#E8E9EA")
(def names
{"aliceblue" "#f0f8ff"
"antiquewhite" "#faebd7"
"aqua" "#00ffff"
"aquamarine" "#7fffd4"
"azure" "#f0ffff"
"beige" "#f5f5dc"
"bisque" "#ffe4c4"
"black" "#000000"
"blanchedalmond" "#ffebcd"
"blue" "#0000ff"
"blueviolet" "#8a2be2"
"brown" "#a52a2a"
"burlywood" "#deb887"
"cadetblue" "#5f9ea0"
"chartreuse" "#7fff00"
"chocolate" "#d2691e"
"coral" "#ff7f50"
"cornflowerblue" "#6495ed"
"cornsilk" "#fff8dc"
"crimson" "#dc143c"
"cyan" "#00ffff"
"darkblue" "#00008b"
"darkcyan" "#008b8b"
"darkgoldenrod" "#b8860b"
"darkgray" "#a9a9a9"
"darkgreen" "#006400"
"darkgrey" "#a9a9a9"
"darkkhaki" "#bdb76b"
"darkmagenta" "#8b008b"
"darkolivegreen" "#556b2f"
"darkorange" "#ff8c00"
"darkorchid" "#9932cc"
"darkred" "#8b0000"
"darksalmon" "#e9967a"
"darkseagreen" "#8fbc8f"
"darkslateblue" "#483d8b"
"darkslategray" "#2f4f4f"
"darkslategrey" "#2f4f4f"
"darkturquoise" "#00ced1"
"darkviolet" "#9400d3"
"deeppink" "#ff1493"
"deepskyblue" "#00bfff"
"dimgray" "#696969"
"dimgrey" "#696969"
"dodgerblue" "#1e90ff"
"firebrick" "#b22222"
"floralwhite" "#fffaf0"
"forestgreen" "#228b22"
"fuchsia" "#ff00ff"
"gainsboro" "#dcdcdc"
"ghostwhite" "#f8f8ff"
"gold" "#ffd700"
"goldenrod" "#daa520"
"gray" "#808080"
"green" "#008000"
"greenyellow" "#adff2f"
"grey" "#808080"
"honeydew" "#f0fff0"
"hotpink" "#ff69b4"
"indianred" "#cd5c5c"
"indigo" "#4b0082"
"ivory" "#fffff0"
"khaki" "#f0e68c"
"lavender" "#e6e6fa"
"lavenderblush" "#fff0f5"
"lawngreen" "#7cfc00"
"lemonchiffon" "#fffacd"
"lightblue" "#add8e6"
"lightcoral" "#f08080"
"lightcyan" "#e0ffff"
"lightgoldenrodyellow" "#fafad2"
"lightgray" "#d3d3d3"
"lightgreen" "#90ee90"
"lightgrey" "#d3d3d3"
"lightpink" "#ffb6c1"
"lightsalmon" "#ffa07a"
"lightseagreen" "#20b2aa"
"lightskyblue" "#87cefa"
"lightslategray" "#778899"
"lightslategrey" "#778899"
"lightsteelblue" "#b0c4de"
"lightyellow" "#ffffe0"
"lime" "#00ff00"
"limegreen" "#32cd32"
"linen" "#faf0e6"
"magenta" "#ff00ff"
"maroon" "#800000"
"mediumaquamarine" "#66cdaa"
"mediumblue" "#0000cd"
"mediumorchid" "#ba55d3"
"mediumpurple" "#9370db"
"mediumseagreen" "#3cb371"
"mediumslateblue" "#7b68ee"
"mediumspringgreen" "#00fa9a"
"mediumturquoise" "#48d1cc"
"mediumvioletred" "#c71585"
"midnightblue" "#191970"
"mintcream" "#f5fffa"
"mistyrose" "#ffe4e1"
"moccasin" "#ffe4b5"
"navajowhite" "#ffdead"
"navy" "#000080"
"oldlace" "#fdf5e6"
"olive" "#808000"
"olivedrab" "#6b8e23"
"orange" "#ffa500"
"orangered" "#ff4500"
"orchid" "#da70d6"
"palegoldenrod" "#eee8aa"
"palegreen" "#98fb98"
"paleturquoise" "#afeeee"
"palevioletred" "#db7093"
"papayawhip" "#ffefd5"
"peachpuff" "#ffdab9"
"peru" "#cd853f"
"pink" "#ffc0cb"
"plum" "#dda0dd"
"powderblue" "#b0e0e6"
"purple" "#800080"
"red" "#ff0000"
"rosybrown" "#bc8f8f"
"royalblue" "#4169e1"
"saddlebrown" "#8b4513"
"salmon" "#fa8072"
"sandybrown" "#f4a460"
"seagreen" "#2e8b57"
"seashell" "#fff5ee"
"sienna" "#a0522d"
"silver" "#c0c0c0"
"skyblue" "#87ceeb"
"slateblue" "#6a5acd"
"slategray" "#708090"
"slategrey" "#708090"
"snow" "#fffafa"
"springgreen" "#00ff7f"
"steelblue" "#4682b4"
"tan" "#d2b48c"
"teal" "#008080"
"thistle" "#d8bfd8"
"tomato" "#ff6347"
"turquoise" "#40e0d0"
"violet" "#ee82ee"
"wheat" "#f5deb3"
"white" "#ffffff"
"whitesmoke" "#f5f5f5"
"yellow" "#ffff00"
"yellowgreen" "#9acd32"})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS (FIXME: this helpers are not in the correct place)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn library-color->color
@@ -181,42 +353,6 @@
:path (get lcolor :path)
:name (get lcolor :name))))
;; --- fill
(defn fill->color
[fill]
(d/without-nils
{:color (:fill-color fill)
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)
:image (:fill-image fill)
:ref-id (:fill-color-ref-id fill)
:ref-file (:fill-color-ref-file fill)}))
(defn set-fill-color
[shape position color opacity gradient image]
(update-in shape [:fills position]
(fn [fill]
(d/without-nils (assoc fill
:fill-color color
:fill-opacity opacity
:fill-color-gradient gradient
:fill-image image)))))
(defn attach-fill-color
[shape position ref-id ref-file]
(d/update-in-when shape [:fills position]
(fn [fill]
(-> fill
(assoc :fill-color-ref-file ref-file)
(assoc :fill-color-ref-id ref-id)))))
(defn detach-fill-color
[shape position]
(d/update-in-when shape [:fills position] dissoc :fill-color-ref-id :fill-color-ref-file))
;; stroke
(defn stroke->color
[stroke]
(d/without-nils
@@ -227,59 +363,10 @@
:ref-id (:stroke-color-ref-id stroke)
:ref-file (:stroke-color-ref-file stroke)}))
(defn set-stroke-color
[shape position color opacity gradient image]
(d/update-in-when shape [:strokes position]
(fn [stroke]
(-> stroke
(assoc :stroke-color color)
(assoc :stroke-opacity opacity)
(assoc :stroke-color-gradient gradient)
(assoc :stroke-image image)
(d/without-nils)))))
(defn attach-stroke-color
[shape position ref-id ref-file]
(d/update-in-when shape [:strokes position]
(fn [stroke]
(-> stroke
(assoc :stroke-color-ref-id ref-id)
(assoc :stroke-color-ref-file ref-file)))))
(defn detach-stroke-color
[shape position]
(d/update-in-when shape [:strokes position] dissoc :stroke-color-ref-id :stroke-color-ref-file))
;; shadow
(defn shadow->color
[shadow]
(:color shadow))
(defn set-shadow-color
[shape position color opacity gradient]
(d/update-in-when shape [:shadow position :color]
(fn [shadow-color]
(-> shadow-color
(assoc :color color)
(assoc :opacity opacity)
(assoc :gradient gradient)
(d/without-nils)))))
(defn attach-shadow-color
[shape position ref-id ref-file]
(d/update-in-when shape [:shadow position :color]
(fn [color]
(-> color
(assoc :ref-id ref-id)
(assoc :ref-file ref-file)))))
(defn detach-shadow-color
[shape position]
(d/update-in-when shape [:shadow position :color] dissoc :ref-id :ref-file))
;; grid
;: FIXME: revisit colors...... WTF
(defn grid->color
[grid]
@@ -291,291 +378,374 @@
:ref-id (-> color :id)
:ref-file (-> color :file-id)})))
(defn set-grid-color
[shape position color opacity gradient]
(d/update-in-when shape [:grids position :params :color]
(fn [grid-color]
(-> grid-color
(assoc :color color)
(assoc :opacity opacity)
(assoc :gradient gradient)
(d/without-nils)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn attach-grid-color
[shape position ref-id ref-file]
(d/update-in-when shape [:grids position :params :color]
(fn [color]
(-> color
(assoc :ref-id ref-id)
(assoc :ref-file ref-file)))))
(def ^:private hex-color-re
#"\#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})")
(defn detach-grid-color
[shape position]
(d/update-in-when shape [:grids position :params :color] dissoc :ref-id :ref-file))
(def ^:private rgb-color-re
#"(?:|rgb)\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)")
;; --- Helpers for all colors in a shape
(defn get-text-node-colors
"Get all colors used by a node of a text shape"
[node]
(concat (map fill->color (:fills node))
(map stroke->color (:strokes node))))
(defn get-all-colors
"Get all colors used by a shape, in any section."
[shape]
(concat (map fill->color (:fills shape))
(map stroke->color (:strokes shape))
(map shadow->color (:shadow shape))
(when (= (:type shape) :frame)
(map grid->color (:grids shape)))
(when (= (:type shape) :text)
(reduce (fn [colors node]
(concat colors (get-text-node-colors node)))
()
(txt/node-seq (:content shape))))))
(defn uses-library-colors?
"Check if the shape uses any color in the given library."
[shape library-id]
(let [all-colors (get-all-colors shape)]
(some #(and (some? (:ref-id %))
(= (:ref-file %) library-id))
all-colors)))
(defn uses-library-color?
"Check if the shape uses the given library color."
[shape library-id color-id]
(let [all-colors (get-all-colors shape)]
(some #(and (= (:ref-id %) color-id)
(= (:ref-file %) library-id))
all-colors)))
(defn- process-shape-colors
"Execute an update function on all colors of a shape."
[shape process-fn]
(let [process-fill (fn [shape [position fill]]
(process-fn shape
position
(fill->color fill)
set-fill-color
attach-fill-color
detach-fill-color))
process-stroke (fn [shape [position stroke]]
(process-fn shape
position
(stroke->color stroke)
set-stroke-color
attach-stroke-color
detach-stroke-color))
process-shadow (fn [shape [position shadow]]
(process-fn shape
position
(shadow->color shadow)
set-shadow-color
attach-shadow-color
detach-shadow-color))
process-grid (fn [shape [position grid]]
(process-fn shape
position
(grid->color grid)
set-grid-color
attach-grid-color
detach-grid-color))
process-text-node (fn [node]
(as-> node $
(reduce process-fill $ (d/enumerate (:fills $)))
(reduce process-stroke $ (d/enumerate (:strokes $)))))
process-text (fn [shape]
(let [content (:content shape)
new-content (txt/transform-nodes process-text-node content)]
(if (not= content new-content)
(assoc shape :content new-content)
shape)))]
(as-> shape $
(reduce process-fill $ (d/enumerate (:fills $)))
(reduce process-stroke $ (d/enumerate (:strokes $)))
(reduce process-shadow $ (d/enumerate (:shadow $)))
(reduce process-grid $ (d/enumerate (:grids $)))
(process-text $))))
(defn remap-colors
"Change the shape so that any use of the given color now points to
the given library."
[shape library-id color]
(letfn [(remap-color [shape position shape-color _ attach-fn _]
(if (= (:ref-id shape-color) (:id color))
(attach-fn shape
position
(:id color)
library-id)
shape))]
(process-shape-colors shape remap-color)))
(defn sync-shape-colors
"Look for usage of any color of the given library inside the shape,
and, in this case, copy the library color into the shape."
[shape library-id library-colors]
(letfn [(sync-color [shape position shape-color set-fn _ detach-fn]
(if (= (:ref-file shape-color) library-id)
(let [library-color (get library-colors (:ref-id shape-color))]
(if (some? library-color)
(set-fn shape
position
(:color library-color)
(:opacity library-color)
(:gradient library-color)
(:image library-color))
(detach-fn shape position)))
shape))]
(process-shape-colors shape sync-color)))
(defn- stroke->color-att
[stroke file-id libraries]
(let [ref-file (:stroke-color-ref-file stroke)
ref-id (:stroke-color-ref-id stroke)
shared-colors (dm/get-in libraries [ref-file :data :colors])
is-shared? (contains? shared-colors ref-id)
has-color? (or (:stroke-color stroke)
(:stroke-color-gradient stroke))
attrs (cond-> (stroke->color stroke)
(not (or is-shared? (= ref-file file-id)))
(dissoc :ref-id :ref-file))]
(when has-color?
{:attrs attrs
:prop :stroke
:shape-id (:shape-id stroke)
:index (:index stroke)})))
(defn- shadow->color-att
[shadow file-id libraries]
(let [color (get shadow :color)
ref-file (get color :ref-file)
ref-id (get color :ref-id)
shared-colors (dm/get-in libraries [ref-file :data :colors])
is-shared? (contains? shared-colors ref-id)
attrs (cond-> (shadow->color shadow)
(not (or is-shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :shadow
:shape-id (:shape-id shadow)
:index (:index shadow)}))
(defn- text->color-att
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
shared-colors (dm/get-in libraries [ref-file :data :colors])
is-shared? (contains? shared-colors ref-id)
attrs (cond-> (fill->color fill)
(not (or is-shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :content
:shape-id (:shape-id fill)
:index (:index fill)}))
(defn- treat-node
[node shape-id]
(map-indexed #(assoc %2 :shape-id shape-id :index %1) node))
(defn- extract-text-colors
[text file-id libraries]
(->> (txt/node-seq txt/is-text-node? (:content text))
(map :fills)
(mapcat #(treat-node % (:id text)))
(map #(text->color-att % file-id libraries))))
(defn- fill->color-att
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
shared-colors (dm/get-in libraries [ref-file :data :colors])
is-shared? (contains? shared-colors ref-id)
has-color? (or (:fill-color fill)
(:fill-color-gradient fill))
attrs (cond-> (fill->color fill)
(not (or is-shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
(when has-color?
{:attrs attrs
:prop :fill
:shape-id (:shape-id fill)
:index (:index fill)})))
(defn extract-all-colors
[shapes file-id libraries]
(reduce
(fn [result shape]
(let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape))
stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape))
shadow-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:shadow shape))]
(if (= :text (:type shape))
(-> result
(into (map #(stroke->color-att % file-id libraries)) stroke-obj)
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)
(into (extract-text-colors shape file-id libraries)))
(-> result
(into (map #(fill->color-att % file-id libraries)) fill-obj)
(into (map #(stroke->color-att % file-id libraries)) stroke-obj)
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)))))
[]
shapes))
(defn colors-seq
[file-data]
(vals (:colors file-data)))
(defn- touch
(defn valid-hex-color?
[color]
(assoc color :modified-at (dt/now)))
(and (string? color)
(some? (re-matches hex-color-re color))))
(defn add-color
[file-data color]
(update file-data :colors assoc (:id color) (touch color)))
(defn parse-rgb
[color]
(let [result (re-matches rgb-color-re color)]
(when (some? result)
(let [r (parse-long (nth result 1))
g (parse-long (nth result 2))
b (parse-long (nth result 3))]
(when (and (<= 0 r 255) (<= 0 g 255) (<= 0 b 255))
[r g b])))))
(defn get-color
[file-data color-id]
(get-in file-data [:colors color-id]))
(defn valid-rgb-color?
[color]
(if (string? color)
(let [result (parse-rgb color)]
(some? result))
false))
(defn get-ref-color
[library-data color]
(when (= (:ref-file color) (:id library-data))
(get-color library-data (:ref-id color))))
(defn- normalize-hex
[color]
(if (= (count color) 4) ; of the form #RGB
(-> color
(str/replace #"\#(.)(.)(.)" "#$1$1$2$2$3$3")
(str/lower))
(str/lower color)))
(defn set-color
[file-data color]
(d/assoc-in-when file-data [:colors (:id color)] (touch color)))
(defn rgb->str
[[r g b a]]
(if (some? a)
(str/ffmt "rgba(%,%,%,%)" r g b a)
(str/ffmt "rgb(%,%,%)" r g b)))
(defn update-color
[file-data color-id f & args]
(d/update-in-when file-data [:colors color-id] #(-> (apply f % args)
(touch))))
(defn rgb->hsv
[[red green blue]]
(let [max (d/max red green blue)
min (d/min red green blue)
val max]
(if (= min max)
[0 0 val]
(let [delta (- max min)
sat (/ delta max)
hue (if (= red max)
(/ (- green blue) delta)
(if (= green max)
(+ 2 (/ (- blue red) delta))
(+ 4 (/ (- red green) delta))))
hue (* 60 hue)
hue (if (< hue 0)
(+ hue 360)
hue)
hue (if (> hue 360)
(- hue 360)
hue)]
[hue sat val]))))
(defn delete-color
[file-data color-id]
(update file-data :colors dissoc color-id))
(defn hsv->rgb
[[h s brightness]]
(if (= s 0)
[brightness brightness brightness]
(let [sextant (int (mth/floor (/ h 60)))
remainder (- (/ h 60) sextant)
brightness (d/nilv brightness 0)
val1 (int (* brightness (- 1 s)))
val2 (int (* brightness (- 1 (* s remainder))))
val3 (int (* brightness (- 1 (* s (- 1 remainder)))))]
(case sextant
1 [val2 brightness val1]
2 [val1 brightness val3]
3 [val1 val2 brightness]
4 [val3 val1 brightness]
5 [brightness val1 val2]
6 [brightness val3 val1]
0 [brightness val3 val1]))))
(defn used-colors-changed-since
"Find all usages of any color in the library by the given shape, of colors
that have ben modified after the date."
[shape library since-date]
(->> (get-all-colors shape)
(keep #(get-ref-color (:data library) %))
(remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil
(map (fn [color] {:shape-id (:id shape)
:asset-id (:id color)
:asset-type :color}))))
(defn hex->rgb
[color]
(try
(let [rgb #?(:clj (Integer/parseInt (subs color 1) 16)
:cljs (js/parseInt (subs color 1) 16))
r (bit-shift-right rgb 16)
g (bit-and (bit-shift-right rgb 8) 255)
b (bit-and rgb 255)]
[r g b])
(catch #?(:clj Throwable :cljs :default) _cause
[0 0 0])))
(defn hex->lum
[color]
(let [[r g b] (hex->rgb color)]
(mth/sqrt (+ (* 0.241 r)
(* 0.691 g)
(* 0.068 b)))))
(defn- int->hex
"Convert integer to hex string"
[v]
#?(:clj (Integer/toHexString v)
:cljs (.toString v 16)))
(defn rgb->hex
[[r g b]]
(let [r (int r)
g (int g)
b (int b)]
(if (or (not= r (bit-and r 255))
(not= g (bit-and g 255))
(not= b (bit-and b 255)))
(throw (ex-info "not valid rgb" {:r r :g g :b b}))
(let [rgb (bit-or (bit-shift-left r 16)
(bit-shift-left g 8) b)]
(if (< r 16)
(dm/str "#" (subs (int->hex (bit-or 0x1000000 rgb)) 1))
(dm/str "#" (int->hex rgb)))))))
(defn rgb->hsl
[[r g b]]
(let [norm-r (/ r 255.0)
norm-g (/ g 255.0)
norm-b (/ b 255.0)
max (d/max norm-r norm-g norm-b)
min (d/min norm-r norm-g norm-b)
l (/ (+ max min) 2.0)
h (if (= max min) 0
(if (= max norm-r)
(* 60 (/ (- norm-g norm-b) (- max min)))
(if (= max norm-g)
(+ 120 (* 60 (/ (- norm-b norm-r) (- max min))))
(+ 240 (* 60 (/ (- norm-r norm-g) (- max min)))))))
s (if (and (> l 0) (<= l 0.5))
(/ (- max min) (* 2 l))
(/ (- max min) (- 2 (* 2 l))))]
[(mod (+ h 360) 360) s l]))
(defn hex->hsv
[v]
(-> v hex->rgb rgb->hsv))
(defn hex->rgba
[data opacity]
(-> (hex->rgb data)
(conj opacity)))
(defn hex->hsl [hex]
(try
(-> hex hex->rgb rgb->hsl)
(catch #?(:clj Throwable :cljs :default) _e
[0 0 0])))
(defn hex->hsla
[data opacity]
(-> (hex->hsl data)
(conj opacity)))
(defn format-hsla
[[h s l a]]
(let [precision 2
rounded-h (int h)
rounded-s (d/format-number (* 100 s) precision)
rounded-l (d/format-number (* 100 l) precision)
rounded-a (d/format-number a precision)]
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
(defn format-rgba
[[r g b a]]
(let [precision 2
rounded-a (d/format-number a precision)]
(str/ffmt "%, %, %, %" r g b rounded-a)))
(defn- hue->rgb
"Helper for hsl->rgb"
[v1 v2 vh]
(let [vh (if (< vh 0)
(+ vh 1)
(if (> vh 1)
(- vh 1)
vh))]
(cond
(< (* 6 vh) 1) (+ v1 (* (- v2 v1) 6 vh))
(< (* 2 vh) 1) v2
(< (* 3 vh) 2) (+ v1 (* (- v2 v1) (- (/ 2 3) vh) 6))
:else v1)))
(defn hsl->rgb
[[h s l]]
(if (= s 0)
(let [o (* l 255)]
[o o o])
(let [norm-h (/ h 360.0)
temp2 (if (< l 0.5)
(* l (+ 1 s))
(- (+ l s)
(* s l)))
temp1 (- (* l 2) temp2)]
[(mth/round (* 255 (hue->rgb temp1 temp2 (+ norm-h (/ 1 3)))))
(mth/round (* 255 (hue->rgb temp1 temp2 norm-h)))
(mth/round (* 255 (hue->rgb temp1 temp2 (- norm-h (/ 1 3)))))])))
(defn hsl->hex
[v]
(-> v hsl->rgb rgb->hex))
(defn hsl->hsv
[hsl]
(-> hsl hsl->rgb rgb->hsv))
(defn hsv->hex
[hsv]
(-> hsv hsv->rgb rgb->hex))
(defn hsv->hsl
[hsv]
(-> hsv hsv->hex hex->hsl))
(defn expand-hex
[v]
(cond
(re-matches #"^[0-9A-Fa-f]$" v)
(dm/str v v v v v v)
(re-matches #"^[0-9A-Fa-f]{2}$" v)
(dm/str v v v)
(re-matches #"^[0-9A-Fa-f]{3}$" v)
(let [a (nth v 0)
b (nth v 1)
c (nth v 2)]
(dm/str a a b b c c))
:else
v))
(defn prepend-hash
[color]
(if (= "#" (subs color 0 1))
color
(dm/str "#" color)))
(defn remove-hash
[color]
(if (str/starts-with? color "#")
(subs color 1)
color))
(defn color-string?
[color]
(and (string? color)
(or (valid-hex-color? color)
(valid-rgb-color? color)
(contains? names color))))
(defn parse
[color]
(when (string? color)
(if (or (valid-hex-color? color)
(valid-hex-color? (dm/str "#" color)))
(normalize-hex color)
(or (some-> (parse-rgb color) (rgb->hex))
(get names (str/lower color))))))
(def color-names
(into [] (keys names)))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
(defn next-rgb
"Given a color in rgb returns the next color"
[[r g b]]
(cond
(and (= 255 r) (= 255 g) (= 255 b))
(throw (ex-info "cannot get next color" {:r r :g g :b b}))
(and (= 255 g) (= 255 b))
[(inc r) 0 0]
(= 255 b)
[r (inc g) 0]
:else
[r g (inc b)]))
(defn reduce-range
[value range]
(/ (mth/floor (* value range)) range))
(defn sort-colors
[a b]
(let [[ah _ av] (hex->hsv (:color a))
[bh _ bv] (hex->hsv (:color b))
ah (reduce-range (/ ah 60) 8)
bh (reduce-range (/ bh 60) 8)
av (/ av 255)
bv (/ bv 255)
a (+ (* ah 100) (* av 10))
b (+ (* bh 100) (* bv 10))]
(compare a b)))
(defn interpolate-color
[c1 c2 offset]
(cond
(<= offset (:offset c1)) (assoc c1 :offset offset)
(>= offset (:offset c2)) (assoc c2 :offset offset)
:else
(let [tr-offset (/ (- offset (:offset c1)) (- (:offset c2) (:offset c1)))
[r1 g1 b1] (hex->rgb (:color c1))
[r2 g2 b2] (hex->rgb (:color c2))
a1 (:opacity c1)
a2 (:opacity c2)
r (+ r1 (* (- r2 r1) tr-offset))
g (+ g1 (* (- g2 g1) tr-offset))
b (+ b1 (* (- b2 b1) tr-offset))
a (+ a1 (* (- a2 a1) tr-offset))]
{:color (rgb->hex [r g b])
:opacity a
:r r
:g g
:b b
:alpha a
:offset offset})))
(defn- offset-spread
[from to num]
(->> (range 0 num)
(map #(mth/precision (+ from (* (/ (- to from) (dec num)) %)) 2))))
(defn uniform-spread?
"Checks if the gradient stops are spread uniformly"
[stops]
(let [cs (count stops)
from (first stops)
to (last stops)
expect-vals (offset-spread (:offset from) (:offset to) cs)
calculate-expected
(fn [expected-offset stop]
(and (mth/close? (:offset stop) expected-offset)
(let [ec (interpolate-color from to expected-offset)]
(and (= (:color ec) (:color stop))
(= (:opacity ec) (:opacity stop))))))]
(->> (map calculate-expected expect-vals stops)
(every? true?))))
(defn uniform-spread
"Assign an uniform spread to the offset values for the gradient"
[from to num-stops]
(->> (offset-spread (:offset from) (:offset to) num-stops)
(mapv (fn [offset]
(interpolate-color from to offset)))))
(defn interpolate-gradient
[stops offset]
(let [idx (d/index-of-pred stops #(<= offset (:offset %)))
start (if (= idx 0) (first stops) (get stops (dec idx)))
end (if (nil? idx) (last stops) (get stops idx))]
(interpolate-color start end offset)))

View File

@@ -15,7 +15,7 @@
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.pages-list :as ctpl]
[app.common.types.plugins :as ctpg]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as cttx]
@@ -30,21 +30,22 @@
(def valid-container-types
#{:page :component})
(sm/register!
^{::sm/type ::container}
[:map
[:id ::sm/uuid]
[:type {:optional true}
[::sm/one-of valid-container-types]]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:optional true}
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:container
(sm/register!
^{::sm/type ::container}
[:map
[:id ::sm/uuid]
[:type {:optional true}
[::sm/one-of valid-container-types]]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:optional true}
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} schema:plugin-data]]))
(def check-container
(sm/check-fn ::container))
(sm/check-fn schema:container))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
@@ -294,8 +295,8 @@
([page component library-data position]
(make-component-instance page component library-data position {}))
([page component library-data position
{:keys [main-instance? force-id force-frame-id keep-ids?]
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false}}]
{:keys [main-instance? force-id force-frame-id keep-ids? force-parent-id]
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false force-parent-id nil}}]
(let [component-page (ctpl/get-page library-data (:main-instance-page component))
component-shape (-> (get-shape component-page (:main-instance-id component))
@@ -303,7 +304,6 @@
(assoc :frame-id uuid/zero)
(remove-swap-keep-attrs))
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract position orig-pos)
@@ -368,7 +368,7 @@
[new-shape new-shapes _]
(ctst/clone-shape component-shape
frame-id
(or force-parent-id frame-id)
(:objects component-page)
:update-new-shape update-new-shape
:force-id force-id
@@ -518,15 +518,31 @@
;; --- SHAPE UPDATE
(defn- get-token-groups
"Get the sync attrs groups that are affected by changes in applied tokens.
If any token has been applied or unapplied in the shape, calculate the corresponding
attributes and get the groups. If some of the attributes are to be applied in the
content nodes of a text shape, also return the content groups (only for attributes,
so the text is not touched)."
[shape new-applied-tokens]
(let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
ctt/all-keys)
changed-groups (into #{}
(comp (map ctt/token-attr->shape-attr)
(map #(get ctk/sync-attrs %))
(filter some?))
changed-token-attrs)]
(let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
ctt/all-keys)
text-shape? (= (:type shape) :text)
attrs-in-text-content? (some #(ctt/attrs-in-text-content %)
changed-token-attrs)
changed-groups (into #{}
(comp (map ctt/token-attr->shape-attr)
(map #(get ctk/sync-attrs %))
(filter some?))
changed-token-attrs)
changed-groups (if (and text-shape?
(d/not-empty? changed-groups)
attrs-in-text-content?)
(conj changed-groups :content-group :text-content-attribute)
changed-groups)]
changed-groups))
(defn set-shape-attr

View File

@@ -16,16 +16,18 @@
[app.common.geom.shapes.tree-seq :as gsts]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.text :as ct]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.library :as ctlb]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.plugins :as ctpg]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.tokens-lib :as ctl]
[app.common.types.text :as txt]
[app.common.types.tokens-lib :refer [schema:tokens-lib]]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
@@ -61,13 +63,13 @@
[:map-of {:gen/max 5} ::sm/uuid ctc/schema:library-color])
(def schema:components
[:map-of {:gen/max 5} ::sm/uuid ::ctn/container])
[:map-of {:gen/max 5} ::sm/uuid ctn/schema:container])
(def schema:typographies
[:map-of {:gen/max 2} ::sm/uuid ::cty/typography])
[:map-of {:gen/max 2} ::sm/uuid cty/schema:typography])
(def schema:pages-index
[:map-of {:gen/max 5} ::sm/uuid ::ctp/page])
[:map-of {:gen/max 5} ::sm/uuid ctp/schema:page])
(def schema:options
[:map {:title "FileOptions"}
@@ -82,8 +84,8 @@
[:colors {:optional true} schema:colors]
[:components {:optional true} schema:components]
[:typographies {:optional true} schema:typographies]
[:plugin-data {:optional true} ::ctpg/plugin-data]
[:tokens-lib {:optional true} ::ctl/tokens-lib]])
[:plugin-data {:optional true} schema:plugin-data]
[:tokens-lib {:optional true} schema:tokens-lib]])
(def schema:file
"A schema for validate a file data structure; data is optional
@@ -521,7 +523,7 @@
(defmethod uses-asset? :color
[_ shape library-id color]
(ctc/uses-library-color? shape library-id (:id color)))
(cts/uses-library-color? shape library-id (:id color)))
(defmethod uses-asset? :typography
[_ shape library-id typography]
@@ -533,10 +535,10 @@
Returns a list ((asset ((container shapes) (container shapes)...))...)"
[file-data library-data asset-type]
(let [assets-seq (case asset-type
:component (ctkl/components-seq library-data)
:color (ctc/colors-seq library-data)
:typography (ctyl/typographies-seq library-data))
(let [assets (case asset-type
:component (ctkl/components-seq library-data)
:color (vals (ctlb/get-colors library-data))
:typography (ctyl/typographies-seq library-data))
find-usages-in-container
(fn [container asset]
@@ -553,7 +555,7 @@
(let [instances (find-asset-usages file-data asset)]
(when (d/not-empty? instances)
[[asset instances]])))
assets-seq)))
assets)))
(defn used-in?
"Checks if a specific asset is used in a given file (by any shape in its pages or in
@@ -574,7 +576,7 @@
(letfn [(used-assets-shape [shape]
(concat
(ctkl/used-components-changed-since shape library since-date)
(ctc/used-colors-changed-since shape library since-date)
(ctlb/used-colors-changed-since shape library since-date)
(ctyl/used-typographies-changed-since shape library since-date)))
(used-assets-container [container]
@@ -693,11 +695,12 @@
(add-component-grid file-data (sort-by #(:name (first %)) used-components))))
;: FIXME: this can be moved to library
(defn- absorb-colors
[file-data used-colors]
(let [absorb-color
(fn [file-data [color usages]]
(let [remap-shape #(ctc/remap-colors % (:id file-data) color)
(let [remap-shape #(cts/remap-colors % (:id file-data) color)
remap-shapes
(fn [file-data [container shapes]]
@@ -710,7 +713,7 @@
%
shapes)))]
(as-> file-data $
(ctc/add-color $ color)
(ctlb/add-color $ color)
(reduce remap-shapes $ usages))))]
(reduce absorb-color
@@ -1046,7 +1049,7 @@
(let [detach-text
(fn [content]
(->> content
(ct/transform-nodes
(txt/transform-nodes
#(cond-> %
(not= file-id (:fill-color-ref-file %))
(dissoc :fill-color-ref-id :fill-color-ref-file)

View File

@@ -1,68 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.fill
(:require
[app.common.schema :as sm]
[app.common.types.color :as types.color]
[app.common.types.fill.impl :as impl]
[clojure.set :as set]))
(def ^:const MAX-GRADIENT-STOPS impl/MAX-GRADIENT-STOPS)
(def ^:const MAX-FILLS impl/MAX-FILLS)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:fill-attrs
[:map {:title "FillAttrs" :closed true}
[:fill-color-ref-file {:optional true} ::sm/uuid]
[:fill-color-ref-id {:optional true} ::sm/uuid]
[:fill-opacity {:optional true} [::sm/number {:min 0 :max 1}]]
[:fill-color {:optional true} types.color/schema:hex-color]
[:fill-color-gradient {:optional true} types.color/schema:gradient]
[:fill-image {:optional true} types.color/schema:image]])
(def fill-attrs
"A set of attrs that corresponds to fill data type"
(sm/keys schema:fill-attrs))
(def valid-fill-attrs
"A set used for proper check if color should contain only one of the
attrs listed in this set."
#{:fill-image :fill-color :fill-color-gradient})
(defn has-valid-fill-attrs?
"Check if color has correct color attrs"
[color]
(let [attrs (set (keys color))
result (set/intersection attrs valid-fill-attrs)]
(= 1 (count result))))
(def schema:fill
[:and schema:fill-attrs
[:fn has-valid-fill-attrs?]])
(def check-fill
(sm/check-fn schema:fill))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTRUCTORS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn from-plain
[o]
(assert (every? check-fill o) "expected valid fills vector")
(impl/from-plain o))
(defn fills?
[o]
(impl/fills? o))

View File

@@ -0,0 +1,156 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.fills
(:refer-clojure :exclude [assoc update])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.flags :as flags]
[app.common.schema :as sm]
[app.common.types.color :as types.color]
[app.common.types.fills.impl :as impl]
[clojure.core :as c]
[clojure.set :as set]))
(def ^:const MAX-GRADIENT-STOPS impl/MAX-GRADIENT-STOPS)
(def ^:const MAX-FILLS impl/MAX-FILLS)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:fill-attrs
[:map {:title "FillAttrs" :closed true}
[:fill-color-ref-file {:optional true} ::sm/uuid]
[:fill-color-ref-id {:optional true} ::sm/uuid]
[:fill-opacity {:optional true} [::sm/number {:min 0 :max 1}]]
[:fill-color {:optional true} types.color/schema:hex-color]
[:fill-color-gradient {:optional true} types.color/schema:gradient]
[:fill-image {:optional true} types.color/schema:image]])
(def fill-attrs
"A set of attrs that corresponds to fill data type"
(sm/keys schema:fill-attrs))
(def valid-fill-attrs
"A set used for proper check if color should contain only one of the
attrs listed in this set."
#{:fill-image :fill-color :fill-color-gradient})
(defn has-valid-fill-attrs?
"Check if color has correct color attrs"
[color]
(let [attrs (set (keys color))
result (set/intersection attrs valid-fill-attrs)]
(= 1 (count result))))
(def schema:fill
[:and schema:fill-attrs
[:fn has-valid-fill-attrs?]])
(def check-fill
(sm/check-fn schema:fill))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTRUCTORS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn from-plain
[o]
(assert (every? check-fill o) "expected valid fills vector")
(impl/from-plain o))
(defn fills?
[o]
(impl/fills? o))
(defn coerce
[o]
(cond
(nil? o)
(impl/from-plain [])
(impl/fills? o)
o
(vector? o)
(impl/from-plain o)
:else
(ex/raise :type :internal
:code :invalid-type
:hint (str "cannot coerce " (pr-str o) " to fills"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPE HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-image-ids
[fills]
(if (vector? fills)
(into #{}
(comp (keep :fill-image)
(map :id))
fills)
(impl/-get-image-ids fills)))
(defn get-byte-size
[fills]
(impl/-get-byte-size fills))
(defn write-to
[fills buffer offset]
(impl/-write-to fills buffer offset))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TRANSFORMATION & CREATION HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn assoc
[fills position fill]
(if (contains? flags/*current* :frontend-binary-fills)
(if (nil? fills)
(impl/from-plain [fill])
(-> (coerce fills)
(c/assoc position fill)))
(if (nil? fills)
[fill]
(-> (coerce fills)
(c/assoc position fill)))))
(defn update
[fills f & args]
(let [fills (vec fills)
fills (apply f fills args)]
(if (contains? flags/*current* :frontend-binary-fills)
(impl/from-plain fills)
(vec fills))))
(defn create
[& elements]
(let [fills (vec elements)]
(if (contains? flags/*current* :frontend-binary-fills)
(impl/from-plain fills)
fills)))
(defn prepend
"Prepend a fill to existing fills"
[fills fill]
(let [fills (into [fill] fills)]
(if (contains? flags/*current* :frontend-binary-fills)
(impl/from-plain fills)
fills)))
(defn fill->color
[fill]
(d/without-nils
{:color (:fill-color fill)
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)
:image (:fill-image fill)
:ref-id (:fill-color-ref-id fill)
:ref-file (:fill-color-ref-file fill)}))

View File

@@ -4,13 +4,14 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.fill.impl
(ns app.common.types.fills.impl
(:require
#?(:clj [clojure.data.json :as json])
#?(:cljs [app.common.weak-map :as weak-map])
[app.common.buffer :as buf]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.math :as mth]
[app.common.transit :as t]))
@@ -22,7 +23,7 @@
(def ^:const GRADIENT-STOP-SIZE 8)
(def ^:const GRADIENT-BYTE-SIZE 156)
(def ^:const SOLID-BYTE-SIZE 4)
(def ^:const IMAGE-BYTE-SIZE 28)
(def ^:const IMAGE-BYTE-SIZE 36)
(def ^:const METADATA-BYTE-SIZE 36)
(def ^:const FILL-BYTE-SIZE
(+ 4 (mth/max GRADIENT-BYTE-SIZE
@@ -35,6 +36,13 @@
(def ^:private xf:take-fills
(take MAX-FILLS))
(defprotocol IHeapWritable
(-write-to [_ buffer offset] "write the content to the specified buffer")
(-get-byte-size [_] "get byte size"))
(defprotocol IBinaryFills
(-get-image-ids [_] "get referenced image ids"))
(defn- hex->rgb
"Encode an hex string as rgb (int32)"
[hex]
@@ -64,16 +72,16 @@
n (unsigned-bit-shift-right n 24)]
(mth/precision (/ (float n) 0xff) 2)))
(defn- write-solid-fill
[offset buffer color alpha]
(defn write-solid-fill
[offset buffer opacity color]
(buf/write-byte buffer (+ offset 0) 0x00)
(buf/write-int buffer (+ offset 4)
(-> (hex->rgb color)
(rgb->rgba alpha)))
(rgb->rgba opacity)))
(+ offset FILL-BYTE-SIZE))
(defn- write-gradient-fill
[offset buffer gradient opacity]
(defn write-gradient-fill
[offset buffer opacity gradient]
(let [start-x (:start-x gradient)
start-y (:start-y gradient)
end-x (:end-x gradient)
@@ -108,16 +116,18 @@
(+ offset' GRADIENT-STOP-SIZE)))
(+ offset FILL-BYTE-SIZE)))))
(defn- write-image-fill
(defn write-image-fill
[offset buffer opacity image]
(let [image-id (get image :id)
image-width (get image :width)
image-height (get image :height)]
(let [image-id (get image :id)
image-width (get image :width)
image-height (get image :height)
keep-aspect-ratio (get image :keep-aspect-ratio false)]
(buf/write-byte buffer (+ offset 0) 0x03)
(buf/write-uuid buffer (+ offset 4) image-id)
(buf/write-float buffer (+ offset 20) opacity)
(buf/write-int buffer (+ offset 24) image-width)
(buf/write-int buffer (+ offset 28) image-height)
(buf/write-bool buffer (+ offset 32) keep-aspect-ratio)
(+ offset FILL-BYTE-SIZE)))
(defn- write-metadata
@@ -128,21 +138,21 @@
(when mtype
(let [val (case mtype
"image/jpeg" 0x01
"image/png" 0x02
"image/gif" 0x03
"image/webp" 0x04
"image/jpeg" 0x01
"image/png" 0x02
"image/gif" 0x03
"image/webp" 0x04
"image/svg+xml" 0x05)]
(buf/write-short buffer (+ offset 2) val)))
(if (and (some? ref-file)
(some? ref-id))
(do
(buf/write-byte buffer (+ offset 0) 0x01)
(buf/write-bool buffer (+ offset 0) true)
(buf/write-uuid buffer (+ offset 4) ref-file)
(buf/write-uuid buffer (+ offset 20) ref-id))
(do
(buf/write-byte buffer (+ offset 0) 0x00)))))
(buf/write-bool buffer (+ offset 0) false)))))
(defn- read-stop
[buffer offset]
@@ -193,7 +203,8 @@
:type type}})
3
(let [id (buf/read-uuid dbuffer (+ doffset 4))
(let [ratio (buf/read-bool dbuffer (+ doffset 32))
id (buf/read-uuid dbuffer (+ doffset 4))
alpha (buf/read-float dbuffer (+ doffset 20))
width (buf/read-int dbuffer (+ doffset 24))
height (buf/read-int dbuffer (+ doffset 28))
@@ -209,6 +220,7 @@
:width width
:height height
:mtype mtype
:keep-aspect-ratio ratio
;; FIXME: we are not encodign the name, looks useless
:name "sample"}}))]
@@ -278,7 +290,20 @@
:cljs
#_:clj-kondo/ignore
(deftype Fills [size dbuffer mbuffer cache ^:mutable __hash]
(deftype Fills [size dbuffer mbuffer image-ids cache ^:mutable __hash]
IHeapWritable
(-get-byte-size [_]
(- (.-byteLength dbuffer) 4))
(-write-to [_ heap offset]
(let [buffer' (.-buffer ^js/DataView dbuffer)]
(.set heap (js/Uint32Array. buffer' 4) offset)))
IBinaryFills
(-get-image-ids [_]
image-ids)
cljs.core/ISequential
cljs.core/IEquiv
(-equiv [this other]
@@ -353,7 +378,26 @@
(when (< i size)
(cons (read-fill dbuffer mbuffer i)
(lazy-seq (next-seq (inc i))))))
0)))))
0)))
cljs.core/IPrintWithWriter
(-pr-writer [this writer _]
(binding [*print-dup* true]
(cljs.core/-write writer (str "#penpot/fills \"" (pr-str (vec this)) "\""))))))
#?(:clj
(defmethod print-method Fills
[o ^java.io.Writer writer]
(.write writer "#penpot/fills \"")
(print-dup (vec o) writer)
(.write writer "\"")))
#?(:clj
(defmethod print-dup Fills
[o ^java.io.Writer writer]
(.write writer "#penpot/fills \"")
(print-dup (vec o) writer)
(.write writer "\"")))
(defn from-plain
[fills]
@@ -364,8 +408,9 @@
(buf/write-byte dbuffer 0 total)
(loop [index 0]
(when (< index total)
(loop [index 0
image-ids #{}]
(if (< index total)
(let [fill (nth fills index)
doffset (+ 4 (* index FILL-BYTE-SIZE))
moffset (* index METADATA-BYTE-SIZE)
@@ -373,23 +418,26 @@
(if-let [color (get fill :fill-color)]
(do
(write-solid-fill doffset dbuffer color opacity)
(write-solid-fill doffset dbuffer opacity color)
(write-metadata moffset mbuffer fill)
(recur (inc index)))
(recur (inc index) image-ids))
(if-let [gradient (get fill :fill-color-gradient)]
(do
(write-gradient-fill doffset dbuffer gradient opacity)
(write-gradient-fill doffset dbuffer opacity gradient)
(write-metadata moffset mbuffer fill)
(recur (inc index)))
(recur (inc index) image-ids))
(if-let [image (get fill :fill-image)]
(do
(write-image-fill doffset dbuffer opacity image)
(write-metadata moffset mbuffer fill)
(recur (inc index)))
(recur (inc index))))))))
(recur (inc index)
(conj image-ids (get image :id))))
(ex/raise :type :internal
:code :invalid-fill
:hint "found invalid fill on encoding fills to binary format")))))
#?(:cljs (Fills. total dbuffer mbuffer (weak-map/create) nil)
:clj (Fills. total dbuffer mbuffer nil))))
#?(:cljs (Fills. total dbuffer mbuffer image-ids (weak-map/create) nil)
:clj (Fills. total dbuffer mbuffer nil))))))
(defn fills?
[o]

View File

@@ -6,9 +6,8 @@
(ns app.common.types.grid
(:require
[app.common.colors :as clr]
[app.common.schema :as sm]
[app.common.types.color :refer [schema:hex-color]]))
[app.common.types.color :as clr]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
@@ -16,7 +15,7 @@
(def schema:grid-color
[:map {:title "PageGridColor"}
[:color schema:hex-color]
[:color clr/schema:hex-color]
[:opacity ::sm/safe-number]])
(def schema:column-params

View File

@@ -0,0 +1,87 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.library
"Exposes file library type data helpers.
WARNING: It belongs to FILE types in hierarchy of types so: file
types can import this ns but, but this ns can't import file types."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.time :as dt]
[app.common.types.shape :as types.shape]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLOR LIBRARY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-colors
[file-data]
(:colors file-data))
(defn get-color
[file-data color-id]
(dm/get-in file-data [:colors color-id]))
(defn get-ref-color
[library-data color]
(when (= (:ref-file color) (:id library-data))
(get-color library-data (:ref-id color))))
(defn- touch
[color]
(assoc color :modified-at (dt/now)))
(defn add-color
[file-data color]
(update file-data :colors assoc (:id color) (touch color)))
(defn set-color
[file-data color]
(d/assoc-in-when file-data [:colors (:id color)] (touch color)))
(defn update-color
[file-data color-id f & args]
(d/update-in-when file-data [:colors color-id] #(-> (apply f % args)
(touch))))
(defn delete-color
[file-data color-id]
(update file-data :colors dissoc color-id))
(defn used-colors-changed-since
"Find all usages of any color in the library by the given shape, of colors
that have ben modified after the date."
[shape library since-date]
(->> (types.shape/get-all-colors shape)
(keep #(get-ref-color (:data library) %))
(remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil
(map (fn [color]
{:shape-id (:id shape)
:asset-id (:id color)
:asset-type :color}))))
;: FIXME: revisit the API of this, i think we should pass the whole
;; library data here instead of only colors
(defn sync-colors
"Look for usage of any color of the given library inside the shape,
and, in this case, copy the library color into the shape."
[shape library-id library-colors]
(letfn [(sync-color [shape position shape-color set-fn _ detach-fn]
(if (= (:ref-file shape-color) library-id)
(let [library-color (get library-colors (:ref-id shape-color))]
(if (some? library-color)
(set-fn shape
position
(:color library-color)
(:opacity library-color)
(:gradient library-color)
(:image library-color))
(detach-fn shape position)))
shape))]
(types.shape/process-shape-colors shape sync-color)))

View File

@@ -17,8 +17,8 @@
[app.common.geom.shapes.effects :as gse]
[app.common.geom.shapes.strokes :as gss]
[app.common.math :as mth]
[app.common.text :as txt]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[clojure.core :as c]))
;; --- Modifiers

View File

@@ -53,10 +53,10 @@
[:name :string]
[:index {:optional true} ::sm/int]
[:objects schema:objects]
[:default-grids {:optional true} ::ctg/default-grids]
[:default-grids {:optional true} ctg/schema:default-grids]
[:flows {:optional true} schema:flows]
[:guides {:optional true} schema:guides]
[:plugin-data {:optional true} ::ctpg/plugin-data]
[:plugin-data {:optional true} ctpg/schema:plugin-data]
[:background {:optional true} ctc/schema:hex-color]
[:comment-thread-positions {:optional true}

View File

@@ -25,7 +25,10 @@
(def ^:cosnt bool-group-style-properties bool/group-style-properties)
(def ^:const bool-style-properties bool/style-properties)
(def ^:const default-bool-fills bool/default-fills)
(defn get-default-bool-fills
[]
(bool/get-default-fills))
(def schema:content impl/schema:content)
(def schema:segments impl/schema:segments)

View File

@@ -6,17 +6,23 @@
(ns app.common.types.path.bool
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.flags :as flags]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.math :as mth]
[app.common.types.color :as clr]
[app.common.types.fills :as types.fills]
[app.common.types.path.helpers :as helpers]
[app.common.types.path.segment :as segment]
[app.common.types.path.subpath :as subpath]))
(def default-fills
[{:fill-color clr/black}])
(defn get-default-fills
[]
(let [fills [{:fill-color clr/black}]]
(if (contains? flags/*current* :frontend-binary-fills)
(types.fills/from-plain fills)
fills)))
(def group-style-properties
#{:shadow :blur})

View File

@@ -7,7 +7,6 @@
(ns app.common.types.shape
(:require
#?(:clj [app.common.fressian :as fres])
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
@@ -18,10 +17,9 @@
[app.common.record :as cr]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.text :as txt]
[app.common.transit :as t]
[app.common.types.color :as types.color]
[app.common.types.fill :refer [schema:fill]]
[app.common.types.color :as clr]
[app.common.types.fills :refer [schema:fill fill->color]]
[app.common.types.grid :as ctg]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segment]
@@ -33,6 +31,7 @@
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctsx]
[app.common.types.text :as txt]
[app.common.types.token :as cto]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
@@ -148,9 +147,9 @@
[::sm/one-of stroke-caps]]
[:stroke-cap-end {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-color {:optional true} types.color/schema:hex-color]
[:stroke-color-gradient {:optional true} types.color/schema:gradient]
[:stroke-image {:optional true} types.color/schema:image]])
[:stroke-color {:optional true} clr/schema:hex-color]
[:stroke-color-gradient {:optional true} clr/schema:gradient]
[:stroke-image {:optional true} clr/schema:image]])
(def stroke-attrs
"A set of attrs that corresponds to stroke data type"
@@ -416,12 +415,14 @@
;; Valid attributes
(def ^:private allowed-shape-attrs #{:page-id :component-id :component-file :component-root :main-instance
:remote-synced :shape-ref :touched :blocked :collapsed :locked
:hidden :masked-group :fills :proportion :proportion-lock :constraints-h
:constraints-v :fixed-scroll :r1 :r2 :r3 :r4 :opacity :grids :exports
:strokes :blend-mode :interactions :shadow :blur :grow-type :applied-tokens
:plugin-data})
(def ^:private allowed-shape-attrs
#{:page-id :component-id :component-file :component-root :main-instance
:remote-synced :shape-ref :touched :blocked :collapsed :locked
:hidden :masked-group :fills :proportion :proportion-lock :constraints-h
:constraints-v :fixed-scroll :r1 :r2 :r3 :r4 :opacity :grids :exports
:strokes :blend-mode :interactions :shadow :blur :grow-type :applied-tokens
:plugin-data})
(def ^:private allowed-shape-geom-attrs #{:x :y :width :height})
(def ^:private allowed-shape-base-attrs #{:id :name :type :selrect :points :transform :transform-inverse :parent-id :frame-id})
(def ^:private allowed-bool-attrs #{:shapes :bool-type :content})
@@ -755,3 +756,201 @@
(d/patch-object (select-keys props basic-extract-props))
(cond-> (cfh/text-shape? shape) (patch-text-props props))
(cond-> (cfh/frame-shape? shape) (patch-layout-props props)))))
(defn- set-fill-color
[shape position color opacity gradient image]
(update-in shape [:fills position]
(fn [fill]
(d/without-nils (assoc fill
:fill-color color
:fill-opacity opacity
:fill-color-gradient gradient
:fill-image image)))))
(defn- attach-fill-color
[shape position ref-id ref-file]
(d/update-in-when shape [:fills position]
(fn [fill]
(-> fill
(assoc :fill-color-ref-file ref-file)
(assoc :fill-color-ref-id ref-id)))))
(defn- detach-fill-color
[shape position]
(d/update-in-when shape [:fills position] dissoc :fill-color-ref-id :fill-color-ref-file))
(defn- set-stroke-color
[shape position color opacity gradient image]
(d/update-in-when shape [:strokes position]
(fn [stroke]
(-> stroke
(assoc :stroke-color color)
(assoc :stroke-opacity opacity)
(assoc :stroke-color-gradient gradient)
(assoc :stroke-image image)
(d/without-nils)))))
(defn- attach-stroke-color
[shape position ref-id ref-file]
(d/update-in-when shape [:strokes position]
(fn [stroke]
(-> stroke
(assoc :stroke-color-ref-id ref-id)
(assoc :stroke-color-ref-file ref-file)))))
(defn- detach-stroke-color
[shape position]
(d/update-in-when shape [:strokes position] dissoc :stroke-color-ref-id :stroke-color-ref-file))
(defn- set-shadow-color
[shape position color opacity gradient]
(d/update-in-when shape [:shadow position :color]
(fn [shadow-color]
(-> shadow-color
(assoc :color color)
(assoc :opacity opacity)
(assoc :gradient gradient)
(d/without-nils)))))
(defn- attach-shadow-color
[shape position ref-id ref-file]
(d/update-in-when shape [:shadow position :color]
(fn [color]
(-> color
(assoc :ref-id ref-id)
(assoc :ref-file ref-file)))))
(defn- detach-shadow-color
[shape position]
(d/update-in-when shape [:shadow position :color] dissoc :ref-id :ref-file))
(defn- set-grid-color
[shape position color opacity gradient]
(d/update-in-when shape [:grids position :params :color]
(fn [grid-color]
(-> grid-color
(assoc :color color)
(assoc :opacity opacity)
(assoc :gradient gradient)
(d/without-nils)))))
(defn- attach-grid-color
[shape position ref-id ref-file]
(d/update-in-when shape [:grids position :params :color]
(fn [color]
(-> color
(assoc :ref-id ref-id)
(assoc :ref-file ref-file)))))
(defn- detach-grid-color
[shape position]
(d/update-in-when shape [:grids position :params :color] dissoc :ref-id :ref-file))
(defn process-shape-colors
"Execute an update function on all colors of a shape."
[shape process-fn]
(let [process-fill (fn [shape [position fill]]
(process-fn shape
position
(fill->color fill)
set-fill-color
attach-fill-color
detach-fill-color))
process-stroke (fn [shape [position stroke]]
(process-fn shape
position
(clr/stroke->color stroke)
set-stroke-color
attach-stroke-color
detach-stroke-color))
process-shadow (fn [shape [position shadow]]
(process-fn shape
position
(clr/shadow->color shadow)
set-shadow-color
attach-shadow-color
detach-shadow-color))
process-grid (fn [shape [position grid]]
(process-fn shape
position
(clr/grid->color grid)
set-grid-color
attach-grid-color
detach-grid-color))
process-text-node (fn [node]
(as-> node $
(reduce process-fill $ (d/enumerate (:fills $)))
(reduce process-stroke $ (d/enumerate (:strokes $)))))
process-text (fn [shape]
(let [content (:content shape)
new-content (txt/transform-nodes process-text-node content)]
(if (not= content new-content)
(assoc shape :content new-content)
shape)))]
(as-> shape $
(reduce process-fill $ (d/enumerate (:fills $)))
(reduce process-stroke $ (d/enumerate (:strokes $)))
(reduce process-shadow $ (d/enumerate (:shadow $)))
(reduce process-grid $ (d/enumerate (:grids $)))
(process-text $))))
(defn- get-text-node-colors
"Get all colors used by a node of a text shape"
[node]
(concat (map fill->color (:fills node))
(map clr/stroke->color (:strokes node))))
(defn get-all-colors
"Get all colors used by a shape, in any section."
[shape]
;; FIXME: all this functions should be really in color?
(concat (map fill->color (:fills shape))
(map clr/stroke->color (:strokes shape))
(map clr/shadow->color (:shadow shape))
(when (= (:type shape) :frame)
(map clr/grid->color (:grids shape)))
(when (= (:type shape) :text)
(reduce (fn [colors node]
(concat colors (get-text-node-colors node)))
()
(txt/node-seq (:content shape))))))
(defn uses-library-color?
"Check if the shape uses the given library color."
[shape library-id color-id]
(let [all-colors (get-all-colors shape)]
(some #(and (= (:ref-id %) color-id)
(= (:ref-file %) library-id))
all-colors)))
(defn uses-library-colors?
"Check if the shape uses any color in the given library."
[shape library-id]
(let [all-colors (get-all-colors shape)]
(some #(and (some? (:ref-id %))
(= (:ref-file %) library-id))
all-colors)))
(defn remap-colors
"Change the shape so that any use of the given color now points to
the given library."
[shape library-id color]
(letfn [(remap-color [shape position shape-color _ attach-fn _]
(if (= (:ref-id shape-color) (:id color))
(attach-fn shape
position
(:id color)
library-id)
shape))]
(process-shape-colors shape remap-color)))

View File

@@ -6,7 +6,7 @@
(ns app.common.types.shape.attrs
(:require
[app.common.colors :as clr]))
[app.common.types.color :as clr]))
(def default-color clr/gray-20)

View File

@@ -7,7 +7,7 @@
(ns app.common.types.shape.text
(:require
[app.common.schema :as sm]
[app.common.types.fill :refer [schema:fill]]
[app.common.types.fills :refer [schema:fill]]
[app.common.types.shape :as-alias shape]
[app.common.types.shape.text.position-data :as-alias position-data]))

View File

@@ -6,8 +6,194 @@
(ns app.common.types.text
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[clojure.set :as set]))
[app.common.flags :as flags]
[app.common.types.color :as clr]
[app.common.types.fills :as types.fills]
[clojure.set :as set]
[clojure.walk :as walk]
[cuerdas.core :as str]))
;; -- Attrs
(def text-typography-attrs
[:typography-ref-id
:typography-ref-file])
(def text-fill-attrs
[:fill-color
:fill-opacity
:fill-color-ref-id
:fill-color-ref-file
:fill-color-gradient])
(def text-font-attrs
[:font-id
:font-family
:font-variant-id
:font-size
:font-weight
:font-style])
(def text-align-attrs
[:text-align])
(def text-direction-attrs
[:text-direction])
(def text-spacing-attrs
[:line-height
:letter-spacing])
(def text-valign-attrs
[:vertical-align])
(def text-decoration-attrs
[:text-decoration])
(def text-transform-attrs
[:text-transform])
(def text-fills
[:fills])
(def shape-attrs
[:grow-type])
(def root-attrs
text-valign-attrs)
(def paragraph-attrs
(d/concat-vec
text-align-attrs
text-direction-attrs))
(def text-node-attrs
(d/concat-vec
text-typography-attrs
text-font-attrs
text-spacing-attrs
text-decoration-attrs
text-transform-attrs
text-fills))
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
(def text-style-attrs
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
(def default-root-attrs
{:vertical-align "top"})
(def default-text-fills
[{:fill-color clr/black
:fill-opacity 1}])
(def default-text-attrs
{:font-id "sourcesanspro"
:font-family "sourcesanspro"
:font-variant-id "regular"
:font-size "14"
:font-weight "400"
:font-style "normal"
:line-height "1.2"
:letter-spacing "0"
:text-transform "none"
:text-align "left"
:text-decoration "none"
:text-direction "ltr"})
(defn get-default-text-fills
"Return calculated default text fills"
[]
(if (contains? flags/*current* :frontend-binary-fills)
(types.fills/from-plain default-text-fills)
default-text-fills))
(defn get-default-text-attrs
"Return calculated default text attrs.
NOTE: is implemented as function because it needs resolve at runtime
the activated flag for properly encode the fills"
[]
(assoc default-text-attrs :fills (get-default-text-fills)))
(def typography-fields
[:font-id
:font-family
:font-variant-id
:font-size
:font-weight
:font-style
:line-height
:letter-spacing
:text-transform])
(def default-typography
(-> default-text-attrs
(select-keys typography-fields)
(assoc :name "Source Sans Pro Regular")))
(defn node-seq
([root] (node-seq identity root))
([match? root]
(->> (tree-seq map? :children root)
(filter match?)
(seq))))
(defn is-text-node?
[node]
(and (nil? (:type node))
(string? (:text node))))
(defn is-paragraph-set-node?
[node]
(= "paragraph-set" (:type node)))
(defn is-paragraph-node?
[node]
(= "paragraph" (:type node)))
(defn is-root-node?
[node]
(= "root" (:type node)))
(defn is-node?
[node]
(or ^boolean (is-text-node? node)
^boolean (is-paragraph-node? node)
^boolean (is-paragraph-set-node? node)
^boolean (is-root-node? node)))
(defn is-content-node?
"Only matches content nodes, ignoring the paragraph-set nodes."
[node]
(or ^boolean (is-text-node? node)
^boolean (is-paragraph-node? node)
^boolean (is-root-node? node)))
(defn transform-nodes
([transform root]
(transform-nodes identity transform root))
([pred transform root]
(walk/postwalk
(fn [item]
(if (and (is-node? item) (pred item))
(transform item)
item))
root)))
(defn update-text-content
[shape pred-fn update-fn attrs]
(let [update-attrs-fn #(update-fn % attrs)
transform #(transform-nodes pred-fn update-attrs-fn %)]
(-> shape
(update :content transform))))
(defn generate-shape-name
[text]
(subs text 0 (min 280 (count text))))
(defn- compare-text-content
"Given two content text structures, conformed by maps and vectors,
@@ -83,21 +269,12 @@
compare them, and returns a set with the type of differences.
The possibilities are
:text-content-text
:text-content-attribute,
:text-content-structure
:text-content-structure-same-attrs."
:text-content-attribute
:text-content-structure"
[a b]
(let [diff-type (compare-text-content a b
{:text-cb (fn [acc] (conj acc :text-content-text))
:attribute-cb (fn [acc _] (conj acc :text-content-attribute))})]
(if-not (contains? diff-type :text-content-structure)
diff-type
(let [;; get attrs of the first paragraph of the first paragraph-set
attrs (get-first-paragraph-text-attrs a)]
(if (and (equal-attrs? a attrs)
(equal-attrs? b attrs))
#{:text-content-structure :text-content-structure-same-attrs}
diff-type)))))
(compare-text-content a b
{:text-cb (fn [acc] (conj acc :text-content-text))
:attribute-cb (fn [acc _] (conj acc :text-content-attribute))}))
(defn get-diff-attrs
"Given two content text structures, conformed by maps and vectors,
@@ -127,7 +304,8 @@
entries"
[a b]
(cond
(not= (type a) (type b))
(and (not= (type a) (type b))
(not (and (map? a) (map? b)))) ;; Sometimes they are both maps but of different subtypes
false
(map? a)
@@ -166,3 +344,91 @@
(if (= :children k)
[k (vec (map #(copy-attrs-keys %1 attrs) v))]
[k (get attrs k v)]))))
(defn content->text
"Given a root node of a text content extracts the texts with its associated styles"
[content]
(letfn [(add-node [acc node]
(cond
(is-paragraph-node? node)
(conj acc [])
(is-text-node? node)
(let [i (dec (count acc))]
(update acc i conj (:text node)))
:else
acc))]
(->> (node-seq content)
(reduce add-node [])
(map #(str/join "" %))
(str/join "\n"))))
(defn content->text+styles
"Given a root node of a text content extracts the texts with its associated styles"
[node]
(letfn
[(rec-style-text-map [acc node style]
(let [node-style (merge style (select-keys node text-all-attrs))
head (or (-> acc first) [{} ""])
[head-style head-text] head
new-acc
(cond
(not (is-text-node? node))
(reduce #(rec-style-text-map %1 %2 node-style) acc (:children node))
(not= head-style node-style)
(cons [node-style (:text node "")] acc)
:else
(cons [node-style (dm/str head-text "" (:text node))] (rest acc)))
;; We add an end-of-line when finish a paragraph
new-acc
(if (= (:type node) "paragraph")
(let [[hs ht] (first new-acc)]
(cons [hs (dm/str ht "\n")] (rest new-acc)))
new-acc)]
new-acc))]
(-> (rec-style-text-map [] node {})
reverse)))
(defn change-text
"Changes the content of the text shape to use the text as argument. Will use the styles of the
first paragraph and text that is present in the shape (and override the rest)"
[content text]
(let [root-styles (select-keys content root-attrs)
paragraph-style
(merge
default-text-attrs
(select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs))
text-style
(merge
default-text-attrs
(select-keys (->> content (node-seq is-text-node?) first) text-all-attrs))
paragraph-texts
(str/split text "\n")
paragraphs
(->> paragraph-texts
(mapv
(fn [pt]
(merge
paragraph-style
{:type "paragraph"
:children [(merge {:text pt} text-style)]}))))]
(d/patch-object
{:type "root"
:children
[{:type "paragraph-set"
:children paragraphs}]}
root-styles)))

View File

@@ -10,6 +10,7 @@
[app.common.schema :as sm]
[clojure.data :as data]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -29,20 +30,21 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-size "fontSizes"
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
:font-size "fontSizes"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
(def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type))
@@ -133,8 +135,16 @@
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def typography-keys (set/union font-size-keys letter-spacing-keys))
(def ^:private schema:font-family
[:map
[:font-family {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def typography-keys (set/union font-size-keys letter-spacing-keys font-family-keys))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
(def ff-typography-keys (set/difference typography-keys font-size-keys))
(def ^:private schema:number
@@ -167,6 +177,7 @@
schema:number
schema:font-size
schema:letter-spacing
schema:font-family
schema:dimensions])
(defn shape-attr->token-attrs
@@ -196,6 +207,7 @@
(font-size-keys shape-attr) #{shape-attr}
(letter-spacing-keys shape-attr) #{shape-attr}
(font-family-keys shape-attr) #{shape-attr}
(border-radius-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr}
@@ -261,6 +273,13 @@
[attributes token-type]
(seq (appliable-attrs attributes token-type)))
;; Token attrs that are set inside content blocks of text shapes, instead
;; at the shape level.
(def attrs-in-text-content
(set/union
typography-keys
#{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -289,3 +308,23 @@
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
"Splits font family `value` string from into vector of font families.
Doesn't handle possible edge-case of font-families with `,` in their font family name."
[font-value]
(let [families (str/split font-value ",")
xform (comp
(map str/trim)
(remove str/empty?))]
(into [] xform families)))
(defn join-font-family
"Joins font family `value` into a string to be edited with a single input."
[font-families]
(str/join ", " font-families))

View File

@@ -1333,13 +1333,25 @@ Will return a value that matches this schema:
(walk/postwalk
(fn [node]
(cond-> node
;; Handle sequential values that are objects with type
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(sequential? (get node "value"))
(map? (first (get node "value"))))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
;; Keep array of font families
(and (map? node)
(contains? node "type")
(= "fontFamilies" (get node "type"))
(contains? node "value")
(sequential? (get node "value"))
(not (map? (first (get node "value")))))
identity
;; Rename keys for all token nodes
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
@@ -1371,7 +1383,16 @@ Will return a value that matches this schema:
(assoc tokens child-path (make-token
:name child-path
:type token-type
:value (get v "$value")
:value (cond-> (get v "$value")
;; Split string of font-families
(and (= :font-family token-type)
(string? (get v "$value")))
cto/split-font-family
;; Keep array of font-families
(and (= :font-family token-type)
(sequential? (get v "$value")))
identity)
:description (get v "$description")))
;; Discard unknown type tokens
tokens)))))

View File

@@ -7,8 +7,8 @@
(ns app.common.types.typographies-list
(:require
[app.common.data :as d]
[app.common.text :as txt]
[app.common.time :as dt]))
[app.common.time :as dt]
[app.common.types.text :as txt]))
(defn typographies-seq
[file-data]

View File

@@ -8,8 +8,8 @@
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.text :as txt]
[app.common.types.plugins :as ctpg]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -59,6 +59,8 @@
:text-transform (or text-transform "none")}
(d/without-nils)))
;; FIXME: this function should not be here it belongs to shape and not typography
(defn uses-library-typographies?
"Check if the shape uses any typography in the given library."
[shape library-id]
@@ -70,6 +72,7 @@
#(and (some? (:typography-ref-id %))
(= (:typography-ref-file %) library-id))))))
;; FIXME: this function should not be here it belongs to shape and not typography
(defn uses-library-typography?
"Check if the shape uses the given library typography."
[shape library-id typography-id]

View File

@@ -7,8 +7,7 @@
(ns common-tests.colors-test
(:require
#?(:cljs [goog.color :as gcolors])
[app.common.colors :as colors]
[app.common.data :as d]
[app.common.types.color :as colors]
[clojure.test :as t]))
(t/deftest valid-hex-color

View File

@@ -19,7 +19,7 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-sync-unchanged-copy-when-changed-attribute
(t/deftest sync-unchanged-copy-when-changed-attribute
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
@@ -55,7 +55,7 @@
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-unchanged-copy-when-changed-text
(t/deftest sync-unchanged-copy-when-changed-text
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
@@ -91,7 +91,7 @@
(t/is (= "14" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-unchanged-copy-when-changed-both
(t/deftest sync-unchanged-copy-when-changed-both
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame-with-text :main-root :main-child "hello world")
@@ -129,7 +129,7 @@
(t/is (= "32" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-attribute
(t/deftest 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")
@@ -176,7 +176,7 @@
(t/is (= "14" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-text
(t/deftest 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")
@@ -223,7 +223,7 @@
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-attr-copy-when-changed-both
(t/deftest 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")
@@ -273,7 +273,7 @@
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-attribute
(t/deftest 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")
@@ -320,7 +320,7 @@
(t/is (= "32" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-text
(t/deftest 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")
@@ -367,7 +367,7 @@
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-text-copy-when-changed-both
(t/deftest 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")
@@ -417,7 +417,7 @@
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-attribute
(t/deftest 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")
@@ -466,7 +466,7 @@
(t/is (= "14" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-text
(t/deftest 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")
@@ -515,7 +515,7 @@
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest test-sync-updated-both-copy-when-changed-both
(t/deftest 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")
@@ -567,7 +567,7 @@
;; 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
(t/deftest 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")
@@ -619,7 +619,7 @@
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-same-attrs-copy-when-changed-text
(t/deftest 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")
@@ -668,7 +668,7 @@
;; 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
(t/deftest 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")
@@ -722,7 +722,7 @@
;; 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
(t/deftest 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")
@@ -775,7 +775,7 @@
(t/is (= "14" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest test-sync-updated-structure-diff-attrs-copy-when-changed-text
(t/deftest 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")
@@ -825,7 +825,7 @@
;; 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
(t/deftest 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")
@@ -878,4 +878,4 @@
;; 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)))))
(t/is (= "hello world" (:text line)))))

View File

@@ -103,7 +103,7 @@
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/is (= #{:content-group :text-content-structure} (:touched copy-child')))))
(t/deftest test-text-copy-changed-structure-diff-attrs
(let [;; ==== Setup

View File

@@ -14,8 +14,8 @@
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.tokens :as tht]
[app.common.text :as txt]
[app.common.types.container :as ctn]
[app.common.types.text :as txt]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[clojure.test :as t]))
@@ -62,7 +62,11 @@
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-letter-spacing"
:type :letter-spacing
:value 2))))
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))))
(tho/add-frame :frame1)
(tho/add-text :text1 "Hello World!")))
@@ -77,7 +81,8 @@
(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-font-size" [:font-size] [:font-size] 24)
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)))
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"])))
(t/deftest apply-tokens-to-shape
(let [;; ==== Setup
@@ -93,6 +98,7 @@
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")
token-font-family (tht/get-token file "test-token-set" "token-font-family")
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)
@@ -132,7 +138,10 @@
:attributes [:font-size]})
(cto/apply-token-to-shape {:token token-letter-spacing
:shape $
:attributes [:letter-spacing]})))
:attributes [:letter-spacing]})
(cto/apply-token-to-shape {:token token-font-family
:shape $
:attributes [:font-family]})))
(:objects page)
{}))
@@ -157,9 +166,10 @@
(t/is (= (:fill applied-tokens') "token-color"))
(t/is (= (:width applied-tokens') "token-dimensions"))
(t/is (= (:height applied-tokens') "token-dimensions"))
(t/is (= (count text1-applied-tokens) 2))
(t/is (= (count text1-applied-tokens) 3))
(t/is (= (:font-size text1-applied-tokens) "token-font-size"))
(t/is (= (:letter-spacing text1-applied-tokens) "token-letter-spacing"))))
(t/is (= (:letter-spacing text1-applied-tokens) "token-letter-spacing"))
(t/is (= (:font-family text1-applied-tokens) "token-font-family"))))
(t/deftest unapply-tokens-from-shape
(let [;; ==== Setup
@@ -189,7 +199,8 @@
(fn [shape]
(-> shape
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])))
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(:objects page)
{}))
@@ -240,7 +251,8 @@
d/txt-merge
{:fills (ths/sample-fills-color :fill-color "#fabada")
:font-size "1"
:letter-spacing "0"}))
:letter-spacing "0"
:font-family "Arial"}))
(:objects page)
{}))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,12 @@
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.library :as ctl]
[app.common.types.pages-list :as ctpl]
[app.common.types.text :as txt]
[app.common.types.typographies-list :as ctyl]
[clojure.test :as t]))
@@ -80,7 +80,7 @@
_ (thf/validate-file! file')
;; Get
colors' (ctc/colors-seq (ctf/file-data file'))
colors' (vals (ctl/get-colors (ctf/file-data file')))
shape1' (ths/get-shape file' :shape1)
fill' (first (:fills shape1'))]

View File

@@ -6,16 +6,11 @@
(ns common-tests.types.fill-test
(:require
#?(:clj [app.common.fressian :as fres])
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.math :as mth]
[app.common.pprint :as pp]
[app.common.pprint :as pp]
[app.common.schema.generators :as sg]
[app.common.schema.test :as smt]
[app.common.transit :as trans]
[app.common.types.fill :as types.fill]
[app.common.types.fills :as types.fills]
[app.common.uuid :as uuid]
[clojure.test :as t]))
@@ -85,8 +80,8 @@
:fill-opacity 0.7})
(t/deftest build-from-plain-1
(let [fills (types.fill/from-plain [sample-fill-1])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-1])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-1))))
@@ -99,8 +94,8 @@
:keep-aspect-ratio false}})
(t/deftest build-from-plain-2
(let [fills (types.fill/from-plain [sample-fill-2])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-2])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-2))))
@@ -117,8 +112,8 @@
:stops [{:color "#631aa8", :offset 0.5}]}})
(t/deftest build-from-plain-3
(let [fills (types.fill/from-plain [sample-fill-3])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-3])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-3))))
@@ -136,8 +131,8 @@
:fill-color-ref-id #uuid "2eef07f1-e38a-8062-8006-3aa264d5b785"})
(t/deftest build-from-plain-4
(let [fills (types.fill/from-plain [sample-fill-4])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-4])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-4))))
@@ -154,8 +149,8 @@
:stops [{:color "#bba1aa", :opacity 0.37, :offset 0.84}]}})
(t/deftest build-from-plain-5
(let [fills (types.fill/from-plain [sample-fill-5])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-5])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-5))))
@@ -170,14 +165,14 @@
:stops [{:color "#e15610", :offset 0.4} {:color "#005a9e", :opacity 0.62, :offset 0.81}]}})
(t/deftest build-from-plain-6
(let [fills (types.fill/from-plain [sample-fill-6])]
(t/is (types.fill/fills? fills))
(let [fills (types.fills/from-plain [sample-fill-6])]
(t/is (types.fills/fills? fills))
(t/is (= 1 (count fills)))
(t/is (equivalent-fill? (first fills) sample-fill-6))))
(t/deftest fills-datatype-roundtrip
(smt/check!
(smt/for [fill (->> (sg/generator types.fill/schema:fill)
(smt/for [fill (->> (sg/generator types.fills/schema:fill)
(sg/fmap d/without-nils)
(sg/fmap (fn [fill]
(cond-> fill
@@ -187,27 +182,27 @@
(contains? fill :fill-color-ref-file)))
(-> (assoc :fill-color-ref-file (uuid/next))
(assoc :fill-color-ref-id (uuid/next)))))))]
(let [bfills (types.fill/from-plain [fill])]
(let [bfills (types.fills/from-plain [fill])]
(and (= (count bfills) 1)
(equivalent-fill? (first bfills) fill))))
{:num 2000}))
(t/deftest equality-operation
(let [fills1 (types.fill/from-plain [sample-fill-6])
fills2 (types.fill/from-plain [sample-fill-6])]
(let [fills1 (types.fills/from-plain [sample-fill-6])
fills2 (types.fills/from-plain [sample-fill-6])]
(t/is (= fills1 fills2))))
(t/deftest reduce-impl
(let [fills1 (types.fill/from-plain [sample-fill-6])
(let [fills1 (types.fills/from-plain [sample-fill-6])
fills2 (reduce (fn [result fill]
(conj result fill))
[]
fills1)
fills3 (types.fill/from-plain fills2)]
fills3 (types.fills/from-plain fills2)]
(t/is (= fills1 fills3))))
(t/deftest indexed-access
(let [fills1 (types.fill/from-plain [sample-fill-6])
(let [fills1 (types.fills/from-plain [sample-fill-6])
fill0 (nth fills1 0)
fill1 (nth fills1 1)]
(t/is (nil? fill1))

View File

@@ -7,26 +7,36 @@
(ns common-tests.types.text-test
(:require
[app.common.text :as txt]
[app.common.types.shape :as cts]
[app.common.types.text :as cttx]
[clojure.test :as t :include-macros true]))
(def content-base (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text "hello world")
(assoc :position-data nil)
:content))
(def content-base
(-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(get :content)
(cttx/change-text "hello world")))
(def content-changed-text (assoc-in content-base [:children 0 :children 0 :children 0 :text] "changed"))
(def content-changed-attr (assoc-in content-base [:children 0 :children 0 :children 0 :font-size] "32"))
(def content-changed-both (-> content-base
(assoc-in [:children 0 :children 0 :children 0 :text] "changed")
(assoc-in [:children 0 :children 0 :children 0 :font-size] "32")))
(def line (get-in content-base [:children 0 :children 0 :children 0]))
(def content-changed-structure (update-in content-base [:children 0 :children 0 :children]
#(conj % (assoc line :font-weight "700"))))
(def content-changed-structure-same-attrs (update-in content-base [:children 0 :children 0 :children]
#(conj % line)))
(def content-changed-text
(assoc-in content-base [:children 0 :children 0 :children 0 :text] "changed"))
(def content-changed-attr
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] "32"))
(def content-changed-both
(-> content-base
(assoc-in [:children 0 :children 0 :children 0 :text] "changed")
(assoc-in [:children 0 :children 0 :children 0 :font-size] "32")))
(def line
(get-in content-base [:children 0 :children 0 :children 0]))
(def content-changed-structure
(update-in content-base [:children 0 :children 0 :children]
#(conj % (assoc line :font-weight "700"))))
(def content-changed-structure-same-attrs
(update-in content-base [:children 0 :children 0 :children] #(conj % line)))
(t/deftest test-get-diff-type
(let [diff-text (cttx/get-diff-type content-base content-changed-text)
@@ -39,7 +49,7 @@
(t/is (= #{:text-content-attribute} diff-attr))
(t/is (= #{:text-content-text :text-content-attribute} diff-both))
(t/is (= #{:text-content-structure} diff-structure))
(t/is (= #{:text-content-structure :text-content-structure-same-attrs} diff-structure-same-attrs))))
(t/is (= #{:text-content-structure} diff-structure-same-attrs))))
(t/deftest test-get-diff-attrs

View File

@@ -0,0 +1,91 @@
FROM ubuntu:24.04
LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC
ARG IMAGEMAGICK_VERSION=7.1.1-47
RUN set -e; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
autoconf \
binutils \
build-essential \
ca-certificates \
curl \
libfftw3-dev \
libheif-dev \
libjpeg-dev \
liblcms2-dev \
libltdl-dev \
liblzma-dev \
libopenexr-dev \
libpng-dev \
librsvg2-dev \
libtiff-dev \
libtool \
libwebp-dev \
libzip-dev \
libzstd-dev \
pkg-config \
; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share; \
apt-get -qqy --autoremove purge \
autoconf \
binutils \
build-essential \
ca-certificates \
curl \
libfftw3-dev \
libheif-dev \
libjpeg-dev \
liblcms2-dev \
libltdl-dev \
liblzma-dev \
libopenexr-dev \
libpng-dev \
librsvg2-dev \
libtiff-dev \
libtool\
libwebp-dev \
libzip-dev \
libzstd-dev \
pkg-config \
;\
apt-get -qqy --no-install-recommends install \
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
libgomp1 \
libheif1 \
libjpeg-turbo8 \
liblcms2-2 \
libopenexr-3-1-30 \
libopenjp2-7 \
libpng16-16 \
librsvg2-2 \
libtiff6 \
libwebp7 \
libwebpdemux2 \
libwebpmux3 \
libxml2 \
libzip4t64 \
libzstd1 \
;\
apt-get -qqy clean; \
rm -rf /var/lib/apt/lists/*;
ENTRYPOINT ["/opt/imagick/bin/magick"]

View File

@@ -6,37 +6,18 @@ ENV LANG='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=v22.16.0 \
IMAGEMAGICK_VERSION=7.1.1-47 \
TZ=Etc/UTC
RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
curl \
ca-certificates \
binutils \
build-essential autoconf libtool pkg-config \
libltdl-dev \
libpng-dev libjpeg-dev libtiff-dev libwebp-dev libopenexr-dev libfftw3-dev \
libzip-dev \
liblcms2-dev liblzma-dev libzstd-dev \
libheif-dev librsvg2-dev \
ca-certificates \
curl \
; \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share;
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
@@ -105,33 +86,33 @@ RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
tzdata \
ca-certificates \
fontconfig \
woff-tools \
woff2 \
fontforge \
python3 \
python3-tabulate \
fontforge \
tzdata \
woff-tools \
woff2 \
\
libpng16-16 \
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
libgomp1 \
libheif1 \
libjpeg-turbo8 \
liblcms2-2 \
libopenexr-3-1-30 \
libopenjp2-7 \
libpng16-16 \
librsvg2-2 \
libtiff6 \
libwebp7 \
libopenexr-3-1-30 \
libfreetype6 \
libfontconfig1 \
libglib2.0-0 \
libxml2 \
liblcms2-2 \
libheif1 \
libopenjp2-7 \
libzstd1 \
librsvg2-2 \
libgomp1 \
libwebpmux3 \
libwebpdemux2 \
libwebpmux3 \
libxml2 \
libzip4t64 \
libzstd1 \
; \
find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \
rm -rf /var/lib /var/cache; \
@@ -144,7 +125,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=build /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.1-47 /opt/imagick /opt/imagick
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
USER penpot:penpot

View File

@@ -1,14 +1,17 @@
#!/usr/bin/env bash
set -x
DOCKER_CLI_EXPERIMENTAL=enabled
IMAGE=${1:-backend}
OUTPUT="type=registry"
if [ "--local" = "$2" ]; then
OUTPUT="type=docker"
fi
ORG=${PENPOT_DOCKER_NAMESPACE:-penpotapp};
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
IMAGE=${PENPOT_BUILD_IMAGE:-backend}
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64,linux/arm64};
VERSION=${PENPOT_BUILD_VERSION:-latest}
DOCKER_IMAGE="$ORG/$IMAGE";
OPTIONS="-t $DOCKER_IMAGE:$VERSION";
@@ -20,7 +23,7 @@ for element in "${TAGS[@]}"; do
done
docker buildx inspect penpot > /dev/null 2>&1;
docker run --privileged --rm tonistiigi/binfmt --install all
docker run --privileged --rm tonistiigi/binfmt --install all > /dev/null;
if [ $? -eq 1 ]; then
docker buildx create --name=penpot --use
@@ -32,4 +35,5 @@ fi
unset IFS;
docker buildx build --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE "$@" .;
shift;
docker buildx build --output $OUTPUT --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE .;

View File

@@ -28,5 +28,6 @@ Use Docker if you already know the tool, if need full control of the process or
and do not want to depend on any external provider, or need to do any special customization.
</p>
Or you can try <a href="#unofficial-self-host-options">other options</a>,
offered by Penpot community.
Or you can try [other options][1], offered by Penpot community.
[1]: /technical-guide/getting-started/unofficial-options/

View File

@@ -97,6 +97,11 @@ file itself, which you can use as a basis for creating your own settings.
You can also consult the list of parameters on the
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
### Using OpenShift?
If you are deploying Penpot on OpenShift, we recommend following the specific guidelines provided in our Penpot-helm documentation:
<a href="https://artifacthub.io/packages/helm/penpot/penpot#-openshift-requirements" target="_blank">`Installing the chart with OpenShift requirements`</a>
Make sure to review the section **OpenShift Requirements** for important security and compatibility considerations.
## Upgrade Penpot

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

@@ -121,14 +121,13 @@ test("Renders a file with styled texts", async ({ page }) => {
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"
"4f89252d-ebbc-813e-8006-8699e4170e17",
"4f89252d-ebbc-813e-8006-8699e4170e18"
],
"render-wasm/assets/pattern.png",
);
@@ -145,6 +144,28 @@ test("Renders a file with texts with images", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with text decoration", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"d6c33e7b-7b64-80f3-8006-78509a3a2d21",
],
"render-wasm/assets/pattern.png",
);
await mockGetEmojiFont(workspace);
await mockGetJapaneseFont(workspace);
await workspace.mockGetFile("render-wasm/get-file-text-decoration.json");
await workspace.goToWorkspace({
id: "d6c33e7b-7b64-80f3-8006-785098582f1d",
pageId: "d6c33e7b-7b64-80f3-8006-785098582f1e",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with multiple emoji", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -29,33 +29,33 @@
const iterations = parseInt(url.searchParams.get('iterations') ?? 1_000, 10);
function prepare(Module, canvas) {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
setup({
instance: Module,
canvas
})
const children = [];
for (let shape = 0; shape < shapes; shape++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
children.push(
addShape({
parent: "00000000-0000-0000-0000-000000000000",
type: "rect",
selrect: {
x: getRandomInt(0, canvas.width),
y: getRandomInt(0, canvas.height),
width: getRandomInt(20, 100),
height: getRandomInt(20, 100)
},
fills: [{ type: "solid", color: getRandomColor(), opacity: getRandomFloat(0.1, 1.0) }]
})
);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
addShape({
id: "00000000-0000-0000-0000-000000000000",
children
})
}
function createElement(tag, attribs, children) {

View File

@@ -27,12 +27,12 @@ export function assignCanvas(canvas) {
context.getExtension("WEBGL_debug_renderer_info");
Module._init(canvas.width, canvas.height);
Module._set_render_options(0, 1);
Module._set_render_options(1, 1);
}
export function hexToU32ARGB(hex, opacity = 1) {
const rgb = parseInt(hex.slice(1), 16);
const a = Math.floor(opacity * 0xFF);
const a = Math.floor(opacity * 0xff);
const argb = (a << 24) | rgb;
return argb >>> 0;
}
@@ -42,9 +42,9 @@ export function getRandomInt(min, max) {
}
export function getRandomColor() {
const r = getRandomInt(0, 256).toString(16).padStart(2, '0');
const g = getRandomInt(0, 256).toString(16).padStart(2, '0');
const b = getRandomInt(0, 256).toString(16).padStart(2, '0');
const r = getRandomInt(0, 256).toString(16).padStart(2, "0");
const g = getRandomInt(0, 256).toString(16).padStart(2, "0");
const b = getRandomInt(0, 256).toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
@@ -103,12 +103,12 @@ export function addShapeSolidStrokeFill(argb) {
function serializePathAttrs(svgAttrs) {
return Object.entries(svgAttrs).reduce((acc, [key, value]) => {
return acc + key + '\0' + value + '\0';
}, '');
return acc + key + "\0" + value + "\0";
}, "");
}
export function draw_star(x, y, width, height) {
const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE
export function drawStar(x, y, width, height) {
const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE
const ptr = allocBytes(len * 28);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
@@ -120,7 +120,7 @@ export function draw_star(x, y, width, height) {
const star = [];
for (let i = 0; i < 10; i++) {
const angle = Math.PI / 5 * i - Math.PI / 2;
const angle = (Math.PI / 5) * i - Math.PI / 2;
const r = i % 2 === 0 ? outerRadius : innerRadius;
const px = cx + r * Math.cos(angle);
const py = cy + r * Math.sin(angle);
@@ -149,7 +149,7 @@ export function draw_star(x, y, width, height) {
Module._set_shape_path_content();
const str = serializePathAttrs({
"fill": "none",
fill: "none",
"stroke-linecap": "round",
"stroke-linejoin": "round",
});
@@ -158,7 +158,6 @@ export function draw_star(x, y, width, height) {
Module.stringToUTF8(str, offset, size);
Module._set_shape_path_attrs(3);
}
export function setShapeChildren(shapeIds) {
const offset = allocBytes(shapeIds.length * 16);
@@ -176,7 +175,7 @@ export function useShape(id) {
Module._use_shape(...buffer);
}
export function set_parent(id) {
export function setParent(id) {
const buffer = getU32(id);
Module._set_parent(...buffer);
}
@@ -227,8 +226,12 @@ export function setupInteraction(canvas) {
}
});
canvas.addEventListener("mouseup", () => { isPanning = false; });
canvas.addEventListener("mouseout", () => { isPanning = false; });
canvas.addEventListener("mouseup", () => {
isPanning = false;
});
canvas.addEventListener("mouseout", () => {
isPanning = false;
});
}
export function addTextShape(x, y, fontSize, text) {
@@ -312,4 +315,57 @@ export function addTextShape(x, y, fontSize, text) {
// Call the WebAssembly function
Module._set_shape_text_content();
}
}
export function setup(options) {
init(options.instance)
assignCanvas(options.canvas)
Module._set_canvas_background(hexToU32ARGB(options?.backgroundColor ?? "#FABADA", 1));
Module._set_view(options?.zoom ?? 1, options?.x ?? 0, options?.y ?? 0);
Module._init_shapes_pool(options.shapes + 1);
setupInteraction(options.canvas);
}
function getShapeType(type) {
switch (type) {
default:
case "rect": return 3;
}
}
export function addShape(init) {
const uuid = init?.id ?? crypto.randomUUID()
useShape(uuid);
setParent(init?.parent ?? "00000000-0000-0000-0000-000000000000");
Module._set_shape_type(getShapeType(init?.type));
if (init.selrect) {
Module._set_shape_selrect(
init.selrect.x,
init.selrect.y,
init.selrect.x + init.selrect.width,
init.selrect.y + init.selrect.height,
);
}
if (Array.isArray(init?.fills)) {
for (const fill of init.fills) {
const argb = hexToU32ARGB(fill.color, fill.opacity);
addShapeSolidFill(argb);
}
}
if (Array.isArray(init?.strokes)) {
for (const stroke of init.strokes) {
Module._add_shape_center_stroke(stroke.width, 0, 0, 0);
const argb = hexToU32ARGB(stroke.color, stroke.opacity);
addShapeSolidStrokeFill(argb);
}
}
if (Array.isArray(init?.children)) {
setShapeChildren(init.children);
}
return uuid
}

View File

@@ -25,8 +25,8 @@
<script type="module">
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
addShapeSolidFill, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, addShape, setShapeChildren, setup
} from './js/lib.js';
const canvas = document.getElementById("canvas");
@@ -37,46 +37,43 @@
const shapes = params.get("shapes") || 1000;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
Module._init_shapes_pool(shapes + 1);
setupInteraction(canvas);
setup({
instance: Module,
canvas,
shapes
})
const children = [];
for (let i = 0; i < shapes; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
Module._add_shape_center_stroke(10, 0, 0, 0);
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidStrokeFill(argb2);
for (let shape = 0; shape < shapes; shape++) {
const color = getRandomColor()
children.push(
addShape({
parent: "00000000-0000-0000-0000-000000000000",
type: "rect", // rect
selrect: {
x: getRandomInt(0, canvas.width),
y: getRandomInt(0, canvas.height),
width: getRandomInt(20, 100),
height: getRandomInt(20, 100),
},
fills: [{ type: "solid", color, opacity: getRandomFloat(0.1, 1.0) }],
strokes: [{ width: 10, type: "solid", color, opacity: getRandomFloat(0.1, 1.0) }]
})
);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
addShape({
id: "00000000-0000-0000-0000-000000000000",
children: children
})
performance.mark('render:begin');
Module._render(Date.now());
Module._render(performance.now(), true);
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
// alert(`render time: ${duration.toFixed(2)}ms`);
});
</script>
</body>
</html>

View File

@@ -37,12 +37,12 @@
check-safari-16? (fn [] (and (check-safari?) (str/includes? user-agent "version/16")))
check-safari-17? (fn [] (and (check-safari?) (str/includes? user-agent "version/17")))]
(cond
(check-edge?) :edge
(check-chrome?) :chrome
(check-firefox?) :firefox
(check-safari-16?) :safari-16
(check-safari-17?) :safari-17
(check-safari?) :safari
^boolean (check-edge?) :edge
^boolean (check-chrome?) :chrome
^boolean (check-firefox?) :firefox
^boolean (check-safari-16?) :safari-16
^boolean (check-safari-17?) :safari-17
^boolean (check-safari?) :safari
:else :other)))
(defn- parse-platform
@@ -52,9 +52,9 @@
check-linux? (fn [] (str/includes? user-agent "linux"))
check-macos? (fn [] (str/includes? user-agent "mac os"))]
(cond
(check-windows?) :windows
(check-linux?) :linux
(check-macos?) :macos
^boolean (check-windows?) :windows
^boolean (check-linux?) :linux
^boolean (check-macos?) :macos
:else :other)))
(defn- parse-target
@@ -104,6 +104,12 @@
(def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" [])))
(def templates-uri (obj/get global "penpotTemplatesUri" "https://penpot.github.io/penpot-files/"))
;; We set the current parsed flags under common for make
;; it available for common code without the need to pass
;; the flags all arround on parameters.
(set! app.common.flags/*current* flags)
(defn- normalize-uri
[uri-str]
(let [uri (u/uri uri-str)]

View File

@@ -245,4 +245,4 @@
(defn event
[props]
(ptk/data-event ::events props))
(ptk/data-event ::event props))

View File

@@ -78,6 +78,23 @@
(filter selectable?)
selected)))))
(defn split-text-shapes
"Split text shapes from non-text shapes"
[objects ids]
(loop [ids (seq ids)
text-ids []
shape-ids []]
(if-let [id (first ids)]
(let [shape (get objects id)]
(if (cfh/text-shape? shape)
(recur (rest ids)
(conj text-ids id)
shape-ids)
(recur (rest ids)
text-ids
(conj shape-ids id))))
[text-ids shape-ids])))
;; DEPRECATED
(defn lookup-selected-raw
[state]

View File

@@ -80,8 +80,7 @@
(cb/format-code style-type)))
markup-code
(-> (cg/generate-markup-code objects markup-type [shape])
(cb/format-code markup-type))]
(cg/generate-formatted-markup-code objects markup-type [shape])]
(update-preview-window
preview

View File

@@ -195,6 +195,7 @@
value (.-value sd-token)
has-references? (str/includes? (:value origin-token) "{")
parsed-token-value (case (:type origin-token)
:font-family {:value (-> (js->clj value) (flatten))}
:color (parse-sd-token-color-value value)
:opacity (parse-sd-token-opacity-value value has-references?)
:stroke-width (parse-sd-token-stroke-width-value value has-references?)

View File

@@ -20,9 +20,11 @@
[app.common.logic.shapes :as cls]
[app.common.transit :as t]
[app.common.types.component :as ctc]
[app.common.types.fills :as types.fills]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
@@ -80,6 +82,7 @@
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[clojure.walk :as walk]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
@@ -133,10 +136,30 @@
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn process-fills
"A function responsible to analyze the file data or shape for references
and apply lookup-index on it."
[data]
(letfn [(process-map-form [form]
(let [fills (get form :fills)]
(if (vector? fills)
(assoc form :fills (types.fills/from-plain fills))
form)))
(process-form [form]
(if (map? form)
(process-map-form form)
form))]
(if (contains? cf/flags :frontend-binary-fills)
(walk/postwalk process-form data)
data)))
(defn- resolve-file
[file]
(->> (fpmap/resolve-file file)
(rx/map :data)
(rx/map process-fills)
(rx/mapcat
(fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index))

View File

@@ -36,7 +36,7 @@
head
(cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills path/default-bool-fills))
(assoc :fills (path/get-default-bool-fills)))
shape
{:id shape-id
@@ -62,7 +62,7 @@
head (if (= type :difference) (first shapes) (last shapes))
head (cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills path/default-bool-fills))]
(assoc :fills (path/get-default-bool-fills)))]
(-> group
(assoc :type :bool)
(assoc :bool-type type)

View File

@@ -19,7 +19,6 @@
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logic.libraries :as cll]
[app.common.schema :as sm]
[app.common.text :as txt]
[app.common.transit :as t]
[app.common.types.component :as ctc]
[app.common.types.container :as ctn]
@@ -28,6 +27,7 @@
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.text :as types.text]
[app.common.types.text :as txt]
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -346,8 +346,8 @@
(gsh/translate-to-frame % (get objects parent-frame-id)))
shapes (mapv maybe-translate selected)
svg (svg/generate-markup objects shapes)]
(wapi/write-to-clipboard svg)))))
svg-formatted (svg/generate-formatted-markup objects shapes)]
(wapi/write-to-clipboard svg-formatted)))))
(defn copy-selected-css
[]
@@ -925,7 +925,7 @@
(let [paragraphs (->> (str/lines text)
(map str/trim)
(mapv #(hash-map :type "paragraph"
:children [(merge txt/default-text-attrs {:text %})])))]
:children [(merge (txt/get-default-text-attrs) {:text %})])))]
;; if text is composed only by line breaks paragraphs is an empty list and should be nil
(when (d/not-empty? paragraphs)
{:type "root"

View File

@@ -6,19 +6,18 @@
(ns app.main.data.workspace.colors
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.schema :as sm]
[app.common.text :as txt]
[app.common.types.color :as types.color]
[app.common.types.fill :as types.fill]
[app.common.types.color :as clr]
[app.common.types.fills :as types.fills]
[app.common.types.library :as ctl]
[app.common.types.shape :as shp]
[app.common.types.shape.shadow :refer [check-shadow]]
[app.common.types.text :as txt]
[app.config :as cfg]
[app.main.broadcast :as mbc]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as md]
[app.main.data.workspace.layout :as layout]
@@ -103,11 +102,7 @@
(defn assoc-shape-fill
[shape position fill]
(update shape :fills
(fn [fills]
(if (nil? fills)
[fill]
(assoc fills position fill)))))
(update shape :fills types.fills/assoc position fill))
(defn transform-fill*
"A lower-level companion function for `transform-fill`"
@@ -153,7 +148,7 @@
(d/without-nils)
:always
(types.fill/check-fill))
(types.fills/check-fill))
transform-attrs
#(transform % fill)]
@@ -167,17 +162,28 @@
(assoc-in [attr index] second)
(assoc-in [attr new-index] first))))
(defn- swap-fills-index
[fills index new-index]
(let [first (get fills index)
second (get fills new-index)]
(-> fills
(assoc index second)
(assoc new-index first))))
(defn reorder-fills
[ids index new-index]
(ptk/reify ::reorder-fills
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
(let [objects
(dsh/lookup-page-objects state)
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
shape-ids (remove is-text? ids)
transform-attrs #(swap-attrs % :fills index new-index)]
[text-ids shape-ids]
(dsh/split-text-shapes objects ids)
transform-attrs
(fn [object]
(update object :fills types.fills/update swap-fills-index index new-index))]
(rx/concat
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
@@ -190,7 +196,7 @@
(assert (every? uuid? ids) "expect a coll of uuids for `ids`")
(assert (number? position) "expect a number for position")
(let [color (types.color/check-color color)]
(let [color (clr/check-color color)]
(ptk/reify ::change-fill
ptk/WatchEvent
(watch [_ state _]
@@ -207,7 +213,7 @@
(ptk/reify ::change-fill-and-clear
ptk/WatchEvent
(watch [_ state _]
(let [change-fn (fn [shape attrs] (assoc shape :fills [attrs]))
(let [change-fn (fn [shape attrs] (assoc shape :fills (types.fills/create attrs)))
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
@@ -219,14 +225,13 @@
([ids color options]
(assert (every? uuid? ids) "expected a valid coll of uuid's")
(let [color (types.color/check-color color)]
(let [color (clr/check-color color)]
(ptk/reify ::add-fill
ptk/WatchEvent
(watch [_ state _]
(let [change-fn
(fn [shape attrs]
(-> shape
(update :fills #(into [attrs] %))))
(update shape :fills types.fills/prepend attrs))
undo-id
(js/Symbol)]
(rx/concat
@@ -247,13 +252,13 @@
ptk/WatchEvent
(watch [_ state _]
(let [detach-fn
(fn [values index]
(update values index dissoc :fill-color-ref-id :fill-color-ref-file))
(fn [fills index]
(update fills index dissoc :fill-color-ref-id :fill-color-ref-file))
change-fn
;; The `node` can be a shape or a text content node
(fn [node]
(update node :fills detach-fn position))
(update node :fills types.fills/update detach-fn position))
undo-id
(js/Symbol)]
@@ -275,17 +280,17 @@
ptk/WatchEvent
(watch [_ state _]
(let [remove-fill-by-index
(fn [values index]
(fn [fills index]
(into []
(comp
(map-indexed (fn [i o] (when (not= i index) o)))
(filter some?))
values))
fills))
change-fn
;; The `node` can be a shape or a text content node
(fn [node]
(update node :fills remove-fill-by-index position))
(update node :fills types.fills/update remove-fill-by-index position))
undo-id
(js/Symbol)]
@@ -303,7 +308,7 @@
(ptk/reify ::remove-all-fills
ptk/WatchEvent
(watch [_ state _]
(let [change-fn (fn [node] (assoc node :fills []))
(let [change-fn (fn [node] (assoc node :fills (types.fills/create)))
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
@@ -560,7 +565,7 @@
(assoc-in [:workspace-global :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
:type :colorpicker
:props {:data {:color cc/black
:props {:data {:color clr/black
:opacity 1}
:disable-opacity false
:disable-gradient false
@@ -576,16 +581,26 @@
:fill-color-ref-file (:ref-file color)
:fill-color-gradient (:gradient color)}))
(defn change-text-color
(defn- change-text-color
[old-color new-color index node]
(let [fills (map #(dissoc % :fill-color-ref-id :fill-color-ref-file) (:fills node))
parsed-color (-> (color-att->text old-color)
(dissoc :fill-color-ref-id :fill-color-ref-file))
parsed-new-color (color-att->text new-color)
has-color? (d/index-of fills parsed-color)]
(cond-> node
(some? has-color?)
(assoc-in [:fills index] parsed-new-color))))
(update node :fills types.fills/update
(fn [fills]
(let [fills'
(map #(dissoc % :fill-color-ref-id :fill-color-ref-file) fills)
parsed-color
(-> (color-att->text old-color)
(dissoc :fill-color-ref-id :fill-color-ref-file))
parsed-new-color
(color-att->text new-color)
has-color?
(d/index-of fills' parsed-color)]
(cond-> fills
(some? has-color?)
(assoc index parsed-new-color))))))
(def ^:private schema:change-color-operation
[:map
@@ -601,17 +616,9 @@
(defn change-color-in-selected
[operations new-color old-color]
(assert (check-change-color-operations operations)
"expected valid color operations")
(assert
(types.color/check-color new-color)
"expected valid color structure")
(assert
(types.color/check-color old-color)
"expected valid color structure")
(assert (check-change-color-operations operations))
(assert (clr/check-color new-color))
(assert (clr/check-color old-color))
(ptk/reify ::change-color-in-selected
ptk/WatchEvent
@@ -632,7 +639,7 @@
(defn apply-color-from-palette
[color stroke?]
(let [color (types.color/check-color color)]
(let [color (clr/check-color color)]
(ptk/reify ::apply-color-from-palette
ptk/WatchEvent
(watch [_ state _]
@@ -667,7 +674,7 @@
(defn apply-color-from-colorpicker
[color]
(let [color (types.color/check-color color)]
(let [color (clr/check-color color)]
(ptk/reify ::apply-color-from-colorpicker
ptk/UpdateEvent
(update [_ state]
@@ -708,7 +715,7 @@
(defn add-recent-color
[color]
(let [color (types.color/check-color color)]
(let [color (clr/check-color color)]
(ptk/reify ::add-recent-color
ptk/UpdateEvent
(update [_ state]
@@ -728,11 +735,11 @@
(defn apply-color-from-assets
[file-id color stroke?]
(let [color (types.color/check-library-color color)]
(let [color (clr/check-library-color color)]
(ptk/reify ::apply-color-from-asserts
ptk/WatchEvent
(watch [_ _ _]
(let [color (types.color/library-color->color color file-id)]
(let [color (clr/library-color->color color file-id)]
(rx/of (apply-color-from-palette color stroke?)
(add-recent-color color)))))))
@@ -742,9 +749,9 @@
(defn split-color-components
[{:keys [color opacity] :as data}]
(let [value (if (cc/valid-hex-color? color) color cc/black)
[r g b] (cc/hex->rgb value)
[h s v] (cc/hex->hsv value)]
(let [value (if (clr/valid-hex-color? color) color clr/black)
[r g b] (clr/hex->rgb value)
[h s v] (clr/hex->hsv value)]
(merge data
{:hex (or value "000000")
:alpha (or opacity 1)
@@ -893,11 +900,11 @@
(update state :colorpicker
(fn [{:keys [stops editing-stop] :as state}]
(let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills))
can-add-stop? (or (not cap-stops?) (< (count stops) types.fill/MAX-GRADIENT-STOPS))]
can-add-stop? (or (not cap-stops?) (< (count stops) types.fills/MAX-GRADIENT-STOPS))]
(if can-add-stop?
(if (cc/uniform-spread? stops)
(if (clr/uniform-spread? stops)
;; Add to uniform
(let [stops (->> (cc/uniform-spread (first stops) (last stops) (inc (count stops)))
(let [stops (->> (clr/uniform-spread (first stops) (last stops) (inc (count stops)))
(mapv split-color-components))]
(-> state
(assoc :current-color (get stops editing-stop))
@@ -918,7 +925,7 @@
half-point-offset
(+ from-offset (/ (- to-offset from-offset) 2))
new-stop (-> (cc/interpolate-gradient stops half-point-offset)
new-stop (-> (clr/interpolate-gradient stops half-point-offset)
(split-color-components))
stops (conj stops new-stop)
@@ -938,18 +945,24 @@
(update state :colorpicker
(fn [state]
(let [stops (:stops state)
cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills))
can-add-stop? (or (not cap-stops?) (< (count stops) types.fill/MAX-GRADIENT-STOPS))]
(if can-add-stop? (let [new-stop (-> (cc/interpolate-gradient stops offset)
(split-color-components))
stops (conj stops new-stop)
stops (into [] (sort-by :offset stops))
editing-stop (d/index-of-pred stops #(= new-stop %))]
(-> state
(assoc :editing-stop editing-stop)
(assoc :current-color (get stops editing-stop))
(assoc :stops stops)))
state)))))))
cap-stops?
(or (features/active-feature? state "render-wasm/v1")
(contains? cfg/flags :frontend-binary-fills))
can-add-stop?
(or (not cap-stops?) (< (count stops) types.fills/MAX-GRADIENT-STOPS))]
(if can-add-stop?
(let [new-stop (-> (clr/interpolate-gradient stops offset)
(split-color-components))
stops (conj stops new-stop)
stops (into [] (sort-by :offset stops))
editing-stop (d/index-of-pred stops #(= new-stop %))]
(-> state
(assoc :editing-stop editing-stop)
(assoc :current-color (get stops editing-stop))
(assoc :stops stops)))
state)))))))
(defn update-colorpicker-stops
[stops]
@@ -963,7 +976,7 @@
(contains? cfg/flags :frontend-binary-fills))
stops (mapv split-color-components
(if cap-stops?
(take types.fill/MAX-GRADIENT-STOPS stops)
(take types.fills/MAX-GRADIENT-STOPS stops)
stops))]
(-> state
(assoc :current-color (get stops stop))
@@ -1116,35 +1129,114 @@
(assoc :type :image)
(dissoc :editing-stop :stops :gradient)))))))
(defn select-color
[position add-color]
;; FIXME: revisit
(ptk/reify ::select-color
ptk/WatchEvent
(watch [_ state _]
(let [selected (dsh/lookup-selected state)
shapes (dsh/lookup-shapes state selected)
shape (first shapes)
fills (if (cfh/text-shape? shape)
(:fills (dwt/current-text-values
{:editor-state (dm/get-in state [:workspace-editor-state (:id shape)])
:shape shape
:attrs (conj txt/text-fill-attrs :fills)}))
(:fills shape))
fill (first fills)
single? (and (= 1 (count selected))
(= 1 (count fills)))
data (if single?
(d/without-nils {:color (:fill-color fill)
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)})
{:color "#406280"
:opacity 1})]
(rx/of (md/show :colorpicker
{:x (:x position)
:y (:y position)
:on-accept add-color
:data data
:position :right})
(ptk/event ::ev/event {::ev/name "add-asset-to-library"
:asset-type "color"}))))))
(defn- stroke->color-att
[stroke file-id libraries]
(let [ref-file (:stroke-color-ref-file stroke)
ref-id (:stroke-color-ref-id stroke)
colors (-> libraries
(get ref-file)
(get :data)
(ctl/get-colors))
is-shared? (contains? colors ref-id)
has-color? (or (:stroke-color stroke)
(:stroke-color-gradient stroke))
attrs (cond-> (clr/stroke->color stroke)
(not (or is-shared? (= ref-file file-id)))
(dissoc :ref-id :ref-file))]
(when has-color?
{:attrs attrs
:prop :stroke
:shape-id (:shape-id stroke)
:index (:index stroke)})))
(defn- shadow->color-att
[shadow file-id libraries]
(let [color (get shadow :color)
ref-file (get color :ref-file)
ref-id (get color :ref-id)
colors (-> libraries
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
attrs (cond-> (clr/shadow->color shadow)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :shadow
:shape-id (:shape-id shadow)
:index (:index shadow)}))
(defn- text->color-att
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :content
:shape-id (:shape-id fill)
:index (:index fill)}))
(defn- extract-text-colors
[text file-id libraries]
(let [treat-node
(fn [node shape-id]
(map-indexed #(assoc %2 :shape-id shape-id :index %1) node))]
(->> (txt/node-seq txt/is-text-node? (:content text))
(map :fills)
(mapcat #(treat-node % (:id text)))
(map #(text->color-att % file-id libraries)))))
(defn- fill->color-att
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
has-color? (or (:fill-color fill)
(:fill-color-gradient fill))
attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
(when has-color?
{:attrs attrs
:prop :fill
:shape-id (:shape-id fill)
:index (:index fill)})))
(defn extract-all-colors
[shapes file-id libraries]
(reduce
(fn [result shape]
(let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape))
stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape))
shadow-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:shadow shape))]
(if (= :text (:type shape))
(-> result
(into (keep #(stroke->color-att % file-id libraries)) stroke-obj)
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)
(into (extract-text-colors shape file-id libraries)))
(-> result
(into (keep #(fill->color-att % file-id libraries)) fill-obj)
(into (keep #(stroke->color-att % file-id libraries)) stroke-obj)
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)))))
[]
shapes))

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.fix-deleted-fonts
(:require
[app.common.files.helpers :as cfh]
[app.common.text :as txt]
[app.common.types.text :as txt]
[app.main.data.changes :as dwc]
[app.main.data.helpers :as dsh]
[app.main.fonts :as fonts]

View File

@@ -22,7 +22,8 @@
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.shape.layout :as ctl]
[app.common.types.library :as ctl]
[app.common.types.shape.layout :as ctsl]
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -36,6 +37,7 @@
[app.main.data.workspace :as-alias dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.notifications :as-alias dwn]
[app.main.data.workspace.pages :as-alias dwpg]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.specialized-panel :as dwsp]
@@ -194,7 +196,7 @@
(if (str/empty? new-name)
(rx/empty)
(let [data (dsh/lookup-file-data state)
color (-> (ctc/get-color data id)
color (-> (ctl/get-color data id)
(assoc :name new-name)
(d/without-nils)
(ctc/check-library-color))]
@@ -700,7 +702,7 @@
(fn [page-id shape-id]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dw/initialize-page))
(rx/filter (ptk/type? ::dwpg/initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (select-and-zoom shape-id))))
@@ -961,8 +963,8 @@
orig-shapes (when keep-touched? (cfh/get-children-with-self objects (:id shape)))
;; If the target parent is a grid layout we need to pass the target cell
target-cell (when (ctl/grid-layout? parent)
(ctl/get-cell-by-shape-id parent (:id shape)))
target-cell (when (ctsl/grid-layout? parent)
(ctsl/get-cell-by-shape-id parent (:id shape)))
index (find-shape-index objects (:parent-id shape) (:id shape))

View File

@@ -16,6 +16,7 @@
[app.common.media :as media]
[app.common.schema :as sm]
[app.common.types.container :as ctn]
[app.common.types.fills :as types.fills]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -63,18 +64,22 @@
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [name width height id mtype]} image
fills (types.fills/create
{:fill-opacity 1
:fill-image {:width width
:height height
:mtype mtype
:id id
:keep-aspect-ratio true}})
shape {:name name
:width width
:height height
:x (mth/round (- x (/ width 2)))
:y (mth/round (- y (/ height 2)))
:fills [{:fill-opacity 1
:fill-image {:name name
:width width
:height height
:mtype mtype
:id id
:keep-aspect-ratio true}}]}]
:fills fills}]
(rx/of (dwsh/create-and-add-shape :rect x y shape))))))
(defn svg-uploaded
@@ -299,7 +304,7 @@
:name (:name root-svg-shape)
:frame-id uuid/zero
:parent-id uuid/zero
:fills []})
:fills (types.fills/create)})
root-svg-shape
(-> root-svg-shape
@@ -335,19 +340,21 @@
:frame-id uuid/zero
:parent-id uuid/zero})
img-fills (types.fills/create
{:fill-opacity 1
:fill-image {:id id
:width width
:height height
:mtype mtype
:keep-aspect-ratio true}})
img-shape (cts/setup-shape
{:type :rect
:x (:x pos)
:y (:y pos)
:width width
:height height
:fills [{:fill-opacity 1
:fill-image {:name name
:id id
:width width
:height height
:mtype mtype
:keep-aspect-ratio true}}]
:fills img-fills
:name name
:frame-id (:id frame-shape)
:parent-id (:id frame-shape)})]

View File

@@ -847,3 +847,25 @@
(if undo-transation?
(rx/of (dwu/commit-undo-transaction undo-id))
(rx/empty))))))))
;; Pure function to determine next grow-type for text layers
(defn next-grow-type [current-grow-type resize-direction]
(cond
(= current-grow-type :fixed)
:fixed
(and (= resize-direction :horizontal)
(= current-grow-type :auto-width))
:auto-height
(and (= resize-direction :horizontal)
(= current-grow-type :auto-height))
:auto-height
(and (= resize-direction :vertical)
(or (= current-grow-type :auto-width)
(= current-grow-type :auto-height)))
:fixed
:else
current-grow-type))

View File

@@ -174,12 +174,15 @@
add-component-copy
(fn [objs id shape]
(let [component (ctkl/get-component fdata (:component-id shape))
parent-id (when (not= (:parent-id shape) uuid/zero) (:parent-id shape))
[new-shape new-shapes]
(ctn/make-component-instance page
component
fdata
(gpt/point (:x shape) (:y shape))
{:keep-ids? true :force-frame-id (:frame-id shape)})
{:keep-ids? true
:force-frame-id (:frame-id shape)
:force-parent-id parent-id})
children (into {} (map (fn [shape] [(:id shape) shape]) new-shapes))
objs (assoc objs id new-shape)]
(merge objs children)))
@@ -201,10 +204,8 @@
(assoc :id id)
(assoc :objects
objects))
changes (-> (pcb/empty-changes it)
(pcb/add-page id page))]
(rx/of (dch/commit-changes changes))))))
(s/def ::rename-page

View File

@@ -6,7 +6,6 @@
(ns app.main.data.workspace.shape-layout
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
@@ -16,6 +15,7 @@
[app.common.geom.shapes.flex-layout :as flex]
[app.common.geom.shapes.grid-layout :as grid]
[app.common.logic.libraries :as cll]
[app.common.types.color :as clr]
[app.common.types.component :as ctc]
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]

View File

@@ -8,7 +8,7 @@
(:require
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.text :as txt]
[app.common.types.text :as txt]
[app.main.data.shortcuts :as ds]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu]

View File

@@ -15,9 +15,9 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.text :as txt]
[app.common.types.fill :as types.fill]
[app.common.types.fills :as types.fills]
[app.common.types.modifiers :as ctm]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
@@ -196,8 +196,8 @@
ptk/UpdateEvent
(update [_ state]
(let [text-state (some->> content ted/import-content)
attrs (d/merge txt/default-text-attrs
(get-in state [:workspace-global :default-font]))
attrs (merge (txt/get-default-text-attrs)
(get-in state [:workspace-global :default-font]))
editor (cond-> (ted/create-editor-state text-state decorator)
(and (nil? content) (some? attrs))
(ted/update-editor-current-block-data attrs))]
@@ -237,7 +237,9 @@
(defn- to-new-fills
[data]
[(d/without-nils (select-keys data types.fill/fill-attrs))])
;; FIXME: maybe export this as a specific helper ?
(types.fills/create
(d/without-nils (select-keys data types.fills/fill-attrs))))
(defn- shape-current-values
[shape pred attrs]
@@ -245,17 +247,21 @@
nodes (->> (txt/node-seq pred root)
(map (fn [node]
(if (txt/is-text-node? node)
(let [fills
(let [default-text-attrs
(txt/get-default-text-attrs)
fills
(cond
(types.fill/has-valid-fill-attrs? node)
(types.fills/has-valid-fill-attrs? node)
(to-new-fills node)
(some? (:fills node))
(:fills node)
:else
(:fills txt/default-text-attrs))]
(-> (merge txt/default-text-attrs node)
(:fills default-text-attrs))]
(-> (merge default-text-attrs node)
(assoc :fills fills)))
node))))]
(attrs/get-attrs-multi nodes attrs)))
@@ -290,7 +296,9 @@
[{:keys [editor-state attrs]}]
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
(select-keys attrs))
result (if (empty? result) txt/default-text-attrs result)]
result (if (empty? result)
(txt/get-default-text-attrs)
result)]
result))
(defn current-text-values
@@ -466,24 +474,24 @@
(defn migrate-node
[node]
(let [color-attrs (not-empty (select-keys node types.fill/fill-attrs))]
(let [color-attrs (not-empty (select-keys node types.fills/fill-attrs))]
(cond-> node
(nil? (:fills node))
(assoc :fills [])
(assoc :fills (types.fills/create))
;; Migrate old colors and remove the old fromat
color-attrs
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
(update :fills conj color-attrs))
(update :fills types.fills/update conj color-attrs))
;; We don't have the fills attribute. It's an old text without color
;; so need to be black
(and (nil? (:fills node)) (empty? color-attrs))
(update :fills conj txt/default-text-attrs)
(assoc :fills (txt/get-default-text-fills))
;; Remove duplicates from the fills
:always
(update :fills (comp vec distinct)))))
(update :fills types.fills/update distinct))))
(defn migrate-content
[content]
@@ -905,9 +913,9 @@
(ptk/reify ::v2-update-text-editor-styles
ptk/UpdateEvent
(update [_ state]
(let [merged-styles (d/merge txt/default-text-attrs
(get-in state [:workspace-global :default-font])
new-styles)]
(let [merged-styles (merge (txt/get-default-text-attrs)
(get-in state [:workspace-global :default-font])
new-styles)]
(update-in state [:workspace-v2-editor-state id] (fnil merge {}) merged-styles)))))
(defn v2-update-text-shape-position-data

View File

@@ -8,12 +8,13 @@
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.text :as txt]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.text :as txt]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.style-dictionary :as sd]
@@ -24,6 +25,7 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.fonts :as fonts]
[app.main.store :as st]
[beicon.v2.core :as rx]
[clojure.set :as set]
@@ -364,6 +366,26 @@
{:ignore-touched true
:page-id page-id})))))
(defn update-font-family
([value shape-ids attributes] (update-font-family value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [font-family (first value)
font (some-> font-family
(fonts/find-font-family))
text-attrs (if font
{:font-id (:id font)
:font-family (:family font)}
{:font-id (str uuid/zero)
:font-family font-family})
update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))]
(when text-attrs
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? d/txt-merge text-attrs)
{:ignore-touched true
:page-id page-id})))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
([value shape-ids _attributes page-id]
@@ -421,6 +443,14 @@
:fields [{:label "Letter Spacing"
:key :letter-spacing}]}}
:font-family
{:title "Font Family"
:attributes ctt/font-family-keys
:on-update-shape update-font-family
:modal {:key :tokens/font-family
:fields [{:label "Font Family"
:key :font-family}]}}
:stroke-width
{:title "Stroke Width"
:attributes ctt/stroke-width-keys

View File

@@ -47,7 +47,7 @@
(ntf/show {:content (tr "workspace.tokens.unknown-token-type-message")
:detail (->> (for [[token-type tokens] type->tokens]
(tr "workspace.tokens.unknown-token-type-section" token-type (count tokens)))
(str/join "\n"))
(str/join "<br>"))
:type :toast
:level :info})))

View File

@@ -35,6 +35,7 @@
#{:line-height} dwta/update-line-height
#{:font-size} dwta/update-font-size
#{:letter-spacing} dwta/update-letter-spacing
#{:font-family} dwta/update-font-family
#{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin

View File

@@ -218,16 +218,23 @@
(gpt/add resize-origin displacement)
resize-origin)
;; Determine resize direction for grow-type logic
resize-direction (cond
(or (= handler :left) (= handler :right)) :horizontal
(or (= handler :top) (= handler :bottom)) :vertical
:else nil)
;; Calculate new grow-type for text layers
new-grow-type (when (cfh/text-shape? shape)
(dwm/next-grow-type (dm/get-prop shape :grow-type) resize-direction))
;; When the horizontal/vertical scale a flex children with auto/fill
;; we change it too fixed
change-width?
(not (mth/close? (dm/get-prop scalev :x) 1))
change-height?
(not (mth/close? (dm/get-prop scalev :y) 1))
auto-width-text? (and (cfh/text-shape? shape) (= :auto-width (dm/get-prop shape :grow-type)))
auto-height-text? (and (cfh/text-shape? shape) (= :auto-height (dm/get-prop shape :grow-type)))]
(not (mth/close? (dm/get-prop scalev :y) 1))]
(cond-> (ctm/empty)
(some? displacement)
@@ -242,11 +249,9 @@
^boolean change-height?
(ctm/change-property :layout-item-v-sizing :fix)
(and auto-width-text? (or change-width? change-height?))
(ctm/change-property :grow-type :fixed)
(and auto-height-text? change-height?)
(ctm/change-property :grow-type :fixed)
;; Set grow-type if it should change
(and new-grow-type (not= new-grow-type (dm/get-prop shape :grow-type)))
(ctm/change-property :grow-type new-grow-type)
^boolean scale-text
(ctm/scale-content (dm/get-prop scalev :x)))))

View File

@@ -6,7 +6,6 @@
(ns app.main.data.workspace.variants
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
@@ -14,6 +13,7 @@
[app.common.geom.point :as gpt]
[app.common.logic.variant-properties :as clvp]
[app.common.logic.variants :as clv]
[app.common.types.color :as clr]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctkl]
[app.common.types.shape.layout :as ctsl]

View File

@@ -102,23 +102,27 @@
(print-trace! error)
(print-data! error))))
;; We receive a explicit authentication error;
;; If the uri is for workspace, dashboard or view assign the
;; exception for the 'Oops' page. Otherwise this explicitly clears
;; all profile data and redirect the user to the login page. This is
;; here and not in app.main.errors because of circular dependency.
;; We receive a explicit authentication error; If the uri is for
;; workspace, dashboard, viewer or settings, then assign the exception
;; for show the error page. Otherwise this explicitly clears all
;; profile data and redirect the user to the login page. This is here
;; and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication
[e]
(let [msg (tr "errors.auth.unable-to-login")
uri (.-href glob/location)
show-oops? (or (str/includes? uri "workspace")
(str/includes? uri "dashboard")
(str/includes? uri "view"))]
(if show-oops?
(st/async-emit! (rt/assign-exception e))
[error]
(let [message (tr "errors.auth.unable-to-login")
uri (rt/get-current-href)
show-error?
(or (str/includes? uri "workspace")
(str/includes? uri "dashboard")
(str/includes? uri "view")
(str/includes? uri "settings"))]
(if show-error?
(st/async-emit! (rt/assign-exception error))
(do
(st/emit! (da/logout))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
(ts/schedule 500 #(st/emit! (ntf/warn message)))))))
;; Error that happens on an active business model validation does not
;; passes an validation (example: profile can't leave a team). From

View File

@@ -11,7 +11,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.text :as txt]
[app.common.types.text :as txt]
[app.config :as cf]
[app.util.dom :as dom]
[app.util.globals :as globals]
@@ -78,6 +78,15 @@
data))
(vals @fontsdb)))
(defn find-font-family
"Case insensitive lookup of font-family."
[family]
(let [family' (str/lower family)]
(d/seek
(fn [{:keys [family]}]
(= family' (str/lower family)))
(vals @fontsdb))))
(defn resolve-variants
[id]
(get-in @fontsdb [id :variants]))
@@ -295,7 +304,7 @@
(let [current-font
(if (some? font-id)
(select-keys node [:font-id :font-variant-id])
(select-keys txt/default-text-attrs [:font-id :font-variant-id]))]
(select-keys txt/default-typography [:font-id :font-variant-id]))]
(conj result current-font)))
#{})))

View File

@@ -13,7 +13,6 @@
(:require
["react-dom/server" :as rds]
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
@@ -23,6 +22,7 @@
[app.common.geom.shapes.bounds :as gsb]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.types.color :as clr]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.modifiers :as ctm]

View File

@@ -28,6 +28,7 @@
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
[app.main.ui.releases :refer [release-notes-modal]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
@@ -362,6 +363,8 @@
;; initialize themes
(theme/use-initialize profile)
(dom/prevent-browser-gesture-navigation!)
[:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/current-profile) {:value profile}
(if edata

View File

@@ -6,8 +6,8 @@
(ns app.main.ui.components.color-input
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.types.color :as cc]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.globals :as globals]

View File

@@ -61,7 +61,7 @@
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.professional.top-title")
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom-description", subscription-href)
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom", subscription-href)
:has-dropdown true}]
"unlimited"
@@ -75,7 +75,7 @@
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.unlimited-plan")
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-description", subscription-href)
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom", subscription-href)
:has-dropdown true}])
"enterprise"

View File

@@ -17,6 +17,7 @@
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.dom.normalize-wheel :as nw]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.storage :as storage]
@@ -171,41 +172,48 @@
(mf/defc templates-section*
{::mf/props :obj}
[{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]
(filterv #(and
(not= (:id %) "welcome")
(not= (:id %) "tutorial-for-beginners")) templates))
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]
(filterv #(and
(not= (:id %) "welcome")
(not= (:id %) "tutorial-for-beginners")) templates))
route (mf/deref refs/route)
route-name (get-in route [:data :name])
section (if (= route-name :dashboard-files)
(if (= project-id default-project-id)
"dashboard-drafts"
"dashboard-project")
(name route-name))
route (mf/deref refs/route)
route-name (get-in route [:data :name])
section (if (= route-name :dashboard-files)
(if (= project-id default-project-id)
"dashboard-drafts"
"dashboard-project")
(name route-name))
collapsed* (mf/use-state
#(get storage/global ::collapsed))
collapsed (deref collapsed*)
collapsed* (mf/use-state
#(get storage/global ::collapsed))
collapsed (deref collapsed*)
can-move (mf/use-state {:left false :right true})
can-move (mf/use-state {:left false :right true})
total (count templates)
total (count templates)
;; We need space for total plus the libraries&templates link
content-ref (mf/use-ref)
move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0))
move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0))
content-ref (mf/use-ref)
on-toggle-collapse
(mf/use-fn
(fn [_event]
(swap! collapsed* not)))
on-wheel
(mf/use-fn
(fn [^js event]
(let [event* (nw/normalize-wheel event)
deltaY (.-spinY event*)
deltaX (.-spinX event*)
node (mf/ref-val content-ref)]
(when (> (abs deltaY) (abs deltaX))
(.scrollBy node #js {:left (* 300 deltaY) :mode "smooth"})))))
on-scroll
(mf/use-fn
(fn [e]
@@ -219,16 +227,10 @@
:right (> scroll-available client-width)}))))
on-move-left
(mf/use-fn #(move-left))
on-move-left-key-down
(mf/use-fn #(move-left))
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) -300 0))
on-move-right
(mf/use-fn #(move-right))
on-move-right-key-down
(mf/use-fn #(move-right))
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) 300 0))
on-import-template
(mf/use-fn
@@ -236,7 +238,7 @@
(fn [template _event]
(import-template! template team-id project-id default-project-id section)))]
(mf/with-effect [content-ref templates]
(mf/with-effect [templates]
(let [content (mf/ref-val content-ref)]
(when (and (some? content) (some? templates))
(dom/scroll-to content #js {:behavior "instant" :left 0 :top 0})
@@ -258,6 +260,7 @@
[:div {:class (stl/css :content)
:on-scroll on-scroll
:on-wheel on-wheel
:ref content-ref}
(for [index (range (count templates))]
@@ -279,13 +282,13 @@
[:button {:class (stl/css :move-button :move-left)
:tab-index (if ^boolean collapsed "-1" "0")
:on-click on-move-left
:on-key-down on-move-left-key-down}
:on-key-down on-move-left}
arrow-icon])
(when (:right @can-move)
[:button {:class (stl/css :move-button :move-right)
:tab-index (if collapsed "-1" "0")
:on-click on-move-right
:aria-label (tr "labels.next")
:on-key-down on-move-right-key-down}
:on-key-down on-move-right
:aria-label (tr "labels.next")}
arrow-icon])]))

View File

@@ -20,7 +20,7 @@
[:class {:optional true} :string]
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]
[:has-hint {:optional true} :boolean]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]
[:type {:optional true} :string]

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