14386 Commits

Author SHA1 Message Date
alonso.torres
5c10410b4d 💄 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-10-29 09:56:32 +01:00
alonso.torres
4fa940e2de 🐛 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-10-29 09:56:32 +01:00
alonso.torres
b0fa06741e 🐛 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-10-29 09:56:32 +01:00
alonso.torres
46209cef13 🐛 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-10-29 09:56:15 +01:00
alonso.torres
84d4db0ec8 ✨ 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-10-29 09:56:15 +01:00
alonso.torres
ef3f146ca0 🐛 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-10-29 09:56:03 +01:00
alonso.torres
942299b079 ✨ 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-10-29 09:54:42 +01:00
alonso.torres
fdfadd359f 🐛 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-10-29 09:51:29 +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
alonso.torres
f23eb31bd7 ✨ Send pending edits when the user leaves the tab or window
Edits wait up to 3s in a buffer before they are sent. Closing the
laptop or switching away right after an edit could leave that batch
unsent, and a crash or a discarded tab then lost it.

The batch is now sent at once when the tab is hidden (another tab, a
minimized window, a closed tab or laptop) or the window loses focus
(another app or a plugin iframe). The 3s window itself is unchanged.
pagehide and beforeunload are left out: they come too late for an
async save, and the tab is already hidden by then.
2026-10-16 15:24:22 +02:00
alonso.torres
a3fc7c8465 ✨ Save before downloading a .penpot file and warn if behind
The .penpot download is built from the server copy. Users who
downloaded a file to keep their work while saves were failing got an
old or empty file, with no warning.

Opening the download dialog now sends the pending changes of the files
and waits up to 10s for them. If some are still not saved, the dialog
says the download will not include them.
2026-10-16 15:24:22 +02:00
alonso.torres
a1e51aacba 🐛 Keep the workspace open when an export fails
A failed export raised the exporter error to the global handler, which
replaced the workspace with the error page. Users then reloaded and
lost the changes that were not saved yet.

- A failed single or multiple export now shows an error toast and
  keeps the workspace. Exporter defects still report as unhandled.
- A failed multiple export request ends its progress listeners, so the
  leave-page warning it turned on is released.
- Before exporting, the pending changes of the exported files are sent
  and the export waits up to 10s for them, instead of 400ms. Exports
  are built from the server copy, so if some changes are still unsaved
  a toast warns that the export may not include them.

force-persist-files-and-wait waits only for the given files, so a stuck
save of another file does not delay the export.
2026-10-16 15:24:22 +02:00
alonso.torres
781ac534e0 ✨ Count save failures with a save-failed audit event
We only learn how often users lose work from support tickets. Each time
the user is told a save failed, persistence now emits a save-failed
audit event with the cause type, the code, whether the queue is still
retrying and how many changes are waiting.

It follows the same rule as the failure toast: a repeated failure of
the slow retry cycle is not counted again. Failures handed to another
handler, such as an expired session, are counted too, since they also
stop the save.
2026-10-16 15:24:22 +02:00
alonso.torres
4b668acf96 🐛 Retry saves that a busy backend refused or rolled back
A save waits for the lock that another save holds on the same file. A
slow save, such as one on a large file, blocks the other saves to that
file. The waiting request also holds the user's only save slot on the
backend, so the user's saves to other files wait behind it too.

Those saves ended in answers that stopped autosave for good:

- a 429 after 60s, when no save slot freed up in time;
- a 504 written by the backend after 300s, with :statement-timeout,
  when Postgres cancelled the wait for the lock.

In both cases the backend never applied the save, so sending it again
is safe. transient-error? now treats them as temporary, and the save
takes the retry path. A statement timeout takes 300s per attempt, so
the slow failure limit still bounds it. GET requests and error reports
are unchanged: the check lives in the save pipeline, not in
retryable-types.

Both answers were reproduced in the devenv by holding the file lock
from a separate database session.
2026-10-16 15:24:22 +02:00
Andrey Antukh
03134d35ae
🐛 Fix silent failure when uploading custom fonts (#12315)
* 🐛 Fix silent failure when uploading custom fonts

Custom font upload crashed with re-seq must match
against a string when opentype.js returned nil
subfamily names, leaving the user with no feedback.
Unknown signatures and unparseable files were also
silently dropped.

Make variant parsing nil-safe, harden entry
preparation for missing tables, report every
per-file failure with a bad-font notification, and
show a generic error if the stream itself fails.

Closes #12304

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

* ♻️ Rename font mtype helper and mark helpers private

Avoid confusion with app.worker.import/parse-mtype,
which detects binfile signatures. The font helper now
lives as parse-font-mtype, and the upload helpers only
used internally are private, with tests reaching them
through var references.

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

* 💄 Fix formatting in font upload tests

Realign map keys shifted by the var-quote prefix so
cljfmt check passes again. No behavior change.

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

* 🐛 Fix infer warnings in fonts metrics code

Add missing ^js type hints to the hhea and os2 metric lookups in
prepare-font so shadow-cljs can infer the interop targets and the
release build reports zero warnings.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-16 12:51:48 +02:00
Andrey Antukh
9ea4c3404f Merge remote-tracking branch 'origin/main' into staging 2026-10-16 11:25:25 +02:00
Pablo Alba
0afe1e193d
🐛 Fix team switcher height (#12371) 2026-10-16 09:13:03 +02:00
Pablo Alba
296ad9704f
🐛 Fix bad message changing team to another organization (#12354) 2026-10-15 21:41:36 +02:00