14395 Commits

Author SHA1 Message Date
alonso.torres
c0f0cf22f8 ✨ 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-03 11:17:40 +01:00
alonso.torres
4c9d30ef6a ✨ 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-03 11:17:34 +01:00
alonso.torres
73337d6af8 💄 Put the team organization update handler on its own line
`ptk/UpdateEvent` of `handle-change-team-organization` sat on the same
line as the end of its watch body.
2026-11-03 11:17:28 +01:00
alonso.torres
799473f66a 🐛 Keep the workspace open when closing the v2 text editor
Closing the v2 text editor with Escape could crash the workspace. The
text options panel renders once more with the editor it saw last, after
closing has disposed it, and reading the style of a disposed editor
throws.

The panel now takes the values from the shape when the editor is gone.
2026-11-03 11:17:27 +01:00
alonso.torres
899f05114e 🐛 Fix small save bugs found by the rollback test plan
- Ending a drag inside the frame the shapes already belong to sent an
  empty reparent save and bumped the revision. The move commits only
  when some shape changes parent.
- When a user restored a version, other sessions reloaded the file with
  no notice. They now see a dialog naming who restored it.
- Exports drawn in the browser (PDF, SVG, and PNG when the WASM renderer
  draws it) and duplicating a file from the dashboard started at once,
  so an export could show changes a pending save then rejected, and a
  copy could miss queued edits. Both wait for the file's pending saves.
  A client export warns that it includes unsaved changes; a copy warns
  that it misses them.
2026-11-03 11:17:25 +01:00
alonso.torres
7d59cfe1a8 🐛 Keep the UI in step with a rollback
A rollback can remove or change shapes the UI still refers to.

- The selection drops shapes that are gone and the color picker that
  edited them closes.
- The undo history is cut at the reverted action; it stays when no
  entry holds a reverted change. Undo and redo commits carry real undo
  changes and the history index they moved from, so a reverted Ctrl+Z
  puts the change and the index back.
- An open path edition whose shape the rollback changed closes without
  saving the editor's stale copy. The v2 text editor loads the reverted
  text again; the WASM editor already showed it.
- The Alt measurements look the hovered shape up in the current data.
- Plugin setters, resize() and rotate() on a removed shape throw
  "Shape not found", whatever the plugin's throwValidationErrors flag.
2026-11-03 11:17:23 +01:00
alonso.torres
170a78e3ff ✨ Keep autosave and the workspace alive when saves fail
- A save that failed on a short platform failure (a lost or refused
  database connection, lack of resources, a server shutting down, a
  serialization failure or deadlock, a socket or HTTP timeout, a host
  that did not resolve, an interruption) answered 500, which stops
  autosave. It now answers 503, which the frontend retries. Other I/O
  and database errors keep answering 500, timeouts 504.
- A save answered not-found no longer replaces the workspace with the
  access page. It is a terminal save error: the workspace stays open
  with the failed save notice, report and reload link. When edit rights
  are lost, the notice says how many changes could not be saved.
- A library sync marked the library synced three seconds later,
  whatever happened to its save. A rejected sync was undone but stayed
  marked: copies stayed out of date and the "updates available" notice
  never came back. The mark now waits for the save, a rollback of the
  sync brings the notice back, and a failed mark (e.g. the user just
  became a viewer) is logged instead of leaving the workspace.
- While saves fail the workspace is read-only. The view-mode banner
  says editing is disabled because changes can't be saved, with the
  Inspect banner's size and bold lead and no Done button. Users can
  still switch pages, pan, zoom and inspect. The lock lifts when a save
  lands and brings back the mode the user had or picked meanwhile; the
  WebGL context lock keeps the workspace read-only if it lasts longer.
  The lock lives in app.main.data.persistence.lock.
- A viewer is never locked. A role change that leaves the user a viewer
  keeps the edits that lost permission, queued and on screen, and shows
  the count notice again. The role change that brings edit rights back
  releases them and rolls them back on screen, as a rejected save would.
  Any other terminal failure is released by the next role change or file
  opened.

The debug faults `io` (socket timeout, 503) and `eof` (500) cover both
sides of the I/O rule.
2026-11-03 11:17:21 +01:00
alonso.torres
9e8be56eb3 🐛 Revert whole user actions in a rollback
A rollback reverts whole undo groups, across files and saves, so that
no user action is left half done. Some actions still split:

- An action that commits several times inside one undo transaction
  (e.g. applying a token to several shapes) gave each commit its own
  group, so a rejection could revert part of it: shapes ended up linked
  to a token whose value was never applied. An undo transaction now
  opens with a commit group that every commit made inside it takes.
- The commit that undoes saved commits of a rejected action joined an
  open transaction's group, so a later rejection there reverted the
  undo too. It gets a group of its own.
- Emptying the open transaction during a rollback dropped its group, so
  its later commits were no longer one unit. It keeps its group.
- Waiting for a save ended as soon as the queue emptied, while later
  commits could still wait in the buffer. Waiters now also wait for an
  empty buffer.

A test covers a token action of three commits in one save whose third
commit is rejected.
2026-11-03 11:17:19 +01:00
alonso.torres
909ba438a9 ✨ Keep the valid commits of a save that fails validation
A save whose changes failed validation was rejected as a whole: autosave
stopped, the edits made after it piled up unsent, and a reload lost
them all, the valid ones included.

The frontend now sends each commit of a save as its own group with its
commit id (:changes-with-metadata). When applying and validating all of
them at once fails, the backend searches by bisection for the first
commit that fails, keeps the commits before it and answers with
:rejected (that commit and every later one). Only the kept changes are
stored, snapshotted and broadcast, so other users never see the rejected
ones. Flat :changes and groups without ids stay all-or-nothing; database
and network errors still fail the whole request. A rejection without an
error type is reported as an internal error.

The frontend rolls the file back to what the server holds: it undoes
every pending commit of that file, applies again the remote changes that
arrived meanwhile, and redoes the kept commits. Undoing only the
rejected commits would write stale values over a peer's later change to
the same attribute. The rejected and later commits leave the queue, the
buffer and the undo history, and a warning that hides after ten seconds
tells the user. The pure bookkeeping lives in
app.main.data.persistence.commits.

The pending notice of a commit the rollback dropped no longer leaves the
save status at pending, which made "Pin this version" hang and fail.

Debug tools drive these cases by hand: debug.commitInvalidChange() makes a
save fail validation, holdSaves() delays the answers, failNextSave() and
failNextRollback() fail them on purpose (backend flag debug-save-faults),
and persistenceState() shows the queue.
2026-11-03 11:17:14 +01:00
alonso.torres
8bacc7b58b 🐛 Apply remote changes under buffered local edits too
A remote commit undoes the pending local commits, applies the remote
changes and replays the local ones, so the local file ends like the
server will: remote first, local last. Only queued commits took part;
the ones still waiting in the persistence buffer (up to ~3s of edits)
stayed under the remote change. When both touched the same attribute,
the remote value won locally but lost on the server.

The buffer now lives in the store ([:persistence :buffer]) instead of
an rx buffer, so the rebase can reach it. A flush merges it per file
into queue entries that keep the list of commits they carry, and the
leave-page guard reads buffer and queue from the store. A buffer left
by a previous session is queued when persistence starts.
2026-11-03 11:17:11 +01:00
Pablo Alba
57f4fc19ff
🐛 Fix variant reparent generates referential-integrity problem (#12706)
* 🐛 Fix variant reparent generates referential-integrity problem

* ✨ Mr changes
2026-10-30 21:42:14 +01:00
Pablo Alba
11c2292ac6
🐛 Fix variant changed from another page (#12693) 2026-10-30 14:45:19 +01:00
Eva Marco
a69d5d15d2
🐛 Wrap long shortcut key lists in the shortcuts panel (#12735)
* 🐛 Wrap long shortcut key lists in the shortcuts panel

The Next board and Previous board rows of the viewer have five key
combinations. Their keys ran past the panel edge and covered the label.

Since #11081 the label sits inside a span with no flex rules, and the
key list never wrapped. Now each combination is a group that never
breaks, and the list wraps between groups. The comma stays at the end
of its group and "or" leads the last one, so no line starts with a
separator. The label keeps its width and the keys take the rest.

Closes #12641

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

* ♻️ Drop unused deprecated imports from the shortcuts styles

shortcuts.scss no longer uses anything from refactor/ or ds/mixins.
Remove both imports and use the typography module without a
namespace. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-30 14:12:49 +01:00
Alonso Torres
cb8f563bb2
🐛 Keep swapped and moved children in place when a component syncs (#12716)
* 🐛 Keep swapped and moved children in place when a component syncs

A sync resets an untouched shape of a copy to the main's geometry and the
layout then moves it back. Children the sync keeps, because they are
swapped or the user moved them, did not take the first move but did take
the second, so each sync pushed them away from their parent by the gap
between the copy and the main (e.g. a swapped icon in a copy that fills a
wider layout).

The sync now moves those children by the offset it applies to their
parent. Flex-child attributes still sync as before, so swapped children
keep their absolute position.

Closes #12657

* 🐛 Place swapped children where the swap puts them on sync and switch

A swapped child that the user never moved now goes to the top-left of the
shape it replaced, as a swap does: on a sync, the place of the main's slot
occupant; on a variant switch, the place of the new variant's occupant.
A swapped child the user moved keeps its offset inside its parent.

This stops the variant switch from leaving the swapped child at its old
position while the new variant is created at the main's size, and puts
back children that drifted before the sync fix the next time their copy
syncs.

* 🐛 Make kept children follow their parent's constraints on component sync

When a sync resets the size of a copy's shape, for example a copy root
that fills a wider layout, the layout then grows it back and moves its
children by their constraints. Children the sync keeps (moved by the
user, or swapped) only followed the parent's top-left, so with a right,
center or scale constraint they drifted on every sync, and a swapped
child with a scale constraint grew on every sync.

The sync now passes the parent's geometry before and after down the
tree, and kept children follow that change through their constraints.
Untouched swapped children also undo the change through their
constraints before they go to the swap place, so they keep their size.

The default constraint modifier returns empty modifiers instead of an
empty vector, which add-modifiers could not take.

* 🐛 Make swapped children follow their parent's constraints on switch

A variant switch only moved a swapped child by the parent's position
change. When the new variant's root had a different size, a user-moved
child with a right constraint drifted, and a scale-constrained one kept
its grown size.

The switch now applies the parent change from the old parent to the new
one through the child's constraints. An untouched swapped child then
takes the place of the shape it replaces, as a swap does.

* 🐛 Sync nested swapped copies before the copies that hold them

A swapped child is a copy of its own component, so one sync handles it
twice: once on its own, and once when the copy that holds it places it.
Each step read the shape from the data as it was before the sync, so
the step that ran last won, and the order came from the order of shape
ids. When the swapped child came last, it wrote its old size and
position back over the placement, leaving its x and selrect apart, and
a swapped child with a scale constraint grew on every sync.

The sync now visits the most deeply nested shapes first, and each step
reads the shapes as the earlier steps left them, so the placement
always comes last and builds on the child's own sync.

* 🐛 Keep moved children with a scale constraint at their size on switch

A variant switch carries the size of a child the user moved as an
override. In a copy that fills a wider layout, a child with a scale
constraint already has the size the layout gave it, so the switch
carried that size onto the new variant, created at the main's size, and
the layout then scaled it again: the child grew on every switch.

Before comparing, the switch now resizes the previous child the way its
constraints follow the change from its old parent to its new one. A
child that only grew with the layout then matches the main and is not
carried as an override.

* 🐛 Keep the content of swapped children at its size on sync and switch

When a sync or a variant switch undid the parent change on a swapped
child through its constraints, it applied the child's resize to every
shape inside it. The layout then grows the child back through its own
constraint, but its content follows its own constraints (left and top
by default), so it stayed shrunk: the shape inside a swapped icon with
a scale constraint lost most of its width on every switch.

The change of the swapped child now goes down its subtree the way a
layout update does, each shape through its own constraints.

* 🐛 Accept mixed and empty values in the layout size constraints menu

With several shapes or a group selected, the sidebar merges the layout
item attributes and tokens of the shapes: nil when a shape lacks one,
:multiple when they differ. The props schema of layout-size-constraints*
only accepted the values of a single shape, so selecting such shapes
with fill sizing raised "invalid props".

The schema now accepts nil and :multiple, as the numeric inputs those
values go to do.

* ♻️ Share the swapped child follow-parent step between sync and switch

The component sync and the variant switch both make a swapped child and
its children follow the parent change through their constraints and then
move them to the swap place. Both now use generate-follow-parent, so the
two paths stay the same. The helper that writes geometry without marking
it touched is renamed to update-geometry-untouched.

Adds a test for a swapped child without absolute position in a slot
without layout, and fixes the test comments on why constraints are set
again after a swap.
2026-10-29 17:30:16 +01:00
Andrey Antukh
8491cdaf63 Merge branch 'main-2.18.x' 2026-10-29 15:43:06 +01:00
Andrey Antukh
857c666022 ⬆️ Update pnpm to 12.16.1 and node to 24.22.2
pnpm 12.5.1 -> 12.16.1: the tarball pin of docker/devenv and of the
media-processor, exporter and mcp images, the packageManager field of
every workspace manifest, and the lockfile metadata of the 11
workspaces. The new pnpm also normalizes one engine range per lockfile
(>=18.* to >=18).

node 24.21.0 -> 24.22.2: devenv NODE_VERSION and both NODE_SHA256,
.nvmrc, and the runtime pin of the two plugins-deploy workflows. The
dhi.io image tags stay as they are until their digests are available.

AI-assisted-by: deepseek-v4.1-flash
2026-10-29 14:09:54 +00:00
Pablo Alba
9dc222c642
🐛 Fix bad characters allowed on variants properties (#12688) 2026-10-29 10:13:33 +01:00
Eva Marco
a6c225d510
🐛 Move the CSS @charset to the start of the bundled stylesheet (#12678)
Sass adds @charset "UTF-8" to every module with non-ASCII characters,
and concatSass joined the modules as they came, so main.css carried it
in the middle of the file. Browsers only honor @charset as the first
rule, and the server sends text/css with no charset. When the browser
did not take the page encoding for the stylesheet, it read main.css as
Latin-1 and CSS content such as the bullet of the library element
counts showed as "•".

Drop every @charset while joining the modules and put one at the start.

Closes #12677

AI-assisted-by: claude-opus-5-5
2026-10-29 09:28:53 +01:00
Eva Marco
705ea42622
🐛 Keep the default cursor in the token dropdown of numeric inputs (#12680)
A numeric input that can be scrubbed sets the ew-resize cursor on its
wrapper, and the token dropdown renders inside that wrapper, so the
whole list inherited the scrub cursor. Give the options dropdown the
default cursor, so it never takes the cursor of the control that opens
it.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:04:05 +01:00
Eva Marco
1962860f04
🐛 Reduce thumbnail re-renders and report throttled update loops (#12610)
* 🐛 Stop thumbnail subscribers re-rendering on every store change

A thumbnail loaded from the server keeps a media id as :uri, and
workspace-thumbnail-by-id resolves it into a new map on each read. The
ref compared values with identical?, so every root frame, every
component in the assets panel and the interactions layer re-rendered
on every store change, mouse moves included. Compare by value instead.

This does not loop by itself, but it adds work to every commit and
brings other update loops closer to React's limit.

Relates to #12580

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

* ✨ Report update loops throttled by the update scheduler

When the scheduler throttles an update loop, it only logs a console
warning, so a loop that no longer crashes leaves no trace. Send an
error report too, with the events in flight and the last events, at
most once per report window.

Relates to #12580

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

* 🐛 Report throttled update loops as unhandled exceptions

The throttled update loop report used the handled-exception event, which
only lands in the audit log: error reports and alerts read
unhandled-exception and exception-page. A throttled loop is an
application defect (its cause has :type :internal), and the point of the
report is to find those loops, so it is now an unhandled exception.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:02:55 +01:00
Eva Marco
a3fe43c1ec
🐛 Report raw fetch failures as connectivity errors (#12624)
* 🐛 Report raw fetch failures as connectivity errors

A fetch that fails outside app.util.http (zip.js, plugins, schema
loads) rejects with a bare TypeError such as "Failed to fetch". It
reached the global handlers and was reported as an unhandled crash,
with no stack and no URL.

The global handlers now wrap the browser's network TypeErrors as
:network (or :offline when the browser says so), so they get the
connection toast and a compact handled report. Response body reads
in app.util.http also wrap a TypeError as :network with the request
uri, as fetch itself already did.

Relates to #12581

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

* 🐛 Treat only network TypeErrors on body read as connectivity errors

process-response-type turned every TypeError from a body read into a
:network error, so a bug such as "body stream already read" showed a
connection toast. It now uses network-failure?, which moves to
app.util.http so that both the http client and the global error
handlers share it. Add Firefox's "Error in body stream" message.

Tests now check :offline and :network separately by setting
navigator.onLine, and read-error builds on async/->promise.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:02:45 +01:00
Pablo Alba
0532f15cee
🐛 Fix tab on variant name doesn't validate formula (#12655)
* 🐛 Fix tab on variant name doesn't validate formula

* ✨ Move helpers to common

* ✨ Mr proposed changes
2026-10-28 11:44:38 +01:00
Pablo Alba
f1d15aee4b
✨ Keep toast for file reparation open until user closes it (#12662) 2026-10-27 19:47:10 +01:00
Alonso Torres
a70712fce4
🐛 Fix issue with compononent not sync on move (#11981) 2026-10-27 15:44:30 +01:00
Eva Marco
86e01d56a2
🐛 Update canvas and keep local sets when switching tokens source (#12442)
Setting a library as tokens source did not update shapes that use tokens,
because set-tokens-source never ran the token propagation that every other
token operation runs. Run it after the commit.

Going back to the local tokens, or unlinking the library that was the
source, left no local set active: the file has a single tokens status, and
switching to a library replaced it with the library's one. Keep the status
of the file's own tokens lib aside in a new optional :own-tokens-status
attribute while an external library is the source, and restore it when the
file is its own source again. Exporting without libraries and absorbing a
deleted source library restore it as well.

Closes #12430

AI-assisted-by: claude-opus-5-5
2026-10-26 15:23:05 +01:00
Alonso Torres
4fe73011ae
🐛 Reject restoring components of a linked library from plugins (#12611)
LibraryComponent.restore() did not check that the component belongs to
the current file's library. Called on a deleted component of a connected
shared library, it restored that component and saved the change to the
library file, while remove(), annotation and duplicate() reject such
handles with a validation error.

restore() now rejects them the same way: "Can only restore components
from the current file's library".

Closes #12607
2026-10-26 12:21:58 +01:00
Juan de la Cruz
55ad7d1da8
✨ Add new slides content for 2.19 release (#12185)
* ✨ Add new slides content for 2.19 release

* 🎉 Update user settings instructions in v2_19.cljs

Signed-off-by: Juan de la Cruz <delacruzgarciajuan@gmail.com>

---------

Signed-off-by: Juan de la Cruz <delacruzgarciajuan@gmail.com>
2026-10-26 12:11:12 +01:00
Eva Marco
68ecfb1234
🐛 Fix default shortcuts that fail on some layouts and browsers (#12506)
Fixes the problems found after #12326 and listed in #12327.

Mousetrap now tries several names for each key press, in order, and
fires the first one with a binding: letters and digits by keyCode when
a modifier is held, punctuation by position (layout map, or a US table
when the browser has none), the typed glyph, and the typed glyph
without the Shift it needed. With AltGr the typed glyph comes before
the position. The shortcut recorder asks Mousetrap for the key it
stores, so a recorded shortcut always fires.

This makes "?" and "+" work on every layout, Shift and Ctrl+Shift with
punctuation work in Firefox and Safari, Option+letter work on macOS,
and AltGr+º hide the UI on Linux with a Spanish layout.

Chrome on Linux returns the map of the first configured layout, not
the active one. Mousetrap now drops the map when an unmodified
punctuation key types something else.

The layout twins are no longer needed and are removed: "shift+'" made
"?" zoom to the selection on Spanish layouts. The Add node shortcut is
removed too: on US layouts its key is the "+" zoom key, and on Firefox
with a Spanish layout it cannot fire. The Firefox and Safari warning
in the shortcuts settings is gone, since those shortcuts now work
there. The user guide and translations follow these changes.

AI-assisted-by: claude-opus-5-5
2026-10-26 11:56:09 +01:00
Pablo Alba
3cd073fc6a
✨ Add auto-repair for integrity error files (#12524)
* ✨ Add auto-repair for integrity error files

* ✨ MR changes
2026-10-26 11:04:06 +01:00
Luis de Dios
4bf70de73c
🐛 Restrict per-side stroke width tokens to boards and rects in plugin API (#12332)
* 🐛 Fix restrict per-side stroke width tokens to rectangles in plugins API

* ♻️ Reuse per-side stroke restriction check across plugin API

Move the per-side stroke width restriction into the public
`per-side-stroke-attrs-restricted?` helper, now taking `page-id`, and reuse
it from both `shape.applyToken` and the token application path. Keep the
error message in a single `per-side-stroke-widths-message` def used by all
three rejection sites.

Wait for token resolution in the uniform stroke width regression test
instead of relying on a bare `settle`, which could assert before the
applied token lands.

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

---------

Co-authored-by: Luis de Dios <luis.dedios@kaieldos.net>
2026-10-23 13:32:39 +02:00
Andrey Antukh
c04af5ddc5 Merge remote-tracking branch 'origin/main' into staging 2026-10-23 13:11:52 +02:00
Eva Marco
ce5276065e
🐛 Skip unknown entries and reserve keys in shortcut settings (#12473)
* 🐛 Skip unknown entries when importing shortcuts

The shortcuts import rejected the whole file when it held a single entry
it did not know, so a profile exported from an older Penpot version, or
edited by hand, could not be imported at all. The spec in #9924 asks to
skip unknown keys, tell the user which ones, and apply the rest.

Importing now keeps the entries that map to a known shortcut with a valid
key combination and skips the rest: unknown contexts, unknown shortcut
keys, and commands that are not a key combination. The import modal
lists the skipped entries. The file is only rejected when it is not a
map or holds nothing that can be applied.

A context whose entries are all skipped is left out: an empty context
clears the user's shortcuts on import, so keeping it would wipe them.

valid-command? reads a command the way mousetrap does, so it accepts
every default shortcut and rejects strings mousetrap could never match.

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

* 📎 Sort requires in shortcuts settings test

cljfmt expects the namespace requires in alphabetical order.

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

* ✨ Reserve cancel, copy and paste shortcuts

Cancel, copy and paste are no longer customizable, in the workspace and
in the path editor, which shares their shortcut keys. Their key
combinations (Escape, Ctrl/Cmd+C, Ctrl/Cmd+V) are now reserved: the
shortcut recorder warns and blocks saving one of them for another
action, and the import skips entries that use them.

Reserved combinations come from the non-customizable workspace
shortcuts, so marking another one as non-customizable reserves its key
too. The path editor's cancel also binds V, the move tool key, so path
shortcuts are left out. Combinations compare the way mousetrap matches
them: aliases and modifier order do not matter.

apply-custom-overrides now ignores overrides of non-customizable
shortcuts. An override saved before this change would otherwise still
apply, with no way to reset it from the settings page.

AI-assisted-by: claude-opus-5-5
2026-10-22 12:24:12 +02:00
Elena Torró
452d751dff
🔧 Fix dashboard thumbnails crashing on deployed environments (#12465)
* 🐛 Pass version tag to worker so it loads the current render wasm

* 🐛 Keep wasm thumbnail queue alive when one render fails

* 🐛 Fail wasm thumbnail request when canvas context cannot init
2026-10-21 16:04:12 +02:00
María Valderrama
338d009c1e
🐛 Fix sorting for accented teams and organizations (#12505)
* 🐛 Fix sorting for accented teams and organizations

* 📎 Code review
2026-10-21 15:12:41 +02:00
Andrés Moya
3863e92993
🐛 Fix loss of swap slot when copying to clipboard (#12434)
* 🐛 Fix loss of swap slot when copying to clipboard

* 🔧 Add test and make test helpers run same code as real functions

* 🔧 Changes after review
2026-10-21 10:35:40 +02:00
Elena Torró
a1606c42ff
🐛 Export bitmaps over the GPU surface limit through the exporter (#12455) 2026-10-20 15:37:07 +02:00
Andrey Antukh
4635eaa467
🐛 Show request access modal on viewer and dashboard urls (#12456)
The exception page compared the screen query param against literals
that never occur ("view", "dashboard"), so viewer and dashboard urls
of inaccessible files and teams fell through to the plain not-found
page instead of the request access modal. Classify the param with a
tested helper that recognizes "viewer" and every "dashboard-*" route.

Closes #11224

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 22:33:10 +02:00
Andrey Antukh
e94e148ac6 Merge remote-tracking branch 'origin/staging' into staging 2026-10-19 20:10:29 +02:00
Andrey Antukh
fdd66978fd Merge branch 'main' into staging 2026-10-19 20:10:20 +02:00
Elena Torró
0801674af2
🐛 Ignore export scale in render-wasm PDF export (#12447) 2026-10-19 18:21:02 +02:00
Elena Torró
7f53483a32
🐛 Keep full quality rendering during the v3 text layout preview (#12360) 2026-10-19 17:17:34 +02:00
Andrey Antukh
38a6961ff1
✨ Make keyboard shortcuts layout-independent (#12326)
* Revert "🐛 Fix shortcuts with modifiers on letters and digits (#12292)"

This reverts commit 21ecfb67b70ac2d6f4defb05a543f0fc18d692a2.

* 🐛 Migrate mousetrap to key with sane modifiers

Finish the half-done mousetrap migration from code/keyCode to
key: canonical lowercase tokens on both bind and event sides so
modifiers match again, US-only shift remap and keypress legacy
removed, and AltGr phantom ctrl filtered on Windows.

Record punctuation positionally through getLayoutMap (American
fallback) so Shift+Period stores shift+. on every layout, while
letters and digits stay glyph-based for AZERTY. Drop dead 219
aliases and revert the stray CamelCase escape command.

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

* ♻️ Rewrite mousetrap as ESM class without behavior changes

Turn the 2012 constructor-plus-prototype shape into a class
with underscore-private methods, const tables and arrow
listeners. Drop dead _belongsTo, fold one-line prevent/stop
helpers into _fireCallback, and let handleKey absorb the old
_handleKey indirection. Public API and matching logic stay
identical; verified by existing tests plus a runtime smoke.

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

* 🎉 Add node:test suite for mousetrap shortcuts

Cover canonical key normalization, modifiers with AltGr,
bind arrays plus aliases keyup reset overwrite, sequences
with fake timers, and the full stopCallback matrix, all
through isolated instances and synthetic events. Hook the
zero-dependency suite into frontend/scripts/test for CI.

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

* 🐛 Record shortcuts by glyph instead of position

Drop the getLayoutMap positional layer and the
American fallback: punctuation now records event.key
lowercased, the same token Mousetrap dispatches, so
Shift combos match with and without a layout map.
Keeps the Numpad numN branch both sides share.
Updates the pinned positional tests and adds the
Spanish Shift-plus regression.

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

* 🐛 Resolve shifted digits to the digit itself

Shift+DigitN now resolves to the digit on every layout, so
shift+0 fires on QWERTY ()) as well as AZERTY (0), like the
old keyCode behavior. Applies in the recorder and Mousetrap;
unshifted digits and numpad are untouched. Also stages the
layout-map suite left untracked.

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

* 🐛 Cover punctuation defaults with layout twins

Some defaults only fired where their glyph is unshifted (+ dead
on US layouts, shift++ dead outside ES/DE, \ dead where it
needs AltGr). Bind the twin token per layout family and freeze
the display with :show-command. No tooltip or translation
changes.

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

* ✨ Warn when the keyboard layout API is missing

Shows a warning on the shortcuts settings page in browsers
without navigator.keyboard.getLayoutMap, where Shift with
punctuation may not fire. Uses a synchronous probe, English
and Spanish strings, and documents the layout limitations in
the user guide.

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

* 🐛 Fix hide-ui twins and qualify layout docs

The ctrl+alt+\ twin could never fire (the AltGr filter drops
Ctrl), so it goes away; alt+º covers Chrome on Spanish
layouts where AltGr+º resolves to º. The user guide now
states which claims need the layout map and which hold
everywhere.

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

---------

Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2026-10-19 16:08:47 +02:00
Belén Albeza
c99980327f
🐛 Resize fit-content flex boards while typing in v3 text editor (#12330)
* 🐛 Resize fit-content flex boards while typing in v3 text editor

Typing in an auto-width text did not resize its fit-content layout
parent until the editor closed. The v3 editor now previews the live
text size in the layout on each keystroke, also when the text sits
in a group.

* 🐛 Preview text layout when typing with a pending caret style

Both v3 editor commit paths share v3-update-editor-content.

* 🐛 Skip the layout preview for absolute texts

Absolute layout items do not affect their board.

* ⚡ Preview text layout only when the text size changes

Each preview propagates modifiers, so skip unchanged sizes.

* 🐛 Leave interactive render mode when the v3 editor closes

Finalize only ends it when it applies geometry.
2026-10-19 15:06:56 +02:00
Eva Marco
4c655e6e9e
🐛 Keep the picture in place in the SVG preview of an image crop (#12387)
With the SVG renderer, resizing a shape with an image fill and the mod
key showed the picture scaled during the drag, as in a normal resize.
The crop only showed after releasing the mouse.

The SVG preview applies the resize matrix to the shape node and ignores
the fill transform change carried by the modifiers. Now the pictures of
the shape fill patterns get a matrix that takes them from the old fill
transform to the new one, which undoes that resize and keeps them in
place. This works for shapes and boards.

Closes #12349

AI-assisted-by: claude-opus-5-5
2026-10-19 13:40:26 +02:00
Eva Marco
f4f581ab74
🐛 Fix export scale dropdown clipped in Inspect (#12351)
* 🐛 Fix export scale dropdown clipped in Inspect

The export selects in the Inspect tab used a dropdown class that always
opened them upwards, overriding the select's own up/down choice. On
elements with few properties the export section sits near the top of
the panel, so the scale list (now eight options) went past the top of
the scroll area and hid 0.5x. Keep only the dropdown width and let the
select pick the direction.

Closes #12346

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

* ♻️ Move Inspect exports styles to DS tokens

Replace the deprecated spacing and size variables, the flex mixins, the
color aliases and the typography mixins with DS tokens, and use logical
properties. The typographies switch to the closest DS ones: body-small
for the suffix input (line-height 1.3 instead of 1.4) and headline-small
for the export button (12px instead of 11px). The @extend placeholders
still come from refactor/ and are left for a later migration.

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

* ♻️ Use DS buttons in Inspect exports

Replace the add, remove and export buttons in the Inspect exports section
with the DS icon-button and button, as the workspace export section does.
Delete their now unused styles and the sizes import; the export button
class only stretches the button and centers its label.

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

* 🐛 Fix add export button lookup in the Inspect exports e2e test

The add export button in Inspect is a DS icon button now and lost its
.add-export class, so the test timed out waiting for it. Find it by its
role and accessible name instead.

AI-assisted-by: claude-opus-5-5
2026-10-19 10:50:32 +02:00
Eva Marco
a5678657f5
🐛 Fix padding on sets section (#12422) 2026-10-19 10:48:39 +02:00
makesomethingshit
91a5b3a9fb
🐛 Fix token tree path handling for repeated and nested segments (#11591)
* 🐛 Fix token tree collapsing repeated path segments

Tokens whose path contains the same segment name twice in a row
(e.g. test.pointer.pointer) collapsed to a single level in the
token tree.

build-tree-node marked a node as a leaf when the current segment
name matched the first segment of every remaining path. Because
group-by-first-segment rewrites nested paths into the segment
name, a still-nested remaining path was indistinguishable from a
token ending at the current node, so test.pointer.pointer merged
into one leaf.

Mark rewritten segments with an internal ::remaining? marker and
strip it when the rest-path is fully consumed; is-leaf? only
accepts unmarked remaining segments as leaves.

Closes #11588

AI-assisted-by: glm-5.3-flash
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Preserve ancestor tokens in path trees

Keep a node's token and descendants independently when imported paths
contain both an ancestor and a child. Render both in the token tree and
include the ancestor token in existing path-based group operations.

Cover accepted same-set imports, leaf IDs, repeated segments, and internal
marker removal without changing import or collision validation.

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

* ♻️ Rework token tree path building

Carry the segments left to descend as a ::remaining-path vector on each
segment and advance it by subvec, instead of rewriting the segment :name
and re-splitting it later.

With that, build-tree-node finds the leaf token with a predicate over
::remaining-path and takes children from the segments that still have
one, so the pre-grouping children-first sort no longer changes the
result. Drop sort-children-first and its segment-has-children? helper,
and document inside-path?.

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

* 🐛 Fix and relocate the token tree tests

The ancestor-and-descendant test called a function that does not exist
in app.common.types.tokens-lib, so the namespace failed to load and the
whole common suite errored. Use make-tokens-status-from-lib and the
two-argument get-tokens-in-active-sets instead.

Move the build-tree-root tests to path-names-tree-test so
path-names-test stays a pure split/join unit test.

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

* ♻️ Refactor the token tree rendering and share path helpers

Extract the token pill into a leaf-node* component and treat a node as a
leaf only when it holds a token and has no children, so a node can carry
both a token and its children.

Make token-separator public and reuse it from the token tree and the
management panel instead of local copies. Both management path filters
inlined the same ancestor-or-descendant rule; extract it as
token-in-path? and cover it with a unit test.

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

---------

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: luisddm <luis.dedios@kaieldos.net>
2026-10-19 09:44:32 +02:00
Andrey Antukh
d1aa36b814 ⬆️ Update pnpm to 12.14.0 and Node to v24.22.0
Bump the pnpm pin to 12.14.0 in the devenv and the production images
and stamp the same version into every package.json, realigning the
drift between the devenv (12.9.1) and the rest (12.6.0).

Bump Node to v24.22.0, the newest patch of the Krypton LTS line, in
.nvmrc, the devenv and image Dockerfiles and the plugins-deploy
workflows.

The pnpm arch checksums are those of the GitHub release tarballs, not
of the npm package. Lockfile changes are metadata only.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 14:00:43 +02:00
Marina López
cec8080351
🐛 Show trial status on subscription page (#12389) 2026-10-16 18:22:51 +02:00
Alonso Torres
02e10a005a
✨ Add token source to plugins (#12183)
* ✨ Add token source to plugins

* ✨ Changes after review

* ✨ Move plugins code to library-edit
2026-10-16 17:58:51 +02:00