24715 Commits

Author SHA1 Message Date
elhombretecla
1cce70f408 ✨ Add scale options panel to the right sidebar 2026-11-12 15:26:52 +01:00
Luis de Dios
185fef889b
🐛 Fix collapsing token set groups after reordering (#12671)
* 🐛 Fix collapsing token set groups after reordering

The handler captured the first render path, and the tree is keyed by
index, so after a reorder it toggled the path of another group.

Closes #12625

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Refresh double click handler of token set groups

Add the start edition callback to the hook deps so the handler does
not keep the initial closure.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add regression test for folding reordered token set groups

Reorder a group in the sets tree, fold it, and assert that only that
group collapses. The tree is keyed by index, so the test also proves
the collapse handler does not keep a stale path after a reorder.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Refresh double click handler of token set nodes

Add the start edition callback to the hook deps so the handler does not
keep the initial closure.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Key token set tree nodes by identity instead of index

React reuses a node by its key. Keyed by the tree index, a reordered
group moved the instance to another node and its handlers kept the
previous paths. Groups are now keyed by path and sets by id, so each
node keeps its own handlers.

The set being created has no persisted id yet, so the tree walk
generates a new one on every render. Keep one uuid for its whole
creation instead.

AI-assisted-by: deepseek-v4.1-flash
2026-11-12 13:49:43 +01:00
Miguel de Benito Delgado
ef725c0cc2
🔧 Enable checking commit messages from files (#12951)
Adds a new flag --message-file to check-commit

AI-assisted-by: deepseek-flash
2026-11-12 13:28:35 +01:00
Eva Marco
d829e47a04
🔧 Run the Playwright tests with a dark color scheme (#12989)
* 🔧 Run the Playwright tests with a dark color scheme

Since #12480 a profile without a theme follows the system one, and
headless Chrome reports a light scheme, so the e2e tests ran with the
light UI. Set colorScheme to dark in the shared Playwright options so
the tests use the default dark UI again. Tests that need another scheme
still set it with page.emulateMedia.

AI-assisted-by: claude-opus-5-5

* 🔧 Drop the dark color scheme overrides now set for every test

The shared Playwright options now set the dark color scheme, so the ds
project override and the emulateMedia call in the selection size badge
test do nothing.

AI-assisted-by: claude-opus-5-5
2026-11-12 13:27:52 +01:00
penpot-renovate[bot]
88e1b29e5c
⬆️ Update DHI base images (#13008)
- dhi.io/debian-base trixie-debian13@c12f605 (docker/imagemagick/Dockerfile)
- dhi.io/nginx 1.31.8-debian13-dev@a5fc6ab (docker/images/Dockerfile.frontend)
- dhi.io/node 24.22.2-debian13-dev@8a0d16b (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.2-debian13@fb9c8a5 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@8a0d16b (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@8a0d16b (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-12 13:10:05 +01:00
JiMyung Lee
61154e6bfe
✨ Keep the swap component search when changing library (#12873)
Changing the library in the Swap component menu cleared the search
field, so checking several connected libraries for the same component
meant typing the name again for each one. Keep the term and reset only
the library id and the group path, so the new library is shown already
filtered.

Closes #12335

AI-assisted-by: claude-opus-5-5

Signed-off-by: JiMyung Lee <lee.ji.myung@gmail.com>
2026-11-12 12:45:36 +01:00
Belén Albeza
86a8ac34cd
🎉 Add basic bullet lists to the v3 text editor (#12720)
* 🎉 Add UI for toggling bullet lists (under config flag)

* 🎉 Store list-style-type as a paragraph attribute

The bullet list toggle now saves list-style-type ("none" or "disc") on
the selected paragraphs and reads it back. A paragraph without the attr
counts as "none", so a bullet paragraph next to an older one shows as
mixed.

WASM keeps the value on each paragraph: the paragraph bytes grow from
12 to 16, Enter keeps it on the new paragraph, and the v3 editor reports
it in the current styles, so the sidebar shows it while editing too.

Refs #12630

AI-assisted-by: claude-opus-5-5

* ✨ Draw bullets for list paragraphs

A paragraph with list-style-type "disc" now gets a solid round bullet,
sized as Chrome draws a CSS disc marker (measured with Source Sans Pro):
a radius of 0.16 font sizes, centered 0.32 font sizes above the first
baseline, with a gap of 0.33 font sizes before the text. The bullet
hangs outside the text box, before its start edge (after it for RTL),
like CSS list-style-position: outside, so the text layout, caret and
selection stay as they were.

The bullet takes the paint of its line's first style run, so fills,
strokes, shadows and masks apply to it as they do to the glyphs. An
empty item, which has no run, borrows the previous item's paint. The
cached paint, the text extrect and text-to-path share one walker over
the paragraphs, so they always agree: the extrect grows to hold the
bullets, like shadows and blur, and flattening a text keeps them.

Refs #12630

AI-assisted-by: claude-opus-5-5

* ✨ Show the list style in Inspect

Text paragraphs with list-style-type "disc" now show a "List Style" row
in the Inspect Styles and Computed views, after Text Transform. Its copy
action gives "list-style-type: disc;". Text without a list shows no row.
The list item check lives in common as list-paragraph?, so every place
that needs it shares one rule.

Refs #12630

AI-assisted-by: claude-opus-5-5

* ✨ Generate list HTML and CSS for bullet paragraphs

In the Inspect code, neighbouring paragraphs with list-style-type
"disc" become one <ul> with an <li> each, keeping the paragraph's class
and dir. Other paragraphs stay <p>. Only code mode groups them; text
rendering still uses <p>.

The CSS sets list-style-type on the items and resets the list's margin
and padding, so the browser hangs the bullet before the text as the
canvas does. Paragraph rules set font-size: 0, so each item also gets a
::marker rule with its first span's font size and text color. A
::marker can't paint a gradient, so a gradient fill gives its first
stop.

Refs #12630

AI-assisted-by: claude-opus-5-5
2026-11-12 11:50:05 +01:00
Belén Albeza
00e77a0f53
🐛 Fix slow variant switch with the WASM renderer (#12985)
Changing a component inside a big main component made the WASM renderer
draw that component's thumbnail at full size on the main thread, before
the viewport redraw. On the Fauna file the 2140x1642 thumbnail took about
1.2 s to render and PNG-encode, so the switch showed up late.

- Fit the thumbnail's longest side between 200 and 512 px. Small
  components still scale up; big ones now scale down.
- Draw the thumbnail only on a frame with no viewport render pending

Closes #12968

AI-assisted-by: claude-opus-5-5
2026-11-12 11:49:01 +01:00
Miguel de Benito Delgado
756541939a
🔧 Add renderer benchmark runner (#12756)
Add a CLI under frontend/performance/benches/render_wasm that builds the
renderer, runs seeded scenes (rects, plus, paths, masks) in headless
Chromium and saves per-case timings.

- build, run: prepare or reuse builds; measure cases with warmups,
  repetitions, deadlines
- ab: build and compare Cargo feature configurations
- compare, merge: median changes with bootstrap CIs between saved runs,
  guarded by scene, build and settings checks
- purge: delete managed builds, runs or comparisons
- GPU runs through `run-devenv --gpu`

Lint, format and pnpm scripts now cover performance/ and its tests.

AI-assisted-by: gpt-6.1-sol, deepseek-flash, muse-spark, glm-5.3, gpt6-astra, claude-5.5-sonnet, claude-haiku-5.5
2026-11-12 11:02:34 +01:00
Andrey Antukh
8b9558b380
♻️ Reduce and speed up the frontend e2e token suite (#13001)
* ✨ Add unit tests for the token name schema

Cover make-token-name-schema, the schema that rejects a token name whose
path clashes with an existing token or group. The lower-level
token-name-path-exists? already had a test; the schema that composes it
did not.

The cases pin an exact duplicate, a token inside a token, a token name
that collides with a group, an existing child path, valid siblings, the
fail-closed nil tree, invalid syntax and the length limit. This is the
unit-test base for reducing the 15 group-name-clash e2e cases to two.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Remove duplicate token tests and fix the spread step

The three top-level tests duplicated the ones inside the creation
describe; only the indentation differed. Keep the originals.

The shadow token test labelled "Valid spread" filled the blur field, so
the token under test ended with blur 0 and the spread never changed.
Fill spread instead and expect the referenced token to resolve with
Blur: 4.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Reduce the token group-name-clash e2e matrix to two tests

The fifteen cases repeated the same setup and only changed the token
type and value. The rule is now covered by the token name schema unit
test, so keep two representatives: thirteen simple types in one test
with a test.step each, and the two composite forms (Typography and
Shadow) in a second test.

The helper no longer re-runs the file setup, so each test loads the
page once: 15 page loads become 2 and the block drops from 433 s to
about 21 s.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Cover the numeric token creation types in one test

The nine creation tests repeated the same validation ladder and only
changed the token label and placeholder. Run the ladder once in the
border radius step, keep the reference token creation, and create the
other eight types with a test.step each.

The new helpers split the flow: open the form, assert the ladder, save
a token. The e2e drops from nine page loads to one.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Reduce the remapping e2e spec to three tests

Keep one remapping flow per token shape: box shadow with alias
references, border radius with a value change, and a group rename with
undo. The two cancel paths lose their own page loads and become steps
inside the shadow test: close keeps the original name, and "don't
remap" renames the token and breaks the reference.

The deleted typography and shadow applied-shape cases exercise the same
data path, which the remapping unit tests already cover.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Shrink the tooltip and numeric-revert e2e tests

The tooltip test asserted five message shapes over eleven layer
selections. Keep the three that have no unit test: a deleted token, a
reference outside the active sets, and a token in an inactive set. Each
is one step with a pill label and its tooltip text.

Three of its five messages were already covered by the token pill unit
test, including both resolved-value cases.

Keep one numeric-revert test and drop the fixed sleeps: await the value
with toHaveValue instead. Division by zero and the min/max clamp are
unit-tested in the numeric input tests.

AI-assisted-by: deepseek-v4.1-flash
2026-11-12 10:58:52 +01:00
makesomethingshit
dfc7919f7b
🐛 Preserve HSL values while editing lightness (#12629)
* 🐛 Preserve HSL values while editing lightness

Keep HSL editing components through black, white, and grayscale.
Use the same draft for numeric inputs, sliders, and gradient previews.
Strip the draft before emitting color data and reset it on other edits.

Closes #12615

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
AI-assisted-by: GPT-6

* 🐛 Keep HSL drafts consistent across gradient and ramp edits

Match RGB components to HEX so gradient echoes preserve HSL drafts.
Discard stale HSL values when editing Ramp hue, saturation or value,
while preserving them for opacity-only changes.
Cover grayscale gradient echoes and saturation restoration.

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
AI-assisted-by: GPT-6

---------

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
2026-11-12 10:11:56 +01:00
penpot-renovate[bot]
b438676d6c
⬆️ Update dhi.io/node image to f6ba5e2 (#12975)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-11 16:55:19 +01:00
Gabriel Ribeiro
7e9a3c3c28 📚 Fix typos in technical and user guides
Fix five misspelled words in the docs: "notifiy" and "occured" in the
integration guide, "enviroment" in the developer guide index, "succesful"
in the authentication subsystem page and "achive" in the layers page.

AI-assisted-by: claude-opus-5-5
Signed-off-by: Gabriel Ribeiro <267068126+gabrielribeiro3105@users.noreply.github.com>
2026-11-11 15:24:13 +01:00
andrés gonzález
a301c2b43f
✨ Keep board names visibility after reloading the workspace (#12872)
Hiding board names from the View menu reset on every reload, while
rulers and comments kept their state. Add the flag to the layout flags
persisted in local storage so it behaves the same way.

Closes #12871

AI-assisted-by: claude-opus-5-5
2026-11-11 14:52:35 +01:00
Andrey Antukh
671ff4a05f Merge remote-tracking branch 'origin/staging' into develop 2026-11-10 20:06:46 +01:00
Andrey Antukh
8172059b2e Merge branch 'staging-2.19.x' into staging 2026-11-10 19:53:23 +01:00
Andrey Antukh
5e5722dfe7 📚 Update changelog 2.19.1 2026-11-10 19:24:39 +01:00
Andrey Antukh
a58ace4df4 Merge branch 'staging-2.19.x' 2026-11-10 19:18:41 +01:00
Andrey Antukh
d5c4bfb234 ✨ Add security fixes section to the changelog flow
The 🔒 section is generated from advisories instead of being written
by hand: an advisory becomes an entry when its patched_versions include
the release version, so unpublished (triage/draft) advisories are valid.

- gh.py advisories --patched-version filters on the exact token of
  vulnerabilities[*].patched_versions; the list output keeps the summary
  schema.
- changelog.py security renders the entries sorted by GHSA id, links the
  merged milestone PRs labelled "pr pt" that reference the GHSA id and
  warns when a reference has no matching patched version. The subsection
  follows 🚀 (or 💥, or the version header) and is removed
  when the version has no advisories.
- Tests cover the filter, the entry rendering, PR discovery and the
  subsection placement.
- Memories and the update-changelog skill document the contract.
- CHANGES.md: add 2.19.1 with its security fixes and the remaining bug
  fix.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 18:18:00 +00:00
Andrey Antukh
e4fcc663c0 Merge remote-tracking branch 'origin/staging-2.19.x' into staging 2026-11-10 18:09:07 +01:00
Andrey Antukh
88b09efd87 📚 Note breaking changes must not be hard-wrapped
The changelog generator copies the note lines verbatim, so a note
hard-wrapped at 80 columns ends up wrapped in CHANGES.md. Record
that the breaking-change note must keep one line per paragraph or
bullet, in both the issue and changelog workflow memories.

AI-assisted-by: inkling-2-small-free
2026-11-10 16:58:21 +00:00
Yamila Moreno
a3f50887b3 📚 Improve 2.19 documentation regarding admin-console
Signed-off-by: Yamila Moreno <yamila.moreno@kaleidos.net>
2026-11-10 16:13:06 +01:00
Andrey Antukh
1db1487c7f
🐛 Require thread access when marking threads as read (#12894)
* 🐛 Require thread access when marking threads as read

mark-all-threads-as-read took any list of thread ids and wrote the read
status of the caller on each of them, without checking that the caller
can access the file that owns the thread. It was the only comment
mutation that did not check permissions, and it took the ids as an
unbounded array, so a single request could keep a database connection
and a transaction open for a long time.

The rpc now resolves the threads in one query and checks the comment
permissions of every distinct file, answering not-found when a thread
does not exist or is not accessible. Both cases answer the same error
on purpose, so the response cannot be used to tell existing threads
apart.

The status rows are now written with a single insert, keeping the
modified-at date one second in the future that makes the thread count
as read. Repeated ids are deduped, the command returns nil again like
the sibling commands with no result, and the schema limits the number
of threads per call to 500 while the dashboard sends the ids in batches
of that size.

Tests cover access rejection, unknown threads, mixed batches that write
nothing, repeated ids, empty lists, marking twice, and the frontend
batch split.

Relates to GHSA-j4vc-cmf6-jhr7

AI-assisted-by: minimax-m3.1-flash

* 🐛 Refresh the unread list when a batch fails

The dashboard sends the thread ids in batches of 500 and each batch
commits on the server. A failure in a later batch left the unread list
stale: the re-read only ran when every batch succeeded, so the threads
already marked as read stayed visible as unread.

The re-read now runs in both paths. On failure the event is emitted
before the error is propagated, so the error toast still shows and the
success toast stays limited to a full success.

The backend also answers an empty list without opening a transaction,
since there is nothing to check and nothing to write.

Relates to GHSA-j4vc-cmf6-jhr7

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:54:11 +01:00
Andrey Antukh
8510daef52
🐛 Restrict the team invitation token and list to admins (#12892)
* 🐛 Restrict the team invitation token and list to admins

The token of a team invitation is a bearer credential: whoever holds it
accepts the invitation with the role of the invitation row. The RPC that
mints it only required edition permissions, while the rest of the
invitation commands require admin, so a team editor could get the token of
any pending invitation, including the ones that grant the owner role.

The token now requires admin, and the token of an owner invitation is
restricted to the team owner: an admin cannot create nor promote an owner
invitation, so it cannot keep its token either. It reuses the error code
already used for that rule.

Minting the token now leaves an audit event named as an action, carrying
the granted role and the id of the invitation, so the extraction does not
go unnoticed.

The list of pending invitations is admin only as well: it carries the
email of the invited person and the role offered, and the dashboard was
painting those emails to any member. The frontend catches the permission
error and shows the permission notice instead, and the copy action is not
offered for an owner invitation, which the backend rejects for an admin.

The five invitation commands now answer with the same permission error,
so they speak one language.

Relates to GHSA-4mmc-j8rh-74pf

AI-assisted-by: minimax-m3.1-flash

* 🐛 Clear the cached invitations and gate the list on team admin

The pending invitations are a team admin only list. The dashboard still
offered the entry to every member and keyed the section on the invite
policy of the organization, so a team admin blocked by that policy saw
the no-permission notice although the backend lets them list.

- `fetch-invitations` answers a denied refetch with an
  `invitations-fetched` event carrying an empty list, so a demoted admin
  does not keep the cached emails and the header no longer paints the
  invite button from a stale list.
- The header tabs and the team options dropdown hide the invitations
  entry without `:is-admin`, which already includes the team owner.
- `invitations-section-content` keys the section on `:is-admin`, and the
  invite policy only gates the invite actions: resending keeps the
  `can-invite?` check, deleting and row selection only need team admin.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:53:43 +01:00
Andrey Antukh
0420db1ca9
🐛 Apply the share-link page scope to the comment queries and writes (#12888)
* 🐛 Apply the share-link page scope to the comment queries and writes

A share link can be restricted to a subset of the pages of a file, and
the comment queries enforced that scope. But the RPC that resolves the
profiles of the commenters authorized the request with
check-comment-permissions! and dropped the returned perms, so its query
filtered only by file id. The holder of a page scoped link got the id,
email, fullname and photo of every profile that commented on any page
of the file, including the pages the owner left out of the link.

The query now takes the pages of the share link and only selects the
authors of the threads on them. The profile of the caller is still
added, so the viewer keeps its own avatar.

The scope was missing as well on the comment writes that take a page id
or a thread id: create-comment-thread accepted a page outside the link,
create-comment accepted a thread of an unshared page, and
update-comment-thread, update-comment-thread-position and
update-comment-thread-frame accepted a thread of an unshared page. They
now raise not-found, the same way the comment queries already did.
Replying to a thread of an unshared page also sent the emails to the
thread participants.

The page scope rule now lives in one place: share-link-pages returns
the pages granted by a share link and check-page-scope raises not-found
when a page is outside of it, so get-comment-threads reuses the helper
instead of checking the pages inline.

The docstring of get-profiles-for-file-comments said that it returns
the participants of all the comment threads of the file, which is not
true anymore for a share link. The docs are public, so the text and
the changes list are updated.

The tests of the share link page scope are hardened: the setup asserts
that the comment threads are created, the membership case has a second
commenter so it can no longer pass for the wrong reason, and the link
without pages, the allowed paths of the thread updates and the
who-comment=team denial are covered.

Relates to GHSA-89cq-3wv7-987r

AI-assisted-by: minimax-m3.1-flash

* 🐛 Enforce the share-link page scope on the remaining comment writes

Check the page scope of the share link before mutating in the four
comment writes that still skipped it: update-comment-thread-status,
update-comment, delete-comment-thread and delete-comment. The check
runs before the ownership checks, so an out-of-scope write fails with
not-found and the response does not reveal that the comment or thread
exists. Load the file in create-comment only after the permissions and
page scope checks, matching create-comment-thread, so a rejected reply
does not load the file data.

Cover the denial and the in-scope paths of the four commands, and the
fail-closed case of a link without pages on the comment reads. Move
the outsider thread creation after the profiles assertion so the
caller-union assertion fails when the union is removed. Document the
page scope invariant in the backend permissions memory.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:53:07 +01:00
Andrey Antukh
4874da450f
🐛 Escape SVG values and sanitize every SVG upload (#12887)
* 🐛 Escape SVG values and sanitize every SVG upload

The SVG sanitizer removed dangerous elements and attributes by name and
then wrote the tree back with clojure.xml/emit, which escapes nothing.
A payload hidden in an attribute value or in a text node became live
markup again: an id of "x' onload='alert(1)'" was stored as a real
onload handler, and an entity encoded script tag was stored as a real
script element.

Write the filtered tree with a serializer that escapes text and
attribute values, and route every path that stores an uploaded SVG
through it. The thumbnail writers (create-file-object-thumbnail and
create-file-thumbnail) and the organization logo stored the upload
untouched, so they could plant live markup in a public bucket.

The filter only knows about SVG elements, so it also accepted
documents that are not SVG at all: a page with an iframe or a meta
refresh was stored as an image. The sanitizer now rejects any document
whose root is not an svg element in the SVG namespace, and it compares
the scheme of an url the way the browser does, so a scheme split with
a tab or a line break no longer slips through the javascript: filter.
The attributes are written sorted by name, because the parser hands
them over in the reverse order of the document and the output changed
on every sanitization.

The profile picture of an identity provider is not stored either when
it is an SVG: the avatar is always rendered as an image, so an SVG
would only add the risk of keeping live markup in the storage. The
dimensions of an SVG are now read from its local name, so an SVG with
a namespace prefix can be uploaded like any other.

The logo had no size limit and the sanitizer reads the whole file, so
it now honors the media max file size.

The output shape changes with it: no line breaks, double quotes, and
the single quote of an attribute value escaped as an entity. The hash
of every stored SVG object changes with it.

The media tests use cuerdas.core instead of clojure.string, as the rest
of the codebase does.

Relates to GHSA-qwhj-rchj-58mg

AI-assisted-by: minimax-m3.1-flash

* 🐛 Drop non-SVG namespace subtrees in the SVG sanitizer

The sanitizer only checked the namespace of the root, so a child could
rebind xmlns to XHTML and smuggle live HTML (iframe/srcdoc/meta) past the
blocklist. The serializer escapes attribute values, but the browser decodes
them again, so srcdoc stays live.

Track the namespace bindings through the recursion and drop any element
that is not in the SVG namespace, with its subtree. The root is validated
up front with the same model, so a document that is not an svg document is
rejected with an honest hint instead of a misleading one.

The default namespace is stored under a namespaced key, because a prefix
literally named `default` collided with the internal key and re-opened the
same hole through a prefixed binding. The attribute filter now keeps
`xmlns`/`xmlns:*` declarations, which are not event handlers, so a prefix
named like one does not end up undeclared in the output.

The blocklist also drops HTML-only elements written in the svg namespace
(iframe, meta, embed, link, base).

Add regression tests for the child rebinding, for the prefixed variants,
for the `default` prefix, for the root local name, for the
create-file-media-object-from-url path (mocked download), and for the
HTML-only tags. Assert double-quoted attributes in the clean content test.

Relates to GHSA-qwhj-rchj-58mg

AI-assisted-by: longcat-2.5-free
2026-11-10 15:52:28 +01:00
Andrey Antukh
c39e3e8301
✨ Strictly validate uploads against file bytes on disk (#12801)
* ✨ Strictly validate uploads against file bytes on disk

Sniff the real content type of every upload with simplemagic magic
bytes instead of trusting the client-supplied mtype header. Declared
types outside the allowlist keep the not-allowed code; allowed
declarations whose bytes differ are rejected as mismatch. Covers SVG
by probing its root tag over leading bytes (magic never matches XML
text), TTF/OTF by magic name and WOFF/WOFF2 by four-cc.

Read the real file size from disk instead of trusting the declared
:size. Files over the limit are rejected even when the declared size
lies downwards, and small files pass even when it lies upwards.

Run the path check first everywhere so forged server paths keep the
params-validation code. Recheck from-url downloads after the write,
with a declared-only fast fail at header time. Thread the assembled
path through font validation.

Refresh the affected tests around real and sparse fixture files,
including a PNG-as-font chunked-upload rejection case.

Closes #12796

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Accept BOM-prefixed SVG uploads and cover WOFF/WOFF2/OTF/WEBP

Address review comments on #12801:

- SVG files saved with a UTF-8 BOM were rejected as
  :media-type-mismatch: read-head kept U+FEFF and the <svg probe
  never ran. A leading BOM is now stripped before the probe, and a
  generic XML sniff (application/xml, text/xml) is treated as
  "no magic evidence" for declared image/svg+xml, so the root-tag
  probe decides whether the file opens with an <svg root.
- sniff-match?/svg-file? docstrings claimed the magic database never
  matches SVG; simplemagic 1.17 does match an SVG with an XML prolog
  as image/svg+xml. Reworded to match reality.
- Added matching-pass tests for WOFF, WOFF2, OTF and WEBP (they hit
  the read-four-cc, magic-name and direct magic branches) and three
  BOM SVG regression cases.

AI-assisted-by: inkling-2-small-free
2026-11-10 15:51:48 +01:00
Elena Torró
7f0b62b994
🐛 Keep drop shadows out from under translucent shapes (#12904)
* 🐛 Keep drop shadows out from under translucent shapes at every zoom

Cut the shape out of its drop shadow, as CSS box-shadow does, and stop
large shadows from being cut off at high zoom.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep drop shadows out from under translucent shapes in SVG and PDF

Match the canvas: clip the shape out of its drop shadow on export.

AI-assisted-by: claude-opus-5-5
2026-11-10 15:05:08 +01:00
Juanfran
e1f924cf00 🐛 Send organization invitations to unregistered emails (#12937)
send-template drops nil values before enqueuing the mail job, but the
invite-to-organization schema required the :user-name key. Inviting an
email with no Penpot account left :user-name nil, the key was dropped
and the job failed its schema check, so the invitation email was never
sent. :user-name is now optional in invite-to-organization and in
renewal-notice, which has the same shape.

New test renders both templates through the queue with a nil
:user-name.

AI-assisted-by: claude-opus-5-5
2026-11-10 14:12:27 +01:00
penpot-renovate[bot]
c9d3cbb383
⬆️ update GitHub Actions (#12898)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker-devenv.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker-devenv.yml)
- zizmor 1.32.0 (.github/workflows/lint-workflows.yml)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-10 13:48:10 +01:00
Miguel de Benito Delgado
cbd4c9634c
🔧 Use correct :cache-root in shadow-cljs.edn (#12883) 2026-11-10 13:47:50 +01:00
Marina López
4e8f05171e
🐛 Forward attribute deletion events to admin console (#12932) 2026-11-10 13:47:13 +01:00
Alonso Torres
79d4936cae
⬆️ Release plugins and mcp (#12934)
* ⬆️ Updated plugins release to 1.6.0

* ⬆️ Updated mcp release to 2.19.1
2026-11-10 13:44:51 +01:00
Juanfran
ae24118caf
🐛 Send organization invitations to unregistered emails (#12937)
send-template drops nil values before enqueuing the mail job, but the
invite-to-organization schema required the :user-name key. Inviting an
email with no Penpot account left :user-name nil, the key was dropped
and the job failed its schema check, so the invitation email was never
sent. :user-name is now optional in invite-to-organization and in
renewal-notice, which has the same shape.

New test renders both templates through the queue with a nil
:user-name.

AI-assisted-by: claude-opus-5-5
2026-11-10 13:44:26 +01:00
Andrés Moya
10b08d5224 🐛 Improve sync of layout item attributes in components (#12851)
* 🐛 Preserve layout item touched groups when switching variant

* 🔧 Add systematic tests to check layout items component sync

* 🔧 Add small updates after comments

* 🔧 Add tests for partial overrides
2026-11-10 13:41:54 +01:00
penpot-renovate[bot]
2e6c799bba
⬆️ Update dhi.io/node image to f943257 (#12931)
- dhi.io/node 24.22.2-debian13-dev@f943257 (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.2-debian13-dev@f943257 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@f943257 (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-10 11:45:57 +01:00
Filip Sajdak
ee3e2a0e61
🔧 Keep the CI expect timeout in the default and ds e2e projects (#12922)
The Playwright config gives the retrying expects 40 seconds on CI and 5
seconds elsewhere, but the default and ds projects set their own expect
block for the screenshot tolerance, and a project block replaces the
top-level one instead of merging with it. On CI those projects have run
with Playwright's 5 second default.

That is too short for the first dashboard load on a busy runner, where the
dashboard module is fetched lazily: specs that wait for the dashboard
heading time out and pass on the retry (flaky), or fail after the
retries. In the shard of one recent run that was 7 flaky and 4 failed
specs out of 112, and develop itself failed the same families of specs on
several runs the same day.

Name the timeout once and repeat it in both projects. A probe config that
imports this one and polls a condition that never holds reports 40000 ms
for both projects with this change, and 5000 ms without it. The
render-wasm project already repeats its own value.

AI-assisted-by: claude-opus-5-5
AI-assisted-by: claude-sonnet-5-5

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-11-10 11:33:42 +01:00
Filip Sajdak
90d98b1989
🐛 Call the team hook on every render of the team container (#12920)
team-container* called mf/deref only after returning early for a team id
that is not a uuid. When the same component rendered once without a team
id and then with one, the number of hooks changed between the two renders
and React threw "Rendered more hooks than during the previous render"
(error #310), which sent the user to the internal error page. The reverse
order throws "Rendered fewer hooks than expected".

Call the hook on every render and keep the uuid check in the condition
that builds the providers.

Two tests mount the component in jsdom and render it with a team id that
arrives and with one that leaves; both fail without this change.

Closes #12728

AI-assisted-by: claude-opus-5-5
AI-assisted-by: claude-sonnet-5-5

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-11-10 11:32:58 +01:00
Alonso Torres
4fb0ea9eee
🐛 Give unique values to same-name components combined as variants (#12881)
When components whose name matches the variant container are moved into
it, removing the container name leaves an empty value. Combining several
components named the same produced "Value 1" for the first variant and
empty values for the rest, so variants shared the same combination.

Assign the first unused "Value N" to the last property when all computed
values are blank, tracking values given within the same batch.

Combine moves the remaining mains into the container one at a time, in
the combine order, so the generated values follow that order instead of
the layer order.

Closes #12878
2026-11-10 09:16:08 +01:00
Eva Marco
72575e5c05
🐛 Make Add to an organization a keyboard button (#12722)
"Add to an organization" in the team settings opens a modal but was an
<a> without href: the keyboard could not reach it. It is now a
<button type="button"> that keeps its text-link look (accent color,
medium weight, underline on hover, the same box), so Tab reaches it and
Enter or Space opens the modal.

New visual test "Shows the Add to an organization link", with fixtures
for a team in no organization and an organization the user belongs to:
0 px of difference at rest and on hover, and 0 px on the whole settings
page.

AI-assisted-by: claude-opus-5-5
2026-11-10 08:33:04 +01:00
Luke Lau
cd1ece4beb
⚡ Keep the translation flag on undo and redo of a move (#12870)
* ⚡ Keep the translation flag on undo and redo of a move

Previously undos and redos always forced a component sync + thumbnail
rerender for every component touched even if the underlying commits were just
translations. This propagates the translation flag to undos and redos and
propagates it to commit-changes. An entry merged from several undos/redos is
a translation only if all of its undos/redos are translations.

This reduces a UI freeze when undoing a move of a board with 453 components
from 2.42s -> 0.22s.

AI-assisted-by: claude-sonnet-5-5

* :translation? -> :translation

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-09 20:47:42 +01:00
penpot-renovate[bot]
bdf16bcfda
⬆️ Update DHI base images (#12897)
- dhi.io/debian-base trixie-debian13-dev@94571f3 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13-dev@94571f3 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13@5c9d8c2 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@94571f3 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@94571f3 (docker/devenv/Dockerfile)
- dhi.io/nginx 1.31.8-debian13-dev@e8db455 (docker/images/Dockerfile.frontend)
- dhi.io/node 24.22.2-debian13-dev@ee39b15 (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.2-debian13@7f8db65 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@ee39b15 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@ee39b15 (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-09 17:39:54 +01:00
Luke Lau
6846afd08c
⚡ Send objects-map pages to the worker without a second encode (#12811)
* ⚡ Send objects-map pages to the worker without a second encode

When switching between pages on a large file e.g Material Design Icons, there's
some UI freeze from :index/initialize encoding object map shapes to send to the
worker. An objects map already stores the encoded JSON for each shape, and
`encode-str` was wrapping those strings in a second layer of JSON on the main
thread.

We can just transmit the already encoded JSON in a js/Map alongisde the
payload, similar to :blob, so structured clone just copies the strings verbaitm
without re-encoding. To do this this PR adds two functions to ObjectsMap that
converts to and from a js/Map.

The "Main components" page of the Material Design Icons has ~30k objects, so
this reduces the freeze when switching pages from 306ms -> 189ms (1.63x) on a
release build.

AI-assisted-by: claude-sonnet-5-5

* Split property tests out into new deftest

---------

Signed-off-by: Luke Lau <luke_lau@icloud.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-09 17:35:20 +01:00
Luke Lau
55360c52f5
✨ Cache the transit reader when decoding strings (#12704)
* ✨ Cache the transit reader when decoding strings

On the cljs side, we create a new transit reader on every call to decode-str.
This turns out to have a fair bit overhead in both transit-cljs and transit-js,
since it has to merge the handlers, call clj->js, allocate a new ReadCache,
etc.

By reusing it the measured decode time goes from ~58us -> ~41us, so about 1.4x
faster. This particularly helps for some operations like whenever a component
is edited and every object needs to be decoded, see
https://github.com/penpot/penpot/issues/9344, although they could probably be
made more efficient in other ways.

The reader usually clears its read cache after a successful read, but whenever
it throws it hangs around. So in this case we need to reset the cache
ourselves, otherwise the next read will be from a clobbered cache.

The tests were written by Claude Sonnet 5.5, and should cover the clobbering
behaviour above.

Signed-off-by: Luke Lau <luke@igalia.com>

* Keep one transit reader per type and rebuild it on add-handlers!

---------

Signed-off-by: Luke Lau <luke@igalia.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-09 17:34:24 +01:00
Luke Lau
3de7e62aa9
⚡ Don't send file data contents back from duplicate-file (#12911)
The client calls the duplicate-file command in two places, the dashboard and in
the plugin API. In both places the :data is dropped via `(dissoc file :data)`,
so the client never uses that field.

However the file data takes a while to send over the wire and decode. When
duplicating penpot-app there's a roughly 1s freeze whenever the response is
returned from the server with the file data. We can avoid it by dropping the
data from the response.

I also did a quick check and neither the MCP server nor any public client of
the API on GitHub seem to use the :data field.

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-09 17:23:39 +01:00
Belén Albeza
fb7468c3f5
🔧 Make all CI jobs use Rust 1.99 (#12903)
* 🔧 Bump Rust version of the devenv image to 1.99

* 🔧 Run the pinned Rust setup from the lint and format scripts

./build and ./test switch to the Rust version in render-wasm/_build_env
through setup, but ./lint did not, and formatting called cargo fmt
directly. In CI both ran on the devenv image's default toolchain.

Make ./lint run setup and add a ./fmt script that does the same, so all
render-wasm entry points use the pinned version. CI and scripts/ci now
call ./fmt instead of cargo fmt.

AI-assisted-by: claude-opus-5-5
2026-11-09 16:23:47 +01:00
Miguel de Benito Delgado
01191051c7
🐛 Replace stale atlas pixels when zooming (#12884)
Copy finished tiles with BlendMode::Src so transparent pixels replace what the atlas slot held before
2026-11-09 14:28:13 +01:00
Miguel de Benito Delgado
33602b9114
🔧 Allow skipping renderer build setup via env. var (#12882)
Set PENPOT_WASM_PREPARED=1 to speed up repeated builds
2026-11-09 12:53:18 +01:00
Andrey Antukh
4b5dd51b39 📚 Add 2.20.0 changelog section
Populate the 2.20.0 release notes from the milestone: 19 bugs fixed
and 31 new features & enhancements, classified by issue type with the
community contribution attributions and the merged PR references. No
💥 or 🚀 sections: no issue carries the breaking change or
release highlight labels. No GHSA entries: no published advisory lists
2.20.0 as patched version.

AI-assisted-by: inkling-2-small-free
2026-11-09 11:19:50 +00:00
Luke Lau
943e63a99e
⚡ Group color updates in selections (#12875) 2026-11-09 12:08:36 +01:00