Compare commits

...

91 Commits

Author SHA1 Message Date
Alejandro Alonso
ed5875f29a Merge pull request #7154 from penpot/niwinz-staging-bug-1
🐛 Fix incorrect show request-access dialog on not-found on viewer
2025-08-22 09:19:47 +02:00
Andrey Antukh
ad38a21053 🐛 Fix incorrect show request-access dialog on not-found on viewer
When a user is not-authenticated
2025-08-20 13:35:20 +02:00
Andrey Antukh
adffac4eec Merge remote-tracking branch 'origin/main' into staging 2025-08-20 12:49:31 +02:00
Yamila Moreno
73dfe12ec9 📚 Update k8s documentation 2025-08-20 09:04:25 +02:00
Eva Marco
ff2e845f2c 🐛 Fix double click on set name input (#7096) 2025-08-13 09:23:53 +02:00
Alejandro Alonso
8e0a6e4123 🐛 Fix auto height is fixed in the HTML inspect tab for text elements (#7078) 2025-08-11 09:07:43 +02:00
Marina López
0131cd6f8b Display the total price of the subscription and the cap amount (#7088) 2025-08-11 09:07:24 +02:00
Andrey Antukh
288a7b21d6 Merge tag '2.9.0-RC8' 2025-08-08 09:47:42 +02:00
andrés gonzález
32bd08533d 💄 Remove slide about overrides in the release notes (#7086) 2025-08-08 09:46:40 +02:00
Yamila Moreno
c1aae12327 📎 Improve gh actions 2025-08-07 18:08:25 +02:00
Yamila Moreno
23a6f4b7c1 📎 Improve gh actions 2025-08-07 18:07:47 +02:00
Andrey Antukh
133e6e1e68 Merge tag '2.9.0-RC7' 2025-08-07 16:30:30 +02:00
Andrey Antukh
6abd045273 🐛 Add missing generator for token-set file change operation (#7080)
* 🐛 Add missing generator for token-set file change operation

* 🐛 Use ::sm/any instead of :any for on get-file-data-for-thumbnail rpc method

Mainly because :any will use a very generic generator that can generate
instances of Character that are not directly serializable to JSON
2025-08-07 12:36:14 +02:00
Marina López
778a608854 🐛 Fix tooltip for icon plans from team dropdown (#7075) 2025-08-07 07:43:49 +02:00
Marina López
a76a9fae41 🐛 Fix an unused translation (#7074) 2025-08-06 13:28:02 +02:00
Andrey Antukh
f7cfbdd229 🐛 Comment the problematic migration 2025-08-05 22:05:52 +02:00
Andrey Antukh
e28d2842f6 🐛 Revert the revert of orientation detection on media
This reverts commit 515cbf7bef.
2025-08-05 22:03:09 +02:00
Andrey Antukh
ccc3ca0948 Disable virtual threads on http server 2025-08-05 20:34:47 +02:00
Andrey Antukh
515cbf7bef 🐛 Revert orientation detection on media 2025-08-05 19:30:01 +02:00
Andrey Antukh
c320cbc47b 🐛 Revert to semaphore based climit impl 2025-08-05 19:17:35 +02:00
Andrey Antukh
46969585ed Disable native buffers usage on xnio
A temporal change for investigate native memory leak
2025-08-04 22:13:08 +02:00
Andrey Antukh
47882c5419 Add missing parameter on climit instance creation 2025-08-04 19:53:56 +02:00
andrés gonzález
019d5e083a 💄 Change copys at the 2.9 release slides (#7063) 2025-08-04 19:53:50 +02:00
Andrey Antukh
85f6cf32ae 🐛 Several bugfixes (#7062)
* 🐛 Fix incorrect status validation on subscription internal api

* 🐛 Make the shortcuts overwritting optional
2025-08-04 13:54:29 +02:00
Marina López
ded8e39e73 🐛 Fix hidden button in subscribe modal when there is a large number of teams (#7061) 2025-08-04 13:16:58 +02:00
Andrey Antukh
e730200873 🐛 Fix pinned project ordering on dashboard sidebar (#7060) 2025-08-04 12:07:19 +02:00
Francis Santiago
4501d13961 📚 Clarify OpenShift requirements (#6937)
* 📚 Clarify OpenShift requirements

* 📚 Remove the click for expanding
2025-08-01 16:26:04 +02:00
Juan de la Cruz
baa1cfb2f8 🎉 Add 2.9 release slides (#7019) 2025-08-01 14:59:11 +02:00
Eva Marco
905699d15a Add info to apply-token events (#7050) 2025-08-01 14:00:30 +02:00
Eva Marco
fe53869308 🐛 Fix small details on number token application (#7051) 2025-08-01 13:52:09 +02:00
Andrey Antukh
50076bac83 Merge remote-tracking branch 'origin/main' into staging 2025-08-01 13:10:52 +02:00
Brandon Currell
44bc4b7fa4 🐳 Add missing package in the exporter Docker image (#7026)
penpot-exporter requires poppler-utils for exporting to a PDF, but it is missing.
Added the package to the Dockerfile in the RUN section where dependencies are
being installed.

Signed-off-by: Brandon Currell <brandon+git@currell.pw>
2025-08-01 13:00:51 +02:00
Yamila Moreno
5c14f486d7 🐳 Update Imagemagick version 2025-08-01 12:49:11 +02:00
Andrey Antukh
0cbd980b68 🎉 Add imagemagick docker image build scripts (#6925)
* 🎉 Add imagemagick docker image build scripts

* 📎 Add PR feedback changes
2025-08-01 12:48:47 +02:00
Eva Marco
95dda2b1af 🐛 Fix stroke width token application (#7039) 2025-07-31 14:59:48 +02:00
Andrey Antukh
5170872961 Merge pull request #7031 from penpot/eva-fix-export-button-width
🐛 Fix export button width on inspect tab
2025-07-31 12:25:03 +02:00
Andrey Antukh
871ca68e1e 📎 Allow revert commits on github commit checker 2025-07-31 12:14:29 +02:00
Andrey Antukh
0ab896fc76 Revert " Highlight first font in font selector search, apply on Enter/click"
This reverts commit e62567d09e.
2025-07-31 12:14:29 +02:00
Andrey Antukh
6a4b548457 Revert "🐛 Fix font selector highlight inconsistency (#6990)"
This reverts commit 708a40bff1.
2025-07-31 12:14:29 +02:00
Eva Marco
695a399941 🐛 Fix export button width on inspect tab 2025-07-31 09:30:46 +02:00
Eva Marco
a32463fada 🐛 Fix tooltip position after several shows and hides (#7022) 2025-07-31 09:00:05 +02:00
Eva Marco
5d44c88988 🐛 Fix token pill not showing position application on dimension token type (#7018) 2025-07-30 14:24:10 +02:00
Andrey Antukh
ce87d797d1 Merge pull request #7014 from penpot/niwinz-staging-regression-3
🐛 Fix several issues related to font/text related tokens
2025-07-30 12:25:28 +02:00
Andrey Antukh
7fde1436e1 🐛 Add missing styles to the empty node on editor-v1 2025-07-30 11:45:39 +02:00
Andrey Antukh
e1c5a32fcb 💄 Fix indentation style on generate-unapply-tokens 2025-07-30 11:45:19 +02:00
Andrey Antukh
b262e6a46f 🐛 Fix incorrect condition on checking text shape attrs 2025-07-30 11:44:07 +02:00
Andrey Antukh
2e726b62c3 📎 Update changelog 2025-07-29 20:07:05 +02:00
Andrey Antukh
02acd81c2c 🐛 Add missing profile prop to access style component (#7007)
* 💄 Fix request-access component style

* 🐛 Add missing profile prop to access style component
2025-07-29 16:04:15 +02:00
Andrey Antukh
bae2de75ff Merge branch 'main' into staging 2025-07-29 15:21:58 +02:00
Andrey Antukh
b68c426cd1 🐛 Fix exception on fills menu when binary-fills flag is active
And multiple shapes are selected.
2025-07-29 15:10:32 +02:00
Andrey Antukh
5161ef15bf 🐛 Fix regression on show access request dialog (#7005) 2025-07-29 14:58:02 +02:00
Eva Marco
36d3d94ec9 🐛 Fix X & Y position do not sincronize with tokens (#7004) 2025-07-29 14:32:06 +02:00
Andrey Antukh
17447d7610 Remove restriction of duplicate bindings on mousetrap 2025-07-29 14:14:19 +02:00
andrés gonzález
708a40bff1 🐛 Fix font selector highlight inconsistency (#6990)
* 🐛 Fix font selector highlight inconsistency

*  Add minor performance enhancements

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-07-29 13:12:54 +02:00
Marina López
efaf6573bd 📎 Update monetization texts (#7002) 2025-07-29 12:42:11 +02:00
Yamila Moreno
001bcbce59 Merge pull request #6995 from penpot/yms-update-imagemagick-version
🐳 Update Imagemagick version
2025-07-29 10:58:32 +02:00
Yamila Moreno
c195c07a3f 🐳 Update Imagemagick version 2025-07-29 10:37:11 +02:00
Alejandro Alonso
f5298f51e7 🐛 Fix the context menu always closes after any action (#6944) 2025-07-29 09:50:55 +02:00
Alejandro Alonso
46c440fef2 🐛 Fix remove color button in the gradient editor (#6993) 2025-07-28 17:48:05 +02:00
Alejandro Alonso
e77f8b572a Merge pull request #6953 from penpot/superalex-fix-component-changes-not-propagated
🐛 Fix component changes not propagated
2025-07-28 12:53:37 +02:00
Alejandro Alonso
ade5eecf80 🐛 Fix component changes not propagated 2025-07-28 12:38:09 +02:00
andrés gonzález
97fc7702b8 📚 Improve and clarify 'Hide and lock layers' section (#6975) 2025-07-25 14:53:32 +02:00
andrés gonzález
54fcd58531 📚 Add doc for resizing text (#6974)
* 📚 Add doc for resizing text

* 📚 Update docs for text resizing

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
Signed-off-by: andrés gonzález <andres.gonzalez79@gmail.com>

---------

Signed-off-by: andrés gonzález <andres.gonzalez79@gmail.com>
Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
2025-07-25 13:20:52 +02:00
andrés gonzález
b7a8747f00 📚 Add doc for tokens zip file import option (#6973) 2025-07-25 13:20:39 +02:00
Andrey Antukh
d00de7d5a4 Merge pull request #6960 from penpot/xaviju-remove-image-shape-type-fix
🐛 Remove image type from inspect tab panels
2025-07-25 12:49:57 +02:00
andrés gonzález
5ae4dde222 📚 Add font size token doc (#6972) 2025-07-25 12:30:56 +02:00
Xaviju
2fbd4b07e0 🐛 Remove image type from inspect tab panels 2025-07-25 12:08:01 +02:00
Florian Schroedl
58a843ea23 Remove token when applying tyopgraphic asset style 2025-07-24 17:51:53 +02:00
Florian Schroedl
f6b97af148 🐛 Fix spacing menu not available in dimensions token 2025-07-24 15:20:10 +02:00
Alejandro Alonso
76b7287bf1 Merge pull request #6864 from penpot/niwinz-staging-snapshot-migrations
 Add migrations handling on file snapshots
2025-07-24 11:41:17 +02:00
Andrey Antukh
019bc2f183 Add migrations handling on file snapshots 2025-07-24 11:40:54 +02:00
Florian Schroedl
8c96a617be Add test for spacing token application rules 2025-07-24 11:01:49 +02:00
Florian Schroedl
1f15e9b81e Fix spacing token for frame children 2025-07-24 11:01:49 +02:00
Alejandro Alonso
f7627e515a Merge pull request #6876 from penpot/niwinz-develop-minor-changes-logical-deletion
 Change default status filtering for logical deletion
2025-07-24 10:58:49 +02:00
Andrey Antukh
d08c94d5a6 Change default status filtering for logical deletion 2025-07-24 10:43:45 +02:00
Xaviju
01896501c1 🐛 Remove image type from inspect tab panels (#6959) 2025-07-24 09:37:38 +02:00
Andrey Antukh
3f9a1525ca Merge pull request #6954 from penpot/alotor-fix-gradient-stroke
🐛 Fix opacity on stroke gradients
2025-07-24 08:59:02 +02:00
alonso.torres
52c1e227d5 🐛 Fix change from gradient to solid color 2025-07-24 08:58:48 +02:00
alonso.torres
955538b12a 🐛 Fix opacity on stroke gradients 2025-07-24 08:58:46 +02:00
Alonso Torres
8254af27cb 🐛 Fix problem when changing between flex/grid layout (#6949) 2025-07-24 08:54:07 +02:00
Elena Torró
f76391ecbb 🐛 Enable switch to system theme on options menu (#6946) 2025-07-24 08:43:03 +02:00
Andrés Moya
c49e9fbf18 🐛 Fix last migration of token sets (#6957) 2025-07-24 08:42:16 +02:00
Marina López
122701ee7b 🐛 Fix modal submit button for unpaid or canceled subscriptions (#6947) 2025-07-24 08:41:39 +02:00
Andrés Moya
351362bb50 🐛 Fix migration from tokens lib version 1.2 2025-07-23 15:28:53 +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
Eva Marco
b20b272eae 📚 Update changelog 2025-07-23 09:53:49 +02:00
Alejandro Alonso
d46b519524 🐛 Fix remove color button in the gradient editor (#6942) 2025-07-23 09:04:54 +02:00
78 changed files with 1145 additions and 577 deletions

View File

@@ -1,28 +1,14 @@
name: Build and Upload Penpot Bundles non-prod
name: Build and Upload Penpot Bundles
on:
# Create bundler for every tag
push:
tags:
- '**' # Pattern matched against refs/tags
# Create bundler every hour between 5:00 and 20:00 on working days
schedule:
- cron: '0 5-20 * * 1-5'
# Create bundler from manual action
# Create bundle from manual action
workflow_dispatch:
workflow_call:
inputs:
zip_mode:
# zip_mode defines how the build artifacts are packaged:
# - 'individual': creates one ZIP file per component (frontend, backend, exporter)
# - 'all': creates a single ZIP containing all components
# - null: for the rest of cases (non-manual events)
description: 'Bundle packaging mode'
required: false
default: 'individual'
type: choice
options:
- individual
- all
gh_ref:
description: 'Name of the branch'
type: string
required: true
jobs:
build-bundles:
@@ -38,15 +24,15 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract somer useful variables
- name: Extract some useful variables
id: vars
run: |
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "gh_branch=${{ github.base_ref || github.ref_name }}" >> $GITHUB_OUTPUT
# Set up Docker Buildx for multi-arch build
- name: Set up Docker Buildx
- name: Set up Docker Buildx for multi-arch build
uses: docker/setup-buildx-action@v3
- name: Run manage.sh build-bundle from host
@@ -57,73 +43,22 @@ jobs:
mkdir zips
mv bundles penpot
- name: Create zip bundles for zip_mode == 'all'
if: ${{ github.event.inputs.zip_mode == 'all' }}
- name: Create zip bundles
run: |
echo "📦 Packaging Penpot 'all' bundles..."
zip -r zips/penpot-all-bundles.zip penpot
echo "📦 Packaging Penpot bundles..."
zip -r zips/penpot.zip penpot
- name: Create zip bundles for zip_mode != 'all'
if: ${{ github.event.inputs.zip_mode != 'all' }}
- name: Upload Penpot bundle to S3
run: |
echo "📦 Packaging Penpot 'individual' bundles..."
zip -r zips/penpot-frontend.zip penpot/frontend
zip -r zips/penpot-backend.zip penpot/backend
zip -r zips/penpot-exporter.zip penpot/exporter
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_branch}}-latest.zip
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
- name: Upload unified 'all' bundle
if: ${{ github.event.inputs.zip_mode == 'all' }}
uses: actions/upload-artifact@v4
with:
name: penpot-all-bundles
path: zips/penpot-all-bundles.zip
- name: Upload individual bundles
if: ${{ github.event.inputs.zip_mode != 'all' }}
uses: actions/upload-artifact@v4
with:
name: penpot-individual-bundles
path: |
zips/penpot-frontend.zip
zips/penpot-backend.zip
zips/penpot-exporter.zip
- name: Upload unified 'all' bundle to S3
if: ${{ github.event.inputs.zip_mode == 'all' }}
run: |
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.gh_branch}}.zip
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.commit_hash }}.zip
- name: Upload 'individual' bundles to S3
if: ${{ github.event.inputs.zip_mode != 'all' }}
run: |
for name in penpot-frontend penpot-backend penpot-exporter; do
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.gh_branch }}-latest.zip
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.commit_hash }}.zip
done
- name: Notify Mattermost about automatic bundles
if: github.event_name == 'pull_request'
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
TEXT: |
📦 *Penpot bundle automatically generated*
📄 PR: ${{ github.event.pull_request.title }}
🔁 From: \`${{ github.head_ref }}\` to \`{{ github.base_ref }}\`
*[PENPOT] Error during the execution of the job*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_branch}}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Notify Mattermost about manual bundles
if: github.event_name == 'workflow_dispatch'
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
TEXT: |
📦 *Penpot bundle manually generated*
📄 Triggered from branch: `${{ github.ref_name}}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Print artifact summary URL
run: |
echo "📦 Artifacts available at:"
echo "🔗 https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"

12
.github/workflows/build-develop.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Build and Upload Penpot DEVELOP Bundles
on:
schedule:
- cron: '16 5-20 * * 1-5'
jobs:
build-develop-bundle:
uses: ./.github/workflows/build-bundles.yml
secrets: inherit
with:
gh_ref: "develop"

12
.github/workflows/build-staging.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Build and Upload Penpot STAGING Bundles
on:
schedule:
- cron: '0 5 * * 1-5'
jobs:
build-staging-bundle:
uses: ./.github/workflows/build-bundles.yml
secrets: inherit
with:
gh_ref: "staging"

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(Merge|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
pattern: '^(Merge|Revert|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

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

@@ -25,8 +25,11 @@
- 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)
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
### :bug: Bugs fixed
@@ -40,15 +43,32 @@
- 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 UI theme selection from main menu [Taiga #11567](https://tree.taiga.io/project/penpot/issue/11567)
- 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)
- Fix problem when changing between flex/grid layout [Taiga #11625](https://tree.taiga.io/project/penpot/issue/11625)
- Fix opacity on stroke gradients [Taiga #11646](https://tree.taiga.io/project/penpot/issue/11646)
- Fix change from gradient to solid color [Taiga #11648](https://tree.taiga.io/project/penpot/issue/11648)
- Fix the context menu always closes after any action [Taiga #11624](https://tree.taiga.io/project/penpot/issue/11624)
- Fix X & Y position do not sincronize with tokens [Taiga #11617](https://tree.taiga.io/project/penpot/issue/11617)
- Fix tooltip position after first time [Taiga #11688](https://tree.taiga.io/project/penpot/issue/11688)
- Fix inconsistent ordering of pinned projects on dashboard sidebar [Taiga #11674](https://tree.taiga.io/project/penpot/issue/11674)
- Fix export button width on inspect tab [Taiga #11394](https://tree.taiga.io/project/penpot/issue/11394)
- Fix stroke width token application [Taiga #11724](https://tree.taiga.io/project/penpot/issue/11724)
- Fix number token application on shape [Taiga #11331](https://tree.taiga.io/project/penpot/task/11331)
- Fix auto height is fixed in the HTML inspect tab for text elements [Taiga #11680](https://tree.taiga.io/project/penpot/task/11680)
## 2.8.1 (Unreleased)
## 2.8.1
### :bug: Bugs fixed
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
- Fix error on inspect tab when selecting multiple shapes [Taiga #11655](https://tree.taiga.io/project/penpot/issue/11655)
- Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
## 2.8.0

View File

@@ -10,18 +10,19 @@
[app.config :as cf]
[app.util.time :as dt]))
(def ^:private canceled-status
#{"canceled" "unpaid"})
(defn get-deletion-delay
"Calculate the next deleted-at for a resource (file, team, etc) in function
of team settings"
[team]
(if-let [subscription (get team :subscription)]
(if-let [{:keys [type status]} (get team :subscription)]
(cond
(and (= (:type subscription) "unlimited")
(= (:status subscription) "active"))
(and (= "unlimited" type) (not (contains? canceled-status status)))
(dt/duration {:days 30})
(and (= (:type subscription) "enterprise")
(= (:status subscription) "active"))
(and (= "enterprise" type) (not (contains? canceled-status status)))
(dt/duration {:days 90})
:else

View File

@@ -25,6 +25,7 @@
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.worker :as wrk]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
@@ -63,15 +64,16 @@
(assert (sm/check schema:server-params params)))
(defmethod ig/init-key ::server
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :as cfg}]
(l/info :hint "starting http server" :port port :host host)
(let [options {:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/dispatch :virtual
:xnio/dispatch executor
:ring/compat :ring2
:socket/backlog 4069}

View File

@@ -231,7 +231,8 @@
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::wrk/executor (ig/ref ::wrk/executor)}
::ldap/provider
{:host (cf/get :ldap-host)

View File

@@ -438,7 +438,10 @@
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
{:name "0139-mod-file-change-table.sql"
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}
{:name "0140-mod-file-change-table.sql"
:fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,2 @@
ALTER TABLE file_change
ADD COLUMN migrations text[];

View File

@@ -178,12 +178,12 @@
(measure metrics mlabels stats nil)
(log "enqueued" req-id stats limit-id limit-label limit-params nil))
(px/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure metrics mlabels stats elapsed)
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
(handler))))
(pbh/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure metrics mlabels stats elapsed)
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
(handler))))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]

View File

@@ -8,6 +8,7 @@
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
@@ -15,6 +16,7 @@
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.features.file-migrations :refer [reset-migrations!]]
[app.main :as-alias main]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
@@ -27,6 +29,13 @@
[app.util.time :as dt]
[cuerdas.core :as str]))
(defn decode-row
[{:keys [migrations] :as row}]
(when row
(cond-> row
(some? migrations)
(assoc :migrations (db/decode-pgarray migrations)))))
(def sql:get-file-snapshots
"WITH changes AS (
SELECT id, label, revn, created_at, created_by, profile_id
@@ -74,10 +83,7 @@
(assert (#{:system :user :admin} created-by)
"expected valid keyword for created-by")
(let [conn
(db/get-connection cfg)
created-by
(let [created-by
(name created-by)
deleted-at
@@ -101,12 +107,15 @@
(blob/encode (:data file))
features
(db/encode-pgarray (:features file) conn "text")]
(into-array (:features file))
(l/debug :hint "creating file snapshot"
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
migrations
(into-array (:migrations file))]
(l/dbg :hint "creating file snapshot"
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
(db/insert! cfg :file-change
{:id snapshot-id
@@ -114,6 +123,7 @@
:data data
:version (:version file)
:features features
:migrations migrations
:profile-id profile-id
:file-id (:id file)
:label label
@@ -159,7 +169,17 @@
{:file-id file-id
:id snapshot-id}
{::db/for-share true})
(feat.fdata/resolve-file-data cfg))]
(feat.fdata/resolve-file-data cfg)
(decode-row))
;; If snapshot has tracked applied migrations, we reuse them,
;; if not we take a safest set of migrations as starting
;; point. This is because, at the time of implementing
;; snapshots, migrations were not taken into account so we
;; need to make this backward compatible in some way.
file (assoc file :migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67)))]
(when-not snapshot
(ex/raise :type :not-found
@@ -180,12 +200,16 @@
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
;; If the file was already offloaded, on restring the snapshot
;; we are going to replace the file data, so we need to touch
;; the old referenced storage object and avoid possible leaks
;; If the file was already offloaded, on restoring the snapshot we
;; are going to replace the file data, so we need to touch the old
;; referenced storage object and avoid possible leaks
(when (feat.fdata/offloaded? file)
(sto/touch-object! storage (:data-ref-id file)))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(reset-migrations! conn file)
(db/update! conn :file
{:data (:data snapshot)
:revn (inc (:revn file))
@@ -253,7 +277,7 @@
:deleted-at nil}
{:id snapshot-id}
{::db/return-keys true})
(dissoc :data :features)))
(dissoc :data :features :migrations)))
(defn- get-snapshot
"Get a minimal snapshot from database and lock for update"

View File

@@ -185,7 +185,7 @@
[:map {:title "PartialFile"}
[:id ::sm/uuid]
[:revn {:min 0} ::sm/int]
[:page :any]])
[:page [:map-of :keyword ::sm/any]]])
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used

View File

@@ -186,7 +186,7 @@
"canceled"
"incomplete"
"incomplete_expired"
"pass_due"
"past_due"
"paused"
"trialing"
"unpaid"]]

View File

@@ -30,7 +30,7 @@
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"}
funcool/datoteka

View File

@@ -418,7 +418,14 @@
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
[:token-set [:maybe [:fn ctob/token-set?]]]]]
;; FIXME: we should not pass private types as part of changes
;; protocol, the changes protocol should reflect a
;; method/protocol for perform surgical operations on file data,
;; this has nothing todo with internal types of a file data
;; structure.
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
[:maybe [:fn ctob/token-set?]]]]]
[:set-token
[:map {:title "SetTokenChange"}

View File

@@ -81,7 +81,7 @@
(update :migrations set/union diff)
(vary-meta assoc ::migrated (not-empty diff)))))
(defn- generate-migrations-from-version
(defn generate-migrations-from-version
"A function that generates new format migration from the old,
version based migration system"
[version]
@@ -1618,4 +1618,4 @@
"0007-clear-invalid-strokes-and-fills-v2"
"0008-fix-library-colors-v4"
"0009-clean-library-colors"
"0009-add-partial-text-touched-flags"]))
#_"0009-add-partial-text-touched-flags"]))

View File

@@ -7,10 +7,12 @@
(ns app.common.logic.shapes
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.logic.variant-properties :as clvp]
[app.common.text :as ct]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.pages-list :as ctpl]
@@ -21,9 +23,12 @@
[app.common.uuid :as uuid]
[clojure.set :as set]))
(def text-typography-attrs (set ct/text-typography-attrs))
(defn- generate-unapply-tokens
"When updating attributes that have a token applied, we must unapply it, because the value
of the attribute now has been given directly, and does not come from the token."
of the attribute now has been given directly, and does not come from the token.
When applying a typography asset style we also unapply any typographic tokens."
[changes objects changed-sub-attr]
(let [new-objects (pcb/get-objects changes)
mod-obj-changes (->> (:redo-changes changes)
@@ -32,29 +37,38 @@
text-changed-attrs
(fn [shape]
(let [new-shape (get new-objects (:id shape))
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))]
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))
;; Unapply token when applying typography asset style
attrs (if (seq (set/intersection text-typography-attrs attrs))
(into attrs cto/typography-keys)
attrs)]
(apply set/union (map cto/shape-attr->token-attrs attrs))))
check-attr (fn [shape changes attr]
(let [tokens (get shape :applied-tokens {})
token-attrs (if (or (not= (:type shape) :text) (not= attr :content))
(cto/shape-attr->token-attrs attr changed-sub-attr)
(text-changed-attrs shape))]
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [(:id shape)] #(cto/unapply-token-id % token-attrs))
changes)))
check-attr
(fn [shape changes attr]
(let [shape-id (dm/get-prop shape :id)
tokens (get shape :applied-tokens {})
token-attrs (if (and (cfh/text-shape? shape) (= attr :content))
(text-changed-attrs shape)
(cto/shape-attr->token-attrs attr changed-sub-attr))]
check-shape (fn [changes mod-obj-change]
(let [shape (get objects (:id mod-obj-change))
xf (comp (filter #(= (:type %) :set))
(map :attr))
attrs (into [] xf (:operations mod-obj-change))]
(reduce (partial check-attr shape)
changes
attrs)))]
(reduce check-shape
changes
mod-obj-changes)))
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
changes)))
check-shape
(fn [changes mod-obj-change]
(let [shape (get objects (:id mod-obj-change))
attrs (into []
(comp (filter #(= (:type %) :set))
(map :attr))
(:operations mod-obj-change))]
(reduce (partial check-attr shape)
changes
attrs)))]
(reduce check-shape changes mod-obj-changes)))
(defn generate-update-shapes
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects?]}]

View File

@@ -292,7 +292,7 @@
(fix-gradients)
(assoc :text text))))
(split-texts [text styles]
(split-texts [text styles data]
(let [cpoints (text->code-points text)
children (->> (parse-draft-styles styles)
(build-style-index (count cpoints))
@@ -301,7 +301,7 @@
(mapv #(extract-text cpoints %)))]
(cond-> children
(empty? children)
(conj {:text ""}))))
(conj (assoc data :text "")))))
(build-paragraph [block]
(let [key (get block :key)
@@ -312,7 +312,7 @@
(-> data
(assoc :key key)
(assoc :type "paragraph")
(assoc :children (split-texts text styles)))))]
(assoc :children (split-texts text styles data)))))]
{:type "root"
:children

View File

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

@@ -0,0 +1,20 @@
;; 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.stroke
(:require
[app.common.colors :as clr]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def default-stroke
{:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1})

View File

@@ -29,20 +29,20 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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-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))
@@ -92,19 +92,32 @@
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing
(def ^:private schema:spacing-gap
[:map
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin]))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions
@@ -115,6 +128,15 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:axis
[:map
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map
[:rotation {:optional true} token-name-ref]])
@@ -152,6 +174,7 @@
opacity-keys
spacing-keys
dimensions-keys
axis-keys
rotation-keys
typography-keys
number-keys))
@@ -203,7 +226,8 @@
(opacity-keys shape-attr) #{shape-attr}
(spacing-keys shape-attr) #{shape-attr}
(rotation-keys shape-attr) #{shape-attr}
(number-keys shape-attr) #{shape-attr})))
(number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
@@ -263,6 +287,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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -167,6 +167,11 @@
(get-tokens [_] "return an ordered sequence of all tokens in the set")
(get-tokens-map [_] "return a map of tokens in the set, indexed by token-name"))
;; TODO: this structure is temporary. It's needed to be able to migrate TokensLib
;; from 1.2 to 1.3 when TokenSet datatype was changed to a deftype. This should
;; be removed after migrations are consolidated.
(defrecord TokenSetLegacy [id name description modified-at tokens])
(deftype TokenSet [id name description modified-at tokens]
#?@(:clj [clojure.lang.IDeref
(deref [_] {:id id
@@ -181,6 +186,10 @@
:modified-at modified-at
:tokens tokens})])
#?@(:clj
[json/JSONWriter
(-write [this writter options] (json/-write (deref this) writter options))])
#?@(:cljs [cljs.core/IEncodeJS
(-clj->js [_] (js-obj "id" (clj->js id)
"name" (clj->js name)
@@ -255,6 +264,10 @@
[o]
(instance? TokenSet o))
(defn token-set-legacy?
[o]
(instance? TokenSetLegacy o))
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
@@ -283,7 +296,9 @@
(declare make-token-set)
(def schema:token-set
(sm/required-keys schema:token-set-attrs))
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
(sm/required-keys schema:token-set-attrs)])
(sm/register! ::token-set schema:token-set) ;; need to register for the recursive schema of token-sets
@@ -1717,10 +1732,11 @@ Will return a value that matches this schema:
migrate-sets-node
(fn recurse [node]
(if (token-set? node)
(assoc node
:id (uuid/next)
:tokens (d/update-vals (:tokens node) migrate-token))
(if (token-set-legacy? node)
(make-token-set
(assoc node
:id (uuid/next)
:tokens (d/update-vals (:tokens node) migrate-token)))
(d/update-vals node recurse)))
sets
@@ -1748,6 +1764,26 @@ Will return a value that matches this schema:
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- read-tokens-lib-v1-3
"Reads the tokens lib data structure and removes the TokenSetLegacy data type,
needed for a temporary migration step."
[r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)
migrate-sets-node
(fn recurse [node]
(if (token-set-legacy? node)
(make-token-set node)
(d/update-vals node recurse)))
sets
(d/update-vals sets migrate-sets-node)]
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- write-tokens-lib
[n w ^TokensLib o]
@@ -1776,6 +1812,11 @@ Will return a value that matches this schema:
(make-token obj)))}
{:name "penpot/token-set/v1"
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenSetLegacy obj)))}
{:name "penpot/token-set/v2"
:class TokenSet
:wfn (fn [n w o]
(fres/write-tag! w n 1)
@@ -1803,8 +1844,11 @@ Will return a value that matches this schema:
{:name "penpot/tokens-lib/v1.2"
:rfn read-tokens-lib-v1-2}
;; CURRENT TOKENS LIB READER & WRITTER
{:name "penpot/tokens-lib/v1.3"
:rfn read-tokens-lib-v1-3}
;; CURRENT TOKENS LIB READER & WRITTER
{:name "penpot/tokens-lib/v1.4"
:class TokensLib
:wfn write-tokens-lib
:rfn read-tokens-lib}))

View File

@@ -125,7 +125,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=penpotapp/imagemagick:7.1.1-47 /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
USER penpot:penpot

View File

@@ -39,6 +39,7 @@ RUN set -ex; \
fonts-wqy-zenhei \
fonts-tlwg-loma-otf \
fonts-freefont-ttf \
poppler-utils \
\
libasound2t64 \
libatk-bridge2.0-0t64 \

View File

@@ -103,6 +103,11 @@ If you are deploying Penpot on OpenShift, we recommend following the specific gu
Make sure to review the section **OpenShift Requirements** for important security and compatibility considerations.
### Using Rancher?
If you are deploying Penpot on Rancher, we recommend following the specific guidelines provided in the official documentation:
<a href="https://docs.apps.rancher.io/reference-guides/penpot/" target="_blank">Reference guides / Penpot</a>.
## Upgrade Penpot
When a new version of Penpot's chart is released, or when you want to change the

View File

@@ -205,6 +205,10 @@ title: 10· Design Tokens
<h4>Y Position (dimension)</h4>
<p>The Y property specifies the position of the element on the Y axis of the canvas.</p>
<h3 id="design-tokens-font-size">Font Size</h3>
<p>Font size tokens allow you to define and standardize font-size values across your design system. These tokens can be applied to the <strong>font-size</strong> property in text layers, ensuring consistent typography throughout your designs.</p>
<p class="advice">Font size token values are always computed as <strong>px</strong> (pixels).</p>
<h3 id="design-tokens-opacity">Opacity</h3>
<p>Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.</p>
<p>Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with <strong>`%`</strong> sign at the end of the value. For example, you can use <strong>45%</strong> which would resolve to <strong>.45</strong>.</p>
@@ -378,7 +382,15 @@ title: 10· Design Tokens
</ol>
<h3 id="design-tokens-import-options">Import Options</h3>
<h4>Single file</h4>
<h4>ZIP file</h4>
<p>You can import tokens from a <strong>.zip</strong> file. This file can either contain a single JSON file or a folder structure with multiple files. The ZIP import option provides flexibility for organizing your tokens before importing them into Penpot.</p>
<ul>
<li>If the ZIP contains a single JSON file, it will be imported as a single set of tokens.</li>
<li>If the ZIP contains a folder structure, each file and folder will be interpreted as separate token sets, following the same rules as the multifile import.</li>
</ul>
<h4>Single JSON file</h4>
<p>You can import a JSON file comprising all tokens, token sets and token themes.</p>
<p>When importing a single file, the first-level keys of the json file will be interpreted as the set name.</p>

View File

@@ -34,7 +34,13 @@ desc: Master layer basics with Penpot's user guide! Learn to create, manipulate,
<p>Layers are displayed from the bottom to the top of the layer stack, with layers above on the stack being shown on top in the image.</p>
<h2 id="hide-lock">Hide and lock layers</h2>
<p>Click on the eye icon to change the visibility of a layer. Click on the lock icon to lock or unlock a layer. A locked layer can not be modified.</p>
<h3>Hide and show layers</h3>
<p>You can control the visibility of any layer by clicking the eye icon next to it in the Layers panel. When a layer is hidden, it will not appear on the canvas, but you can still select it in the Layers panel, move its order, or modify its properties. The eye icon always indicates whether a layer is visible or hidden, making it easy to manage complex designs.</p>
<h3>Lock and unlock layers</h3>
<p>Locking a layer helps prevent accidental changes or movement on the canvas. When a layer is locked, it cannot be moved or edited directly in the canvas area. However, you can still select a locked layer in the Layers panel and adjust its properties, such as color, effects, or name. The lock icon next to the layers name shows its locked status, helping you keep your design organized and protected.</p>
<figure>
<video title="Layers hide and lock" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-hide-lock.webp" height="auto">
<source src="/img/layers/layers-hide-lock.mp4" type="video/mp4">

View File

@@ -142,6 +142,11 @@ a design.</p>
<source src="/img/objects/text-create.mp4" type="video/mp4">
</video>
</figure>
<p><strong>Tips for resizing</strong></p>
<ul>
<li>Double-click on the right side of the bounding box to set the resize setting to auto-width.</li>
<li>Double-click on the bottom side of the bounding box to set the resize setting to auto-height.</li>
</ul>
<h3>Edit and style text content</h3>
<p>Press <kbd>Enter</kbd> with a text layer selected to start editing the text content. You can style parts of the text content as rich text.</p>
<figure>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -79,14 +79,29 @@
(defprotocol Event
(-data [_] "Get event data"))
(defn- coerce-to-string
[v]
(cond
(keyword? v)
(name v)
(string? v)
v
(nil? v)
nil
:else
(str v)))
(def ^:private xf:coerce-to-string
(keep coerce-to-string))
(defn- simplify-props
"Removes complex data types from props."
[data]
(reduce-kv (fn [data k v]
(cond
(map? v) (assoc data k :placeholder/map)
(vector? v) (assoc data k :placeholder/vec)
(set? v) (assoc data k :placeholder/set)
(vector? v) (assoc data k (into [] xf:coerce-to-string v))
(set? v) (assoc data k (into [] xf:coerce-to-string v))
(coll? v) (assoc data k :placeholder/coll)
(fn? v) (assoc data k :placeholder/fn)
(nil? v) (dissoc data k)

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

@@ -20,7 +20,6 @@
[app.plugins.register :as plugins.register]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -159,9 +158,6 @@
(update-in state [:profile :theme]
(fn [current]
(let [current (cond
(= current "system")
(theme/get-system-theme)
;; NOTE: this is a workaround for
;; the old data on the database
;; where whe have `default` value
@@ -172,7 +168,8 @@
current)]
(case current
"dark" "light"
"light" "dark"
"light" "system"
"system" "dark"
; Failsafe for missing data
"dark")))))

View File

@@ -148,17 +148,17 @@
(defn- bind!
[shortcuts]
(let [msbind (fn [command callback type]
(->> shortcuts
(remove #(:disabled (second %)))
(run! (fn [[key {:keys [command fn type overwrite]}]]
(let [callback (wrap-cb key fn)
undefined (js* "(void 0)")
commands (if (vector? command)
(into-array command)
#js [command])]
(if type
(mousetrap/bind command callback type)
(mousetrap/bind command callback)))]
(->> shortcuts
(remove #(:disabled (second %)))
(run! (fn [[key {:keys [command fn type]}]]
(let [callback (wrap-cb key fn)]
(if (vector? command)
(run! #(msbind % callback type) command)
(msbind command callback type))))))))
(mousetrap/bind commands callback type overwrite)
(mousetrap/bind commands callback undefined overwrite)))))))
(defn- reset!
([]

View File

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

View File

@@ -1080,7 +1080,11 @@
(fn [state]
(-> state
(assoc :type :color)
(dissoc :editing-stop :stops :gradient)))))))
(dissoc :editing-stop :stops :gradient)))))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (update-colorpicker-color {} false)))))
(defn activate-colorpicker-gradient
[type]

View File

@@ -1095,15 +1095,24 @@
(when (seq (:redo-changes changes))
(rx/of (dch/commit-changes changes)))
(when-not (empty? updated-frames)
(rx/merge
(rx/of (ptk/data-event :layout/update {:ids (map :id updated-frames) :undo-group undo-group}))
(->> (rx/from updated-frames)
(rx/mapcat
(fn [shape]
(rx/of
(dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
(when-not (= (:frame-id shape) uuid/zero)
(dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame"))))))))
(let [frames-by-page (->> updated-frames
(group-by :page-id))]
(rx/merge
;; Emit one layout/update event for each page
(rx/from
(map (fn [[page-id frames]]
(ptk/data-event :layout/update
{:page-id page-id
:ids (map :id frames)
:undo-group undo-group}))
frames-by-page))
(->> (rx/from updated-frames)
(rx/mapcat
(fn [shape]
(rx/of
(dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
(when-not (= (:frame-id shape) uuid/zero)
(dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame")))))))))
(when (not= file-id library-id)
;; When we have just updated the library file, give some time for the

View File

@@ -131,11 +131,12 @@
;; they are process together. It will get a better performance.
(rx/buffer-time 100)
(rx/filter #(d/not-empty? %))
(rx/map
(rx/mapcat
(fn [data]
(let [page-id (->> data (keep :page-id) first)
ids (reduce #(into %1 (:ids %2)) #{} data)]
(update-layout-positions {:page-id page-id :ids ids}))))
(->> (group-by :page-id data)
(map (fn [[page-id items]]
(let [ids (reduce #(into %1 (:ids %2)) #{} items)]
(update-layout-positions {:page-id page-id :ids ids})))))))
(rx/take-until stopper))))))
(defn finalize-shape-layout

View File

@@ -11,6 +11,7 @@
[app.common.text :as txt]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.stroke :as cts]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typography :as cty]
@@ -31,88 +32,6 @@
(declare token-properties)
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
shape-ids (or (->> (select-keys objects shape-ids)
(filter (fn [[_ shape]]
(ctt/any-appliable-attr? attributes (:type shape))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)]
(rx/of
(st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape})))))))
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
;; (note that dwsh/update-shapes function returns an event)
@@ -173,8 +92,10 @@
(when (number? value)
(dwsh/update-shapes shape-ids
(fn [shape]
(when (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)))
(if (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)
(let [stroke (assoc cts/default-stroke :stroke-width value)]
(assoc shape :strokes [stroke]))))
{:reg-objects? true
:ignore-touched true
:page-id page-id
@@ -332,53 +253,157 @@
(dwsl/update-layout-child shape-ids props {:ignore-touched true
:page-id page-id}))))))))
(defn generate-text-shape-update
[txt-attrs shape-ids page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge txt-attrs)
(cty/remove-typography-from-node)))]
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))
(defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:line-height value})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:line-height value} shape-ids page-id))))
(defn update-letter-spacing
([value shape-ids attributes] (update-letter-spacing value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:letter-spacing (str value)})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:font-size (str value)})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:font-size (str value)} shape-ids page-id))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
selected-shapes (select-keys objects shape-ids)
shape-ids (or (->> selected-shapes
(filter (fn [[_ shape]]
(or
(and (ctsl/any-layout-immediate-child? objects shape)
(some ctt/spacing-margin-keys attributes))
(ctt/any-appliable-attr? attributes (:type shape)))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/of
(st/emit! (ev/event {::ev/name "apply-tokens"
:type type
:applyed-to attributes}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn apply-spacing-token
"Handles edge-case for spacing token when applying token via toggle button.
Splits out `shape-ids` into seperate default actions:
- Layouts take the `default` update function
- Shapes inside layout will only take margin"
[{:keys [token shapes]}]
(ptk/reify ::apply-spacing-token
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
{:keys [attributes on-update-shape]}
(get token-properties (:type token))
{:keys [other frame-children]}
(group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)]
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids (map :id other)
:on-update-shape on-update-shape})
(apply-token {:attributes ctt/spacing-margin-keys
:token token
:shape-ids (map :id frame-children)
:on-update-shape update-layout-item-margin}))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(case (:type token)
:spacing
(apply-spacing-token {:token token
:shapes shapes})
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
;; Map token types to different properties used along the cokde ---------------------------------------------
@@ -444,6 +469,7 @@
ctt/spacing-keys
ctt/sizing-keys
ctt/border-radius-keys
ctt/axis-keys
ctt/stroke-width-keys)
:on-update-shape update-shape-dimensions
:modal {:key :tokens/dimensions

View File

@@ -34,7 +34,6 @@
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
@@ -196,7 +195,7 @@
:on-edit on-edit-open
:on-menu-close on-menu-close}]]))
(mf/defc sidebar-search
(mf/defc sidebar-search*
[{:keys [search-term team-id] :as props}]
(let [search-term (or search-term "")
focused? (mf/use-state false)
@@ -586,8 +585,8 @@
:data-testid "delete-team"}
(tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch
[{:keys [team profile] :as props}]
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [teams (mf/deref refs/teams)
teams-without-default (into {} (filter (fn [[_ v]] (= false (:is-default v))) teams))
team-ids (map #(str "teams-selector-" %) (keys teams-without-default))
@@ -606,10 +605,6 @@
(when (get-in team [:permissions :is-owner])
"teams-options-delete-team")]
;; _ (prn "--------------- sidebar-team-switch")
;; _ (app.common.pprint/pprint teams)
handle-show-team-click
(fn [event]
(dom/stop-propagation event)
@@ -716,8 +711,7 @@
:profile profile}]]]))
(mf/defc sidebar-content*
{::mf/private true
::mf/props :obj}
{::mf/private true}
[{:keys [projects profile section team project search-term default-project] :as props}]
(let [default-project-id
(get default-project :id)
@@ -730,6 +724,7 @@
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
container (mf/use-ref nil)
overflow* (mf/use-state false)
overflow? (deref overflow*)
@@ -740,13 +735,14 @@
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)
(ts/schedule-on-idle
(fn []
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
(st/emit!
(dcm/go-to-dashboard-recent :team-id team-id)
(ts/schedule-on-idle
(fn []
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
go-fonts
(mf/use-fn
@@ -756,14 +752,17 @@
go-fonts-with-key
(mf/use-fn
(mf/deps team)
#(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
(when font-title
(dom/set-attribute! font-title "tabindex" "0")
(dom/focus! font-title)
(dom/set-attribute! font-title "tabindex" "-1")))))))
(fn []
(st/emit!
(dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
(when font-title
(dom/set-attribute! font-title "tabindex" "0")
(dom/focus! font-title)
(dom/set-attribute! font-title "tabindex" "-1"))))))))
go-drafts
(mf/use-fn
(mf/deps team-id default-project-id)
@@ -785,39 +784,43 @@
go-libs
(mf/use-fn
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)))
(fn [] (st/emit! (dcm/go-to-dashboard-libraries :team-id team-id))))
go-libs-with-key
(mf/use-fn
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
(when libs-title
(dom/set-attribute! libs-title "tabindex" "0")
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1")))))))
pinned-projects
(->> projects
(remove :is-default)
(filter :is-pinned))]
(fn []
(st/emit!
(dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
(when libs-title
(dom/set-attribute! libs-title "tabindex" "0")
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1"))))))))
(mf/use-layout-effect
(mf/deps pinned-projects)
(fn []
(let [dom (mf/ref-val container)
client-height (obj/get dom "clientHeight")
scroll-height (obj/get dom "scrollHeight")]
(reset! overflow* (> scroll-height client-height)))))
pinned-projects
(mf/with-memo [projects]
(->> projects
(remove :is-default)
(filter :is-pinned)
(sort-by :name)
(not-empty)))]
(mf/with-layout-effect [pinned-projects]
(let [node (mf/ref-val container)
client-height (.-clientHeight ^js node)
scroll-height (.-scrollHeight ^js node)]
(reset! overflow* (> scroll-height client-height))))
[:*
[:div {:class (stl/css-case :sidebar-content true)
:ref container}
[:& sidebar-team-switch {:team team :profile profile}]
[:> sidebar-team-switch* {:team team :profile profile}]
[:& sidebar-search {:search-term search-term
:team-id (:id team)}]
[:> sidebar-search* {:search-term search-term
:team-id (:id team)}]
[:div {:class (stl/css :sidebar-content-section)}
[:ul {:class (stl/css :sidebar-nav)}
@@ -861,7 +864,7 @@
:data-testid "pinned-projects"}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.pinned-projects")]
(if (seq pinned-projects)
(if (some? pinned-projects)
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
(for [item pinned-projects]
[:& sidebar-project
@@ -876,7 +879,6 @@
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]))
(mf/defc profile-section*
{::mf/props :obj}
[{:keys [profile team]}]
(let [show* (mf/use-state false)
show (deref show*)

View File

@@ -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", subscription-href)
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-text", subscription-href)
:has-dropdown true}])
"enterprise"
@@ -125,7 +125,11 @@
(mf/defc menu-team-icon*
[{:keys [subscription-type]}]
[:span {:class (stl/css :subscription-icon) :data-testid "subscription-icon"}
[:span {:class (stl/css :subscription-icon)
:title (if (= subscription-type "unlimited")
(tr "subscription.dashboard.power-up.unlimited-plan")
(tr "subscription.dashboard.power-up.enterprise-plan"))
:data-testid "subscription-icon"}
(case subscription-type
"unlimited" i/character-u
"enterprise" i/character-e)])

View File

@@ -154,12 +154,17 @@
the dom with the result."
[tooltip placement origin-brect offset]
(show-popover tooltip)
(let [tooltip-brect (dom/get-bounding-rect tooltip)
(let [saved-height (dom/get-data tooltip "height")
saved-width (dom/get-data tooltip "width")
tooltip-brect (dom/get-bounding-rect tooltip)
tooltip-brect (assoc tooltip-brect :height (or saved-height (:height tooltip-brect)) :width (or saved-width (:width tooltip-brect)))
window-size (dom/get-window-size)]
(when-let [[placement placement-rect] (find-matching-placement placement tooltip-brect origin-brect window-size offset)]
(let [height (if (or (= placement "right") (= placement "left"))
(- (:height placement-rect) arrow-height)
(:height placement-rect))]
(dom/set-data! tooltip "height" (:height tooltip-brect))
(dom/set-data! tooltip "width" (:width tooltip-brect))
(dom/set-css-property! tooltip "block-size" (dm/str height "px"))
(dom/set-css-property! tooltip "inset-block-start" (dm/str (:top placement-rect) "px"))
(dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left placement-rect) "px")))

View File

@@ -27,13 +27,12 @@
[rumext.v2 :as mf]))
(def type->options
{:multiple [:fill :stroke :image :text :shadow :blur :layout-element]
{:multiple [:fill :stroke :text :shadow :blur :layout-element]
:frame [:visibility :geometry :fill :stroke :shadow :blur :layout :layout-element]
:group [:visibility :geometry :svg :layout-element]
:rect [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:circle [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:path [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:image [:visibility :image :geometry :fill :stroke :shadow :blur :svg :layout-element]
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})

View File

@@ -149,8 +149,7 @@
(mf/use-memo
(mf/deps markup-type shapes images-data)
(fn []
(-> (cg/generate-markup-code objects markup-type shapes)
(cb/format-code markup-type))))
(cg/generate-formatted-markup-code objects markup-type shapes)))
on-markup-copied
(mf/use-fn

View File

@@ -101,5 +101,5 @@
@extend .button-secondary;
@include uppercaseTitleTipography;
height: $s-32;
width: $s-252;
width: 100%;
}

View File

@@ -35,6 +35,7 @@
[app.main.ui.releases.v2-6]
[app.main.ui.releases.v2-7]
[app.main.ui.releases.v2-8]
[app.main.ui.releases.v2-9]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.v2 :as mf]))
@@ -99,4 +100,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.8")))
(rc/render-release-notes (assoc params :version "2.9")))

View File

@@ -0,0 +1,112 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.releases.v2-9
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
(defmethod c/render-release-notes "2.9"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-slide-0.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Penpot 2.9 is here!"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats new in Penpot?"]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:span {:class (stl/css :feature-title)}
"Penpot 2.9 is out!"]
[:p {:class (stl/css :feature-content)}
"We're keeping the momentum going with another exciting round of improvements and features!"]
[:p {:class (stl/css :feature-content)}
"This release brings major progress in Design Token management (including our very first typography token!), smarter text overrides for components, and a rich collection of quality-of-life enhancements."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-font-size.gif"
:class (stl/css :start-image)
:border "0"
:alt "New typography token type"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New typography token type"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"You can now define and manage font size tokens right from the Design Tokens panel. This is just the first of many typography token types to come. Font family token is next!"]
[:p {:class (stl/css :feature-content)}
"And theres more progress on Tokens, including support for importing multiple token files via .zip, and smarter token visibility, only showing the relevant tokens for each layer type."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 2}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-qol.gif"
:class (stl/css :start-image)
:border "0"
:alt "Quality-of-life galore"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Quality-of-life galore"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"This release packs dozens of small yet impactful usability improvements, including enhanced UX writing (thanks to community contributions!), a new visual indicator for comments directly in the design space, a reorganized dashboard sidebar, improved text resizing behavior, and much more."]
[:p {:class (stl/css :feature-content)}
"As always, we've squashed plenty of bugs and made underlying performance improvements to keep everything running smoothly."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 2}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))

View File

@@ -0,0 +1,102 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: $s-324 1fr;
height: $s-500;
width: $s-888;
border-radius: $br-8;
background-color: var(--modal-background-color);
border: $s-2 solid var(--modal-border-color);
}
.start-image {
width: $s-324;
border-radius: $br-8 0 0 $br-8;
}
.modal-content {
padding: $s-40;
display: grid;
grid-template-rows: auto 1fr $s-32;
gap: $s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: $s-8;
}
.version-tag {
@include flexCenter;
@include headlineSmallTypography;
height: $s-32;
width: $s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: $br-8;
}
.modal-title {
@include headlineLargeTypography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: $s-16;
width: $s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: $s-8;
}
.feature-title {
@include bodyLargeTypography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include bodyMediumTypography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include bodyMediumTypography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: $s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend .button-primary;
width: $s-100;
justify-self: flex-end;
grid-area: button;
}

View File

@@ -150,7 +150,16 @@
:class (stl/css :input-field)}]]
[:div {:class (stl/css :editors-cost)}
[:span {:class (stl/css :modal-text-small)}
(tr "subscription.settings.management.dialog.price-month" (or (get-in @form [:clean-data :min-members]) 0))]
(when (> (get-in @form [:clean-data :min-members]) 25)
[:> i18n/tr-html*
{:class (stl/css :modal-text-cap)
:tag-name "span"
:content (tr "subscription.settings.management.dialog.price-month" "175")}])
[:> i18n/tr-html*
{:class (stl/css-case :text-strikethrough (> (get-in @form [:clean-data :min-members]) 25))
:tag-name "span"
:content (tr "subscription.settings.management.dialog.price-month"
(* 7 (or (get-in @form [:clean-data :min-members]) 0)))}]]
[:span {:class (stl/css :modal-text-small)}
(tr "subscription.settings.management.dialog.payment-explanation")]]]
@@ -179,7 +188,9 @@
:type "button"
:value (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue"))
:on-click (if subscribe-to-trial subscribe-to-enterprise handle-accept-dialog)}]]])]]]))
:on-click (if (or subscribe-to-trial
(contains? #{"unpaid" "canceled"} (:status current-subscription)))
subscribe-to-enterprise handle-accept-dialog)}]]])]]]))
(mf/defc subscription-success-dialog
{::mf/register modal/components
@@ -363,17 +374,17 @@
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.security"),
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.unlimited-storage")]
(tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.security"),
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.unlimited-storage")]
(tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]))
@@ -413,7 +424,7 @@
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage-autosave")]
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "unlimited" subscription-type)
:cta-link #(open-subscription-modal "unlimited" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])
@@ -423,10 +434,10 @@
:price-value "$950"
:price-period (tr "subscription.settings.price-organization-month")
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
:benefits [(tr "subscription.settings.enterprise.security"),
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.unlimited-storage")]
(tr "subscription.settings.enterprise.autosave")]
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "enterprise")
:cta-link #(open-subscription-modal "enterprise" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])]]]))

View File

@@ -199,6 +199,20 @@
@include t.use-typography("body-small");
}
.modal-text-cap {
margin-inline-end: var(--sp-s);
}
.text-strikethrough {
text-decoration: line-through;
}
.modal-text-small strong,
.text-strikethrough strong,
.modal-text-cap strong {
font-weight: $fw700;
}
.modal-content,
.modal-end {
color: var(--color-foreground-secondary);
@@ -251,6 +265,8 @@
list-style-type: disc;
margin-inline-start: var(--sp-xl);
margin-block: var(--sp-xxl);
max-height: $s-216;
overflow-y: auto;
}
.input-field {

View File

@@ -103,15 +103,15 @@
(obj/set! attrs "strokeWidth" width)
(when (some? gradient)
(if (some? gradient)
(let [gradient-id (dm/str "stroke-color-gradient-" render-id "-" index)]
(obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id))))
(obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id)))
(when-not (some? gradient)
(when (some? color)
(obj/set! attrs "stroke" color))
(when (some? opacity)
(obj/set! attrs "strokeOpacity" opacity)))
(obj/set! attrs "stroke" color)))
(when (some? opacity)
(obj/set! attrs "strokeOpacity" opacity))
(when (not= style :svg)
(obj/set! attrs "strokeDasharray" (calculate-dasharray style width)))

View File

@@ -11,6 +11,7 @@
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.main.data.auth :refer [is-authenticated?]]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.refs :as refs]
@@ -180,7 +181,7 @@
[:div {:class (stl/css :form-container)}
[:& recovery-sent-page {:email @user-email}]])]]]))
(mf/defc request-dialog
(mf/defc request-dialog*
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text on-close]}]
(let [on-click (or on-button-click on-close)]
@@ -234,45 +235,45 @@
(cond
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}])))
[:> request-dialog* {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}])))
(mf/defc not-found*
[]
@@ -485,31 +486,31 @@
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [type (:type data)
path (:path route)
(let [type (:type data)
path (:path route)
params (:query-params route)
params (:query-params route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
;; We store the request access info int this state
info* (mf/use-state nil)
info (deref info*)
info* (mf/use-state nil)
info (deref info*)
loaded? (get info :loaded false)
profile (mf/deref refs/profile)
profile (mf/deref refs/profile)
auth-error?
(= type :authentication)
auth-error? (= type :authentication)
not-found? (= type :not-found)
authenticated?
(is-authenticated? profile)
request-access?
(and
(or (= type :not-found) auth-error?)
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
(or (some? (:file-id info))
(some? (:team-id info))))]
(mf/with-effect [params info]
(when-not (:loaded info)
@@ -518,24 +519,25 @@
(partial reset! info* {:loaded true})))))
(if auth-error?
[:> context-wrapper*
{:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> login-dialog* {}]]
(when loaded?
(if request-access?
[:> context-wrapper* {:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:is-workspace workspace?}]]
[:> exception-section* props])))))
(if (or auth-error? not-found?)
(if (not authenticated?)
[:> context-wrapper*
{:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> login-dialog* {}]]
(when (get info :loaded false)
(if request-access?
[:> context-wrapper* {:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:profile profile
:is-workspace workspace?}]]
[:> exception-section* props])))
[:> exception-section* props])))

View File

@@ -275,11 +275,8 @@
handle-gradient-remove-stop
(mf/use-fn
(mf/deps state)
(fn [stop]
(when (> (count (:stops state)) 2)
(when-let [index (d/index-of-pred (:stops state) #(= % stop))]
(st/emit! (dc/remove-gradient-stop index))))))
(fn [index]
(st/emit! (dc/remove-gradient-stop index))))
handle-stop-edit-start
(mf/use-fn

View File

@@ -80,10 +80,10 @@
handle-remove-stop
(mf/use-callback
(mf/deps on-remove-stop stop)
(mf/deps on-remove-stop index)
(fn []
(when on-remove-stop
(on-remove-stop stop))))
(on-remove-stop index))))
handle-focus-stop-offset
(mf/use-fn

View File

@@ -24,5 +24,6 @@
{:delete-stop {:tooltip (ds/supr)
:command ["del" "backspace"]
:subsections [:edit]
:overwrite true
:fn #(st/emit! (dwc/remove-gradient-stop))}}))

View File

@@ -59,6 +59,14 @@
on-unmount children is-selected icon disabled value]}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
on-click'
(mf/use-fn
(mf/deps on-click)
(fn [event]
(st/emit! dw/hide-context-menu)
(when on-click (on-click event))))
on-pointer-enter
(mf/use-fn
(fn []
@@ -96,7 +104,7 @@
:disabled disabled
:data-value value
:ref set-dom-node
:on-click on-click
:on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span
@@ -110,7 +118,7 @@
:disabled disabled
:ref set-dom-node
:data-value value
:on-click on-click
:on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span {:class (stl/css :title)} title]

View File

@@ -280,8 +280,8 @@
:data-testid "toggle-theme"
:id "file-menu-toggle-theme"}
[:span {:class (stl/css :item-name)}
(case (:theme profile) ;; default = dark -> light -> system -> dark and so on
"default" (tr "workspace.header.menu.toggle-light-theme")
(case (:theme profile) ;; dark -> light -> system -> dark and so on
"dark" (tr "workspace.header.menu.toggle-light-theme")
"light" (tr "workspace.header.menu.toggle-system-theme")
"system" (tr "workspace.header.menu.toggle-dark-theme")
(tr "workspace.header.menu.toggle-light-theme"))]

View File

@@ -310,7 +310,7 @@
:id "align-self-end"}]])
(mf/defc layout-item-menu
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent?}
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout?}
::mf/props :obj}
[{:keys [ids values
^boolean is-layout-child?

View File

@@ -7,9 +7,9 @@
(ns app.main.ui.workspace.sidebar.options.menus.stroke
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.stroke :as cts]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
@@ -157,11 +157,7 @@
on-add-stroke
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/add-stroke ids {:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1}))
(st/emit! (dc/add-stroke ids cts/default-stroke))
(when (not (some? (seq strokes))) (open-content)))
disable-drag (mf/use-state false)

View File

@@ -112,6 +112,7 @@
recent-fonts (mf/with-memo [state recent-fonts]
(filter-fonts state recent-fonts))
full-size? (boolean (and full-size show-recent))
select-next
@@ -130,13 +131,6 @@
(dom/prevent-default event)
(swap! selected get-prev-font fonts)))
on-select-and-close
(mf/use-fn
(mf/deps on-select on-close)
(fn [font]
(on-select font)
(on-close)))
on-key-down
(mf/use-fn
(mf/deps fonts)
@@ -145,7 +139,7 @@
(kbd/up-arrow? event) (select-prev event)
(kbd/down-arrow? event) (select-next event)
(kbd/esc? event) (on-close)
(kbd/enter? event) (do (on-select-and-close @selected))
(kbd/enter? event) (on-close)
:else (dom/focus! (mf/ref-val input)))))
on-filter-change
@@ -169,6 +163,9 @@
(when-let [index (:index @selected)]
(.scrollToRow ^js inst index))))
(mf/with-effect [@selected]
(on-select @selected))
(mf/with-effect []
(st/emit! (dsc/push-shortcuts :typography {}))
(fn []
@@ -181,10 +178,6 @@
#(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
(.scrollToPosition ^js inst offset)))))
(mf/with-effect [(:term state) fonts]
(when (and (seq fonts) (not= (:id @selected) (:id (first fonts))))
(reset! selected (first fonts))))
[:div {:class (stl/css :font-selector)}
[:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)}
[:div {:class (stl/css :header)}

View File

@@ -2,6 +2,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
@@ -61,6 +62,10 @@
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [selected-shapes objects]
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
active-theme-tokens
(mf/with-memo [tokens-lib]
(if tokens-lib
@@ -148,6 +153,7 @@
:is-open (get open-status type false)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens tokens}]))
@@ -155,5 +161,6 @@
[:> token-group* {:key (name type)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout :is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens []}])]))

View File

@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
@@ -34,11 +35,13 @@
(some #(contains? m %) ks))
(defn clean-separators
"Cleans up `:separator` inside of `items`
Will clean consecutive items like `[:separator :separator {}]`
And will return nil for lists consisting only of `:separator` items."
"Cleans up `:separator` inside of `items` with these rules:
- Clean consecutive items like `[:separator :separator {}]`
- Returns nil for lists consisting only of `:separator` items.
- Removes `:separator` at the beginning of the `items`"
[items]
(let [items' (dedupe items)]
(let [items' (->> (dedupe items)
(drop-while #(= % :separator)))]
(when-not (every? #(= % :separator) items')
items')))
@@ -190,7 +193,7 @@
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}]
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes is-selected-inside-layout] :as context-data}]
(let [padding-attr-labels {:p1 "Padding top"
:p2 "Padding right"
:p3 "Padding bottom"
@@ -209,7 +212,9 @@
:m2 "Margin right"
:m3 "Margin bottom"
:m4 "Margin left"}
margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels)
margin-items (when (or
is-selected-inside-layout
(key-in-map? allowed-shape-attributes margin-attr-labels))
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels margin-attr-labels
@@ -224,11 +229,13 @@
:hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-spacing}
context-data)]
(concat gap-items
(when padding-items [:separator])
padding-items
(when margin-items [:separator])
margin-items)))
(->> (concat
gap-items
[:separator]
padding-items
[:separator]
margin-items)
(clean-separators))))
(defn sizing-attribute-actions [context-data]
(->>
@@ -446,18 +453,30 @@
(mf/defc token-context-menu-tree
[{:keys [width errors] :as mdata}]
(let [objects (mf/deref refs/workspace-page-objects)
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
token-name (:token-name mdata)
token (mf/deref (refs/workspace-token-in-selected-set token-name))
selected-token-set-name (mf/deref refs/selected-token-set-name)]
token-type (:type token)
selected-token-set-name (mf/deref refs/selected-token-set-name)
selected-shapes
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [token-type selected-shapes objects]
(when (contains? #{:spacing :dimensions} token-type)
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))]
[:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width
:token token
:errors errors
:selected-token-set-name selected-token-set-name
:selected-shapes selected-shapes}]]))
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout}]]))
(mf/defc token-context-menu
[]

View File

@@ -42,7 +42,7 @@
(mf/defc token-group*
{::mf/private true}
[{:keys [type tokens selected-shapes active-theme-tokens is-open]}]
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open]}]
(let [{:keys [modal title]}
(get dwta/token-properties type)
editing-ref (mf/deref refs/workspace-editor-state)
@@ -91,7 +91,7 @@
(mf/deps selected-shapes not-editing?)
(fn [event token]
(dom/stop-propagation event)
(when (and not-editing? (seq selected-shapes))
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
(st/emit! (dwta/toggle-token {:token token
:shapes selected-shapes})))))]
@@ -115,6 +115,7 @@
{:key (:name token)
:token token
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])]])]]))

View File

@@ -21,6 +21,7 @@
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[clojure.set :as set]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -123,7 +124,9 @@
base-title (dm/str "Token: " name "\n"
(tr "workspace.tokens.original-value" value) "\n"
(tr "workspace.tokens.resolved-value" resolved-value))]
(tr "workspace.tokens.resolved-value" resolved-value)
(when (= (:type token) :number)
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
(cond
;; If there are errors, show the appropriate message
@@ -164,17 +167,20 @@
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
[selected-shapes attrs]
(some (fn [shape]
(ctt/any-appliable-attr? attrs (:type shape)))
selected-shapes))
[selected-shapes attrs & {:keys [selected-inside-layout?]}]
(or
;; Edge-case for allowing margin attribute on shapes inside layout parent
(and selected-inside-layout? (set/subset? ctt/spacing-margin-keys attrs))
(some (fn [shape]
(ctt/any-appliable-attr? attrs (:type shape)))
selected-shapes)))
(def token-types-with-status-icon
#{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width})
(mf/defc token-pill*
{::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}]
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes))
@@ -201,7 +207,7 @@
has-selected?
(not applied?)
(not half-applied?)
(not (attributes-match-selection? selected-shapes attributes)))
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
;; FIXME: move to context or props
can-edit? (:can-edit (deref refs/permissions))

View File

@@ -59,7 +59,7 @@
:type "text"
:on-blur on-submit
:on-key-down on-key-down
:maxlength "256"
:max-length "256"
:auto-focus true
:placeholder (tr "workspace.tokens.set-edit-placeholder")
:default-value default-value}]))
@@ -210,7 +210,7 @@
(mf/defc sets-tree-set*
[{:keys [id set label tree-depth tree-path tree-index is-selected is-active is-draggable is-editing
on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit]}]
on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit is-new]}]
(let [set-name (ctob/get-name set)
can-edit? (mf/use-ctx ctx/can-edit?)
@@ -239,7 +239,11 @@
:path tree-path})))))
on-double-click
(mf/use-fn (mf/deps id) #(on-start-edition id))
(mf/use-fn
(mf/deps id is-new)
(fn []
(when-not is-new
(on-start-edition id))))
on-checkbox-click
(mf/use-fn
@@ -392,7 +396,7 @@
:is-editing true
:is-active true
:is-selected true
:is-new true
:tree-path path
:tree-depth depth
:tree-index index
@@ -416,6 +420,7 @@
:tree-path path
:tree-depth depth
:tree-index index
:is-new false
:tree-parent-path parent-path
:on-toggle on-toggle-set
:edition-id edition-id

View File

@@ -410,7 +410,7 @@
:else
(let [objects (u/locate-objects)
shapes (into [] (map u/proxy->shape) shapes)]
(cg/generate-markup-code objects type shapes)))))
(cg/generate-formatted-markup-code objects type shapes)))))
:generateStyle
(fn [shapes options]

View File

@@ -6,6 +6,7 @@
(ns app.util.code-gen
(:require
[app.util.code-beautify :as cb]
[app.util.code-gen.markup-html :as html]
[app.util.code-gen.markup-svg :as svg]
[app.util.code-gen.style-css :as css]))
@@ -18,6 +19,11 @@
"svg" svg/generate-markup)]
(generate-markup objects shapes)))
(defn generate-formatted-markup-code
[objects type shapes]
(let [markup (generate-markup-code objects type shapes)]
(cb/format-code markup type)))
(defn generate-style-code
([objects type root-shapes all-shapes]
(generate-style-code objects type root-shapes all-shapes nil))

View File

@@ -8,6 +8,7 @@
(:require
["react-dom/server" :as rds]
[app.main.render :as render]
[app.util.code-beautify :as cb]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -24,3 +25,8 @@
(->> shapes
(map #(generate-svg objects %))
(str/join "\n")))
(defn generate-formatted-markup
[objects shapes]
(let [markup (generate-markup objects shapes)]
(cb/format-code markup "svg")))

View File

@@ -116,13 +116,17 @@
(let [root? (contains? (:root-shapes options) (:id shape))]
(if (and root? (ctl/any-layout? shape))
:fill
(get-shape-size shape objects :width))))
;; Don't set fixed width for auto-width text shapes
(when-not (and (cfh/text-shape? shape) (= (:grow-type shape) :auto-width))
(get-shape-size shape objects :width)))))
(defmethod get-value :height
[_ shape objects options]
(let [root? (contains? (:root-shapes options) (:id shape))]
(when-not (and root? (ctl/any-layout? shape))
(get-shape-size shape objects :height))))
;; Don't set fixed height for auto-height text shapes
(when-not (and (cfh/text-shape? shape) (= (:grow-type shape) :auto-height))
(get-shape-size shape objects :height)))))
(defmethod get-value :flex-grow
[_ shape _ options]

View File

@@ -450,9 +450,9 @@
(t/testing "token got applied to rect with stroke and shape stroke got updated"
(t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (:name token-target')))
(t/is (= (get-in rect-with-stroke' [:strokes 0 :stroke-width]) 10)))
(t/testing "token got applied to rect without stroke but shape didnt get updated"
(t/testing "token got applied to rect without stroke and shape stroke got updated"
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
(t/is (empty? (:strokes rect-without-stroke')))))))))))
(t/is (= (get-in rect-without-stroke' [:strokes 0 :stroke-width]) 10))))))))))
(t/deftest test-apply-font-size
(t/testing "applies font-size token and updates the text font-size"
@@ -645,6 +645,51 @@
(t/is (= (:r1 (:applied-tokens rect-without-token')) (:name target-token)))
(t/is (= (:r1 (:applied-tokens rect-with-other-token-2')) (:name target-token)))))))))))
(t/deftest test-toggle-spacing-token
(t/testing "applies spacing token only to layouts and layout children"
(t/async
done
(let [spacing-token {:name "spacing.md"
:value "16"
:type :spacing}
file (-> (setup-file-with-tokens)
(ctho/add-frame-with-child :frame-layout :rect-in-layout
{:frame-params {:layout :grid}})
(ctho/add-rect :rect-regular)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token spacing-token))))
store (ths/setup-store file)
frame-layout (cths/get-shape file :frame-layout)
rect-in-layout (cths/get-shape file :rect-in-layout)
rect-regular (cths/get-shape file :rect-regular)
events [(dwta/toggle-token {:token (toht/get-token file "spacing.md")
:shapes [frame-layout rect-in-layout rect-regular]})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
frame-layout' (cths/get-shape file' :frame-layout)
rect-in-layout' (cths/get-shape file' :rect-in-layout)
rect-regular' (cths/get-shape file' :rect-regular)]
(t/testing "frame with layout gets all spacing attributes"
(t/is (= "spacing.md" (:column-gap (:applied-tokens frame-layout'))))
(t/is (= "spacing.md" (:row-gap (:applied-tokens frame-layout'))))
(t/is (= 16 (get-in frame-layout' [:layout-gap :column-gap])))
(t/is (= 16 (get-in frame-layout' [:layout-gap :row-gap]))))
(t/testing "shape inside layout frame gets only margin attributes"
(t/is (= "spacing.md" (:m1 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m2 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m3 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m4 (:applied-tokens rect-in-layout'))))
(t/is (nil? (:column-gap (:applied-tokens rect-in-layout'))))
(t/is (nil? (:row-gap (:applied-tokens rect-in-layout'))))
(t/is (= {:m1 16, :m2 16, :m3 16, :m4 16} (get rect-in-layout' :layout-item-margin))))
(t/testing "regular shape doesn't get spacing attributes"
(t/is (nil? (:applied-tokens rect-regular')))))))))))
(t/deftest test-detach-styles-color
(t/testing "applying a color token to a shape with color styles should detach the styles"
(t/async

View File

@@ -4313,11 +4313,6 @@ msgstr "Please upgrade to match your usage. Contact with the team owner: %s"
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Enterprise plan"
#: src/app/main/ui/dashboard/subscription.cljs:77
#, unused
msgid "subscription.dashboard.power-up.enterprise.description"
msgstr "Advanced security, activity logs, dedicated support and more."
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom"
@@ -4357,9 +4352,9 @@ msgstr "Enterprise plan (trial)"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
msgid "subscription.dashboard.power-up.unlimited.bottom"
msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
"Get extra editors, more storage and backup, advanced security and more. "
"Get extra editors, more backup, unlimited storage and more. "
"[Take a look to the Enterprise plan.|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
@@ -4392,11 +4387,6 @@ msgstr "Subscription"
msgid "subscription.settings.add-payment-to-continue"
msgstr "Add a payment method to continue after your trial"
#: src/app/main/ui/settings/subscription.cljs:82, src/app/main/ui/settings/subscription.cljs:115, src/app/main/ui/settings/subscription.cljs:127
#, fuzzy, unused
msgid "subscription.settings.benefits.all-professiona-benefits"
msgstr ""
#: src/app/main/ui/settings/subscription.cljs:247, src/app/main/ui/settings/subscription.cljs:268, src/app/main/ui/settings/subscription.cljs:303, src/app/main/ui/settings/subscription.cljs:317
msgid "subscription.settings.benefits.all-professional-benefits"
msgstr "All Professional plan benefits and:"
@@ -4417,9 +4407,8 @@ msgstr "Enterprise (trial)"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Unlimited storage and 90-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
msgid "subscription.settings.enterprise.security"
msgstr "Advanced security"
msgid "subscription.settings.enterprise.autosave"
msgstr "90-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.capped-bill"
@@ -4452,8 +4441,10 @@ msgid "subscription.settings.management.dialog.payment-explanation"
msgstr "(No payment will be made now)"
#: src/app/main/ui/settings/subscription.cljs:124
#, markdown
msgid "subscription.settings.management.dialog.price-month"
msgstr "$7 per editor/month x %s"
msgstr ""
"**$%s** per month"
#: src/app/main/ui/settings/subscription.cljs:112
msgid "subscription.settings.management.dialog.select-editors"
@@ -7552,6 +7543,10 @@ msgstr "Type '%s' is not supported (%s)\n"
msgid "workspace.tokens.value-not-valid"
msgstr "The value is not valid"
#: src/app/main/ui/workspace/tokens/token_pill.cljs
msgid "workspace.tokens.more-options"
msgstr "Right click to see options"
#: src/app/main/data/workspace/tokens/errors.cljs:61
msgid "workspace.tokens.value-with-units"
msgstr "Invalid value: Units are not allowed."

View File

@@ -4340,11 +4340,6 @@ msgstr "Por favor, mejóralo para adaptarlo a tu uso. Contacta con el "
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Plan Enterprise"
#: src/app/main/ui/dashboard/subscription.cljs:77
#, unused
msgid "subscription.dashboard.power-up.enterprise.description"
msgstr "Seguridad avanzada, registros de actividad, asistencia dedicada y mucho más."
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom"
@@ -4388,9 +4383,9 @@ msgstr "Plan Unlimited"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
msgid "subscription.dashboard.power-up.unlimited.bottom"
msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
"Consigue editores adicionales, más almacenamiento y copias de seguridad, seguridad avanzada y mucho más. "
"Consigue editores adicionales, copias de seguridad, almacenamiento ilimitado y mucho más. "
"[Echa un ojo al Plan Enterprise|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
@@ -4445,9 +4440,8 @@ msgstr "Enterprise (prueba)"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Almacenamiento ilimitado y versiones de autoguardado de 90 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
msgid "subscription.settings.enterprise.security"
msgstr "Seguridad avanzada"
msgid "subscription.settings.enterprise.autosave"
msgstr "Versiones guardadas automáticamente cada 90 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.capped-bill"
@@ -4480,8 +4474,10 @@ msgid "subscription.settings.management.dialog.payment-explanation"
msgstr "(Ahora no se efectuará ningún pago)"
#: src/app/main/ui/settings/subscription.cljs:124
#, markdown
msgid "subscription.settings.management.dialog.price-month"
msgstr "$7 por editor/mes x %s"
msgstr ""
"**$%s** por mes"
#: src/app/main/ui/settings/subscription.cljs:112
msgid "subscription.settings.management.dialog.select-editors"
@@ -7489,6 +7485,10 @@ msgstr "El tipo '%s' no está soportado (%s)\n"
msgid "workspace.tokens.value-not-valid"
msgstr "El valor no es válido"
#: src/app/main/ui/workspace/tokens/token_pill.cljs
msgid "workspace.tokens.more-options"
msgstr "Click derecho para ver opciones"
#: src/app/main/data/workspace/tokens/errors.cljs:61
msgid "workspace.tokens.value-with-units"
msgstr "Valor no válido: No se permiten unidades."

View File

@@ -821,7 +821,7 @@ function Mousetrap(targetElement) {
* @param {number=} level - what part of the sequence the command is
* @returns void
*/
function _bindSingle(combination, callback, action, sequenceName, level) {
function _bindSingle(combination, callback, action, sequenceName, level, overwrite) {
// store a direct mapped reference for use with Mousetrap.trigger
self._directMap[combination + ':' + action] = callback;
@@ -846,7 +846,9 @@ function Mousetrap(targetElement) {
self._callbacks[info.key] = self._callbacks[info.key] || [];
// remove an existing match if there is one
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
if (overwrite) {
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
}
// add this call back to the array
// if it is a sequence put it at the beginning
@@ -872,9 +874,9 @@ function Mousetrap(targetElement) {
* @param {string|undefined} action
* @returns void
*/
self._bindMultiple = function(combinations, callback, action) {
self._bindMultiple = function(combinations, callback, action, overwrite) {
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
_bindSingle(combinations[i], callback, action, undefined, undefined, overwrite);
}
};
@@ -899,10 +901,10 @@ function Mousetrap(targetElement) {
* @param {string=} action - 'keypress', 'keydown', or 'keyup'
* @returns void
*/
Mousetrap.prototype.bind = function(keys, callback, action) {
Mousetrap.prototype.bind = function(keys, callback, action, overwrite) {
var self = this;
keys = keys instanceof Array ? keys : [keys];
self._bindMultiple.call(self, keys, callback, action);
self._bindMultiple.call(self, keys, callback, action, overwrite);
return self;
};

View File

@@ -7,7 +7,7 @@ export DEVENV_PNAME="penpotdev";
export CURRENT_USER_ID=$(id -u);
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD);
export IMAGEMAGICK_VERSION=7.1.1-47
export IMAGEMAGICK_VERSION=7.1.2-0
# Safe directory to avoid ownership errors with Git
git config --global --add safe.directory /home/penpot/penpot || true