24668 Commits

Author SHA1 Message Date
Miguel de Benito Delgado
e89dcecae1 🚧 WIP Step 16.2 Renames and fixes 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
e455cf7a44 🚧 WIP Step 16.1 Relax scene macro constraints 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
670a63dcc7 🚧 WIP Step 16 Introduce hooks for the browser and load-scene 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
34b4847460 🚧 WIP Step 15.1, cosmetic 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
3102034769 🚧 WIP Step 15, Use transit to simplify validation 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
39002a86ce 🚧 WIP Step 14, Grab correct w/h attrs from context 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
1cf818f701 🚧 WIP Step 13, Track failure causes
AI-assisted-by: muse-spark, GLM-5.3
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
f4c2662250 🚧 WIP Step 12.4, Comments
AI-assisted-by: gpt6-astra
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
67d1f68968 🚧 WIP Step 12.3, Test cleanup + missing coverage
AI-assisted-by: muse-spark, deepseek-flash
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
6f5b8094b8 🚧 WIP Step 12.2, Fix browser bridge, lifecycle
AI-assisted-by: muse-spark, glm-5.3
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
999506e481 🚧 WIP Step 12.1, Improve case validation
AI-assisted-by: muse-spark, glm-5.3
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
767299866b 🚧 WIP Step 12, temporary pilot
- pilot runner for testing
- will be entirely removed later

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
abf8e58a7e 🚧 WIP Step 11, browser bridge
- New schema:viewport
- Browser bridge (with some temp code)
- Move prepare-scene out measured regions
-
AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
1aa3f49e31 🚧 WIP Step 10, scene upload 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
08aba8d571 🚧 WIP Step 10, defcase body validation, docs 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
0ff3bb593a 🚧 WIP Step 9, fix naming conventions 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
f4f02c3d96 🚧 WIP Step 8, simplify and clean up tests 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
12fc07f6ee 🚧 WIP Step 7, scene and case declarations 2026-11-06 16:20:42 +01:00
Miguel de Benito Delgado
330cda8880 🚧 WIP Step 6, ellipses 2026-11-06 16:20:41 +01:00
Miguel de Benito Delgado
407c56c0eb 🚧 WIP Step 5, bools 2026-11-06 16:20:41 +01:00
Miguel de Benito Delgado
7946bb5c3f 🚧 WIP Step 4, frames and groups 2026-11-06 16:20:41 +01:00
Miguel de Benito Delgado
2bee689101 🚧 WIP Step 3, fixture macros, prng, defaults, placeholder scene 2026-11-06 16:20:41 +01:00
Miguel de Benito Delgado
46b50d7e21 🚧 WIP Step 2, common scene code, validation 2026-11-06 16:20:41 +01:00
Miguel de Benito Delgado
0c6a23d59e 🚧 WIP Step 1, scaffold cljs benchmark runner 2026-11-06 16:20:41 +01:00
Elena Torró
7ecf48c52c
⚡ Speed up uploading page shapes to wasm (#12854)
* ⚡ Compute text fonts once per shape when uploading to wasm

* ⚡ Write the shapes batch in place and coerce fills once

* 🐛 Free the upload buffer when a batch record write fails
2026-11-06 15:45:34 +01:00
Belén Albeza
988cde3c9d
✨ Make WebGL the default renderer for new accounts (#12880)
New profiles now store renderer `wasm` in their props at creation, so
every signup path (email, OIDC, LDAP, Nitrate, demo, CLI) starts on
WebGL. A renderer passed by the caller still wins.

Existing profiles without a stored renderer keep the frontend fallback
to SVG, so no backfill or version check is needed to leave them as they
are.

The `register-profile` audit event gains `renderer-default` when the
profile is created in that call, so signups before and after this
change can be told apart.

Closes #11855

AI-assisted-by: claude-opus-5-5
2026-11-06 14:44:24 +01:00
Álvaro Tejero-Cantero
87fc15b5aa
🐛 Open the first page in view mode when the URL has no page id (#12687)
A view URL without a page-id crashed builds with assertions enabled:
the viewer passes a nil page-id to `initialize`, whose schema accepted
a missing key but not a nil value. Release builds skip that check and
draw the first page, but every viewer event reads the page from the
route, so zoom to fit and fill, the next-board arrow, Navigate To
interactions, and comment pins did not work.

`initialize` now accepts a nil page-id, `fetch-bundle` makes it
optional, and `bundle-fetched` replaces a route without page-id by one
naming the file's first page. The viewer then initializes again on the
completed route, as it does when the user changes page. A file with no
pages keeps its route.

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

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
2026-11-06 13:05:39 +01:00
Andrey Antukh
a77cb2608e
🐛 Normalize stored SVG attribute keys to kebab-case (#12006)
* 🐛 Normalize stored SVG attribute keys to kebab-case

The v3 reader already emits kebab-case keys, but birth stored the
camelCase React prop names, so import and export never converged:
fill rules painted wrongly and filter defs lost their blur.

Birth now normalizes every stored attribute key to kebab-case
through the `svg-props` whitelist (`attrs->kebab-props`), applied on
all element paths of the svg importer, on the file builder and on
defs. svg-raw `:content` is kebab-ized in spelling only and keeps
unknown keys, because the sidebar reads them. Non-spec attrs Penpot
must keep live in `penpot-extra-attrs`, seeded with `:stroke-style`.
Render boundaries translate back to camelCase for React and for the
wasm props, and the shape schema declares the closed kebab contract
derived from that same whitelist, so birth and validation cannot
drift.

Migration `0035-fix-shape-svg-attrs-and-defs` converts old files
once with the birth transforms, and carries back the svg-viewbox
rect normalization the withdrawn `0021` had. That old id leaves
`available-migrations`, so files recording it migrate again through
this one, and the binfile cleaner drops its own svg repair.

Review fixes included here: `setup-opacity` and the blend mode read
the stored keys, gradient stops accept `:style` as a CSS string or a
map, `inherit-attributes` reads `:style` so a child's inline style
beats the inherited group attr per CSS, and the svg-attrs menu shows
spec spelling (`viewBox`, `class`) through `stored-attr-display-name`.

Tests cover the migration, the closed svg-attr schemas and the
import/export round trip; the svg-content schema test pins its
sample seed, which dealt no svg-raw shape on unlucky runs.

Closes #11947

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

* 🐛 Keep legacy svg node tags and transforms valid in migration 0035

The closed svg node schemas require keyword tags and map attrs, but
stored files hold def and svg-raw content tags as strings, and some
node `:attrs` as nil. Migration 0035 left them as they were, so the
file data check that runs after migrating rejected those files and
they could not be opened. `normalize-node-tree` now turns string tags
into keywords and nil attrs into an empty map, at birth and in 0035.

With keyword tags, the svg defs renderer applies the shape transform
to user-space defs. Legacy `:svg-transform` values are plain maps,
which multiply into NaN matrices. `gmt/coerce` turns them into
matrices, in 0035 and in the binfile cleaner, because every v3 import
decodes the transform as a plain map again. Legacy masks and gradients
now render as designed in the svg renderer.

The binfile test helper calls `export-files!` with the current config
keys.

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

---------
2026-11-06 12:52:53 +01:00
Elena Torró
93ed713a29
🐛 Free the wasm renderer state on clean up (#12877)
Each page switch leaked the six wasm states (~200 MB on a large page).
Null state getters now panic instead of reading address 0.
2026-11-06 11:43:50 +01:00
María Valderrama
2df4d10bd5
✨ Enforce attribute-based organization permissions (#12868)
* ✨ Enforce attribute-based organization permissions

* 📎 Code review
2026-11-06 11:40:50 +01:00
Elena Torró
7fa225115e
⚡ Reuse uuid instances when decoding transit on the frontend (#12792) 2026-11-06 11:33:37 +01:00
Andrey Antukh
6a1b58c310 Merge remote-tracking branch 'origin/staging' into develop 2026-11-06 10:16:02 +01:00
Andrés Moya
018b857de1
🐛 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-06 09:51:12 +01:00
Andrey Antukh
0333410482 Merge remote-tracking branch 'origin/staging' 2.19.0-RC6 2026-11-06 09:50:49 +01:00
Andrey Antukh
22864cbfd7 📚 Update changelog 2026-11-06 09:50:15 +01:00
Andrey Antukh
55fb8c46fc 📎 Updat elibrary and mcp package version 2026-11-06 08:49:30 +01:00
Andrey Antukh
0b8a428d6b
🐛 Validate binfile library relations against the package (#12862)
Ignore imported library relations that point outside the
package or self-link a file on the v3 import path. Delete
leftover cross-team and self relations from file_library_rel
and their matching file_library_sync rows with migration
0158, sync rows first.

Move the get-files-rels SQL to a var, without behaviour change.

Cover the write-side invariants with state-based tests:
tampered manifests, package containment, self links and the
migration cleanup.

AI-assisted-by: deepseek-v4.1-flash
AI-assisted-by: muse-spark-1.3-contributor
2026-11-06 08:37:02 +01:00
alonso.torres
1a80e98e62 🐛 Keep Inspect from crashing on a shape whose parent is missing
In a damaged file a shape may point to a parent that does not exist.
The Inspect tab computes the position relative to the parent, so
inspecting such a shape replaced the workspace with the internal error
page. Inspect leaves the position out and logs a warning instead.
2026-11-05 23:59:32 +01:00
alonso.torres
eadd1cce70 🐛 Keep a copy linked when the repair finds its component's main
A component whose :main-instance-id points to a missing shape fails
validation on its copies with :ref-shape-not-found. A repair scoped to
the copy's page never sees the main's own error, so it detached the
copy, even when the main was still on the component's page.

The repair of :ref-shape-not-found now looks for the component's main
on its page first and points :main-instance-id back to it, which keeps
the copy linked. A copy is still detached when no main is found.
2026-11-05 23:59:32 +01:00
alonso.torres
7eacbec245 🐛 Keep comments, focus, thumbnails and text in step with a rollback
A rollback reverts the local data with a rebase, which is not a commit,
so the parts of the workspace that follow commits missed it:

- Comment pins moved in the reverted action stayed where they were, on
  screen and on the server, and pins on a board the rollback removed
  kept pointing to it. They go back to the reverted position, and pins
  on a removed board move to the page root.
- Focus mode stayed on with no shape left when the rollback removed the
  focused ones, and the next shape drawn turned it on again. It ends
  when its last shape goes, and the selection drops removed shapes.
- Component thumbnails kept the rejected look. The thumbnails of the
  reverted mains are rendered and uploaded again, and an upload whose
  shapes changed during the render is skipped.
- Under WASM, an open text editor reset to the reverted content but the
  canvas kept the old layout. It is laid out again.
- A library sync whose save failed showed no update notice again once
  the release undid it. The notice is checked again after that
  rollback.
2026-11-05 23:59:32 +01:00
alonso.torres
88416d2542 ✨ Label the copy kept before a repair in the History panel
Before it repairs a damaged file, the backend keeps a copy of it as a
system snapshot, and the repair notice points to the version history to
find it. The History panel listed that copy among the autosaves, so it
could not be told apart.

The History panel lists the copies kept before a repair as their own
entry per day, "Before automatic repair, <date>", above that day's
autosaves, and the preview of such a copy uses the same label.
2026-11-05 23:59:32 +01:00
alonso.torres
61bf66342b ✨ Tell repairs, damaged data and other files apart in save notices
The notice of a rejected save always said the same thing. It did not
name the file when the user had moved to another one, gave no hint when
the page held data that was already invalid, and replaced the notice of
a backend repair still on screen, since the store holds one toast.

The backend flags a rejection whose first commit touches pages or
components that failed validation before it (:preexisting-damage). The
rejection notice names a file other than the open one, says when the
data was damaged before, and folds into a repair notice still on
screen; a later repair notice folds a rejection notice in the same way.
The notice says that the latest changes were undone, no longer that the
rest of the work is saved.
2026-11-05 23:59:32 +01:00
alonso.torres
27c1c38360 🐛 Apply the changes stored before a save before acknowledging it
The answer to a save lists the changes other sessions stored before it,
such as the backend's repair of a damaged file. When the answer came
before the message with such a change, the saved commit was
acknowledged first, and the late change was then applied on top of the
local edits instead of under them, out of the server's order.

A saved commit of the open file now stays pending until this tab has
received every change the answer lists from other sessions, so a late
change is applied under the local edits. If one does not arrive within
10 seconds, the file reloads.
2026-11-05 23:59:32 +01:00
alonso.torres
302f83f072 🐛 Drop the unsaved edits made on a version a peer restored
When a peer restores a version, the file reloads to it. Edits made on
the replaced version stayed queued: their save failed for good and
locked the workspace, and a rollback or a rebase replayed them on the
restored data.

Loading another version forgets the queued, buffered and saved commits
made on the old one. A save of such a commit, in flight or answered, is
dropped instead of failing or being rolled back, and one save carries
the edits of one version only.
2026-11-05 23:59:32 +01:00
alonso.torres
1d862c0dd5 🐛 Refuse every edit while saves fail
While saves fail the workspace turns read-only, but only some tools
checked it. Edits from the asset, color and token panels, and a drag or
a drawing in progress when the lock came, still made commits that
queued behind the failed save.

commit-changes refuses commits while the save-failure lock holds,
except those that are not user edits: the undo of rolled-back commits,
text position data and component touches. Undo and redo do nothing
while locked. When the lock comes, moves, resizes, rotations, drawings
and the drags of flex, guide and grid handles in progress are cancelled
and commit nothing, and the design panel shows the Inspect view.
2026-11-05 23:59:32 +01:00
alonso.torres
e10ceefb5d 🐛 Keep a file reload from doubling the workspace streams
Reloading the open file (a peer's restore, the fallback of a rollback)
runs initialize-workspace again without leaving the workspace. The
streams the first run started kept running next to the new ones, so a
commit added one undo entry per reload and each notification was
handled twice. The old subscription also sent its unsubscribe after the
new subscribe, which ended the new one on the server.

A new initialize-workspace, text reflow, shape layout or notification
subscription stops the one before it, and a subscription replaced by
one to the same file sends no unsubscribe.
2026-11-05 23:59:32 +01:00
alonso.torres
cc4788aabf 🐛 Keep reporting rollbacks the MCP agent has not heard about
The MCP plugin sends the rollbacks it saw with its next task response
and then forgets them. When that response came after the server had
given up on the task, the server dropped it and the agent never heard
about the rollback. A reload of the tab or of the plugin lost them too.

The server now sends the rollbacks of such a late response back to the
plugin, which reports them again with its next response. The plugin
keeps the rollbacks not reported yet in its local storage, one entry
per file, so a reload does not lose them. Both MCP plugin manifests ask
for the allow:localstorage permission for that.
2026-11-05 23:59:32 +01:00
alonso.torres
a6255880da 🐛 Let plugins write in Inspect mode and stop them as viewers
Plugin writes failed whenever the workspace read-only flag was set. The
Inspect tab sets it too, although it only hides the editing tools, so an
MCP agent could not edit while the user was inspecting. A user who
became a viewer had the flag cleared, so plugin writes did not throw and
silently did nothing.

Writes now fail on what makes them unsafe, not on the flag: failing
saves, a user who cannot edit, a version preview or a lost render
context. The error says which one, and the MCP instructions tell the
agent which of them pass by waiting.
2026-11-05 23:59:32 +01:00
alonso.torres
7538d2c187 ✨ Tell plugins and the MCP agent when changes are rolled back
The backend rejects a save after the call that made the changes has
returned, and the workspace then undoes them. A plugin, or the MCP
agent, went on working with shapes that were no longer there and never
learned why.

The rollback event now names the files that lost commits and the pages
and page shapes their changes touched, and plugins get it as the new
`contentrollback` event (`ContentRollback` type) through a subscription
to the store stream, so no rollback is dropped by debouncing.

The MCP plugin keeps the rollbacks it is told about and sends them with
its next task response, failed or not. The server collects them per tool
call and puts a warning first in that tool's response, naming what was
affected; the instructions tell the agent to read the pages again and
redo what is missing.
2026-11-05 23:59:32 +01:00
alonso.torres
9368c83dca ✨ Make plugin writes fail while the workspace is read-only
A read-only workspace (saves failing, a user who cannot edit, a version
preview) only hid the editing tools: plugins, the MCP among them, still
changed shapes, and those edits were never saved. Every plugin write now
throws a read-only error, whatever the throwValidationErrors flag, the
same way a write to a removed shape does.

`check-permission` with a write permission runs the new `check-writable`,
which covers most writers. Writers without a permission check (most
context methods such as create*, group, align or flatten, the z-order
methods, applyTypography on a text range, removing a ruler guide) call it
themselves, and shape setters get it through `check-shape-writable`.
Reads, comments and undo blocks keep working, so a block opened before a
lock still closes.

The MCP instructions tell the agent what the read-only error means.
2026-11-05 23:59:32 +01:00