mirror of
https://github.com/penpot/penpot.git
synced 2026-01-16 02:09:56 -05:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed5875f29a | ||
|
|
ad38a21053 | ||
|
|
adffac4eec | ||
|
|
73dfe12ec9 | ||
|
|
ff2e845f2c | ||
|
|
8e0a6e4123 | ||
|
|
0131cd6f8b | ||
|
|
288a7b21d6 | ||
|
|
32bd08533d | ||
|
|
c1aae12327 | ||
|
|
23a6f4b7c1 | ||
|
|
133e6e1e68 | ||
|
|
6abd045273 | ||
|
|
778a608854 | ||
|
|
a76a9fae41 | ||
|
|
f7cfbdd229 | ||
|
|
e28d2842f6 | ||
|
|
ccc3ca0948 | ||
|
|
515cbf7bef | ||
|
|
c320cbc47b | ||
|
|
46969585ed | ||
|
|
47882c5419 | ||
|
|
019d5e083a | ||
|
|
85f6cf32ae | ||
|
|
ded8e39e73 | ||
|
|
e730200873 | ||
|
|
4501d13961 | ||
|
|
baa1cfb2f8 | ||
|
|
905699d15a | ||
|
|
fe53869308 | ||
|
|
50076bac83 | ||
|
|
44bc4b7fa4 | ||
|
|
5c14f486d7 | ||
|
|
0cbd980b68 | ||
|
|
95dda2b1af | ||
|
|
5170872961 | ||
|
|
871ca68e1e | ||
|
|
0ab896fc76 | ||
|
|
6a4b548457 | ||
|
|
695a399941 | ||
|
|
a32463fada | ||
|
|
5d44c88988 | ||
|
|
ce87d797d1 | ||
|
|
7fde1436e1 | ||
|
|
e1c5a32fcb | ||
|
|
b262e6a46f | ||
|
|
2e726b62c3 | ||
|
|
02acd81c2c | ||
|
|
bae2de75ff | ||
|
|
b68c426cd1 | ||
|
|
5161ef15bf | ||
|
|
36d3d94ec9 | ||
|
|
17447d7610 | ||
|
|
708a40bff1 | ||
|
|
efaf6573bd | ||
|
|
001bcbce59 | ||
|
|
c195c07a3f | ||
|
|
f5298f51e7 | ||
|
|
46c440fef2 | ||
|
|
e77f8b572a | ||
|
|
ade5eecf80 | ||
|
|
97fc7702b8 | ||
|
|
54fcd58531 | ||
|
|
b7a8747f00 | ||
|
|
d00de7d5a4 | ||
|
|
5ae4dde222 | ||
|
|
2fbd4b07e0 | ||
|
|
58a843ea23 | ||
|
|
f6b97af148 | ||
|
|
76b7287bf1 | ||
|
|
019bc2f183 | ||
|
|
8c96a617be | ||
|
|
1f15e9b81e | ||
|
|
f7627e515a | ||
|
|
d08c94d5a6 | ||
|
|
01896501c1 | ||
|
|
3f9a1525ca | ||
|
|
52c1e227d5 | ||
|
|
955538b12a | ||
|
|
8254af27cb | ||
|
|
f76391ecbb | ||
|
|
c49e9fbf18 | ||
|
|
122701ee7b | ||
|
|
351362bb50 | ||
|
|
1acf78d57c | ||
|
|
523373dfa2 | ||
|
|
f55e7d8165 | ||
|
|
9fdc6be465 | ||
|
|
9390c1e7be | ||
|
|
b20b272eae | ||
|
|
d46b519524 |
105
.github/workflows/build-bundles.yml
vendored
105
.github/workflows/build-bundles.yml
vendored
@@ -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
12
.github/workflows/build-develop.yml
vendored
Normal 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
12
.github/workflows/build-staging.yml
vendored
Normal 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"
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -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
6
.gitignore
vendored
@@ -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
|
||||
|
||||
24
CHANGES.md
24
CHANGES.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN migrations text[];
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"pass_due"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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?]}]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
20
common/src/app/common/types/stroke.cljc
Normal file
20
common/src/app/common/types/stroke.cljc
Normal 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})
|
||||
@@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@ RUN set -ex; \
|
||||
fonts-wqy-zenhei \
|
||||
fonts-tlwg-loma-otf \
|
||||
fonts-freefont-ttf \
|
||||
poppler-utils \
|
||||
\
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0t64 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 layer’s 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
frontend/resources/images/features/2.9-font-size.gif
Normal file
BIN
frontend/resources/images/features/2.9-font-size.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
frontend/resources/images/features/2.9-overrides.gif
Normal file
BIN
frontend/resources/images/features/2.9-overrides.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
BIN
frontend/resources/images/features/2.9-qol.gif
Normal file
BIN
frontend/resources/images/features/2.9-qol.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
BIN
frontend/resources/images/features/2.9-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.9-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))))
|
||||
|
||||
|
||||
@@ -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!
|
||||
([]
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*)
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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]})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -101,5 +101,5 @@
|
||||
@extend .button-secondary;
|
||||
@include uppercaseTitleTipography;
|
||||
height: $s-32;
|
||||
width: $s-252;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -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")))
|
||||
|
||||
112
frontend/src/app/main/ui/releases/v2_9.cljs
Normal file
112
frontend/src/app/main/ui/releases/v2_9.cljs
Normal 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)}
|
||||
"What’s 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)}
|
||||
"Let’s 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 there’s 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"]]]]]])))
|
||||
|
||||
102
frontend/src/app/main/ui/releases/v2_9.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_9.scss
Normal 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;
|
||||
}
|
||||
@@ -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}])]]]))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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])))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
{:delete-stop {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:subsections [:edit]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (dwc/remove-gradient-stop))}}))
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 []}])]))
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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}])]])]]))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
14
frontend/vendor/mousetrap/index.js
vendored
14
frontend/vendor/mousetrap/index.js
vendored
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user