Compare commits

...

57 Commits

Author SHA1 Message Date
Luis de Dios
37aa59b164 🐛 Fix hidden advanced frame grid options menu (#7681) 2025-11-04 11:57:52 +01:00
Andrey Antukh
88493f6805 🐛 Fix incorrect query for subscription editors (#7672)
Default teams should be present on the query results
2025-11-03 16:14:24 +01:00
Belén Albeza
83cd9c3db6 🔧 Fix rust linter errors 2025-11-03 11:45:05 +01:00
Andrey Antukh
399feec032 ⬆️ Update rust to 1.91 2025-11-03 11:45:00 +01:00
Luis de Dios
95fdd75030 🐛 Fix misaligned right sidebar menus 2025-11-03 08:34:09 +01:00
Andrey Antukh
febe87aa7b 🐛 Fix incorrect checksum of the jdk on dockerfiles 2025-10-31 18:01:55 +01:00
Andrés Moya
99e8b22672 🐛 Fix theme validation when still no tokens library exists 2025-10-31 16:04:50 +01:00
Andrey Antukh
0581c60800 ⬆️ Update jdk and node on docker images 2025-10-31 14:50:12 +01:00
Andrey Antukh
7e92408807 ⬆️ Update jdk and node on devenv 2025-10-31 14:50:12 +01:00
David Barragán Merino
262937c421 📚 Add recommendations for valkey/redis configuration 2025-10-31 10:45:33 +01:00
Alonso Torres
942e3300dd 🐛 Fix problem when checking usage with removed teams (#7638) 2025-10-31 09:22:31 +01:00
Alejandro Alonso
6a2029ca3b 🐛 Fix error comment message after the demo account creation (#7615) 2025-10-31 08:56:34 +01:00
David Barragán Merino
f32913adcf 📚 Adapt doc with the storage settings changes (#7607) 2025-10-31 08:56:06 +01:00
Juan de la Cruz
d906f05a6f 🎉 Add 2.11 release slides and images (#7606) 2025-10-31 08:54:19 +01:00
Alejandro Alonso
08162c825d Merge pull request #7633 from penpot/superalex-options-button-does-not-work-for-comments-created-in-the-lower-part-of-the-screen-with-an-active-reply-field
🐛 Fix options button does not work for comments created in the lower part of the screen
2025-10-29 16:18:21 +01:00
Alejandro Alonso
bc700334ca 🐛 Fix options button does not work for comments created in the lower part of the screen 2025-10-29 16:17:57 +01:00
Alejandro Alonso
133590f19c Merge pull request #7635 from penpot/alotor-fix-paste-position
🐛 Fix paste without selection sends the new element in the back
2025-10-29 16:16:17 +01:00
alonso.torres
66c5a0570e 🐛 Fix paste without selection sends the new element in the back 2025-10-29 16:15:55 +01:00
Andrés Moya
94cbf9d8f2 🎉 Add integration test to check new validation 2025-10-29 15:40:45 +01:00
Andrés Moya
70143f8ae3 🐛 Fix theme renaming and small refactor tokens forms validation 2025-10-29 15:40:45 +01:00
David Barragán Merino
1b81ddebb4 🐛 Fix some paths and add missed nginx config file for the storybook docker image 2025-10-29 13:46:29 +01:00
David Barragán Merino
6076df5c80 🎉 Detach storybook from the frontend build process 2025-10-29 13:45:54 +01:00
Alejandro Alonso
6d2d66a079 Merge pull request #7634 from penpot/alotor-fix-editable-label
🐛 Fix problem with certain text input and drag/drop
2025-10-29 12:50:03 +01:00
Alejandro Alonso
239af4fb82 🐛 Fix problem with text grow types 2025-10-29 12:40:11 +01:00
alonso.torres
0ad4a9ca7e 🐛 Fix problem with certain text input and drag/drop 2025-10-29 12:35:13 +01:00
Eva Marco
aadc1aac1c 🐛 Fix some error translations 2025-10-29 11:14:20 +01:00
Alejandro Alonso
b033690239 Merge pull request #7618 from penpot/andy-docs-typography-token
📚 Add typography token to the user guide
2025-10-29 08:22:20 +01:00
Alejandro Alonso
474453a503 Merge pull request #7594 from penpot/eva-fix-dropdown-submenu
🐛 Fix submenu visibility
2025-10-29 07:53:06 +01:00
Pablo Alba
e2ce226814 🐛 Fix remove flex button doesn’t work within variant 2025-10-28 09:38:38 +01:00
Andres Gonzalez
a346d29d76 📚 Add typography token to the user guide 2025-10-27 15:18:14 +01:00
Andrés Moya
ed767d9a5b 🐛 Fix library update notificacions showing when they should not 2025-10-27 11:14:41 +01:00
Alejandro Alonso
6290b88d2e Merge pull request #7601 from penpot/alotor-fix-text-grow-type-problem
🐛 Fix problem with text grow types
2025-10-24 09:45:47 +02:00
alonso.torres
7c1205018b 🐛 Fix problem with text grow types 2025-10-23 17:39:18 +02:00
Belén Albeza
f8cebb9d63 🐛 Fix scroll bar in design tab (#7582)
* 🐛 Fix scroll bar in design tab

* ♻️ Remove deprecated css tokens in options.scss
2025-10-23 14:11:11 +02:00
Alejandro Alonso
1e248c7177 🐛 Fix demo accounts creation 2025-10-23 13:45:11 +02:00
Belén Albeza
45af469a11 🐛 Fix invite selection copy
* 🐛 Fix selected invitations copy not being localized/pluralized

*  Add integration test for team invites + fixes unaccessible dom
2025-10-23 12:04:34 +02:00
Eva Marco
232f2271d3 🐛 Fix submenu visibility 2025-10-23 11:52:03 +02:00
Alejandro Alonso
36c986d8e8 🐛 Fix file doesn’t open after deleting the library used in it 2025-10-23 09:51:10 +02:00
Alejandro Alonso
54ac64db4b Merge pull request #7578 from penpot/supealex-fix-selected-colors-children-shapes-in-multiple-selection
🐛 Fix selected colors not showing colors from children shapes in multiple selection
2025-10-22 15:18:58 +02:00
Alejandro Alonso
30ca6bf6ff 🐛 Fix selected colors not showing colors from children shapes in multiple selection 2025-10-22 14:53:06 +02:00
David Barragán Merino
81a364dfc4 🐳 Set default values for maxmemory and maxmemory-policy in Valkey 2025-10-22 13:43:30 +02:00
Pablo Alba
c6b9954af8 🐛 Fix nested variant in a component doesn't keep inherited overrides 2025-10-22 13:35:22 +02:00
Belén Albeza
7ec335ae96 🐛 Fix export element crashing the app 2025-10-22 13:02:55 +02:00
Luis de Dios
e073b89604 🐛 Fix property input remains editable after keeping default property name (#7549)
* 🐛 Fix property input remains focused when keeping default property name

* 📎 PR changes
2025-10-22 10:48:03 +02:00
Pablo Alba
5e6af5aea9 🐛 Fix text override is lost after switch 2025-10-22 09:43:12 +02:00
Pablo Alba
fd596a1371 🐛 Fix incorrect behavior of Alt + Drag for variants 2025-10-21 17:02:10 +02:00
Eva Marco
ca21e7e8b4 🐛 Fix font size placeholder 2025-10-21 12:27:15 +02:00
Alejandro Alonso
9e17a0e65d 🐛 Fix unread comments 2025-10-21 09:30:01 +02:00
Pablo Alba
fec420b6e9 🐛 Fix variants not syncronizing tokens on switch 2025-10-17 13:46:49 +02:00
Luis de Dios
216b2d3072 🐛 Fix drag & drop functionality is swapping instead or reordering (#7489)
* 🐛 Fix drag & drop functionality is swapping instead or reordering

* ♻️ SCSS improvements
2025-10-17 12:12:34 +02:00
Eva Marco
14f6e22610 🐛 Fix composite token placeholders (#7526)
* 🐛 Fix composite token placeholders

* 📚 Recover some translations
2025-10-17 10:57:32 +02:00
Andrés Moya
5ad04e0f4c 🐛 Fix error when selecting set in theme 2025-10-16 16:17:16 +02:00
Eva Marco
e964f9820e 🐛 Fix tooltip position of proportion lock button (#7519) 2025-10-16 11:40:19 +02:00
Alejandro Alonso
9266ace537 Merge pull request #7514 from penpot/ladybenko-12293-fix-scroll-inspect
🐛 Fix scrollbar in the inspect tab
2025-10-16 07:07:56 +02:00
Belén Albeza
b057ed1b9a 🐛 Fix scroll on inspect tab 2025-10-15 15:30:27 +02:00
Alejandro Alonso
2c5abb0cbf Merge pull request #7506 from penpot/niwinz-staging-hotfix-6-comments-threads
 Add minor comment threads queries optimization
2025-10-15 12:04:42 +02:00
Andrey Antukh
7f6bffdbfc Add minor comment threads queries optimization 2025-10-15 11:45:24 +02:00
128 changed files with 2251 additions and 1249 deletions

View File

@@ -46,6 +46,7 @@ jobs:
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
popd
- name: Set up Docker Buildx
@@ -99,3 +100,29 @@ jobs:
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.storybook
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🐳 *[PENPOT] Error building penpot docker images.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -41,6 +41,7 @@
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
- Reorder properties for a component [Taiga #10225](https://tree.taiga.io/project/penpot/us/10225)
- File Data storage layout refactor [Github #7345](https://github.com/penpot/penpot/pull/7345)
- Make several queries optimization on comment threads [Github #7506](https://github.com/penpot/penpot/pull/7506)
### :bug: Bugs fixed
@@ -55,7 +56,25 @@
- Fix auto-width changes to fixed when switching variants [Taiga #12172](https://tree.taiga.io/project/penpot/issue/12172)
- Fix component number has no singular translation string [Taiga #12106](https://tree.taiga.io/project/penpot/issue/12106)
- Fix adding/removing identical text fills [Taiga #12287](https://tree.taiga.io/project/penpot/issue/12287)
- Fix scroll on the inspect tab [Taiga #12293](https://tree.taiga.io/project/penpot/issue/12293)
- Fix lock proportion tooltip [Taiga #12326](https://tree.taiga.io/project/penpot/issue/12326)
- Fix internal Error when selecting a set by name in the token theme editor [Taiga #12310](https://tree.taiga.io/project/penpot/issue/12310)
- Fix drag & drop functionality is swapping instead or reordering [Taiga #12254](https://tree.taiga.io/project/penpot/issue/12254)
- Fix variants not syncronizing tokens on switch [Taiga #12290](https://tree.taiga.io/project/penpot/issue/12290)
- Fix incorrect behavior of Alt + Drag for variants [Taiga #12309](https://tree.taiga.io/project/penpot/issue/12309)
- Fix text override is lost after switch [Taiga #12269](https://tree.taiga.io/project/penpot/issue/12269)
- Fix exporting a board crashing the app [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12384)
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
- Fix selected colors not showing colors from children shapes in multiple selection [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12385)
- Fix scrollbar issue in design tab [Taiga #12367](https://tree.taiga.io/project/penpot/issue/12367)
- Fix library update notificacions showing when they should not [Taiga #12397](https://tree.taiga.io/project/penpot/issue/12397)
- Fix remove flex button doesnt work within variant [Taiga #12314](https://tree.taiga.io/project/penpot/issue/12314)
- Fix an error translation [Taiga #12402](https://tree.taiga.io/project/penpot/issue/12402)
- Fix problem with certain text input in some editable labels (pages, components, tokens...) being in conflict with the drag/drop functionality [Taiga #12316](https://tree.taiga.io/project/penpot/issue/12316)
- Fix not controlled theme renaming [Taiga #12411](https://tree.taiga.io/project/penpot/issue/12411)
- Fix paste without selection sends the new element in the back [Taiga #12382](https://tree.taiga.io/project/penpot/issue/12382)
- Fix options button does not work for comments created in the lower part of the screen [Taiga #12422](https://tree.taiga.io/project/penpot/issue/12422)
- Fix problem when checking usage with removed teams [Taiga #12442](https://tree.taiga.io/project/penpot/issue/12442)
## 2.10.1
@@ -63,12 +82,10 @@
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
### :bug: Bugs fixed
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
## 2.10.0
### :rocket: Epics and highlights
@@ -84,7 +101,7 @@
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
@@ -165,7 +182,6 @@
- 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
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)

View File

@@ -749,7 +749,7 @@
l.version
FROM libs AS l
INNER JOIN project AS p ON (p.id = l.project_id)
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
WHERE l.deleted_at IS NULL;")
(defn get-file-libraries
[conn file-id]

View File

@@ -234,36 +234,39 @@
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id))))
(def ^:private sql:comment-threads
"SELECT DISTINCT ON (ct.id)
ct.*,
pf.fullname AS owner_fullname,
pf.email AS owner_email,
pf.photo_id AS owner_photo_id,
p.team_id AS team_id,
f.name AS file_name,
f.project_id AS project_id,
first_value(c.content) OVER w AS content,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id) AS count_comments,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
FROM comment_thread AS ct
INNER JOIN comment AS c ON (c.thread_id = ct.id)
INNER JOIN file AS f ON (f.id = ct.file_id)
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
WHERE f.deleted_at IS NULL
AND p.deleted_at IS NULL
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)")
(defn- get-comment-threads-sql
[where]
(str/ffmt
"SELECT DISTINCT ON (ct.id)
ct.*,
pf.fullname AS owner_fullname,
pf.email AS owner_email,
pf.photo_id AS owner_photo_id,
p.team_id AS team_id,
f.name AS file_name,
f.project_id AS project_id,
first_value(c.content) OVER w AS content,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id) AS count_comments,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
FROM comment_thread AS ct
INNER JOIN comment AS c ON (c.thread_id = ct.id)
INNER JOIN file AS f ON (f.id = ct.file_id)
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
WHERE f.deleted_at IS NULL
AND p.deleted_at IS NULL
%1
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)"
where))
(def ^:private sql:comment-threads-by-file-id
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE file_id = ?"))
(get-comment-threads-sql "AND ct.file_id = ?"))
(defn- get-comment-threads
[conn profile-id file-id]
@@ -273,34 +276,29 @@
;; --- COMMAND: Get Unread Comment Threads
(def ^:private sql:unread-all-comment-threads-by-team
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?"))
(str "WITH threads AS ("
(get-comment-threads-sql "AND p.team_id = ?")
")"
"SELECT t.* FROM threads AS t
WHERE t.count_unread_comments > 0"))
;; The partial configuration will retrieve only comments created by the user and
;; threads that have a mention to the user.
(def ^:private sql:unread-partial-comment-threads-by-team
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads
WHERE count_unread_comments > 0
AND team_id = ?
AND (owner_id = ? OR ? = ANY(mentions))"))
(str "WITH threads AS ("
(get-comment-threads-sql "AND p.team_id = ? AND (ct.owner_id = ? OR ? = ANY(ct.mentions))")
")"
"SELECT t.* FROM threads AS t
WHERE t.count_unread_comments > 0"))
(defn- get-unread-comment-threads
[cfg profile-id team-id]
(let [profile (-> (db/get cfg :profile {:id profile-id})
(let [profile (-> (db/get cfg :profile {:id profile-id} ::db/remove-deleted false)
(profile/decode-row))
notify (or (-> profile :props :notifications :dashboard-comments) :all)]
(case notify
:all
(->> (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
(into [] xf-decode-row))
:partial
(->> (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
(into [] xf-decode-row))
[])))
notify (or (-> profile :props :notifications :dashboard-comments) :all)
result (case notify
:all (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
:partial (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
[])]
(into [] xf-decode-row result)))
(def ^:private
schema:get-unread-comment-threads
@@ -323,16 +321,17 @@
[:id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]])
(def ^:private sql:get-comment-thread
(get-comment-threads-sql "AND ct.file_id = ? AND ct.id = ?"))
(sv/defmethod ::get-comment-thread
{::doc/added "1.15"
::sm/params schema:get-comment-thread}
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
(db/run! cfg (fn [{:keys [::db/conn]}]
(files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE id = ? AND file_id = ?")]
(-> (db/exec-one! conn [sql profile-id id file-id])
(decode-row))))))
(some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
(decode-row)))))
;; --- COMMAND: Retrieve Comments

View File

@@ -45,6 +45,7 @@
params {:email email
:fullname fullname
:is-active true
:is-demo true
:deleted-at (ct/in-future (cf/get-deletion-delay))
:password (derive-password password)
:props {}}

View File

@@ -107,7 +107,9 @@
(defn get-profile
"Get profile by id. Throws not-found exception if no profile found."
[conn id & {:as opts}]
(-> (db/get-by-id conn :profile id opts)
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
;; are created with a set deleted-at value
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
(decode-row)))
;; --- MUTATION: Update Profile (own)
@@ -473,13 +475,16 @@
p.fullname AS name,
p.email AS email
FROM team_profile_rel AS tpr1
JOIN team as t
ON tpr1.team_id = t.id
JOIN team_profile_rel AS tpr2
ON (tpr1.team_id = tpr2.team_id)
JOIN profile AS p
ON (tpr2.profile_id = p.id)
WHERE tpr1.profile_id = ?
AND tpr1.is_owner IS true
AND tpr2.can_edit IS true")
AND tpr2.can_edit IS true
AND t.deleted_at IS NULL")
(sv/defmethod ::get-subscription-usage
{::doc/added "2.9"}

View File

@@ -37,14 +37,14 @@
;; --- Helpers & Specs
(def ^:private sql:team-permissions
"select tpr.is_owner,
"SELECT tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?
and t.deleted_at is null")
FROM team_profile_rel AS tpr
JOIN team AS t ON (t.id = tpr.team_id)
WHERE tpr.profile_id = ?
AND tpr.team_id = ?
AND t.deleted_at IS NULL")
(defn get-permissions
[conn profile-id team-id]

View File

@@ -1024,6 +1024,29 @@
:clj
(sort comp-fn items))))
(defn reorder
"Reorder a vector by moving one of their items from some position to some space between positions.
It clamps the position numbers to a valid range."
[v from-pos to-space-between-pos]
(let [max-space-pos (count v)
max-prop-pos (dec max-space-pos)
from-pos (max 0 (min max-prop-pos from-pos))
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
(if (= from-pos to-space-between-pos)
v
(let [elem (nth v from-pos)
without-elem (-> []
(into (subvec v 0 from-pos))
(into (subvec v (inc from-pos))))
insert-pos (if (< from-pos to-space-between-pos)
(dec to-space-between-pos)
to-space-between-pos)]
(-> []
(into (subvec without-elem 0 insert-pos))
(into [elem])
(into (subvec without-elem insert-pos)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; String Functions

View File

@@ -1642,7 +1642,8 @@
(pcb/apply-changes-local)))))
(defn- generate-update-tokens
[changes container dest-shape origin-shape touched omit-touched?]
[changes container dest-shape origin-shape touched omit-touched? valid-attrs]
;; valid-attrs is a set of attrs to consider on the update. If it is nil, it will consider all the attrs
(let [attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs
(remove #(= :layout-grid-cells %)))
@@ -1650,8 +1651,8 @@
applied-tokens (reduce (fn [applied-tokens attr]
(let [attr-group (get ctk/sync-attrs attr)
token-attrs (cto/shape-attr->token-attrs attr)]
(if (not (and (touched attr-group)
omit-touched?))
(if (and (or (not omit-touched?) (not (touched attr-group)))
(or (empty? valid-attrs) (contains? valid-attrs attr)))
(into applied-tokens token-attrs)
applied-tokens)))
#{}
@@ -1808,7 +1809,7 @@
:always
(check-detached-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
(let [attr-group (get ctk/sync-attrs attr)
;; position-data is a special case because can be affected by
@@ -2082,12 +2083,14 @@
(recur (next attrs)
roperations'
uoperations'))
(cond-> changes
(> (count roperations) 1)
(add-update-attr-changes current-shape container roperations uoperations)
:always
(generate-update-tokens container current-shape previous-shape touched false))))))
(let [updated-attrs (into #{} (comp (filter #(= :set (:type %)))
(map :attr))
roperations)]
(cond-> changes
(> (count roperations) 1)
(-> (add-update-attr-changes current-shape container roperations uoperations)
(generate-update-tokens container current-shape previous-shape touched false updated-attrs))))))))
(defn- propagate-attrs
"Helper that puts the origin attributes (attrs) into dest but only if
@@ -2798,7 +2801,7 @@
(defn generate-duplicate-changes
"Prepare objects to duplicate: generate new id, give them unique names,
move to the desired position, and recalculate parents and frames as needed."
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props]}]
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props alt-duplication?]}]
(let [shapes (map (d/getf all-objects) ids)
unames (volatile! (cfh/get-used-names (:objects page)))
update-unames! (fn [new-name] (vswap! unames conj new-name))
@@ -2808,10 +2811,22 @@
;; we calculate a new one because the components will have created new shapes.
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication of a variant, change its parent to root
;; so the copy is made as a child of root
;; This is because inside a variant-container can't be a copy
shapes (map (fn [shape]
(if (and alt-duplication? (ctk/is-variant? shape))
(assoc shape :parent-id uuid/zero :frame-id nil)
shape))
shapes)
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects)
(pcb/with-library-data library-data))
changes
(->> shapes
(reduce #(generate-duplicate-shape-change %1

View File

@@ -28,11 +28,7 @@
(pcb/update-component
changes (:id component)
(fn [component]
(d/update-in-when component [:variant-properties pos]
(fn [property]
(-> property
(assoc :name new-name)
(with-meta nil)))))
(d/update-in-when component [:variant-properties pos] #(assoc % :name new-name)))
{:apply-changes-local-library? true}))
changes
related-components)))
@@ -88,7 +84,7 @@
related-components (cfv/find-variant-components data objects variant-id)]
(reduce (fn [changes component]
(let [props (:variant-properties component)
props (ctv/reorder-by-moving-to-position props from-pos to-space-between-pos)
props (d/reorder props from-pos to-space-between-pos)
main-id (:main-instance-id component)
name (ctv/properties-to-name props)]
(-> changes

View File

@@ -11,7 +11,8 @@
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]))
[app.common.uuid :as uuid]
[clojure.set :as set]))
(defn generate-add-new-variant
[changes shape variant-id new-component-id new-shape-id prop-num]
@@ -67,7 +68,6 @@
[[] {}]
shapes))))
(defn- keep-swapped-item
"As part of the keep-touched process on a switch, given a child on the original
copy that was swapped (orig-swapped-child), and its related shape on the new copy
@@ -88,7 +88,6 @@
current-parent (get objects (:parent-id related-shape-in-new))
pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))]
(-> (pcb/concat-changes before-changes changes)
;; Move the previous shape to the new parent
@@ -122,6 +121,44 @@
(subvec (vec ancestors) 1 (dec num-ancestors)))]
(some ctk/get-swap-slot ancestors)))
(defn- find-shape-ref-child-of
"Get the shape referenced by the shape-ref of the near main of the shape,
recursively repeated until find a shape-ref with parent-id as ancestor.
It will return the shape or nil if it doesn't found any"
[container libraries shape parent-id]
(let [ref-shape (ctf/find-ref-shape nil container libraries shape
:with-context? true)
ref-shape-container (when ref-shape (:container (meta ref-shape)))
ref-shape-parents-set (when ref-shape
(->> (cfh/get-parents (:objects ref-shape-container) (:id ref-shape))
(into #{} d/xf:map-id)))]
(if (or (nil? ref-shape) (contains? ref-shape-parents-set parent-id))
ref-shape
(find-shape-ref-child-of ref-shape-container libraries ref-shape parent-id))))
(defn- get-ref-chain
"Returns a vector with the shape ref chain including itself"
[container libraries shape]
(loop [chain [shape]
current shape]
(if-let [ref (ctf/find-ref-shape nil container libraries current :with-context? true)]
(recur (conj chain ref) ref)
chain)))
(defn- add-touched-from-ref-chain
"Adds to the :touched attr of a shape the content of
the :touched of all its chain of ref shapes"
[container libraries shape]
(let [chain (get-ref-chain container libraries shape)
more-touched (->> chain
(map :touched)
(remove nil?)
(apply set/union)
(remove ctk/swap-slot?)
set)]
(update shape :touched #(set/union (or % #{}) more-touched))))
(defn generate-keep-touched
"This is used as part of the switch process, when you switch from
@@ -141,7 +178,10 @@
;; Ignore children of swapped items, because
;; they will be moved without change when
;; managing their swapped ancestor
orig-touched (->> (filter (comp seq :touched) original-shapes)
orig-touched (->> original-shapes
;; Add to each shape also the touched of its ref chain
(map #(add-touched-from-ref-chain container libraries %))
(filter (comp seq :touched))
(remove
#(child-of-swapped? %
page-objects
@@ -158,7 +198,7 @@
;; The original-shape is in a copy. For the relation rules, we need the referenced
;; shape on the main component
orig-ref-shape (ctf/find-ref-shape nil container libraries original-shape {:with-context? true})
orig-ref-shape (ctf/find-remote-shape container libraries original-shape {:with-context? true})
orig-ref-objects (:objects (:container (meta orig-ref-shape)))
;; Adds a :shape-path attribute to the children of the orig-ref-shape,
@@ -171,7 +211,6 @@
;; Creates a map to quickly find a child of the orig-ref-shape by its shape-path
o-ref-shapes-p-map (into {} (map (juxt :id :shape-path)) o-ref-shapes-wp)
;; Process each touched children of the original-shape
[changes parents-of-swapped]
(reduce
@@ -182,8 +221,7 @@
;; orig-child-touched is in a copy. Get the referenced shape on the main component
;; If there is a swap slot, we will get the referenced shape in another way
orig-ref-shape (when-not swap-slot
;; TODO Maybe just get it from o-ref-shapes-wp
(ctf/find-ref-shape nil container libraries orig-child-touched))
(find-shape-ref-child-of container libraries orig-child-touched (:id orig-ref-shape)))
orig-ref-id (if swap-slot
;; If there is a swap slot, find the referenced shape id
@@ -196,6 +234,7 @@
;; Get its related shape in the children of new-shape: the one that
;; has the same shape-path
related-shape-in-new (get new-shapes-map shape-path)
parents-of-swapped (if related-shape-in-new
(conj parent-of-swapped (:parent-id related-shape-in-new))
parent-of-swapped)

View File

@@ -286,7 +286,7 @@
(fn [touched]
(into #{} (remove #(str/starts-with? (name %) "swap-slot-") touched)))))
(defn get-component-root
(defn get-deleted-component-root
[component]
(if (some? (:main-instance-id component))
(get-in component [:objects (:main-instance-id component)])

View File

@@ -276,7 +276,7 @@
(-> file-data
(get-component-page component)
(ctn/get-shape (:main-instance-id component)))
(ctk/get-component-root component)))
(ctk/get-deleted-component-root component)))
(defn get-component-shape
"Retrieve one shape in the component by id. If with-context? is true, add the
@@ -355,7 +355,7 @@
(defn find-remote-shape
"Recursively go back by the :shape-ref of the shape until find the correct shape of the original component"
[container libraries shape]
[container libraries shape & {:keys [with-context?] :or {with-context? false}}]
(let [top-instance (ctn/get-component-shape (:objects container) shape)
component-file (get-in libraries [(:component-file top-instance) :data])
component (ctkl/get-component component-file (:component-id top-instance) true)
@@ -375,8 +375,12 @@
(if (nil? remote-shape)
nil
(if (nil? (:shape-ref remote-shape))
remote-shape
(find-remote-shape component-container libraries remote-shape)))))
(cond-> remote-shape
(and remote-shape with-context?)
(with-meta {:file {:id (:id file-data)
:data file-data}
:container component-container}))
(find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))
(defn direct-copy?
"Check if the shape is in a direct copy of the component (i.e. the shape-ref points to shapes inside
@@ -901,7 +905,7 @@
(println))
(when (seq (:objects component))
(let [root (ctk/get-component-root component)]
(let [root (ctk/get-deleted-component-root component)]
(dump-shape (:id root)
1
(:objects component)

View File

@@ -249,12 +249,16 @@
(defn equal-attrs?
"Given a text structure, and a map of attrs, check that all the internal attrs in
paragraphs and sentences have the same attrs"
[item attrs]
(let [item-attrs (dissoc item :text :type :key :children)]
(and
(or (empty? item-attrs)
(= attrs (dissoc item :text :type :key :children)))
(every? #(equal-attrs? % attrs) (:children item)))))
([item attrs]
;; Ignore the root attrs of the content. We only want to check paragraphs and sentences
(equal-attrs? item attrs true))
([item attrs ignore?]
(let [item-attrs (dissoc item :text :type :key :children)]
(and
(or ignore?
(empty? item-attrs)
(= attrs (dissoc item :text :type :key :children)))
(every? #(equal-attrs? % attrs false) (:children item))))))
(defn get-first-paragraph-text-attrs
"Given a content text structure, extract it's first paragraph

View File

@@ -310,27 +310,3 @@
the real name of the shape joined by the properties values separated by '/'"
[variant]
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
(defn reorder-by-moving-to-position
"Reorder a vector by moving one of their items from some position to some space between positions.
It clamps the position numbers to a valid range."
[props from-pos to-space-between-pos]
(let [max-space-pos (count props)
max-prop-pos (dec max-space-pos)
from-pos (max 0 (min max-prop-pos from-pos))
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
(if (= from-pos to-space-between-pos)
props
(let [elem (nth props from-pos)
without-elem (-> []
(into (subvec props 0 from-pos))
(into (subvec props (inc from-pos))))
insert-pos (if (< from-pos to-space-between-pos)
(dec to-space-between-pos)
to-space-between-pos)]
(-> []
(into (subvec without-elem 0 insert-pos))
(into [elem])
(into (subvec without-elem insert-pos)))))))

View File

@@ -102,3 +102,14 @@
(t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a])
[:a :b :c :d])))
(t/deftest reorder
(let [v ["a" "b" "c" "d"]]
(t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"]))
(t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"]))
(t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"]))
(t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"]))
(t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"]))
(t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"]))))

View File

@@ -45,7 +45,6 @@
;; The rect has width 15 after the switch
(t/is (= (:width rect02') 15))))
(t/deftest test-switch-with-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -125,12 +124,10 @@
;; The rect has width 15 after the switch
(t/is (= (:width rect02') 15))))
(def font-size-path-paragraph [:content :children 0 :children 0 :font-size])
(def font-size-path-0 [:content :children 0 :children 0 :children 0 :font-size])
(def font-size-path-1 [:content :children 0 :children 0 :children 1 :font-size])
(def text-path-0 [:content :children 0 :children 0 :children 0 :text])
(def text-path-1 [:content :children 0 :children 0 :children 1 :text])
(def text-lines-path [:content :children 0 :children 0 :children])
@@ -188,6 +185,8 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -209,6 +208,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -234,6 +235,8 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -248,6 +251,8 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -306,6 +311,8 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -327,6 +334,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -352,6 +361,8 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -366,6 +377,8 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -401,7 +414,6 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
(t/is (= (get-in copy-both-t' text-path-0) "text overriden"))))
(t/deftest test-switch-with-different-text-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -423,6 +435,8 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -444,6 +458,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -469,6 +485,8 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -483,6 +501,8 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -518,7 +538,6 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "25"))
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
(t/deftest test-switch-with-different-text-and-prop-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -542,6 +561,8 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -563,6 +584,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -588,6 +611,8 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -602,6 +627,8 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -637,7 +664,6 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
(t/deftest test-switch-with-identical-structure-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -657,6 +683,8 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -678,6 +706,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -763,7 +793,6 @@
(t/is (= (get-in copy-structure-mixed-t' font-size-path-1) "40"))
(t/is (= (get-in copy-structure-mixed-t' text-path-1) "new line 2"))))
(t/deftest test-switch-with-different-prop-structure-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -784,6 +813,8 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -805,6 +836,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -906,6 +939,8 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -927,6 +962,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -971,6 +1008,8 @@
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
@@ -992,6 +1031,8 @@
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
@@ -1025,6 +1066,8 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -1046,6 +1089,8 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -1090,6 +1135,8 @@
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
@@ -1111,6 +1158,8 @@
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
@@ -1124,7 +1173,6 @@
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
(t/deftest test-switch-variant-for-other-with-same-nested-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1144,6 +1192,8 @@
;; On :copy-cp01, change the width of the rect
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{copy-cp01-rect-id}
(fn [shape]
@@ -1166,8 +1216,6 @@
;; The width of copy-cp02-rect' is 25 (change is preserved)
(t/is (= (:width copy-cp02-rect') 25))))
(t/deftest test-switch-variant-that-has-swaped-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1193,7 +1241,6 @@
;; Switch :c01 for :c02
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)]
(thf/dump-file file')
@@ -1207,7 +1254,6 @@
;;copy-02' had copy-cp02' as child
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))))
(t/deftest test-switch-variant-that-has-swaped-copy-with-changed-attr
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1244,7 +1290,6 @@
;; Switch :c01 for :c02
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
@@ -1262,3 +1307,58 @@
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))
;; The width of copy-cp02-rect' is 25 (change is preserved)
(t/is (= (:width copy-cp02-rect') 25))))
(t/deftest test-switch-variant-without-touched-but-touched-parent
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 5}})
(tho/add-simple-component :external01 :external01-root :external01-child)
(thc/instantiate-component :c01
:c01-in-root
:children-labels [:r01-in-c01-in-root]
:parent-label :external01-root))
;; Make a change on r01-in-c01-in-root so it is touched
page (thf/current-page file)
r01-in-c01-in-root (ths/get-shape file :r01-in-c01-in-root)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id r01-in-c01-in-root)}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
;; Instantiate the component :external01
file (thc/instantiate-component file
:external01
:external-copy01
:children-labels [:external-copy01-rect :c01-in-copy])
page (thf/current-page file)
c01-in-copy (ths/get-shape file :c01-in-copy)
rect01 (get-in page [:objects (-> c01-in-copy :shapes first)])
;; ==== Action
file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
page' (thf/current-page file')
c02-in-copy' (ths/get-shape file' :c02-in-copy)
rect02' (get-in page' [:objects (-> c02-in-copy' :shapes first)])]
(thf/dump-file file :keys [:width :touched])
;; The rect had width 25 before the switch
(t/is (= (:width rect01) 25))
;; The rect still has width 25 after the switch
(t/is (= (:width rect02') 25))))

View File

@@ -159,48 +159,3 @@
(t/testing "update-number-in-repeated-prop-names"
(t/is (= (ctv/update-number-in-repeated-prop-names props) numbered-props)))))
(t/deftest reorder-by-moving-to-position
(let [props [{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}]]
(t/testing "reorder-by-moving-to-position"
(t/is (= (ctv/reorder-by-moving-to-position props 0 2) [{:name "color" :value "blue"}
{:name "border" :value "no"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 3) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "border" :value "no"}
{:name "background" :value "none"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 4) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 0) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 2) [{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "background" :value "none"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 5) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 -1) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 5 -1) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props -1 5) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}])))))

View File

@@ -73,7 +73,7 @@ RUN set -eux; \
FROM base AS setup-node
ENV NODE_VERSION=v22.19.0 \
ENV NODE_VERSION=v22.21.1 \
PATH=/opt/node/bin:$PATH
RUN set -eux; \
@@ -113,12 +113,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@@ -149,18 +149,24 @@ FROM base AS setup-rust
ENV PATH=/opt/cargo/bin:$PATH \
RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/opt/cargo \
RUSTUP_VERSION=1.27.1 \
RUST_VERSION=1.85.0 \
RUSTUP_VERSION=1.28.2 \
RUST_VERSION=1.91.0 \
EMSCRIPTEN_VERSION=4.0.6
WORKDIR /opt
RUN set -eux; \
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
dpkgArch="$(dpkg --print-architecture)"; \
case "${dpkgArch##*-}" in \
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
'amd64') \
rustArch='x86_64-unknown-linux-gnu'; \
rustupSha256='20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c'; \
;; \
'arm64') \
rustArch='aarch64-unknown-linux-gnu'; \
rustupSha256='e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c'; \
;; \
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
esac; \
wget "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \

View File

@@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=v22.19.0 \
NODE_VERSION=v22.21.1 \
TZ=Etc/UTC
RUN set -ex; \
@@ -46,12 +46,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \

View File

@@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v22.19.0 \
NODE_VERSION=v22.21.1 \
DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:$PATH

View File

@@ -0,0 +1,20 @@
FROM nginxinc/nginx-unprivileged:1.29.1
LABEL maintainer="Penpot <docker@penpot.app>"
USER root
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot;
ARG BUNDLE_PATH="./bundle-storybook/"
COPY $BUNDLE_PATH /var/www/
COPY ./files/nginx.storybook.conf /etc/nginx/conf.d/default.conf
RUN chown -R 1001:0 /var/cache/nginx; \
chmod -R g+w /var/cache/nginx; \
chown -R 1001:0 /etc/nginx; \
chmod -R g+w /etc/nginx; \
chown -R 1001:0 /var/www; \
chmod -R g+w /var/www;
USER penpot:penpot

View File

@@ -247,6 +247,11 @@ services:
networks:
- penpot
environment:
# You can increase the max memory size if you have sufficient resources,
# although this should not be necessary.
- VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
## A mailcatch service, used as temporal SMTP server. You can access via HTTP to the
## port 1080 for read all emails the penpot platform has sent. Should be only used as a
## temporal solution while no real SMTP provider is configured.

View File

@@ -0,0 +1,27 @@
server {
listen 8080 default_server;
server_name _;
charset utf-8;
etag off;
gzip on;
gzip_static on;
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_vary on;
error_log /dev/stderr;
access_log /dev/stdout;
root /var/www;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -0,0 +1,27 @@
server {
listen 8080 default_server;
server_name _;
charset utf-8;
etag off;
gzip on;
gzip_static on;
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_vary on;
error_log /dev/stderr;
access_log /dev/stdout;
root /var/www;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -23,7 +23,7 @@ Flags and evironment variables are also used together; for example:
```bash
# This flag enables the use of SMTP email
PENPOT_FLAGS: enable-smtp
PENPOT_FLAGS: [...] enable-smtp
# These environment variables configure the specific SMPT service
# Backend
@@ -36,7 +36,7 @@ the exporter, or all of them; on the other hand, **environment variables** are c
each specific service. For example:
```bash
PENPOT_FLAGS: enable-login-with-google
PENPOT_FLAGS: [...] enable-login-with-google
# Backend
PENPOT_GOOGLE_CLIENT_ID: <client-id>
@@ -56,7 +56,7 @@ Penpot uses anonymous telemetries from the self-hosted instances to improve the
Consider sharing these anonymous telemetries enabling the corresponding flag:
```bash
PENPOT_FLAGS: enable-telemetries
PENPOT_FLAGS: [...] enable-telemetries
```
## Registration and authentication
@@ -402,7 +402,7 @@ This is implemented as specific locations in the penpot-front Nginx. If your org
in a 100% air-gapped environment, you can use the following configuration:
```bash
PENPOT_FLAGS: enable-air-gapped-conf
PENPOT_FLAGS: [...] enable-air-gapped-conf
```
When Penpot starts, it will leave out the Nginx configuration related to external requests. This means that,
@@ -459,11 +459,15 @@ POSTGRES_PASSWORD: penpot
### Storage
Storage refers to storing the user uploaded assets.
Storage refers to storing the user uploaded different objects in Penpot (assets, file data,...).
Assets storage is implemented using "plugable" backends. Currently there are two
Objects storage is implemented using "plugable" backends. Currently there are two
backends available: <code class="language-bash">fs</code> and <code class="language-bash">s3</code> (for AWS S3).
__Since version 2.11.0__
The configuration variables related to storage has been renamed, `PENPOT_STORAGE_ASSETS_*` are now `PENPOT_OBJECTS_STORAGE_*`.
`PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its values now are `fs` and `s3` instead of `assets-fs` or `assets-s3`.
#### FS Backend (default)
This is the default backend when you use the official docker images and the default
@@ -471,8 +475,8 @@ configuration looks like this:
```bash
# Backend
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
PENPOT_OBJECTS_STORAGE_BACKEND: fs
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/objects
```
The main downside of this backend is the hard dependency on nginx approach to serve files
@@ -485,7 +489,7 @@ configuration file][4] used in the docker images.
#### AWS S3 Backend
This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should
This backend uses AWS S3 bucket for store the user uploaded objects. For use it you should
have an appropriate account on AWS cloud and have the credentials, region and the bucket.
This is how configuration looks for S3 backend:
@@ -494,18 +498,36 @@ This is how configuration looks for S3 backend:
# Backend
AWS_ACCESS_KEY_ID: <you-access-key-id-here>
AWS_SECRET_ACCESS_KEY: <your-secret-access-key-here>
PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
PENPOT_STORAGE_ASSETS_S3_REGION: <aws-region>
PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
PENPOT_OBJECTS_STORAGE_BACKEND: s3
PENPOT_OBJECTS_STORAGE_S3_REGION: <aws-region>
PENPOT_OBJECTS_STORAGE_S3_BUCKET: <bucket-name>
# Optional if you want to use it with non AWS, S3 compatible service:
PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <endpoint-uri>
```
<p class="advice">
These settings are equally useful if you have a Minio storage system.
</p>
### File Data Storage
__Since version 2.11.0__
You can change the default file data storage backend with `PENPOT_FILE_DATA_BACKEND` environment variable. Possible values are:
- `legacy-db`: the current default backend, continues storing the file data of files and snapshots in the same location as previous versions of Penpot (< 2.11.0), this is a conservative default behaviour and will be changed to `db` in next versions.
- `db`: stores the file data on an specific table (the future default backend).
- `storage`: stores the file data using the objects storage system (S3 or FS, depending on which one is configured)
This also comes with an additional feature that allows offload the "inactive" files on file storage backend and leaves the database only for the active files. To enable it, you should use the `enable-tiered-file-data-storage` flag and `db` as file data storage backend.
```bash
# Backend
PENPOT_FLAGS: [...] enable-tiered-file-data-storage
PENPOT_FILE_DATA_BACKEND: db
```
### Autosave
By default, Penpot stores manually saved versions indefinitely; these can be found in the History tab and can be renamed, restored, deleted, etc. Additionally, the default behavior of on-premise instances is to not keep automatic version history. This automatic behavior can be modified and adapted to each on-premise installation with the corresponding configuration.
@@ -517,7 +539,7 @@ You need to be very careful when configuring automatic versioning, as it can sig
This is how configuration looks for auto-file-snapshot
```bash
PENPOT_FLAGS: enable-auto-file-snapshot # Enable automatic version saving
PENPOT_FLAGS: [...] enable-auto-file-snapshot # Enable automatic version saving
# Backend
PENPOT_AUTO_FILE_SNAPSHOT_EVERY: 5 # How many save operations trigger the auto-save-version?

View File

@@ -1,5 +1,5 @@
---
title: 1.1 Recommended storage
title: 1.1 Recommended settings
desc: Learn recommended self-hosting settings, Docker & Kubernetes installs, configuration, and troubleshooting tips in Penpot's technical guide.
---
@@ -10,3 +10,33 @@ Disk requirements depend on your usage, with the primary factors being database
As a rule of thumb, start with a **minimum** database size of **50GB** to **100GB** with elastic sizing capability — this configuration should adequately support up to 10 editors. For environments with **more than 10 users**, we recommend adding approximately **5GB** of capacity per additional editor.
Keep in mind that database size doesn't grow strictly proportionally with user count, as it depends heavily on how Penpot is used and the complexity of files created. Most organizations begin with this baseline and elastic sizing approach, then monitor usage patterns monthly until resource requirements stabilize.
# About Valkey / Redis requirements
"Valkey is mainly used for coordinating websocket notifications and, since Penpot 2.11, as a cache. Therefore, disk storage will not be necessary as it will use the instance's RAM.
To prevent the cache from hogging all the system's RAM usage, it is recommended to use two configuration parameters which, both in the docker-compose.yaml provided by Penpot and in the official Helm Chart, come with default parameters that should be sufficient for most deployments:
```bash
## Recommended values for most Penpot instances.
## You can modify this value to follow your policies.
# Set maximum memory Valkey/Redis will use.
# Accepted units: b, k, kb, m, mb, g, gb
maxmemory 128mb
# Choose an eviction policy (see Valkey docs:
# https://valkey.io/topics/memory-optimization/ or for Redis
# https://redis.io/docs/latest/develop/reference/eviction/
# Common choices:
# noeviction, allkeys-lru, volatile-lru, allkeys-random, volatile-random,
# volatile-ttl, volatile-lfu, allkeys-lfu
#
# For Penpot, volatile-lfu is recommended
maxmemory-policy volatile-lfu
```
The `maxmemory` configuration directive specifies the maximum amount of memory to use for the cache data. If you are using a dedicated instance to host Valkey/Redis, we do not recommend using more than 60% of the available RAM.
With `maxmemory-policy` configuration directive, you can select the eviction policy you want to use when the limit set by `maxmemory` is reached. Penpot works fine with `volatile-lfu`, which evicts the least frequently used keys that have been marked as expired.

View File

@@ -423,6 +423,40 @@ ExtraBold Italic
<p>This token can be applied directly to a text element or be used as a reference in a Typography Composite Token.</p>
<h3 id="typography-composite-tokens">Typography composite token</h3>
<p><strong>Typography tokens</strong> are composite entities that group several text properties into a single token definition. They allow you to define and reuse complete text styles in a consistent way.</p>
<p>Each property within a typography token can either reference an existing <a href="#design-tokens-typography">individual typography token</a> (for example, <em>font-size</em> or <em>font-family</em>) or use a hardcoded value. The behavior and syntax of individual typography tokens are described in the previous section of this guide.</p>
<figure>
<img src="/img/design-tokens/36-tokens-composite-typography.webp" alt="Typography composite token" />
</figure>
<h4 id="reference-composite-token">Reference another Typography Composite Token</h4>
<p>You can also reference another existing <strong>Typography Composite Token</strong> instead of defining each property manually. When doing so, Penpot resolves all individual properties from the referenced token.</p>
<figure>
<img src="/img/design-tokens/34-tokens-composite-typography-alias.webp" alt="Typography composite token" />
</figure>
<h4 id="line-height-property">Line height property</h4>
<p>The <strong>Typography Token</strong> includes a <em>line-height</em> property, which is not available as an individual token. This is because line-height depends on the font size to be calculated properly. Make sure the <em>font-size</em> property is defined before setting <em>line-height</em>.</p>
<figure>
<img src="/img/design-tokens/35-tokens-composite-typography-lineheight.webp" alt="Typography composite token" />
</figure>
<p>Accepted values for the line-height input:</p>
<ul>
<li><strong>Unitless number:</strong> interpreted as a multiplier of the font size. This is Penpots default behavior.</li>
<li><strong>Percentage (%):</strong> converted internally to a multiplier.</li>
<li><strong>Pixel (px) or rem value:</strong> if using rem, Penpot calculates the proportion relative to the font size and converts it to a multiplier.</li>
<li><strong>References:</strong> you can also reference <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> tokens.</li>
</ul>
<h4 id="apply-typography-token">Apply a Typography token</h4>
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
<h2 id="design-tokens-sets">Token Sets</h2>
<p>Token Sets allow you to split your tokens up into multiple files in order to create organized groups or collections of tokens. It enables efficient management and customization within design files. For example you can group all your color sets, sizing sets or platform-specific sets. The purpose of tokens sets is to organize them in a way that matches your needs.</p>
<figure>

View File

@@ -0,0 +1,134 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u3e5ffd68-2819-8084-8006-eb1c616a5afd",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Bug 12384",
"~:revn": 4,
"~:modified-at": "~m1761124840773",
"~:vern": 0,
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0004-clean-shadow-color",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects"
]
},
"~:version": 67,
"~:project-id": "~u3e5ffd68-2819-8084-8006-eb1c616e69bf",
"~:created-at": "~m1761123649876",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8"
],
"~:pages-index": {
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\"]]]",
"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7": "[\"~#shape\",[\"^ \",\"~:y\",250,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:width\",265,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",616,\"~:y\",250]],[\"^=\",[\"^ \",\"~:x\",881,\"~:y\",250]],[\"^=\",[\"^ \",\"~:x\",881,\"~:y\",494]],[\"^=\",[\"^ \",\"~:x\",616,\"~:y\",494]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:exports\",[[\"^ \",\"^:\",\"~:png\",\"~:suffix\",\"\",\"~:scale\",1]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",616,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",616,\"~:y\",250,\"^9\",265,\"~:height\",244,\"~:x1\",616,\"~:y1\",250,\"~:x2\",881,\"~:y2\",494]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^O\",244,\"~:flip-y\",null,\"~:shapes\",[\"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3\"]]]",
"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3": "[\"~#shape\",[\"^ \",\"~:y\",297.00000381469727,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",65,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",297.00000381469727]],[\"^<\",[\"^ \",\"~:x\",709.0000085830688,\"~:y\",297.00000381469727]],[\"^<\",[\"^ \",\"~:x\",709.0000085830688,\"~:y\",362.00000381469727]],[\"^<\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",362.00000381469727]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:r1\",0,\"~:id\",\"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3\",\"~:parent-id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:frame-id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:strokes\",[],\"~:x\",644.0000085830688,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",297.00000381469727,\"^8\",65,\"~:height\",65,\"~:x1\",644.0000085830688,\"~:y1\",297.00000381469727,\"~:x2\",709.0000085830688,\"~:y2\",362.00000381469727]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^N\",65,\"~:flip-y\",null]]"
}
},
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a8",
"~:name": "Page 1"
}
},
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("BUG 12359 - Selected invitations count is not pluralized", async ({
page,
}) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await dashboardPage.setupTeamInvitations();
await dashboardPage.goToSecondTeamInvitationsSection();
await expect(page.getByText("test1@mail.com")).toBeVisible();
// NOTE: we cannot use check() or getByLabel() because the checkbox
// is hidden inside the label.
await page.getByText("test1@mail.com").click();
await expect(page.getByText("1 invitation selected")).toBeVisible();
await page.getByText("test2@mail.com").check();
await expect(page.getByText("2 invitations selected")).toBeVisible();
});

View File

@@ -288,3 +288,50 @@ test("BUG 12287 Fix identical text fills not being added/removed", async ({
workspace.page.getByRole("button", { name: "#B1B2B5" }),
).toHaveCount(3);
});
test("BUG 12384 - Export crashing when exporting a board", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "design/get-file-12384.json");
let hasExportRequestBeenIntercepted = false;
await workspace.page.route("**/api/export", (route) => {
if (hasExportRequestBeenIntercepted) {
route.continue();
return;
}
hasExportRequestBeenIntercepted = true;
const payload = route.request().postData();
const parsedPayload = JSON.parse(payload);
expect(parsedPayload["~:exports"]).toHaveLength(1);
expect(parsedPayload["~:exports"][0]["~:file-id"]).toBe(
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
);
expect(parsedPayload["~:exports"][0]["~:page-id"]).toBe(
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8",
);
route.fulfill({
status: 200,
contentType: "application/json",
response: {},
});
});
await workspace.goToWorkspace({
fileId: "fa6ce865-34dd-80ac-8006-fe0dab5539a7",
pageId: "fa6ce865-34dd-80ac-8006-fe0dab5539a8",
});
await workspace.clickLeafLayer("Board");
let exportRequest = workspace.page.waitForRequest("**/api/export");
await workspace.rightSidebar
.getByRole("button", { name: "Export 1 element" })
.click();
await exportRequest;
});

View File

@@ -95,6 +95,24 @@ const setupTypographyTokensFile = async (page, options = {}) => {
});
};
const checkInputFieldWithError = async (tokenThemeUpdateCreateModal, inputLocator) => {
await expect(inputLocator).toHaveAttribute("aria-invalid", "true");
const errorMessageId = await inputLocator.getAttribute("aria-describedby");
await expect(
tokenThemeUpdateCreateModal.locator(`#${errorMessageId}`),
).toBeVisible();
};
const checkInputFieldWithoutError = async (tokenThemeUpdateCreateModal, inputLocator) => {
expect(
await inputLocator.getAttribute("aria-invalid")
).toBeNull();
expect(
await inputLocator.getAttribute("aria-describedby")
).toBeNull();
};
test.describe("Tokens: Tokens Tab", () => {
test("Clicking tokens tab button opens tokens sidebar tab", async ({
page,
@@ -806,18 +824,25 @@ test.describe("Tokens: Themes modal", () => {
})
.click();
await tokenThemeUpdateCreateModal
.getByLabel("Group")
.fill("New Group name");
await tokenThemeUpdateCreateModal
.getByLabel("Theme")
.fill("New Theme name");
const groupInput = tokenThemeUpdateCreateModal.getByLabel("Group");
const nameInput = tokenThemeUpdateCreateModal.getByLabel("Theme");
const saveButton = tokenThemeUpdateCreateModal.getByRole("button", {
name: "Save theme",
});
await groupInput.fill("Core"); // Invalid because "Core / Light" theme already exists
await nameInput.fill("Light");
await tokenThemeUpdateCreateModal
.getByRole("button", {
name: "Save theme",
})
.click();
await checkInputFieldWithError(tokenThemeUpdateCreateModal, nameInput);
await expect(saveButton).toBeDisabled();
await groupInput.fill("New Group name");
await nameInput.fill("New Theme name");
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
await expect(saveButton).not.toBeDisabled();
await saveButton.click();
await expect(
tokenThemeUpdateCreateModal.getByText("New Theme name"),
@@ -845,12 +870,36 @@ test.describe("Tokens: Themes modal", () => {
.first()
.click();
await tokenThemeUpdateCreateModal
.getByLabel("Theme")
.fill("Changed Theme name");
await tokenThemeUpdateCreateModal
.getByLabel("Group")
.fill("Changed Group name");
const groupInput = tokenThemeUpdateCreateModal.getByLabel("Group");
const nameInput = tokenThemeUpdateCreateModal.getByLabel("Theme");
const saveButton = tokenThemeUpdateCreateModal.getByRole("button", {
name: "Save theme",
});
await groupInput.fill("Core"); // Invalid because "Core / Dark" theme already exists
await nameInput.fill("Dark");
await checkInputFieldWithError(tokenThemeUpdateCreateModal, nameInput);
await expect(saveButton).toBeDisabled();
await groupInput.fill("Core"); // Valid because "Core / Light" theme already exists
await nameInput.fill("Light"); // but it's the same theme we are editing
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
await expect(saveButton).not.toBeDisabled();
await nameInput.fill("Changed Theme name"); // New names should be also valid
await groupInput.fill("Changed Group name");
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
await expect(saveButton).not.toBeDisabled();
expect(
await nameInput.getAttribute("aria-invalid")
).toBeNull();
expect(
await nameInput.getAttribute("aria-describedby")
).toBeNull();
const checkboxes = await tokenThemeUpdateCreateModal
.locator('[role="checkbox"]')
@@ -864,11 +913,15 @@ test.describe("Tokens: Themes modal", () => {
}
}
await tokenThemeUpdateCreateModal
.getByRole("button", {
name: "Save theme",
})
.click();
const firstButton = await tokenThemeUpdateCreateModal
.getByTestId('tokens-set-item')
.first();
await firstButton.click();
await expect(saveButton).not.toBeDisabled();
await saveButton.click();
await expect(
tokenThemeUpdateCreateModal.getByText("Changed Theme name"),

View File

@@ -150,8 +150,8 @@ test("User copy paste a variant container", async ({ page }) => {
await workspacePage.clickAt(500, 500);
await workspacePage.page.keyboard.press("Control+v");
const variant_original = await findVariant(workspacePage, 0);
const variant_duplicate = await findVariant(workspacePage, 1);
const variant_original = await findVariant(workspacePage, 1);
const variant_duplicate = await findVariant(workspacePage, 0);
// Expand the layers
await variant_duplicate.container.getByRole("button").first().click();

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# NOTE: this script should be called from the parent directory to
# properly work.
export CURRENT_VERSION=$1;
export BUILD_DATE=$(date -R);
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
export TS=$(date +%s);
export NODE_ENV=production;
echo "Current path:"
echo $PATH
set -ex
corepack enable;
corepack install || exit 1;
yarn install || exit 1;
yarn run build:storybook || exit 1;

View File

@@ -139,9 +139,9 @@
(fn [data]
(assoc file :data (d/removem (comp t/pointer? val) data))))))
(defn- check-libraries-synchronozation
(defn- check-libraries-synchronization
[file-id libraries]
(ptk/reify ::check-libraries-synchronozation
(ptk/reify ::check-libraries-synchronization
ptk/WatchEvent
(watch [_ state _]
(let [file (dsh/lookup-file state file-id)
@@ -154,7 +154,7 @@
libraries)]
(when needs-check?
(->> (rx/of (dwl/notify-sync-file file-id))
(->> (rx/of (dwl/notify-sync-file))
(rx/delay 1000)))))))
(defn- library-resolved
@@ -168,30 +168,32 @@
[file-id features]
(ptk/reify ::fetch-libries
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/concat
(rx/of (dwl/libraries-fetched file-id libraries))
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
(rx/mapcat resolve-file)
(rx/map library-resolved))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))
(rx/of (check-libraries-synchronozation file-id libraries))))))
(watch [_ _ stream]
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> (rx/concat
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/concat
(rx/of (dwl/libraries-fetched file-id libraries))
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
(rx/mapcat resolve-file)
(rx/map library-resolved))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))
(rx/of (check-libraries-synchronization file-id libraries))))))
;; This events marks that all the libraries have been resolved
(rx/of (ptk/data-event ::all-libraries-resolved))))))
;; This events marks that all the libraries have been resolved
(rx/of (ptk/data-event ::all-libraries-resolved)))
(rx/take-until stopper-s))))))
(defn- workspace-initialized
[file-id]

View File

@@ -848,6 +848,10 @@
index
0)
index (if index
index
(dec (count (dm/get-in page-objects [parent-id :shapes]))))
selected (if (and (ctl/flex-layout? page-objects parent-id) (not (ctl/reverse? page-objects parent-id)))
(into (d/ordered-set) (reverse selected))
selected)

View File

@@ -154,23 +154,8 @@
(transform-fill* state ids transform-attrs options))))
(defn swap-attrs [shape attr index new-index]
(let [first (get-in shape [attr index])
second (get-in shape [attr new-index])]
(-> shape
(assoc-in [attr index] second)
(assoc-in [attr new-index] first))))
(defn- swap-fills-index
[fills index new-index]
(let [first (get fills index)
second (get fills new-index)]
(-> fills
(assoc index second)
(assoc new-index first))))
(defn reorder-fills
[ids index new-index]
[ids from-pos to-space-between-pos]
(ptk/reify ::reorder-fills
ptk/WatchEvent
(watch [_ state _]
@@ -182,7 +167,7 @@
transform-attrs
(fn [object]
(update object :fills types.fills/update swap-fills-index index new-index))]
(update object :fills types.fills/update d/reorder from-pos to-space-between-pos))]
(rx/concat
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
@@ -515,22 +500,22 @@
{:attrs [:strokes]}))))))
(defn reorder-shadows
[ids index new-index]
[ids from-pos to-space-between-pos]
(ptk/reify ::reorder-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes
ids
#(swap-attrs % :shadow index new-index))))))
#(update % :shadow d/reorder from-pos to-space-between-pos))))))
(defn reorder-strokes
[ids index new-index]
[ids from-pos to-space-between-pos]
(ptk/reify ::reorder-strokes
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes
ids
#(swap-attrs % :strokes index new-index)
#(update % :strokes d/reorder from-pos to-space-between-pos)
{:attrs [:strokes]})))))
(defn picker-for-selected-shape

View File

@@ -1193,19 +1193,22 @@
(ctf/used-assets-changed-since file-data library sync-date))))))
(defn notify-sync-file
;; file-id is the id of the modified library
[file-id]
(dm/assert! (uuid? file-id))
"Notify the user that there are updates in the libraries used by the
current file, and ask if he wants to update them now."
[]
(ptk/reify ::notify-sync-file
ptk/WatchEvent
(watch [_ state _]
(let [file (dsh/lookup-file state (:current-file-id state))
(let [file-id (:current-file-id state)
file (dsh/lookup-file state file-id)
file-data (get file :data)
ignore-until (get file :ignore-sync-until)
libraries-need-sync
(filter #(seq (assets-need-sync % file-data ignore-until))
(vals (get state :files)))
(->> (vals (get state :files))
(filter #(= (:library-of %) file-id))
(filter #(seq (assets-need-sync % file-data ignore-until))))
do-more-info
#(modal/show! :libraries-dialog {:starting-tab "updates" :file-id file-id})

View File

@@ -191,30 +191,6 @@
(d/not-empty? position-data)
(assoc :position-data position-data))))
(defn update-grow-type
[shape old-shape]
(let [auto-width? (= :auto-width (:grow-type shape))
auto-height? (= :auto-height (:grow-type shape))
changed-width? (> (mth/abs (- (:width shape) (:width old-shape))) 0.1)
changed-height? (> (mth/abs (- (:height shape) (:height old-shape))) 0.1)
;; Check if the shape is in a flex layout context that might cause layout-driven changes
;; We should be more conservative about converting auto-width to fixed when the shape
;; is part of a layout system that could cause automatic resizing
has-layout-item-sizing? (or (:layout-item-h-sizing shape) (:layout-item-v-sizing shape))
;; Only convert auto-width to fixed if:
;; 1. For auto-width: both width AND height changed (indicating user manipulation, not layout)
;; 2. For auto-height: only height changed
;; 3. The shape is not in a layout context where automatic sizing changes are expected
change-to-fixed? (and (not has-layout-item-sizing?)
(or (and auto-width? changed-width? changed-height?)
(and auto-height? changed-height?)))]
(cond-> shape
change-to-fixed?
(assoc :grow-type :fixed))))
(defn- set-wasm-props!
[objects prev-wasm-props wasm-props]
(let [;; Set old value for previous properties
@@ -810,9 +786,7 @@
(-> shape
(gsh/transform-shape modifiers)
(cond-> (d/not-empty? pos-data)
(assoc-position-data pos-data shape))
(cond-> text-shape?
(update-grow-type shape)))))]
(assoc-position-data pos-data shape)))))]
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
@@ -857,23 +831,20 @@
(rx/empty))))))))
;; Pure function to determine next grow-type for text layers
(defn next-grow-type [current-grow-type resize-direction]
(defn next-grow-type
[current-grow-type scalev]
(cond
(= current-grow-type :fixed)
:fixed
(and (= resize-direction :horizontal)
(= current-grow-type :auto-width))
:auto-height
(and (= resize-direction :horizontal)
(= current-grow-type :auto-height))
:auto-height
(and (= resize-direction :vertical)
(and (not (mth/close? (:y scalev) 1.0))
(or (= current-grow-type :auto-width)
(= current-grow-type :auto-height)))
:fixed
(and (not (mth/close? (:x scalev) 1.0))
(= current-grow-type :auto-width))
:auto-height
:else
current-grow-type))

View File

@@ -331,4 +331,4 @@
(watch [_ state _]
(when (contains? (:files state) file-id)
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
(dwl/notify-sync-file file-id))))))
(dwl/notify-sync-file))))))

View File

@@ -463,7 +463,7 @@
library-data (dsh/lookup-file-data state file-id)
changes (-> (pcb/empty-changes it)
(cll/generate-duplicate-changes objects page ids delta libraries library-data file-id)
(cll/generate-duplicate-changes objects page ids delta libraries library-data file-id {:alt-duplication? alt-duplication?})
(cll/generate-duplicate-changes-update-indices objects ids))
tags (or (:tags changes) #{})

View File

@@ -235,13 +235,8 @@
[ids]
(ptk/reify ::remove-shape-layout
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
ids (->> ids
(remove #(->> %
(get objects)
(ctc/is-variant?))))
undo-id (js/Symbol)]
(watch [_ _ _]
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes ids #(apply dissoc % layout-keys))

View File

@@ -17,6 +17,7 @@
[app.common.math :as mth]
[app.common.types.fills :as types.fills]
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
@@ -581,12 +582,17 @@
shape
(cond-> shape
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
(and (or (not (ctl/any-layout-immediate-child? objects shape))
(not (ctl/fill-width? shape)))
(not-changed? shape-width new-width)
(= grow-type :auto-width))
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width new-width {:ignore-lock? true})))
shape
(cond-> shape
(and (not-changed? shape-height new-height)
(and (or (not (ctl/any-layout-immediate-child? objects shape))
(not (ctl/fill-height? shape)))
(not-changed? shape-height new-height)
(or (= grow-type :auto-height) (= grow-type :auto-width)))
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height new-height {:ignore-lock? true})))]
@@ -594,7 +600,8 @@
(let [ids (into #{} (filter changed-text?) (keys props))]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/update-shapes ids update-fn {:reg-objects? true
(dwsh/update-shapes ids update-fn {:with-objects? true
:reg-objects? true
:stack-undo? true
:ignore-touched true})
(ptk/data-event :layout/update {:ids ids})

View File

@@ -23,7 +23,7 @@
:error/fn #(tr "workspace.tokens.invalid-json")}
:error.import/invalid-token-name
{:error/code :error.import/invalid-json-data
{:error/code :error.import/invalid-token-name
:error/fn #(tr "workspace.tokens.invalid-json-token-name")
:error/detail #(tr "workspace.tokens.invalid-json-token-name-detail" %)}

View File

@@ -218,15 +218,10 @@
(gpt/add resize-origin displacement)
resize-origin)
;; Determine resize direction for grow-type logic
resize-direction (cond
(or (= handler :left) (= handler :right)) :horizontal
(or (= handler :top) (= handler :bottom)) :vertical
:else nil)
;; Calculate new grow-type for text layers
new-grow-type (when (cfh/text-shape? shape)
(dwm/next-grow-type (dm/get-prop shape :grow-type) resize-direction))
new-grow-type
(when (cfh/text-shape? shape)
(dwm/next-grow-type (dm/get-prop shape :grow-type) scalev))
;; When the horizontal/vertical scale a flex children with auto/fill
;; we change it too fixed
@@ -387,7 +382,19 @@
get-modifier
(fn [shape]
(ctm/change-dimensions-modifiers shape attr value))
(let [modifiers (ctm/change-dimensions-modifiers shape attr value)]
;; For text shapes, also update grow-type based on the resize
(if (cfh/text-shape? shape)
(let [{sr-width :width sr-height :height} (:selrect shape)
new-width (if (= attr :width) value sr-width)
new-height (if (= attr :height) value sr-height)
scalev (gpt/point (/ new-width sr-width) (/ new-height sr-height))
current-grow-type (dm/get-prop shape :grow-type)
new-grow-type (dwm/next-grow-type current-grow-type scalev)]
(cond-> modifiers
(not= new-grow-type current-grow-type)
(ctm/change-property :grow-type new-grow-type)))
modifiers)))
modif-tree
(-> (dwm/build-modif-tree ids objects get-modifier)

View File

@@ -99,9 +99,25 @@
(defn update-property-name
"Update the variant property name on the position pos
in all the components with this variant-id"
in all the components with this variant-id and remove the focus"
[variant-id pos new-name {:keys [trigger]}]
(ptk/reify ::update-property-name
ptk/UpdateEvent
(update [_ state]
(let [file-id (:current-file-id state)
data (dsh/lookup-file-data state)
objects (dsh/lookup-page-objects state)
related-components (cfv/find-variant-components data objects variant-id)]
(reduce
(fn [s related-component]
(update-in s
[:files file-id :data :components (:id related-component) :variant-properties]
(fn [props] (mapv #(with-meta % nil) props))))
state
related-components)))
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)

View File

@@ -84,6 +84,9 @@
(l/derived :shared-files st/state))
(defn select-libraries
"Find between all the given files, those who are libraries of the file-id.
Also include the file-id file itself.
Return a map of id -> library."
[files file-id]
(persistent!
(reduce-kv (fn [result id file]

View File

@@ -666,11 +666,12 @@
[:div {:class (stl/css :form-buttons-wrapper)}
[:> mentions-button*]
[:> button* {:variant "ghost"
:type "button"
:on-key-down handle-cancel
:on-click on-cancel}
(tr "ds.confirm-cancel")]
(when (some? on-cancel)
[:> button* {:variant "ghost"
:type "button"
:on-key-down handle-cancel
:on-click on-cancel}
(tr "ds.confirm-cancel")])
[:> button* {:variant "primary"
:type "button"
:on-key-down handle-submit
@@ -686,52 +687,39 @@
{::mf/props :obj
::mf/private true}
[{:keys [on-submit]}]
(let [show-buttons? (mf/use-state false)
content (mf/use-state "")
(let [content (mf/use-state "")
disabled? (or (blank-content? @content)
(exceeds-length? @content))
on-focus
on-cancel
(mf/use-fn
#(reset! show-buttons? true))
on-blur
(mf/use-fn
#(reset! show-buttons? false))
#(st/emit! :interrupt))
on-change
(mf/use-fn
#(reset! content %))
on-cancel
(mf/use-fn
#(do (reset! content "")
(reset! show-buttons? false)))
on-submit*
(mf/use-fn
(mf/deps @content)
(fn []
(on-submit @content)
(on-cancel)))]
(reset! content "")))]
[:div {:class (stl/css :form)}
[:> comment-input*
{:value @content
:placeholder (tr "labels.reply.thread")
:autofocus true
:on-blur on-blur
:on-focus on-focus
:on-ctrl-enter on-submit*
:on-change on-change}]
(when (exceeds-length? @content)
[:div {:class (stl/css :error-text)}
(tr "errors.character-limit-exceeded")])
(when (or @show-buttons? (seq @content))
[:> comment-form-buttons* {:on-submit on-submit*
:on-cancel on-cancel
:is-disabled disabled?}])]))
[:> comment-form-buttons* {:on-submit on-submit*
:on-cancel on-cancel
:is-disabled disabled?}]]))
(mf/defc comment-edit-form*
{::mf/private true}

View File

@@ -1057,16 +1057,17 @@
{:profile profile
:on-show-comments handle-show-comments}])]
(case sub-menu
:help-learning
[:> help-learning-menu* {:on-close close-sub-menu :on-click on-click}]
(when show-profile-menu?
(case sub-menu
:help-learning
[:> help-learning-menu* {:on-close close-sub-menu :on-click on-click}]
:community-contributions
[:> community-contributions-menu* {:on-close close-sub-menu}]
:community-contributions
[:> community-contributions-menu* {:on-close close-sub-menu}]
:about-penpot
[:> about-penpot-menu* {:on-close close-sub-menu}]
nil)]))
:about-penpot
[:> about-penpot-menu* {:on-close close-sub-menu}]
nil))]))
(mf/defc sidebar*
{::mf/props :obj

View File

@@ -690,18 +690,18 @@
[:div {:class (stl/css :table-row :table-row-invitations)}
[:div {:class (stl/css :table-field :field-email)}
[:div {:class (stl/css :input-wrapper)}
[:label {:for (str "email-" email)}
[:label
[:span {:class (stl/css-case :input-checkbox true
:global/checked (is-selected? email))}
deprecated-icon/status-tick]
[:input {:type "checkbox"
:id (str "email-" email)
:id (dm/str "email-" email)
:data-attr email
:value email
:checked (is-selected? email)
:on-change on-change}]]]
email]
:on-change on-change}]
email]]]
[:div {:class (stl/css :table-field :field-roles)}
[:> invitation-role-selector*
@@ -930,7 +930,7 @@
[:*
[:div {:class (stl/css :invitations-actions)}
[:div
(str (count @selected) " invitations selected")]
(tr "team.invitations-selected" (i18n/c (count @selected)))]
[:div
[:> button* {:variant "secondary"
:type "button"

View File

@@ -780,28 +780,19 @@
@extend .input-base;
height: auto;
}
// TODO: Fix this nested classes.
// FIXME: This does not conform to our CSS Guidelines. Need to unnest and to use
// custom properties to handle state changes.
.input-wrapper {
display: flex;
align-items: center;
@include t.use-typography("body-large");
label {
@include t.use-typography("body-small");
display: flex;
align-items: center;
gap: px2rem(6);
cursor: pointer;
color: var(--color-foreground-secondary);
span {
@extend .checkbox-icon;
}
input {
margin: 0;
}
&:hover {
span {
border-color: var(--color-accent-primary-muted);
}
}
color: var(--color-foreground-primary);
&:focus,
&:focus-within {
@@ -809,6 +800,22 @@
border-color: var(--color-accent-primary);
}
}
&:hover {
span {
border-color: var(--color-accent-primary-muted);
}
}
}
span {
@extend .checkbox-icon;
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
input {
margin: 0;
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
}

View File

@@ -19,6 +19,7 @@ $sz-36: px2rem(36);
$sz-40: px2rem(40);
$sz-48: px2rem(48);
$sz-88: px2rem(88);
$sz-96: px2rem(96);
$sz-120: px2rem(120);
$sz-154: px2rem(154);
$sz-160: px2rem(160);

View File

@@ -20,13 +20,15 @@
[:icon
[:and :string [:fn #(contains? icon-list %)]]]
[:aria-label :string]
[:tooltip-placement {:optional true}
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]
[:variant {:optional true}
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
(mf/defc icon-button*
{::mf/schema schema:icon-button
::mf/memo true}
[{:keys [class icon icon-class variant aria-label children] :rest props}]
[{:keys [class icon icon-class variant aria-label children tooltip-placement] :rest props}]
(let [variant
(d/nilv variant "primary")
@@ -47,6 +49,7 @@
:aria-labelledby tooltip-id})]
[:> tooltip* {:content aria-label
:placement tooltip-placement
:id tooltip-id}
[:> :button props
[:> icon* {:icon-id icon :aria-hidden true :class icon-class}]

View File

@@ -146,9 +146,12 @@
.viewer-tab-switcher {
--tabs-nav-padding-inline-start: 0;
--tabs-nav-padding-inline-end: var(--sp-m);
--max-inspect-tab-height: calc(100vh - 12rem);
/* same height as .element-options in workspace/sidebar/options.scss */
/* which is one of the parents of this component */
--max-inspect-tab-height: var(--sidebar-element-options-height);
max-block-size: var(--max-inspect-tab-height);
overflow: auto;
}

View File

@@ -29,6 +29,7 @@
[app.main.ui.releases.v2-0]
[app.main.ui.releases.v2-1]
[app.main.ui.releases.v2-10]
[app.main.ui.releases.v2-11]
[app.main.ui.releases.v2-2]
[app.main.ui.releases.v2-3]
[app.main.ui.releases.v2-4]
@@ -101,4 +102,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.10")))
(rc/render-release-notes (assoc params :version "2.11")))

View File

@@ -0,0 +1,189 @@
;; 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-11
(: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.11"
[{: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.11-slide-0.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Penpot 2.11 is here!"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats new in Penpot?"]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:span {:class (stl/css :feature-title)}
"Typography tokens take the stage!"]
[:p {:class (stl/css :feature-content)}
"This release brings one of our most anticipated design system upgrades yet: Typography tokens."]
[:p {:class (stl/css :feature-content)}
"But thats not all. Variants get a nice boost with multi-switching, new creation shortcuts, and draggable property reordering. Invitations are now easier to manage and the user menu has been reorganized. Now showing your current Penpot version and direct access to release info."]
[:p {:class (stl/css :feature-content)}
"And as always, youll notice performance improvements throughout. Faster, smoother, and just a bit more magical every time."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.11-typography-token.gif"
:class (stl/css :start-image)
:border "0"
:alt "Typography token: one token to rule your text"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Typography token: one token to rule your text"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Imagine having just one token to manage all your typography. With the new Typography token, you can create presets that bundle all your text styles (font, weight, size, line height, spacing, and more) into a single reusable definition. Just one clean, flexible token to keep your type consistent across your designs."]
[:p {:class (stl/css :feature-content)}
"The Typography token also marks a big step forward for Penpot: its our first composite token! Composite tokens are special because they can hold multiple properties within one token. Shadow token will be the next composite token coming your way."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[: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.11-variants.gif"
:class (stl/css :start-image)
:border "0"
:alt "Variants get a power-up"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Variants get a power-up"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Variants just got published and they already got a serious quality-of-life boost!"]
[:p {:class (stl/css :feature-content)}
"- Switch several variant copies at once: No more clicking through each one individually when you want to update a property. Just select multiple copies and change their values in one go — fast, smooth, and efficient."]
[:p {:class (stl/css :feature-content)}
"- New ways to create variants, right from the design viewport: No need to dig through menus. The new buttons make it super quick to spin up variant sets directly where youre working."]
[:p {:class (stl/css :feature-content)}
"- Reorder your component properties by drag & drop: Because organization matters, now you can arrange your properties however makes the most sense to you, so you can keep the ones you use most often right where you want them."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.11-invitations.gif"
:class (stl/css :start-image)
:border "0"
:alt "A smoother way to manage invitations"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"A smoother way to manage invitations"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The Invitations section just got a big usability upgrade for admins. Heres whats new:"]
[:p {:class (stl/css :feature-content)}
"Sorting - Organize invitations by role type or status to keep track of whos in and whos pending."]
[:p {:class (stl/css :feature-content)}
"Quicker actions - Main actions (resend and delete) are now visible upfront for quicker access."]
[:p {:class (stl/css :feature-content)}
"Bulk management - Select multiple invitations to resend or delete them all at once."]
[:p {:class (stl/css :feature-content)}
"Invited users will also get clearer emails, including a reminder sent one day before the invite expires (after seven days). Simple, clean, and much more efficient."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.11-menu.gif"
:class (stl/css :start-image)
:border "0"
:alt "User menu makeover"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"User menu makeover"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The user menu got a well-deserved cleanup. Options are now grouped into clear sections like Help & Learning and Community & Contributions, making navigation faster and easier."]
[:p {:class (stl/css :feature-content)}
"Youll also notice a handy new detail: the menu now shows your current Penpot version and gives you quick access to changelog information. This is especially useful for self-hosted setups that want to stay in sync with the latest updates. Simple, organized, and more informative."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))

View File

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

View File

@@ -16,7 +16,6 @@
[app.common.types.modifiers :as ctm]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.workspace.modifiers :as mdwm]
[app.main.data.workspace.texts :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
@@ -44,7 +43,6 @@
(gpt/point old-sr))]
(-> shape
(gsh/transform-shape (ctm/move modifiers deltav))
(mdwm/update-grow-type shape)
(dissoc :modifiers)))
shape))

View File

@@ -169,9 +169,9 @@
on-color-drag-start
(mf/use-fn
(mf/deps color file-id selected item-ref read-only?)
(mf/deps color file-id selected item-ref read-only? editing?)
(fn [event]
(if read-only?
(if (or read-only? editing?)
(dom/prevent-default event)
(cmm/on-asset-drag-start event file-id color selected item-ref :colors identity))))

View File

@@ -391,7 +391,7 @@
;; and the variant-container in which it will be restored still exists
(fn [shape]
(let [component (find-component shape true)
main (ctk/get-component-root component)
main (ctk/get-deleted-component-root component)
objects (dm/get-in libraries [(:component-file shape)
:data
:pages-index

View File

@@ -65,6 +65,7 @@
component-id (:id component)
visible? (h/use-visible item-ref :once? true)
renaming? (= renaming (:id component))
;; NOTE: we don't use reactive deref for it because we don't
;; really need rerender on any change on the file change. If
@@ -82,12 +83,13 @@
on-component-double-click
(mf/use-fn
(mf/deps file-id component is-local)
(mf/deps file-id component is-local renaming?)
(fn [event]
(dom/stop-propagation event)
(if is-local
(st/emit! (dwl/go-to-local-component :id component-id))
(st/emit! (dwl/go-to-component-file file-id component false)))))
(when-not renaming?
(if is-local
(st/emit! (dwl/go-to-local-component :id component-id))
(st/emit! (dwl/go-to-component-file file-id component false))))))
on-drop
(mf/use-fn
@@ -113,18 +115,16 @@
on-component-drag-start
(mf/use-fn
(mf/deps file-id component selected item-ref on-drag-start read-only? is-local)
(mf/deps file-id component selected item-ref on-drag-start read-only? renaming? is-local)
(fn [event]
(if read-only?
(if (or read-only? renaming?)
(dom/prevent-default event)
(cmm/on-asset-drag-start event file-id component selected item-ref :components on-drag-start))))
on-context-menu
(mf/use-fn
(mf/deps on-context-menu component-id)
(partial on-context-menu component-id))
renaming? (= renaming (:id component))]
(partial on-context-menu component-id))]
[:div {:ref item-ref
:class (stl/css-case :component-item true

View File

@@ -76,9 +76,9 @@
on-typography-drag-start
(mf/use-fn
(mf/deps typography file-id selected item-ref read-only?)
(mf/deps typography file-id selected item-ref read-only? renaming? open?)
(fn [event]
(if read-only?
(if (or read-only? renaming? open?)
(dom/prevent-default event)
(cmm/on-asset-drag-start event file-id typography selected item-ref :typographies identity))))

View File

@@ -54,7 +54,7 @@
modifiers (dm/get-in modifiers [shape-id :modifiers])
shape (gsh/transform-shape shape modifiers)
props (mf/spread-props props {:shape shape})]
props (mf/spread-props props {:shape shape :file-id file-id :page-id page-id})]
(case shape-type
:frame [:> frame/options* props]

View File

@@ -5,7 +5,6 @@
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.tool-window {
position: relative;
@@ -15,23 +14,26 @@
}
.tab-spacing {
margin-right: deprecated.$s-12;
margin-inline-end: var(--sp-m);
}
.content-class {
overflow-y: auto;
overflow-x: hidden;
height: calc(100vh - deprecated.$s-96);
height: calc(100vh - #{$sz-96});
scrollbar-gutter: stable;
}
.element-options {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
gap: var(--sp-s);
width: 100%;
height: calc(100vh - $sz-88);
padding-top: deprecated.$s-8;
/* FIXME: This is hacky and prone to break, we should tackle the whole layout
of the sidebar differently */
--sidebar-element-options-height: calc(100vh - #{$sz-88});
height: var(--sidebar-element-options-height);
padding-block-start: var(--sp-s);
}
.read-only {

View File

@@ -7,20 +7,20 @@
(ns app.main.ui.workspace.sidebar.options.common
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(mf/defc advanced-options [{:keys [visible? class children]}]
(mf/defc advanced-options*
[{:keys [class is-visible children]}]
(let [ref (mf/use-ref nil)]
(mf/use-effect
(mf/deps visible?)
(mf/deps is-visible)
(fn []
(when-let [node (mf/ref-val ref)]
(when visible?
(when is-visible
(dom/scroll-into-view-if-needed! node)))))
(when visible?
[:div {:class (dm/str class " " (stl/css :advanced-options-wrapper))
(when is-visible
[:div {:class [class (stl/css :advanced-options-wrapper)]
:ref ref}
children])))

View File

@@ -4,8 +4,8 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
.advanced-options-wrapper {
@include deprecated.flexColumn;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}

View File

@@ -7,7 +7,13 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
grid-column: span 8;
}
.title-spacing-blur {

View File

@@ -7,7 +7,13 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
grid-column: span 8;
}
.title-spacing-selected-colors {
@@ -25,6 +31,7 @@
}
.element-content {
grid-column: span 8;
@include deprecated.flexColumn;
margin-bottom: deprecated.$s-8;
}

View File

@@ -7,7 +7,13 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
grid-column: span 8;
}
.title-spacing-export {

View File

@@ -131,8 +131,8 @@
on-reorder
(mf/use-fn
(mf/deps ids)
(fn [new-index index]
(st/emit! (dc/reorder-fills ids index new-index))))
(fn [from-pos to-space-between-pos]
(st/emit! (dc/reorder-fills ids from-pos to-space-between-pos))))
on-remove
(mf/use-fn
@@ -211,13 +211,13 @@
(dom/set-attribute! checkbox "indeterminate" true)
(dom/remove-attribute! checkbox "indeterminate"))))
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
[:div {:class (stl/css :fill-section)}
[:div {:class (stl/css :fill-title)}
[:> title-bar* {:collapsable has-fills?
:collapsed (not open?)
:on-collapsed toggle-content
:title label
:class (stl/css-case :title-spacing-fill (not has-fills?))}
:class (stl/css-case :fill-title-bar (not has-fills?))}
(when (not (= :multiple fills))
[:> icon-button* {:variant "ghost"
@@ -228,11 +228,11 @@
:icon i/add}])]]
(when open?
[:div {:class (stl/css :element-content)}
[:div {:class (stl/css :fill-content)}
(cond
(= :multiple fills)
[:div {:class (stl/css :element-set-options-group)}
[:div {:class (stl/css :group-label)}
[:div {:class (stl/css :fill-multiple)}
[:div {:class (stl/css :fill-multiple-label)}
(tr "settings.multiple")]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.fill.remove-fill")
@@ -265,7 +265,7 @@
(when (or (= type :frame)
(and (= type :multiple)
(some? hide-on-export)))
[:div {:class (stl/css :checkbox)}
[:div {:class (stl/css :fill-checkbox)}
[:label {:for "show-fill-on-export"
:class (stl/css-case :global/checked (not hide-on-export))}
[:span {:class (stl/css-case :check-mark true

View File

@@ -4,44 +4,61 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
.fill-section {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
margin: 0;
.fill-title {
grid-column: span 8;
}
.title-spacing-fill {
padding-left: deprecated.$s-2;
margin: 0;
.fill-title-bar {
padding-inline-start: var(--sp-xxs);
}
.element-content {
.fill-content {
grid-column: span 8;
display: flex;
flex-direction: column;
gap: deprecated.$s-12;
margin: deprecated.$s-4 0 deprecated.$s-8 0;
gap: var(--sp-m);
margin: var(--sp-xs) 0 var(--sp-s) 0;
}
.element-set-options-group {
@include deprecated.flexRow;
.fill-multiple {
display: flex;
align-items: center;
gap: var(--sp-xs);
}
.group-label {
@extend .mixed-bar;
.fill-multiple-label {
@include t.use-typography("body-small");
display: flex;
align-items: center;
flex-grow: 1;
border-radius: $br-8;
block-size: $sz-32;
padding: var(--sp-s);
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
}
.checkbox {
.fill-checkbox {
// TODO create a checkbox component in the DS
@extend .input-checkbox;
padding-left: deprecated.$s-8;
padding-inline-start: var(--sp-s);
span.checked {
background-color: var(--input-border-color-active);
background-color: var(--color-accent-primary);
svg {
@extend .button-icon-small;
stroke: var(--input-details-color);
stroke: var(--color-background-primary);
}
}
}

View File

@@ -19,7 +19,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options*]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
@@ -190,9 +190,9 @@
:icon i/remove}]]]
(when (:display grid)
[:& advanced-options {:class (stl/css :grid-advanced-options)
:visible? open?
:on-close toggle-advanced-options}
[:> advanced-options* {:class (stl/css :grid-advanced-options)
:is-visible open?
:on-close toggle-advanced-options}
;; square
(when (= :square type)
[:div {:class (stl/css :square-row)}
@@ -316,16 +316,17 @@
#(st/emit! (dw/add-frame-grid id)))]
[:div {:class (stl/css :element-set)}
[:> title-bar* {:collapsable has-frame-grids?
:collapsed (not open?)
:on-collapsed toggle-content
:class (stl/css-case :title-spacing-board-grid (not has-frame-grids?))
:title (tr "workspace.options.guides.title")}
[:div {:class (stl/css :element-title)}
[:> title-bar* {:collapsable has-frame-grids?
:collapsed (not open?)
:on-collapsed toggle-content
:class (stl/css-case :title-spacing-board-grid (not has-frame-grids?))
:title (tr "workspace.options.guides.title")}
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.guides.add-guide")
:on-click handle-create-grid
:icon i/add}]]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.guides.add-guide")
:on-click handle-create-grid
:icon i/add}]]]
(when (and open? (seq frame-grids))
[:div {:class (stl/css :element-set-content)}

View File

@@ -7,7 +7,13 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
grid-column: span 8;
}
.title-spacing-board-grid {
@@ -17,6 +23,7 @@
.element-set-content {
@include deprecated.flexColumn;
grid-column: span 8;
margin: deprecated.$s-4 0 deprecated.$s-8 0;
}

View File

@@ -12,10 +12,21 @@
gap: deprecated.$s-16;
}
.grid-cell-menu {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.grid-cell-menu-title {
grid-column: span 8;
font-size: deprecated.$fs-11;
}
.grid-cell-menu-container {
grid-column: span 8;
}
.row {
@include deprecated.flexRow;
}

View File

@@ -7,8 +7,13 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
.element-title {
grid-column: span 8;
.title-spacing-layout {
padding-left: deprecated.$s-2;
margin: 0;

View File

@@ -528,6 +528,7 @@
:value (:height values)}]]])
[:> icon-button* {:variant "ghost"
:tooltip-placement "top-left"
:icon (if proportion-lock "lock" "unlock")
:class (stl/css-case :selected (true? proportion-lock))
:disabled (= proportion-lock :multiple)

View File

@@ -15,17 +15,12 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.main.ui.workspace.sidebar.options.rows.shadow-row :refer [shadow-row*]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -49,199 +44,6 @@
(filterv (fn [[idx _]] (not= idx index)))
(mapv second)))
(mf/defc shadow-entry*
[{:keys [index shadow is-open
on-reorder
on-toggle-open
on-detach-color
on-update
on-remove
on-toggle-visibility]}]
(let [shadow-style (:style shadow)
shadow-id (:id shadow)
hidden? (:hidden shadow)
on-drop
(mf/use-fn
(mf/deps on-reorder index)
(fn [_ data]
(on-reorder index (:index data))))
[dprops dref]
(h/use-sortable
:data-type "penpot/shadow-entry"
:on-drop on-drop
:detect-center? false
:data {:id (dm/str "shadow-" index)
:index index
:name (dm/str "Border row" index)})
on-remove
(mf/use-fn (mf/deps index) #(on-remove index))
on-update-offset-x
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-x value)))
on-update-offset-y
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-y value)))
on-update-spread
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :spread value)))
on-update-blur
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :blur value)))
on-update-color
(mf/use-fn
(mf/deps index on-update)
(fn [color]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :color color)))
on-detach-color
(mf/use-fn (mf/deps index) #(on-detach-color index))
on-style-change
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :style (keyword value))))
on-toggle-visibility
(mf/use-fn
(mf/deps index)
(fn []
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-toggle-visibility index)))
on-toggle-open
(mf/use-fn
(mf/deps shadow-id on-toggle-open)
#(on-toggle-open shadow-id))
type-options
(mf/with-memo []
[{:value "drop-shadow" :label (tr "workspace.options.shadow-options.drop-shadow")}
{:value "inner-shadow" :label (tr "workspace.options.shadow-options.inner-shadow")}])
on-open-row
(mf/use-fn #(st/emit! (dwu/start-undo-transaction :color-row)))
on-close-row
(mf/use-fn #(st/emit! (dwu/commit-undo-transaction :color-row)))]
[:div {:class (stl/css-case :global/shadow-option true
:shadow-element true
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))}
(when (some? on-reorder)
[:> reorder-handler* {:ref dref}])
[:*
[:div {:class (stl/css :basic-options)}
[:div {:class (stl/css-case :shadow-info true
:hidden hidden?)}
[:> icon-button* {:on-click on-toggle-open
:variant "secondary"
:disabled hidden?
:class (stl/css-case
:disabled hidden?
:more-options true
:selected is-open)
:aria-label "open more options"
:icon i/menu}]
[:div {:class (stl/css :type-select)}
[:& select
{:class (stl/css :shadow-type-select)
:default-value (d/name shadow-style)
:options type-options
:on-change on-style-change}]]]
[:div {:class (stl/css :actions)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.toggle-shadow")
:on-click on-toggle-visibility
:icon (if hidden? "hide" "shown")}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.remove-shadow")
:on-click on-remove
:icon i/remove}]]]
(when is-open
[:& advanced-options {:class (stl/css :shadow-advanced-options)
:visible? is-open
:on-close on-toggle-open}
[:div {:class (stl/css :first-row)}
[:div {:class (stl/css :offset-x-input)
:title (tr "workspace.options.shadow-options.offsetx")}
[:span {:class (stl/css :input-label)}
"X"]
[:> numeric-input* {:class (stl/css :numeric-input)
:no-validate true
:placeholder "--"
:on-change on-update-offset-x
:value (:offset-x shadow)}]]
[:div {:class (stl/css :blur-input)
:title (tr "workspace.options.shadow-options.blur")}
[:span {:class (stl/css :input-label)}
(tr "workspace.options.shadow-options.blur")]
[:> numeric-input* {:class (stl/css :numeric-input)
:no-validate true
:placeholder "--"
:on-change on-update-blur
:min 0
:value (:blur shadow)}]]
[:div {:class (stl/css :spread-input)
:title (tr "workspace.options.shadow-options.spread")}
[:span {:class (stl/css :input-label)}
(tr "workspace.options.shadow-options.spread")]
[:> numeric-input* {:class (stl/css :numeric-input)
:no-validate true
:placeholder "--"
:on-change on-update-spread
:value (:spread shadow)}]]]
[:div {:class (stl/css :second-row)}
[:div {:class (stl/css :offset-y-input)
:title (tr "workspace.options.shadow-options.offsety")}
[:span {:class (stl/css :input-label)}
"Y"]
[:> numeric-input* {:class (stl/css :numeric-input)
:no-validate true
:placeholder "--"
:on-change on-update-offset-y
:value (:offset-y shadow)}]]
[:> color-row* {:class (stl/css :shadow-color)
:color (:color shadow)
:title (tr "workspace.options.shadow-options.color")
:disable-gradient true
:disable-image true
:origin :shadow
:on-change on-update-color
:on-detach on-detach-color
:on-open on-open-row
:on-close on-close-row}]]])]]))
(def ^:private xf:add-index
(map-indexed (fn [index shadow]
(assoc shadow ::index index))))
@@ -279,10 +81,10 @@
handle-reorder
(mf/use-fn
(fn [new-index index]
(fn [from-pos to-space-between-pos]
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/reorder-shadows ids index new-index)))))
(st/emit! (dc/reorder-shadows ids from-pos to-space-between-pos)))))
on-add-shadow
(mf/use-fn
@@ -325,8 +127,8 @@
(-> shadow
(assoc attr value)
(ctss/check-shadow))))))))))]
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
[:div {:class (stl/css :shadow-section)}
[:div {:class (stl/css :shadow-title)}
[:> title-bar* {:collapsable has-shadows?
:collapsed (not show-content?)
:on-collapsed toggle-content
@@ -334,7 +136,7 @@
:multiple (tr "workspace.options.shadow-options.title.multiple")
:group (tr "workspace.options.shadow-options.title.group")
(tr "workspace.options.shadow-options.title"))
:class (stl/css-case :title-spacing-shadow (not has-shadows?))}
:class (stl/css-case :shadow-title-bar (not has-shadows?))}
(when-not (= :multiple shadows)
[:> icon-button* {:variant "ghost"
@@ -346,20 +148,20 @@
(when show-content?
(cond
(= :multiple shadows)
[:div {:class (stl/css :element-set-content)}
[:div {:class (stl/css :multiple-shadows)}
[:div {:class (stl/css :label)} (tr "settings.multiple")]
[:div {:class (stl/css :actions)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.remove-shadow")
:on-click on-remove-all
:icon i/remove}]]]]
[:div {:class (stl/css :shadow-content)}
[:div {:class (stl/css :shadow-multiple)}
[:div {:class (stl/css :shadow-multiple-label)}
(tr "settings.multiple")]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.remove-shadow")
:on-click on-remove-all
:icon i/remove}]]]
(some? shadows)
[:> h/sortable-container* {}
[:div {:class (stl/css :element-set-content)}
[:div {:class (stl/css :shadow-content)}
(for [{:keys [::index id] :as shadow} shadows]
[:> shadow-entry*
[:> shadow-row*
{:key (dm/str index)
:index index
:shadow shadow

View File

@@ -5,34 +5,39 @@
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/_utils.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography.scss" as t;
.element-set {
margin: 0;
.shadow-section {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.title-spacing-shadow {
margin: 0;
.shadow-title {
grid-column: span 8;
}
.shadow-title-bar {
padding-inline-start: var(--sp-xxs);
}
.element-set-content {
margin-block-start: var(--sp-xs);
.shadow-content {
grid-column: span 8;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
margin-block-start: var(--sp-xs);
}
.multiple-shadows {
.shadow-multiple {
display: flex;
align-items: center;
gap: var(--sp-xs);
}
.label {
.shadow-multiple-label {
@include t.use-typography("body-small");
display: flex;
align-items: center;
@@ -43,132 +48,3 @@
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
}
.actions {
display: grid;
grid-template-columns: subgrid;
grid-column: span 2;
}
.shadow-element {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
position: relative;
&:hover {
--reorder-icon-visibility: visible;
}
&.dnd-over-top {
--reorder-top-display: block;
}
&.dnd-over-bot {
--reorder-bottom-display: block;
}
}
.basic-options {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
gap: var(--sp-xs);
}
.shadow-info {
grid-column: span 6;
display: flex;
align-items: center;
gap: px2rem(1);
}
.type-select {
padding: 0;
border-radius: 0 $br-8 $br-8 0;
flex-grow: 1;
}
.shadow-type-select {
flex-grow: 1;
border-radius: 0 $br-8 $br-8 0;
}
// TODO: Remove these nested classes by using DS component
.hidden {
.type-select {
cursor: default;
pointer-events: none;
box-sizing: border-box;
color: var(--color-foreground-secondary);
stroke: var(--color-foreground-secondary);
background-color: transparent;
}
.shadow-type-select {
cursor: default;
pointer-events: none;
box-sizing: border-box;
color: var(--color-foreground-secondary);
stroke: var(--color-foreground-secondary);
background-color: transparent;
border: $b-1 solid var(--color-background-quaternary);
}
}
.shadow-advanced-options {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
gap: var(--sp-xs);
}
.first-row,
.second-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
.offset-x-input,
.blur-input,
.spread-input,
.offset-y-input {
// TODO remove this input by changing the input to DS component
@extend .input-element;
@include t.use-typography("body-small");
.input-label {
padding-inline-start: var(--sp-s);
inline-size: px2rem(60);
}
}
.offset-x-input {
grid-column: span 2;
}
.offset-y-input {
grid-column: span 2;
}
.blur-input {
grid-column: span 3;
}
.spread-input {
grid-column: span 3;
}
.shadow-color {
grid-column: span 6;
}
.more-options {
border-radius: $br-8 0 0 $br-8;
}
.selected {
--button-bg-color: var(--color-background-quaternary);
--button-fg-color: var(--color-accent-primary);
}
.disabled {
border: $b-1 solid var(--color-background-quaternary);
}

View File

@@ -18,7 +18,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.stroke-row :refer [stroke-row]]
[app.main.ui.workspace.sidebar.options.rows.stroke-row :refer [stroke-row*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
@@ -89,10 +89,9 @@
handle-reorder
(mf/use-fn
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/reorder-strokes ids index new-index)))))
(fn [from-pos to-space-between-pos]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/reorder-strokes ids from-pos to-space-between-pos))))
on-stroke-style-change
(mf/use-fn
@@ -177,13 +176,13 @@
:token token
:shape-ids ids}))))]
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
[:div {:class (stl/css :stroke-section)}
[:div {:class (stl/css :stroke-title)}
[:> title-bar* {:collapsable has-strokes?
:collapsed (not open?)
:on-collapsed toggle-content
:title label
:class (stl/css-case :title-spacing-stroke (not has-strokes?))}
:class (stl/css-case :stroke-title-bar (not has-strokes?))}
(when (not (= :multiple strokes))
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.stroke.add-stroke")
@@ -191,12 +190,12 @@
:icon i/add
:data-testid "add-stroke"}])]]
(when open?
[:div {:class (stl/css-case :element-content true
:empty-content (not has-strokes?))}
[:div {:class (stl/css-case :stroke-content true
:stroke-content-empty (not has-strokes?))}
(cond
(= :multiple strokes)
[:div {:class (stl/css :element-set-options-group)}
[:div {:class (stl/css :group-label)}
[:div {:class (stl/css :stroke-multiple)}
[:div {:class (stl/css :stroke-multiple-label)}
(tr "settings.multiple")]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.stroke.remove-stroke")
@@ -205,29 +204,29 @@
(seq strokes)
[:> h/sortable-container* {}
(for [[index value] (d/enumerate (:strokes values []))]
[:& stroke-row {:key (dm/str "stroke-" index)
:stroke value
:title (tr "workspace.options.stroke-color")
:index index
:shapes shapes
:objects objects
:show-caps show-caps
:on-color-change on-color-change
:on-color-detach on-color-detach
:on-stroke-width-change on-stroke-width-change
:on-stroke-style-change on-stroke-style-change
:on-stroke-alignment-change on-stroke-alignment-change
:open-caps-select open-caps-select
:close-caps-select close-caps-select
:on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch
:applied-tokens applied-tokens
:on-detach-token on-detach-token
:on-remove on-remove
:on-reorder (handle-reorder index)
:disable-drag disable-drag
:on-focus on-focus
:select-on-focus (not @disable-drag)
:on-blur on-blur
:disable-stroke-style disable-stroke-style}])])])]))
[:> stroke-row* {:key (dm/str "stroke-" index)
:stroke value
:title (tr "workspace.options.stroke-color")
:index index
:shapes shapes
:objects objects
:show-caps show-caps
:on-color-change on-color-change
:on-color-detach on-color-detach
:on-stroke-width-change on-stroke-width-change
:on-stroke-style-change on-stroke-style-change
:on-stroke-alignment-change on-stroke-alignment-change
:open-caps-select open-caps-select
:close-caps-select close-caps-select
:on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch
:applied-tokens applied-tokens
:on-detach-token on-detach-token
:on-remove on-remove
:on-reorder handle-reorder
:disable-drag disable-drag
:on-focus on-focus
:select-on-focus (not @disable-drag)
:on-blur on-blur
:disable-stroke-style disable-stroke-style}])])])]))

View File

@@ -4,35 +4,51 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
.element-set {
margin: 0;
.stroke-section {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
margin: 0;
.stroke-title {
grid-column: span 8;
}
.title-spacing-stroke {
padding-left: deprecated.$s-2;
margin: 0;
.stroke-title-bar {
padding-inline-start: var(--sp-xxs);
}
.element-content {
.stroke-content {
grid-column: span 8;
display: flex;
flex-direction: column;
gap: deprecated.$s-12;
margin: deprecated.$s-4 0 deprecated.$s-8 0;
&.empty-content {
gap: var(--sp-m);
margin: var(--sp-xs) 0 var(--sp-s) 0;
&.stroke-content-empty {
margin: 0;
}
}
.element-set-options-group {
@include deprecated.flexRow;
.stroke-multiple {
display: flex;
align-items: center;
gap: var(--sp-xs);
}
.group-label {
@extend .mixed-bar;
.stroke-multiple-label {
@include t.use-typography("body-small");
display: flex;
align-items: center;
flex-grow: 1;
border-radius: $br-8;
block-size: $sz-32;
padding: var(--sp-s);
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
}

View File

@@ -7,14 +7,17 @@
@use "refactor/common-refactor.scss" as deprecated;
.element-set {
margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
}
.element-title {
margin: 0;
grid-column: span 8;
}
.element-content {
grid-column: span 8;
@include deprecated.flexColumn;
margin-top: deprecated.$s-4;
}

View File

@@ -69,9 +69,9 @@
(mf/defc color-token-row*
{::mf/private true}
[{:keys [active-tokens color-token color on-swatch-click-token detach-token open-modal-from-token]}]
(let [;; `active-tokens` may be provided as a `delay` (lazy computation).
;; In that case we must deref it (`@active-tokens`) to force evaluation
;; and obtain the actual value. If its already realized (not a delay),
(let [;; `active-tokens` may be provided as a `delay` (lazy computation).
;; In that case we must deref it (`@active-tokens`) to force evaluation
;; and obtain the actual value. If its already realized (not a delay),
;; we just use it directly.
active-tokens (if (delay? active-tokens)
@active-tokens
@@ -313,8 +313,10 @@
on-drop
(mf/use-fn
(mf/deps on-reorder index)
(fn [_ data]
(on-reorder index (:index data))))
(fn [relative-pos data]
(let [from-pos (:index data)
to-space-between-pos (if (= relative-pos :bot) (inc index) index)]
(on-reorder from-pos to-space-between-pos))))
[dprops dref]
(if (some? on-reorder)
@@ -323,9 +325,7 @@
:on-drop on-drop
:disabled disable-drag
:detect-center? false
:data {:id (str "color-row-" index)
:index index
:name (str "Color row" index)})
:data {:index index})
[nil nil])
row-class
@@ -427,4 +427,4 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "settings.select-this-color")
:on-click handle-select
:icon i/move}])]))
:icon i/move}])]))

View File

@@ -0,0 +1,210 @@
;; 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.workspace.sidebar.options.rows.shadow-row
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.data.workspace :as dw]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options*]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc shadow-row*
[{:keys [index shadow is-open
on-reorder
on-toggle-open
on-detach-color
on-update
on-remove
on-toggle-visibility]}]
(let [shadow-style (:style shadow)
shadow-id (:id shadow)
hidden? (:hidden shadow)
on-drop
(mf/use-fn
(mf/deps on-reorder index)
(fn [relative-pos data]
(let [from-pos (:index data)
to-space-between-pos (if (= relative-pos :bot) (inc index) index)]
(on-reorder from-pos to-space-between-pos))))
[dprops dref]
(h/use-sortable
:data-type "penpot/shadow-entry"
:on-drop on-drop
:detect-center? false
:data {:index index})
on-remove
(mf/use-fn (mf/deps index) #(on-remove index))
on-update-offset-x
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-x value)))
on-update-offset-y
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-y value)))
on-update-spread
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :spread value)))
on-update-blur
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :blur value)))
on-update-color
(mf/use-fn
(mf/deps index on-update)
(fn [color]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :color color)))
on-detach-color
(mf/use-fn (mf/deps index) #(on-detach-color index))
on-style-change
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :style (keyword value))))
on-toggle-visibility
(mf/use-fn
(mf/deps index)
(fn []
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-toggle-visibility index)))
on-toggle-open
(mf/use-fn
(mf/deps shadow-id on-toggle-open)
#(on-toggle-open shadow-id))
type-options
(mf/with-memo []
[{:value "drop-shadow" :label (tr "workspace.options.shadow-options.drop-shadow")}
{:value "inner-shadow" :label (tr "workspace.options.shadow-options.inner-shadow")}])
on-open-row
(mf/use-fn #(st/emit! (dwu/start-undo-transaction :color-row)))
on-close-row
(mf/use-fn #(st/emit! (dwu/commit-undo-transaction :color-row)))]
[:div {:class (stl/css-case :global/shadow-option true
:shadow-element true
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))}
(when (some? on-reorder)
[:> reorder-handler* {:ref dref}])
[:*
[:div {:class (stl/css :shadow-basic)}
[:div {:class (stl/css :shadow-basic-info)}
[:> icon-button* {:variant "secondary"
:icon i/menu
:class (stl/css-case :shadow-basic-button true
:selected is-open)
:aria-label "open more options"
:disabled hidden?
:on-click on-toggle-open}]
[:& select {:class (stl/css :shadow-basic-select)
:default-value (d/name shadow-style)
:options type-options
:disabled hidden?
:on-change on-style-change}]]
[:div {:class (stl/css :shadow-basic-actions)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.toggle-shadow")
:on-click on-toggle-visibility
:icon (if hidden? "hide" "shown")}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.shadow-options.remove-shadow")
:on-click on-remove
:icon i/remove}]]]
(when is-open
[:> advanced-options* {:class (stl/css :shadow-advanced)
:is-visible is-open
:on-close on-toggle-open}
[:div {:class (stl/css :shadow-advanced-row)}
[:div {:class (stl/css :shadow-advanced-offset-x)
:title (tr "workspace.options.shadow-options.offsetx")}
[:span {:class (stl/css :shadow-advanced-label)}
"X"]
[:> numeric-input* {:no-validate true
:placeholder "--"
:on-change on-update-offset-x
:value (:offset-x shadow)}]]
[:div {:class (stl/css :shadow-advanced-blur)
:title (tr "workspace.options.shadow-options.blur")}
[:span {:class (stl/css :shadow-advanced-label)}
(tr "workspace.options.shadow-options.blur")]
[:> numeric-input* {:no-validate true
:placeholder "--"
:on-change on-update-blur
:min 0
:value (:blur shadow)}]]
[:div {:class (stl/css :shadow-advanced-spread)
:title (tr "workspace.options.shadow-options.spread")}
[:span {:class (stl/css :shadow-advanced-label)}
(tr "workspace.options.shadow-options.spread")]
[:> numeric-input* {:no-validate true
:placeholder "--"
:on-change on-update-spread
:value (:spread shadow)}]]]
[:div {:class (stl/css :shadow-advanced-row)}
[:div {:class (stl/css :shadow-advanced-offset-y)
:title (tr "workspace.options.shadow-options.offsety")}
[:span {:class (stl/css :shadow-advanced-label)}
"Y"]
[:> numeric-input* {:no-validate true
:placeholder "--"
:on-change on-update-offset-y
:value (:offset-y shadow)}]]
[:> color-row* {:class (stl/css :shadow-advanced-color)
:color (:color shadow)
:title (tr "workspace.options.shadow-options.color")
:disable-gradient true
:disable-image true
:origin :shadow
:on-change on-update-color
:on-detach on-detach-color
:on-open on-open-row
:on-close on-close-row}]]])]]))

View File

@@ -0,0 +1,112 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/_utils.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.shadow-element {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
position: relative;
&:hover {
--reorder-icon-visibility: visible;
}
&.dnd-over-top {
--reorder-top-display: block;
}
&.dnd-over-bot {
--reorder-bottom-display: block;
}
}
.shadow-basic {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
gap: var(--sp-xs);
}
.shadow-basic-info {
grid-column: span 6;
display: flex;
align-items: center;
gap: px2rem(1);
}
.shadow-basic-button {
border-radius: $br-8 0 0 $br-8;
&.selected {
--button-bg-color: var(--color-background-quaternary);
--button-fg-color: var(--color-accent-primary);
}
&:disabled {
border: $b-1 solid var(--color-background-quaternary);
}
}
.shadow-basic-select {
flex-grow: 1;
border-radius: 0 $br-8 $br-8 0;
}
.shadow-basic-actions {
display: grid;
grid-template-columns: subgrid;
grid-column: span 2;
}
.shadow-advanced {
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
gap: var(--sp-xs);
}
.shadow-advanced-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
.shadow-advanced-offset-x,
.shadow-advanced-blur,
.shadow-advanced-spread,
.shadow-advanced-offset-y {
// TODO remove this input by changing the input to DS component
@extend .input-element;
@include t.use-typography("body-small");
.shadow-advanced-label {
padding-inline-start: var(--sp-s);
inline-size: px2rem(60);
}
}
.shadow-advanced-offset-x {
grid-column: span 2;
}
.shadow-advanced-blur {
grid-column: span 3;
}
.shadow-advanced-spread {
grid-column: span 3;
}
.shadow-advanced-offset-y {
grid-column: span 2;
}
.shadow-advanced-color {
grid-column: span 6;
}

View File

@@ -14,14 +14,14 @@
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc stroke-row
{::mf/wrap-props false}
(mf/defc stroke-row*
[{:keys [index
stroke
title
@@ -47,19 +47,22 @@
objects]}]
(let [on-drop
(fn [_ data]
(on-reorder (:index data)))
(mf/use-fn
(mf/deps on-reorder index)
(fn [relative-pos data]
(let [from-pos (:index data)
to-space-between-pos (if (= relative-pos :bot) (inc index) index)]
(on-reorder from-pos to-space-between-pos))))
[dprops dref] (if (some? on-reorder)
(h/use-sortable
:data-type "penpot/stroke-row"
:on-drop on-drop
:disabled @disable-drag
:detect-center? false
:data {:id (str "stroke-row-" index)
:index index
:name (str "Border row" index)})
[nil nil])
[dprops dref]
(if (some? on-reorder)
(h/use-sortable
:data-type "penpot/stroke-row"
:on-drop on-drop
:disabled @disable-drag
:detect-center? false
:data {:index index})
[nil nil])
stroke-color-token (:stroke-color applied-tokens)
@@ -203,52 +206,41 @@
;; Stroke Width, Alignment & Style
[:div {:class (stl/css :stroke-options)}
[:div {:class (stl/css :stroke-width-input-element)
[:div {:class (stl/css :stroke-width-input)
:title (tr "workspace.options.stroke-width")}
[:span {:class (stl/css :icon)}
deprecated-icon/stroke-size]
[:> numeric-input*
{:min 0
:className (stl/css :stroke-width-input)
:value stroke-width
:placeholder (tr "settings.multiple")
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
:on-blur on-blur}]]
[:> icon* {:icon-id i/stroke-size
:size "s"}]
[:> numeric-input* {:value stroke-width
:min 0
:placeholder (tr "settings.multiple")
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
:on-blur on-blur}]]
[:div {:class (stl/css :select-wrapper :stroke-alignment-select)
[:div {:class (stl/css :stroke-alignment-select)
:data-testid "stroke.alignment"}
[:& select
{:default-value stroke-alignment
:options stroke-alignment-options
:on-change on-alignment-change}]]
[:& select {:default-value stroke-alignment
:options stroke-alignment-options
:on-change on-alignment-change}]]
(when-not disable-stroke-style
[:div {:class (stl/css :select-wrapper :stroke-style-select)
[:div {:class (stl/css :stroke-style-select)
:data-testid "stroke.style"}
[:& select
{:default-value stroke-style
:options stroke-style-options
:on-change on-style-change}]])]
[:& select {:default-value stroke-style
:options stroke-style-options
:on-change on-style-change}]])]
;; Stroke Caps
(when show-caps
[:div {:class (stl/css :stroke-caps-options)}
[:div {:class (stl/css :cap-select)}
[:& select
{:default-value (:stroke-cap-start stroke)
:dropdown-class (stl/css :stroke-cap-dropdown-start)
:options stroke-caps-options
:on-change on-caps-start-change}]]
[:button {:class (stl/css :swap-caps-btn)
:on-click on-cap-switch}
deprecated-icon/switch]
[:div {:class (stl/css :cap-select)}
[:& select
{:default-value (:stroke-cap-end stroke)
:dropdown-class (stl/css :stroke-cap-dropdown)
:options stroke-caps-options
:on-change on-caps-end-change}]]])]))
[:& select {:default-value (:stroke-cap-start stroke)
:options stroke-caps-options
:on-change on-caps-start-change}]
[:> icon-button* {:variant "secondary"
:aria-label (tr "labels.switch")
:on-click on-cap-switch
:icon i/switch}]
[:& select {:default-value (:stroke-cap-end stroke)
:options stroke-caps-options
:on-change on-caps-end-change}]])]))

View File

@@ -4,10 +4,13 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
.stroke-data {
@include deprecated.flexColumn;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
position: relative;
@@ -31,46 +34,28 @@
align-items: center;
grid-template-columns: repeat(8, var(--sp-xxxl));
gap: var(--sp-xs);
.stroke-width-input-element {
@extend .input-element;
@include deprecated.bodySmallTypography;
grid-column: span 2;
}
.stroke-alignment-select {
grid-column: span 3;
}
.stroke-style-select {
grid-column: span 3;
}
}
.stroke-width-input {
grid-column: span 2;
// TODO replace with numeric-input* from DS
@extend .input-element;
@include t.use-typography("body-small");
padding-inline-start: var(--sp-xs);
}
.stroke-alignment-select {
grid-column: span 3;
}
.stroke-style-select {
grid-column: span 3;
}
.stroke-caps-options {
display: grid;
--input-width: calc(var(--sp-xxxl) * 3.5 + 3 * var(--sp-xs) - var(--sp-xs) / 2);
grid-template-columns: var(--input-width) var(--sp-xxxl) var(--input-width);
gap: var(--sp-xs);
}
.stroke-cap-dropdown,
.stroke-cap-dropdown-start {
min-width: deprecated.$s-124;
width: fit-content;
max-width: deprecated.$s-252;
right: 0;
left: unset;
}
.stroke-cap-dropdown-start {
left: 0;
right: unset;
}
.swap-caps-btn {
@extend .button-secondary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
svg {
@extend .button-icon;
}
grid-template-columns: 1fr auto 1fr;
column-gap: var(--sp-xs);
}

View File

@@ -501,7 +501,7 @@
[:> color-selection-menu*
{:file-id file-id
:type type
:shapes shapes
:shapes (vals objects)
:libraries libraries}])
(when-not (empty? shadow-ids)

View File

@@ -113,7 +113,7 @@
:data {:id id
:index index
:name (:name page)}
:draggable? (not read-only?))
:draggable? (and (not read-only?) (not editing?)))
on-context-menu
(mf/use-fn

View File

@@ -29,7 +29,6 @@
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
@@ -51,41 +50,6 @@
[malli.error :as me]
[rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(def valid-token-name-regexp
"Only allow letters and digits for token names.
Also allow one `.` for a namespace separator.
Caution: This will allow a trailing dot like `token-name.`,
But we will trim that in the `finalize-name`,
to not throw too many errors while the user is editing."
#"(?!\$)([a-zA-Z0-9-$_]+\.?)*")
(def valid-token-name-schema
(m/-simple-schema
{:type :token/invalid-token-name
:pred #(re-matches valid-token-name-regexp %)
:type-properties {:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))}}))
(defn token-name-schema
"Generate a dynamic schema validation to check if a token path derived from the name already exists at `tokens-tree`."
[{:keys [tokens-tree]}]
(let [path-exists-schema
(m/-simple-schema
{:type :token/name-exists
:pred #(not (cft/token-name-path-exists? % tokens-tree))
:type-properties {:error/fn #(str "A token already exists at the path: " (:value %))}})]
(m/schema
[:and
[:string {:min 1 :max 255}]
valid-token-name-schema
path-exists-schema])))
(def token-description-schema
(m/schema
[:string {:max 2048}]))
;; Helpers ---------------------------------------------------------------------
(defn finalize-name [name]
@@ -103,7 +67,53 @@
(defn valid-value? [value]
(seq (finalize-value value)))
;; Validation ------------------------------------------------------------------
;; Schemas ---------------------------------------------------------------------
(def ^:private well-formed-token-name-regexp
"Only allow letters and digits for token names.
Also allow one `.` for a namespace separator.
Caution: This will allow a trailing dot like `token-name.`,
But we will trim that in the `finalize-name`,
to not throw too many errors while the user is editing."
#"(?!\$)([a-zA-Z0-9-$_]+\.?)*")
(def ^:private well-formed-token-name-schema
(m/-simple-schema
{:type :token/invalid-token-name
:pred #(re-matches well-formed-token-name-regexp %)
:type-properties {:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))}}))
(defn- token-name-schema
"Generate a dynamic schema validation to check if a token path derived from the name already exists at `tokens-tree`."
[{:keys [tokens-tree]}]
(let [path-exists-schema
(m/-simple-schema
{:type :token/name-exists
:pred #(not (cft/token-name-path-exists? % tokens-tree))
:type-properties {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}})]
(m/schema
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
well-formed-token-name-schema
path-exists-schema])))
(defn- validate-token-name
[tokens-tree name]
(let [schema (token-name-schema {:tokens-tree tokens-tree})
validation (m/explain schema (finalize-name name))]
(me/humanize validation)))
(def ^:private token-description-schema
(m/schema
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]))
(defn- validate-token-description
[description]
(let [validation (m/explain token-description-schema description)]
(me/humanize validation)))
;; Value Validation -------------------------------------------------------------
(defn check-empty-value [token-value]
(when (empty? (str/trim token-value))
@@ -330,33 +340,26 @@
warning-name-change? (deref warning-name-change*)
token-name-ref (mf/use-var (:name token))
name-ref (mf/use-ref nil)
name-errors (mf/use-state nil)
validate-name
(mf/use-fn
(mf/deps tokens-tree-in-selected-set)
(fn [value]
(let [schema (token-name-schema {:token token
:tokens-tree tokens-tree-in-selected-set})]
(m/explain schema (finalize-name value)))))
name-errors* (mf/use-state nil)
name-errors (deref name-errors*)
on-blur-name
(mf/use-fn
(mf/deps touched-name? warning-name-change?)
(mf/deps touched-name? warning-name-change? tokens-tree-in-selected-set)
(fn [e]
(let [value (dom/get-target-val e)
errors (validate-name value)]
errors (validate-token-name tokens-tree-in-selected-set value)]
(when touched-name?
(reset! warning-name-change* true))
(reset! name-errors errors))))
(reset! name-errors* errors))))
on-update-name-debounced
(mf/use-fn
(mf/deps touched-name? validate-name)
(mf/deps touched-name? tokens-tree-in-selected-set)
(uf/debounce (fn [token-name]
(let [errors (validate-name token-name)]
(let [errors (validate-token-name tokens-tree-in-selected-set token-name)]
(when touched-name?
(reset! name-errors errors))))
(reset! name-errors* errors))))
300))
on-update-name
@@ -370,7 +373,7 @@
(on-update-name-debounced token-name))))
valid-name-field? (and
(not @name-errors)
(not name-errors)
(valid-name? @token-name-ref))
;; Value
@@ -430,19 +433,20 @@
description-errors* (mf/use-state nil)
description-errors (deref description-errors*)
validate-descripion (mf/use-fn #(m/explain token-description-schema %))
on-update-description-debounced (mf/use-fn
(uf/debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-descripion value)]
(reset! description-errors* errors)))))
on-update-description-debounced
(mf/use-fn
(uf/debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-token-description value)]
(reset! description-errors* errors)))))
on-update-description
(mf/use-fn
(mf/deps on-update-description-debounced)
(fn [e]
(reset! description-ref (dom/get-target-val e))
(on-update-description-debounced e)))
valid-description-field? (not description-errors)
valid-description-field? (empty? description-errors)
;; Form
disabled? (or (not valid-name-field?)
@@ -451,7 +455,7 @@
on-submit
(mf/use-fn
(mf/deps is-create validate-name validate-descripion token active-theme-tokens validate-token)
(mf/deps is-create tokens-tree-in-selected-set token active-theme-tokens validate-token)
(fn [e]
(dom/prevent-default e)
;; We have to re-validate the current form values before submitting
@@ -460,13 +464,13 @@
;; and press enter before the next validations could return.
(let [final-name (finalize-name @token-name-ref)
valid-name? (try
(not (:errors (validate-name final-name)))
(empty? (:errors (validate-token-name tokens-tree-in-selected-set final-name)))
(catch js/Error _ nil))
value (mf/ref-val value-ref)
final-description @description-ref
valid-description? (if final-description
(try
(not (:errors (validate-descripion final-description)))
(empty? (:errors (validate-token-description final-description)))
(catch js/Error _ nil))
true)]
(when (and valid-name? valid-description?)
@@ -560,21 +564,12 @@
:variant "comfortable"
:auto-focus true
:default-value @token-name-ref
:hint-type (when (seq (:errors @name-errors)) "error")
:hint-type (when-not (empty? name-errors) "error")
:hint-message (first name-errors)
:ref name-ref
:on-blur on-blur-name
:on-change on-update-name}])
(for [error (->> (:errors @name-errors)
(map #(-> (assoc @name-errors :errors [%])
(me/humanize)))
(map first))]
[:> hint-message* {:key error
:message error
:type "error"
:id "token-name-hint"}])
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
@@ -611,6 +606,8 @@
:max-length max-input-length
:variant "comfortable"
:default-value @description-ref
:hint-type (when-not (empty? description-errors) "error")
:hint-message (first description-errors)
:on-blur on-update-description
:on-change on-update-description}]]
@@ -1032,7 +1029,7 @@
:font-size
{:label "Font Size"
:icon i/text-font-size
:placeholder (tr "workspace.tokens.token-value-enter")}
:placeholder (tr "workspace.tokens.font-size-value-enter")}
:font-weight
{:label "Font Weight"
:icon i/text-font-weight
@@ -1162,4 +1159,4 @@
:text-case [:> text-case-form* props]
:text-decoration [:> text-decoration-form* props]
:font-weight [:> font-weight-form* props]
[:> form* props])))
[:> form* props])))

View File

@@ -162,7 +162,7 @@
:data {:index index
:is-group true}
:detect-center? true
:draggable? is-draggable)]
:draggable? (and is-draggable (not is-editing)))]
[:div {:ref dref
:data-testid "tokens-set-group-item"
@@ -271,7 +271,7 @@
:on-drop on-drop
:data {:index index
:is-group false}
:draggable? is-draggable)
:draggable? (and is-draggable (not is-editing)))
drop-over
(get dprops :over)]

View File

@@ -31,9 +31,34 @@
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(defn- theme-name-schema
"Generate a dynamic schema validation to check if a theme path derived from the name already exists at `tokens-tree`."
[{:keys [group theme-id tokens-lib]}]
(m/-simple-schema
{:type :token/name-exists
:pred (fn [name]
(if tokens-lib
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme)
(= (ctob/get-id theme) theme-id)))
true)) ;; if still no library exists, cannot be duplicate
:type-properties {:error/fn #(tr "workspace.tokens.theme-name-already-exists")}}))
(defn- validate-theme-name
[tokens-lib group theme-id name]
(let [schema (theme-name-schema {:tokens-lib tokens-lib :theme-id theme-id :group group})
validation (m/explain schema (str/trim name))]
(me/humanize validation)))
;; Form Component --------------------------------------------------------------
(mf/defc empty-themes
[{:keys [change-view]}]
(let [create-theme
@@ -166,25 +191,36 @@
(mf/defc theme-inputs*
[{:keys [theme on-change-field]}]
(let [theme-groups (mf/deref refs/workspace-token-theme-groups)
(let [tokens-lib (mf/deref refs/tokens-lib)
theme-groups (mf/deref refs/workspace-token-theme-groups)
theme-name-ref (mf/use-ref (:name theme))
options (map (fn [group]
{:label group
:id group})
theme-groups)
options (map (fn [group]
{:label group
:id group})
theme-groups)
current-group* (mf/use-state (:group theme))
current-group (deref current-group*)
name-errors* (mf/use-state nil)
name-errors (deref name-errors*)
on-update-group
(mf/use-fn
(mf/deps on-change-field)
#(on-change-field :group %))
(fn [value]
(reset! current-group* value)
(on-change-field :group value)))
on-update-name
(mf/use-fn
(mf/deps on-change-field)
(mf/deps on-change-field tokens-lib current-group)
(fn [event]
(let [value (-> event dom/get-target dom/get-value)]
(on-change-field :name value)
(mf/set-ref-val! theme-name-ref value))))]
(let [value (-> event dom/get-target dom/get-value)
errors (validate-theme-name tokens-lib current-group (ctob/get-id theme) value)]
(reset! name-errors* errors)
(mf/set-ref-val! theme-name-ref value)
(if (empty? errors)
(on-change-field :name value)
(on-change-field :name "")))))]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
[:div {:class (stl/css :group-input-wrapper)}
@@ -202,6 +238,8 @@
:variant "comfortable"
:default-value (mf/ref-val theme-name-ref)
:auto-focus true
:hint-type (when-not (empty? name-errors) "error")
:hint-message (first name-errors)
:on-change on-update-name}]]]))
(mf/defc theme-modal-buttons*
@@ -319,7 +357,8 @@
(mf/use-fn
(mf/deps on-toggle-token-set)
(fn [set-id]
(on-toggle-token-set set-id)))]
(let [set (ctob/get-set lib set-id)]
(on-toggle-token-set (ctob/get-name set)))))]
[:div {:class (stl/css :themes-modal-wrapper)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}

View File

@@ -34,7 +34,6 @@
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
max-height: deprecated.$s-688;
}
.edit-theme-form {

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