11311 Commits

Author SHA1 Message Date
elhombretecla
1b2e7b602e ✨ Add scale options panel to the right sidebar 2026-11-06 19:10:52 +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
Á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
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
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
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
alonso.torres
c8ef46a0a4 💄 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-05 23:59:32 +01:00
alonso.torres
8ef3ba628d 🐛 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-05 23:59:32 +01:00
alonso.torres
6426b17c9c 🐛 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 which version
  (by its name, else its date), saying that later changes are no longer
  in the file and, when they had changes not saved yet, that those were
  lost.
- 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-05 23:59:32 +01:00
alonso.torres
94c05589cf 🐛 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-05 23:59:32 +01:00
alonso.torres
cb26e8a39b ✨ 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-05 23:59:32 +01:00
alonso.torres
f4b20ed08f 🐛 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-05 23:59:32 +01:00
alonso.torres
4f42ab1d2d ✨ 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-05 23:59:32 +01:00
alonso.torres
ef4d66c3aa 🐛 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-05 23:59:32 +01:00
Andrey Antukh
97fe51e08b Merge remote-tracking branch 'origin/staging' into develop 2026-11-05 22:43:31 +01:00
Elena Torró
afc40a3309
🐛 Refresh wasm heap views after allocations and wasm calls (#12833)
Growing the heap detaches typed views taken before an alloc or a call.
Take them after, so reads and writes never hit a detached buffer.
2026-11-05 18:09:59 +01:00
Elena Torró
a32f5a9811
⚡ Index fonts by family when fixing deleted fonts on page load (#12853) 2026-11-05 17:51:42 +01:00
Elena Torró
02223a67fa
🐛 Fix unmoved layout child stretched during wasm gap drag (#12831)
Follow-up to 419513011c: only pure moves pass down to children in wasm.
Remove unused set-wasm-rotation-modifiers.
2026-11-05 17:33:07 +01:00
Eva Marco
eb6e976c01
♻️ Move the dashboard header styles to the DS (#12676)
* ♻️ Move the dashboard header styles to the DS

The page header of the dashboard and settings pages took its styles from
common/refactor/common-dashboard.scss. Move the rules still in use to
dashboard/common/header.scss, written with DS tokens:

- The page titles become heading* (title-large) with a class instead of
  a bare h1; the team section links get a class, and the active class
  moves from the li to the link.
- Drop the rules that matched nothing: .element-name, .context-menu,
  .icon and .btn-secondary inside the header, and the global .btn-primary
  and .btn-secondary (only the fonts page uses them; it keeps the old
  import until its open change lands).
- The sidebar and subscription styles imported the file without using
  it; drop the import.

The header looks the same (0 px in the new visual test). The title line
height goes from 1.15 to 1.4, which does not move the text.

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

* ♻️ Move the dashboard libraries and search styles to the DS

Replace the deprecated spacing, size and border variables with DS
tokens and logical properties, and drop the refactor/ import.

- The search placeholder icon gets a class for its color instead of a
  .icon svg rule; its xl size already gives the 32px box.
- Delete the selectors no markup uses: .dashboard-projects in both
  files and .dashboard-shared in search.

No visual change (0 px in the new libraries and search visual test).

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

* 🔥 Move the fonts page header to the DS and delete common-dashboard

The fonts page was the last user of common/refactor/common-dashboard.scss:
its header now uses the shared DS header styles and a DS heading, like
the other dashboard pages. Nothing else imports common-dashboard, so it
is deleted.

No visual change.

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

* 📎 Remove a double blank line in the dashboard sidebar styles

Prettier rejects the two blank lines left after the imports, which made
the frontend lint fail in CI.

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

* 📎 Remove a blank line between the subscription style imports

Stylelint (at-rule-empty-line-before) rejects the blank line left between
the @use rules after the common-dashboard import was dropped.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:01:04 +01:00
Pablo Alba
f4a0d42363
🐛 Keep multi-select variant switches refreshing every instance (#12857)
* 🐛 Keep multi-select variant switches refreshing every instance

Multi variants-switch ended each swap with select-shape, which replaced
the selection, interrupted layout/WASM work, and navigated once per copy
so only one instance looked updated under flex. Skip per-swap select and
layout in the batch, re-read live geometry, coalesce layout ids via a
companion DataEvent, then restore selection once. The same path covers
Assets component-multi-swap. Frontend tests lock select-shape count,
layout coalesce, and mixed no-op selections.

Closes #12829

AI-assisted-by: Composer
2026-11-05 16:52:52 +01:00
Eva Marco
0f7e3fe92e
♻️ Use DS icon buttons in the path editing bar (#12682)
* ♻️ Use DS icon buttons in the path editing bar

The path editing bar now uses DS icon buttons (ghost, tooltip below) and
DS icons, like the top toolbar. The toggles (draw/move mode, handler
types, snap) use aria-pressed, and the handler type trigger uses the DS
flyout indicator, with no tooltip because its menu opens on hover.

This also shows the handler and snap icons again: since #12659 their
SVGs draw with currentColor, and the legacy %button-icon set color to
transparent, so they were invisible.

Add a visual test of the bar, its hover and the handler menu.

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

* ♻️ Move the path editing bar styles to the DS

Drop the deprecated styles import, resolve --panel-border-color to
--color-background-quaternary and use logical properties, except top and
height of the bar, which its transition names.

Delete the selectors the DS buttons left unused: .handler-trigger and
.flyout-indicator (the DS icon button draws its own indicator).

No visual change.

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

* 📎 Add the blank line stylelint expects in the path bar styles

Stylelint (scss/double-slash-comment-empty-line-before) rejects the
comment right after a declaration, which made the frontend lint fail in
CI.

AI-assisted-by: claude-opus-5-5
2026-11-05 16:28:56 +01:00
Belén Albeza
fa1b3d4708
⚡ Keep only the shape data the worker reads (#12840)
* ⚡ Keep only the shape data the worker reads

The worker kept a full copy of the current page for its selection and
snap indexes. It now keeps only the attributes the indexes and the
change replay read, and re-indexes only the shapes that changed.

On a 52k-shape page the worker heap goes from 698 MB to 286 MB.

Closes #12740

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

* ♻️ Share the worker re-trim with its replay test

The worker and its replay test now share `retrim-objects`.

AI-assisted-by: claude-opus-5-5
2026-11-05 16:25:29 +01:00
Nimesh
9d3d6da0d0
♻️ Migrate inspect components to modern syntax (#12435)
Migrate the remaining legacy components in the inspect module to the
modern rumext syntax: typography-block, text-block, svg-attr, svg-block,
selection-rect, selection-feedback, layer-item and left-sidebar.

Rename the ?-suffixed props of layer-item (component-child? to
is-component-child, hide-toggle? to hide-toggle) and drop the unused
sortable? and filtered? props passed from left-sidebar.

Relates to #9260

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

Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-05 15:47:59 +01:00
Elena Torró
d82b5a4b1e
🐛 Fix pasted text invisible in new text box (#12859) 2026-11-05 15:00:46 +01:00
Belén Albeza
c6a9e62a4a
🐛 Fix typography in Settings (#12841) 2026-11-05 11:39:43 +01:00
Eva Marco
9349c3b778
♻️ Make the workspace logo and project name real links (#12699)
* ♻️ Give the workspace logo a real link and the DS logo asset

The Penpot logo in the workspace header was an <a> without href, so the
keyboard could not reach it and it could not open in a new tab. It now
links to the dashboard Recent page; a plain click still runs the in-app
navigation (prevent default), and Ctrl/Cmd-click opens the link in a
new tab.

The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing), with its own 32px class. The legacy svg kept its 500px height
attribute, so it overflowed the header by 222px above and below. This
was the last legacy icon in left_header.cljs.

New visual test "Shows the Penpot logo link": 0 px of difference at rest
and on hover.

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

* ♻️ Make the workspace project name a real link

The project name in the workspace header was a div with on-click that
opened the project files in a new tab, so the keyboard could not reach
it. It is now a link to the same page with target "_blank": the browser
opens the new tab, and Tab, Enter and "Open link" work as for any link.

The left header visual test gives 0 px of difference.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:32:08 +01:00
Eva Marco
1d8f2fc717
♻️ Give the viewer logo a real link and the DS logo asset (#12702)
The Penpot logo in the viewer header was an <a> without href. For team
members it now links to the dashboard Recent page of the file's team: a
plain click still runs the in-app navigation (prevent default), and
Ctrl/Cmd-click opens the link in a new tab. Without team permission it
stays as before: no href and no click.

The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing, same 28px class). This was the last legacy icon in
viewer/header.cljs.

The "Viewer header" visual test gives 0 px of difference.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:50 +01:00
Eva Marco
f827b7b4a6
🐛 Make the dashboard template cards keyboard buttons (#12710)
A template card was an <a> without href inside a focusable div: Tab
reached the div, and neither Enter nor Space imported the template, so
the cards could not be used from the keyboard. Importing is an action,
so the card is now a <button> with the same look; it takes the tab
index the div had, and the manual Enter handler goes away.

Keyboard focus looks like hover (accent border and "Add to your
project").

The templates visual test gives 0 px of difference at rest, on hover and
for the scroll and collapse buttons; a new shot covers the focus.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:33 +01:00
Eva Marco
686b47742d
🐛 Make the profile page modal links keyboard buttons (#12712)
"Change email" and "Want to remove your account?" were <a> without href
that open modals: the keyboard could not reach them. They are now
<button type="button"> that keep their text-link look (accent color,
underline on hover), so Tab reaches them and Enter or Space opens the
modal.

The profile visual test gives 0 px of difference, now also with a shot
of the hover on "Want to remove your account?".

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:15 +01:00
Eva Marco
8ab9624f0b
🐛 Make Download report.txt a keyboard button (#12723)
On the internal error page and the feedback page, "Download report.txt"
was an <a> without href, so Tab skipped it. It is an action, so it becomes
a <button type="button"> with the same look, underline on hover included.
The type keeps it from sending the feedback form.

A new visual test reaches both pages through a mocked server error and
checks 0 px of difference and the download with Enter.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:30:39 +01:00
Eva Marco
657ce8b054
🐛 Make the login dialog section links keyboard buttons (#12724)
"Forgot password?", "Create an account" and "Login here." switch the
section of the login dialog (viewer and MCP authorize) and of the dialog
on the not found page. They were <a> without href, so Tab skipped them.
They become <button type="button"> with the same look; a class replaces
the bare a selector in static.scss.

A new visual test opens both dialogs, uses the keyboard to switch
sections and checks 0 px of difference at rest and on hover.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:30:23 +01:00
Pablo Alba
7666d7d500 🐛 Fix Creating or combining a variant set renames the component first 2026-11-04 21:44:44 +01:00
Pablo Alba
eeda9c1d18 🐛 Fix renaming a variant set saves half-finished steps 2026-11-04 21:44:44 +01:00
Pablo Alba
d47f366c70 🐛 Fix Creating or combining a variant set saves half-finished steps 2026-11-04 21:44:44 +01:00
makesomethingshit
d42217a32b
🐛 Create Plugin API shapes at the page root (#12499)
* 🐛 Create Plugin API shapes at the page root

Create component instances and SVG shapes at the page root so a board at
the page origin cannot capture them. Continue using the existing forced
frame option for component instances and an explicit parent frame for
SVG imports. Interactive placement retains its current behavior.

Cover component instances, SVG imports and SVG imports with images on
ordinary, flex and grid origin boards, including undo, redo, position
changes and explicit reparenting.

Closes #12086

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

Signed-off-by: choi junsoo <junsoo1172@gmail.com>

* 🐛 Preserve interactive creation placement

Cover component and SVG placement inside and outside an origin board.
Exercise the sidebar move lifecycle and verify parent preservation.
Correct the Plugin API changelog category.

AI-assisted-by: gpt-6
Signed-off-by: choi junsoo <junsoo1172@gmail.com>

* 🔧 Cover page-root creation in the Plugin API test suite

Check that LibraryComponent.instance(), createShapeFromSvg() and
createShapeFromSvgWithImages() create shapes at the page root when a
board covers the page origin. Each test removes the shape it creates,
since it lives outside the scratch board.

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

---------

Signed-off-by: choi junsoo <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-11-04 17:40:14 +01:00
Pablo Alba
69fa682217 🐛 Keep variant sets intact under group, mask, board, and layout 2026-11-04 16:45:23 +01:00
Pablo Alba
b2c9ca1d23 🐛 Fix mask shortcut works on variant main 2026-11-04 16:45:23 +01:00
Elena Torró
4be8df5877
⚡ Load the rasterizer iframe only when the SVG renderer needs it (#12835)
Create it once features say SVG, never in wasm mode. Also skip SVG frame
thumbnails in wasm mode.
2026-11-04 15:50:13 +01:00