24405 Commits

Author SHA1 Message Date
Eva Marco
33d304c0af
♻️ Use DS icons in the dashboard sidebar (#12534)
* ♻️ Use DS icons in the dashboard sidebar

The search, clear search, pinned projects placeholder and logout icons
of the dashboard sidebar use the DS icons, and they get their color
from the color property. The two search buttons had untranslated test
names as their labels ("dashboard-search"); they are now named "Search"
and "Clear search". The pin keeps its place: the DS small icon draws
12px inside a 16px box, so its margins shrink by 2px.

New key: dashboard.clear-search (en, es). The dashboard page object
looks the search button up by its new name. A visual test covers the
search box with and without a term, the pinned projects placeholder and
the profile menu.

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

* ♻️ Move dashboard sidebar styles off the deprecated styles

Legacy color aliases resolve to their DS tokens, deprecated mixins,
sizes and font variables are inlined with DS values, physical
properties become logical ones and the DS typography module is used
without a namespace. An invalid max-width with two values, which the
browser ignored, is gone.

Two rules that match nothing are commented out with a TODO: the profile
menu arrow svg rule (the arrow is the DS icon svg itself) and the
upgrade plan section, which sidebar.cljs does not use.

refactor/ stays only for the profile menu placeholders (%menu-dropdown,
%menu-item-base) and the shared dashboard header styles
(common-dashboard). No visual change.

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

* 🔥 Remove unused styles from the dashboard sidebar

The SCSS refactor of the dashboard sidebar left rules commented out with
a TODO because the module does not use them, or because they target an
svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.profile-dropdown-item .open-arrow svg, .upgrade-plan-section,
.penpot-free, .no-limits, .power-up. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-27 11:41:19 +01:00
Eva Marco
afd838ea5b
♻️ Use DS buttons in the dashboard team modals (#12537)
* ♻️ Use DS buttons in the dashboard team modals

The close buttons of the create/rename team modal, the team permission
notice and the two "before you leave" modals become ghost DS icon
buttons named "Close"; the class that places them goes on the tooltip
wrapper. The cancel and "promote and leave" inputs of the leave modals
become DS buttons: secondary, and destructive once a member is picked
(primary and disabled before).

Two fixtures make the leave and reassign modal testable (the logged-in
user owns the second team, which has another member). A visual test
covers the create team modal and the leave and reassign modal.

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

* ♻️ Move dashboard team modal styles closer to the DS

The change owner and team form stylesheets resolve their legacy color
aliases, spacing and typography mixins to DS tokens and use the DS
typography module without a namespace. The legacy uppercase title
becomes the DS headline-small typography, which differs only in its
size (12px instead of 11px): the "before you leave" title grows 1px and
the modal 2px. Unused selectors are commented out with a TODO.

refactor/ stays only for the modal placeholders (%modal-*), which move
with the modals refactor, and the input placeholders.

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

* 🔥 Remove unused styles from the dashboard team modals

The SCSS refactor of the dashboard team modals left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.input-wrapper, .cancel-button, &.danger. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-27 10:58:13 +01:00
Eva Marco
19fc9ebce8
♻️ Use DS icons in the viewer flows and interactions menus (#12520)
* ♻️ Use DS icons in the viewer flows and interactions menus

The flows and interactions triggers in the viewer header were divs with
a click handler, and their menus lived inside them. They are now
buttons with aria-expanded and aria-haspopup, and each menu sits next
to its button in a wrapper, since a button cannot hold the menu
entries. The play, arrow and tick icons use DS icons. The menu entries
stay as they are.

A visual test covers the interactions menu closed and open; the test
files have no flows, and the flows trigger shares its styles.

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

* ♻️ Move viewer interactions styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The DS icons take their color from the
text and no longer shrink, so the old icon placeholders go away. The
DS body-small line-height is more compact, so the trigger labels sit
1px higher. The refactor/ import stays only for the shared menu
placeholders.

AI-assisted-by: claude-opus-5-5
2026-10-27 10:47:07 +01:00
Eva Marco
9854f8e54f
♻️ Move dashboard subscription styles off the deprecated styles (#12538)
* ♻️ Move dashboard subscription styles off the deprecated styles

The subscription stylesheet inlines the deprecated button-style and
flex-center mixins, resolves its legacy color aliases and sizes to DS
tokens, uses logical properties and the DS typography module without a
namespace. Three rules that match nothing (the arrow svg, .cta-link and
.open-arrow) are commented out with a TODO.

The subscription controls need no DS change: the plan card, the current
plan card and the manage subscription link have no DS equivalent, and
they use no legacy icons. refactor/ stays only for the power up menu
item placeholder and the shared dashboard header styles.

A visual test covers the sidebar plan card (closed and open), the team
settings plan and the members seats warnings. No visual change.

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

* 🔥 Remove unused styles from the dashboard subscription

The SCSS refactor of the dashboard subscription left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.icon-dropdown svg, .cta-link, .open-arrow. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-27 10:23:55 +01:00
Eva Marco
411f661bdb
♻️ Use DS buttons in the delete shared libraries modal (#12554)
* ♻️ Use DS buttons in the delete shared libraries modal

The delete shared libraries modal (unpublish a library, delete a shared
file or project) gets a ghost DS icon button named "Close", placed on
the tooltip wrapper, and DS buttons for cancel (secondary) and accept
(destructive for the danger style, primary otherwise) instead of raw
inputs. The buttons are narrower and the accept button takes the DS
destructive red.

A new empty library references fixture and a visual test cover the
unpublish library modal from the workspace main menu.

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

* ♻️ Move delete shared libraries modal styles closer to the DS

The delete shared libraries stylesheet resolves its legacy modal color
aliases to DS tokens, uses a logical property and the DS typography
module without a namespace. refactor/ stays only for the modal
placeholders (%modal-*), which move with the modals refactor. Two
unused selectors are commented out with a TODO. No visual change.

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

* 🔥 Remove unused styles from the delete shared libraries modal

The SCSS refactor of the delete shared libraries modal left rules
commented out with a TODO because the module does not use them, or
because they target an svg inside a DS icon that is the svg itself. CSS
modules scope the classes to this module and no other source uses them,
so they go: &.transparent, .list-wrapper. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-27 10:20:11 +01:00
Pablo Alba
e4c1b154e3
✨ Add profile name and email to nitrate audit log (#12636)
* ✨ Add profile name and email to nitrate audit log

* ✨ Mr changes
2026-10-27 09:30:28 +01:00
David Barragán Merino
4b13562d02 👷 Use only the closest tag in the build key
The full `git describe` output is not stable across checkouts: the
length of its -g<sha> suffix depends on the local object count, so
build-bundle and build-docker could compute different keys for the same
commit on different runner workspaces. The sha already pins the commit,
so the closest tag is enough to tell versions apart.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-26 20:30:08 +01:00
David Barragán Merino
cf19d7f870 👷 Include the version in the bundle and image build key
The bundle stamps the version into version.txt, so a commit that gets a
new tag (2.18.1-RC1 -> 2.18.1) needs a new bundle and image set. Keying
them by commit sha alone reused the RC artifacts for the final release.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-26 19:55:11 +01:00
Eva Marco
f07452e36b
♻️ Use DS buttons and icons in the inspect code tab (#12521)
* ♻️ Use DS buttons and icons in the inspect code tab

"Copy all code" becomes a DS secondary button, and the expand panel
buttons ghost DS icon buttons labelled "Expand panel" (new key, en and
es). The collapse toggles of the CSS and markup blocks stay raw
buttons, since they sit in a 12px column; their arrow uses DS icons and
they get a name and aria-expanded. The copy buttons belong to the
shared copy button and move with it.

A visual test covers the code tab buttons, expanded and collapsed.

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

* ♻️ Move inspect code tab styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The 11px uppercase titles become the
12px DS headline-small, the only difference. The code typography keeps
its values with a TODO: the DS code-font names "Roboto Mono", while
the app loads "robotomono". The full-width "Copy all code" DS button
centers its label explicitly. The refactor/ import stays only for the
copy buttons, styled here until the shared copy button moves to the DS.

AI-assisted-by: claude-opus-5-5
2026-10-26 17:55:54 +01:00
Eva Marco
96b62f6475
♻️ Use DS buttons in the delete account modal (#12540)
* ♻️ Use DS buttons in the delete account modal

The close button of the delete account modal becomes a ghost DS icon
button named "Close"; the class that places it goes on the tooltip
wrapper. The cancel and delete buttons become DS secondary and
destructive buttons, so their labels no longer wrap and the delete
button takes the DS destructive red. The delete button keeps its test
id.

A visual test covers the modal at rest and with the close and delete
buttons hovered.

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

* ♻️ Move delete account modal styles closer to the DS

The delete account stylesheet resolves its legacy sizes, spacing,
mixins and color aliases to DS tokens and uses logical properties and
the DS typography module without a namespace. The legacy uppercase
title becomes the DS headline-small typography, which differs only in
its size (12px instead of 11px), so the title and the content below sit
1px lower. The organization counts used a fixed dark-theme gray; they
now use the themed DS secondary foreground. Two unused selectors are
commented out with a TODO.

refactor/ stays only for the modal placeholders (%modal-*), which move
with the modals refactor.

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

* 🔥 Remove unused styles from the delete account modal

The SCSS refactor of the delete account modal left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.fields-row, .select-title. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-26 17:17:46 +01:00
Eva Marco
376ecda94e
♻️ Move viewport top bar styles off the deprecated styles (#12488)
* ♻️ Move viewport top bar styles off the deprecated styles

The viewport top bars (view only bar of the inspect tab, path edition
bar) already use DS buttons. Move top_bar.scss to DS sizes, borders,
spacing tokens and logical properties, resolve the panel and shadow
color aliases, and drop the refactor import. The unused done-btn class,
the last user of the deprecated button placeholders here, goes away.
The two z-index values have no DS token, so they stay with a TODO.

Add a visual test of the view only bar; it looks the same as before,
pixel for pixel.

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

* ♻️ Use the body-small typography in the viewport top bar title

The title only set a 12px font size, which body-small already has.
Use the DS typography instead of the inline size and its TODO. The
view only bar looks the same, pixel for pixel.

AI-assisted-by: claude-opus-5-5
2026-10-26 15:53:05 +01:00
Pablo Alba
f2e5b7e20e
🐛 Fix swapped nested component loses absolute position after a lib update (#12556) 2026-10-26 15:24:18 +01:00
Filip Sajdak
576bf4ed3d
✨ Close search and shortcuts panels with Escape (#12374)
* ✨ Close search and shortcuts panels with Escape

The workspace search and shortcuts panels could only be dismissed with
the mouse, which interrupts keyboard-first workflows.

A new use-escape-to-close hook gives each panel an on-key-down handler
that closes it on Escape while the user is working in it: the latest
pointer press or focus change must have landed inside the panel, or the
panel must hold focus when it opens (a panel that autofocuses its search
box, such as Shortcuts, is closed by the first Escape). Focus left
behind in a panel after the user moved on to the canvas does not count,
so Escape keeps its usual meaning there.

The handler never stops the event. The global Escape shortcut still
runs and finishes in-progress operations such as path drawing; an
earlier version stopped propagation and left a half-drawn path that
failed shape validation on the next commit.

In the search panel an open filters menu takes the first Escape and
the panel closes on the second.

The history panel needs no handler: Escape already closes it through
the :interrupt it emits.

Closes #9540

AI-assisted-by: claude-opus-5-5
AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>

* 📎 Add e2e test for closing panels with Escape

Cover the Escape handling added for the layers search and shortcuts
panels with four Playwright cases on the mocked workspace:

- Escape closes the layers search panel.
- With the search filters menu open, the first Escape closes only the
  menu and the second closes the panel.
- Escape closes the shortcuts panel right after it opens with `?`. The
  panel autofocuses its search box, so the first Escape must close it
  instead of only blurring the box.
- A path drawn while focus sits in the shortcuts panel is finished by
  Escape, which proves the panel does not swallow the global shortcut.

The first three cases fail on develop. The shortcuts case also fails
when the hook does not seed its tracker from the focused element.

AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>

* 🐛 Close History with Escape when opened by shortcut or menu

Only the header button opened History through initialize-history, which
removes the :document-history flag on the next :interrupt. The
Ctrl/Cmd+Alt+H shortcut and the "Version history" menu item toggled the
flag directly, so the global Escape left the panel open. Both now emit
initialize-history, like the header, and keep their own event origin.

Two e2e cases cover the shortcut and the menu; both fail without this
change.

AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>

* ♻️ Apply review suggestions to Escape-to-close

Use dom/child? instead of .contains in the Escape hook and drop the
redundant nil checks. Drive the path test through clickAt so its clicks
wait for the path tool. Add a negative Search case: Escape reaches the
panel after the last click landed on the viewport, and the panel stays
open. That case fails when the containment check is removed.

AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>

---------

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-10-26 14:42:43 +01:00
Eva Marco
e24e9dc747
♻️ Use DS buttons and icons in the viewer header (#12523)
* ♻️ Use DS buttons and icons in the viewer header

The section buttons, full screen and edit become ghost DS icon buttons
(the last two were spans), with aria-pressed where they toggle. Share
and "Log in or sign up" become DS buttons. The zoom widget becomes a
button that toggles its menu, like the workspace one, with DS zoom
buttons inside. The breadcrumb and the current board name become
buttons with aria-expanded, and the page menu moves next to its
button. The arrow and tick icons use DS icons. The logo stays: it is
brand art.

A visual test covers the header in two sections and both menus.

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

* ♻️ Move viewer header styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the button placeholders go away.
The section buttons column goes from a fixed 92px to auto, since the
three 32px DS buttons no longer fit; the 1fr side columns keep it
centered. The zoom widget follows the workspace header one. The 11px
project name becomes the 12px DS headline-small. The global .selected
rule also highlighted the current page in the page menu: that row
keeps its highlight through its own rule. The refactor/ import stays
only for the shared menu placeholders; the options z-index (10) has no
DS token and keeps a TODO.

AI-assisted-by: claude-opus-5-5
2026-10-26 14:39:06 +01:00
Eva Marco
48a5875a71
♻️ Use DS icon buttons in the viewer board navigation (#12527)
* ♻️ Use DS icon buttons in the viewer board navigation

The previous, next and first board buttons of the viewer become
secondary DS icon buttons with the DS arrow and reload icons. They keep
their size and the viewer background at rest; on hover they take the DS
secondary look. The first board button had no accessible name; it is
now labelled. The position classes move to the tooltip wrappers, so the
fullscreen and comments sidebar offsets still apply.

New key: viewer.go-to-first-board (en, es). A visual test covers the
buttons at rest and on hover.

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

* ♻️ Move viewer styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the file no longer imports
refactor/. The section scrollbar uses the DS custom-scrollbar mixin; the
text selection and placeholder styles that %new-scrollbar gave to the
comments layer stay, with DS tokens of the same value. An invalid
padding-right with four values, which the browser ignored, is gone.

The DS body-small typography differs from the old one only in a more
compact line-height, so the board counter sits 1px higher. The z-index
values (10, 2) have no DS token and keep a TODO. The .viewer-header,
.not-fixed, .fixed and .frame-children rules are not used by this module
and are commented out with a TODO.

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

* 🔥 Remove unused styles from the viewer

The SCSS refactor of the viewer left rules commented out with a TODO
because the module does not use them, or because they target an svg
inside a DS icon that is the svg itself. CSS modules scope the classes
to this module and no other source uses them, so they go:
.viewer-header, .not-fixed, .fixed, .frame-children g. No visual change.

AI-assisted-by: claude-opus-5-5
2026-10-26 12:48:43 +01:00
Belén Albeza
577cbe43e4
🐛 Validate the text payload before pasting it (#12612)
Text copied in Penpot carries its content in a data attribute of the
clipboard HTML, which plugins and other apps can rewrite. The v3 editor
and canvas paste used it as is, so bad data could reach WASM and the
file and break the shape.

The payload is now dropped, and the paste falls back to plain text in
the caret style, unless its content passes the text shape schema, its
span geometry holds only safe numbers and its file and team ids are
uuids. Payloads in HTML over the paste size limit are not decoded.

Copied text shapes pasted into the editor get the same checks, plus a
numeric position; when they fail, nothing is pasted.

Closes #12167

AI-assisted-by: claude-opus-5-5
2026-10-26 12:36:13 +01:00
Eva Marco
a5fd2df1f0
♻️ Make the grid track direction icon and the swap groups buttons (#12476)
* ♻️ Make the grid track direction icon a button

The direction icon at the start of each grid track value field selected
that column or row from a div with a click handler: no button role, no
name and no keyboard access. Make it a button labeled "Select column" or
"Select row", keeping its class and handler, and reset the browser
border so it looks the same. It stays a raw button: it is a segment of
the track field, not a DS button.

Add the workspace.layout-grid.editor.select-column and select-row keys
(en, es).

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

* ♻️ Make the component swap panel groups buttons

The groups of the component swap panel opened from a div with a click
handler: no button role and no keyboard access. Make each group a
button, keeping its class, title and handler, and reset the browser
button styles so it looks the same. Its name is the visible group name.

Add a visual test with a fixture whose components sit in two groups, so
the swap panel lists them.

AI-assisted-by: claude-opus-5-5
2026-10-26 11:38:33 +01:00
Eva Marco
bac62a27bf
♻️ Use DS icons in the viewer comments filter (#12519)
* ♻️ Use DS icons in the viewer comments filter

The "Comments" filter trigger in the viewer header was a div with a
click handler, and its menu lived inside it. It is now a button with
aria-expanded and aria-haspopup, and the menu sits next to it in a
wrapper, since a button cannot hold the menu entries. The arrow and the
ticks use DS icons. The menu entries stay as they are.

A visual test covers the filter closed and open.

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

* ♻️ Move viewer comments styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The DS icons take their color from the
text and no longer shrink, so the old icon placeholders go away. The
DS body-small line-height is more compact, so "Comments" sits 1px
higher. The refactor/ import stays only for the shared menu
placeholders. The z-index values (1 and 10) have no DS token and keep a
TODO.

AI-assisted-by: claude-opus-5-5
2026-10-26 11:36:46 +01:00
Eva Marco
ef17aea39e
♻️ Use DS icon buttons in the viewer thumbnails panel (#12518)
* ♻️ Use DS icon buttons in the viewer thumbnails panel

The expand, close and scroll buttons of the viewer thumbnails panel
become ghost DS icon buttons. They had no accessible name; they are now
labelled, and the expand button says whether the panel is expanded. The
rotated legacy arrow gives way to the DS arrow icons. The thumbnails
stay raw buttons, since they are list items.

New keys: labels.expand and viewer.thumbnails.scroll-left/right (en,
es). A visual test covers the panel collapsed, on hover and expanded.

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

* ♻️ Move viewer thumbnails panel styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the file no longer imports
refactor/. The scroll buttons keep their 60px height through
--button-height and still show only on hover; their tooltip wrappers
take the grid cells. The DS body-small typography differs from the old
one only in a more compact line-height, so the counter and the board
names sit 1px higher. The panel z-index (10) has no DS token and keeps
a TODO.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:33:11 +01:00
Eva Marco
9911f82087
♻️ Use a DS icon in the grid editor track options button (#12486)
* ♻️ Use a DS icon in the grid editor track options button

The options button of each grid track on the canvas drew a deprecated
icon and had no accessible name. Use the DS menu icon and label the
button "Track options". The button stays raw: it lives on the canvas,
where the track editor keeps a fixed screen size at any zoom.

The DS icon has a fixed 16px size, so its class scales it down with the
zoom (transform: scale(1 / zoom)), as the old svg rule did with its
width. It looks the same, but for a few antialiasing pixels.

Add the workspace.layout-grid.editor.track-options key (en, es) and a
visual test of a track at two zoom levels.

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

* ♻️ Move grid editor styles off the deprecated styles

Move grid_layout_editor.scss to DS sizes, borders, spacing tokens,
colors and logical properties, and drop the refactor import. The canvas
sizes keep their division by the zoom. The grid editor color aliases
resolve to their DS tokens, and the unused close-btn class, the last
user of the deprecated button placeholders here, goes away.

The track editor and the editing bar look the same as before.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:32:06 +01:00
Eva Marco
7ff8df6ffb
♻️ Name the grid track expand buttons and make the annotation toggle a button (#12489)
* ♻️ Name the grid track expand buttons

The menu button that expands the columns or rows of a grid layout had no
accessible name. Label it "Columns" or "Rows" and expose its state with
aria-expanded. The track title next to it also toggles on click; it
stays a div, as a larger pointer target for the same action, so the
keyboard does not stop twice on it.

Add the workspace.layout-grid.editor.columns and rows keys (en, es).

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

* ♻️ Make the annotation title toggle a button

The annotation title of a component toggled from a div with a click
handler, and it holds the edit, delete, save and discard buttons, so it
cannot become a single button. Move the toggle to a button with the
arrow and the "Annotation" name, with aria-expanded, that takes the
whole free width of the title. While editing or creating, the title
stays text and no longer collapses the annotation on click, as its
cursor already suggested.

Extend the annotation visual test with the collapsed state; it looks the
same as before, pixel for pixel.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:31:51 +01:00
Eva Marco
ac08e38c1a
♻️ Use DS buttons and icons in the export assets modal (#12517)
* ♻️ Use DS buttons and icons in the export assets modal

The export modal close button becomes a ghost DS icon button and the
progress toast close button an action one, which fits its 24px grid
column; both are labelled "Close". Cancel and Export become DS buttons.
The checkbox ticks and the toast icons use DS icons. The selection rows
stay raw buttons, since they are list rows, and so does the link-styled
retry/cancel button of the toast.

The export frames e2e spec now finds the Export button by role, and a
visual test covers the modal with all, some and no rows selected.

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

* ♻️ Make the export progress ring a button

The export progress ring in the header opens the export details, but it
was a div with a click handler. It is now a button named after the
export status, with aria-expanded, and keeps its look.

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

* ♻️ Move export assets modal styles off the deprecated styles

Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the button placeholders go away.
The classes left without use are removed: the old modal buttons, the
old close icon, and the file export entries, which live in files.scss.

The refactor/ import stays only for the shared modal and checkbox
placeholders, which move to the DS with the modals and the checkboxes.
The checkbox placeholder shrinks a ticked icon to 12px, which pushed
the DS tick off center: the tick keeps its 16px box now, and does not
shrink inside the 14px flex box either.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:31:34 +01:00
Eva Marco
29b00f8535
♻️ Use DS icons in the text palette and its menu (#12452)
* ♻️ Use DS icons in the text palette and its menu

The scroll arrows of the text palette and the tick of the selected
palette in its menu used deprecated icons; use the DS arrow and tick
icons instead. The arrows are full-height scroll buttons with a fade, not
DS button sizes, so they stay raw buttons.

Add a visual test that opens a file with 16 local typographies, so the
arrows show, and captures the palette at rest, with the pointer over the
right arrow, after scrolling and with its menu open. Its fixture is the
library fixture with more typographies.

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

* ♻️ Move text palette styles off the deprecated styles

Inline the button, flex and ellipsis mixins, move spacing, sizes and
borders to DS tokens and logical properties, and resolve the color
aliases to their DS tokens. The arrow, item and menu colors that change
on hover, disable or selection go through local variables, and the new
icons take them through currentColor instead of svg selectors. The three
typography lines share their ellipsis rule. The .disabled ::after rule
of the left arrow never matched and goes away; :disabled::after already
covers it.

The items, the empty message and the menu use the DS body-small
typography, whose line-height is 1.3 instead of 1.4. The arrows keep
their z-index, now commented, and the menu its legacy context menu one.

text_palette.scss and text_palette_ctx_menu.scss no longer import
refactor/.

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

* 🐛 Show the text palette scroll arrows at rest

The enabled scroll arrows of the text palette had no color at rest, so
they only showed on hover, while the color palette arrows show in the
secondary color. Give them the same rest color.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:31:14 +01:00
Eva Marco
904f456c61
♻️ Use DS icons in the color palette and its menu (#12444)
* ♻️ Use DS icons in the color palette and its menu

The scroll arrows of the color palette and the tick of the selected
palette in its menu used deprecated icons; use the DS arrow and tick
icons instead. The arrows are full-height scroll buttons with a fade, not
DS button sizes, so they stay raw buttons. The color cells are swatches
and stay as they are.

Add a visual test that opens a file with 24 local colors, so the arrows
show, and captures the palette before and after scrolling and with its
menu open. Its fixture is the library fixture with more colors.

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

* ♻️ Move color palette styles off the deprecated styles

Inline the button, flex and ellipsis mixins, move spacing, sizes and
borders to DS tokens and logical properties, and resolve the color
aliases to their DS tokens. The arrow and menu item colors that change
on hover, disable or selection go through local variables, and the new
icons take them through currentColor instead of svg selectors.

The empty palette message and the menu items use the DS body-small
typography, whose line-height is 1.3 instead of 1.4. The arrows keep
their z-index, now commented: the left arrow comes before the
positioned color list, so without it the colors would cover its fade.
The menu keeps its legacy context menu z-index, also commented.

color_palette.scss and color_palette_ctx_menu.scss no longer import
refactor/.

AI-assisted-by: claude-opus-5-5
2026-10-26 10:27:13 +01:00
David Barragán Merino
14449d9f3d Merge branch 'staging' into develop, keeping the subpath support 2026-10-26 10:03:57 +01:00
David Barragán Merino
7cf914088a Merge branch 'main' into staging 2026-10-26 10:01:26 +01:00
David Barragán Merino
f44566963d ⏪ Revert the subpath support brought by the docker/ backport
The backport of docker/ from develop (2df5f417a) also brought the subpath
support from #12358 and its fix (95bc48a08). It needs the Admin Console
from 2.20, so with the current one every Admin Console URL fails. Restore
the previous nginx configuration and docker-compose.yaml.

AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2.18.5-RC1 2.18.5
2026-10-26 09:59:00 +01:00
Alejandro Alonso
e764b17c31 Merge remote-tracking branch 'origin/staging' into develop 2026-10-26 09:53:59 +01:00
Alejandro Alonso
a0ef927faa
🚑 Drop invoke_* glue checks from staging CI (#12623)
* 🚑 Drop invoke_* glue checks from staging CI

The .github backport from develop added check-no-invoke-v to WASM and
Exporter CI, but staging still ships glue that uses invoke_* wrappers.
Remove those steps (and the WASM production build only used by the
check) so staging CI matches the binary it builds.

* 🔧 Disable credential persistence in commit-checker checkout

zizmor fails CI: Workflows when actions/checkout omits
persist-credentials: false. Match the rest of the workflows.
2026-10-26 07:05:13 +01:00
Andrey Antukh
d9aab9ba79 Merge remote-tracking branch 'origin/main' into staging 2026-10-23 18:43:10 +02:00
Andrey Antukh
d362266694 📖 Add rewrite-commit-message skill 2026-10-23 18:42:48 +02:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +02: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
Belén Albeza
2045fc4900
🐛 Keep text image fills in place when pasting (#12552)
A text image fill was always laid over the whole text box, so a pasted
image span showed another part of the picture in a box of another size.

Text image fills now honour the image transform, as shape fills do: the
image covers the transform rect instead of the whole box. Paste gives
each pasted image span a transform that keeps the image at the same
pixel offset from the span, like the gradients.

The SVG export emits one linked image per span transform, rather than
one per image id, and the style panel keeps the transform it reads back.

AI-assisted-by: claude-opus-5-5
2026-10-23 13:00:53 +02:00
Andrey Antukh
2df5f417a0 ⏪ Backport .github and docker/ changes from develop 2.18.4-RC5 2.18.4 2026-10-23 12:40:24 +02:00
Andrey Antukh
76a83387d9
⬆️ Update JVM, Node, pnpm and devenv dependencies (#12600)
Move the backend JDK to its latest maintenance build within Java 26 and
refresh the dependency versions pinned by hand in the development and
production images.

- Backend: Zulu JDK 26.0.2 to 26.0.2.1. The jlink module list keeps
  jdk.management, so the prometheus process metrics stay exported.
- Node.js 24.22.1 in .nvmrc, the development image tarball and the two
  plugins deploy workflows that pin it.
- pnpm 12.15.0 in the four Dockerfiles, the 34 packageManager fields and
  the eleven lockfiles.
- Development tools: clj-kondo 2026.10.20, Babashka 1.13.226, Pixi
  0.83.0, GitHub CLI 2.103.0, AWS CLI 2.38.5 and uv 0.13.5.
- Infra services: RustFS 1.0.3, Mailpit v1.32.0, and Valkey pinned to
  8.1.10 instead of the floating 8.1 tag.

Fix SERENA_UPDATE_VERSION in the compose file. A value there makes the
start script reinstall that Serena release on every start, overriding
the one baked into the image; it was replacing 1.7.0 with 1.6.1.

The hardened base image updates are left to Renovate.

Closes #12599

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

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-10-23 12:36:11 +02:00
penpot-renovate[bot]
033b9a3362
⬆️ update GitHub Actions (#12598)
- actions/create-github-app-token v3.3.0 (.github/workflows/auto-label.yml)
- pnpm/setup v3.1.1 (.github/workflows/plugins-deploy-styles-doc.yml)
- pnpm/setup v3.1.1 (.github/workflows/plugins-deploy-api-doc.yml)
- zizmor 1.31.1 (.github/workflows/lint-workflows.yml)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-10-23 11:53:02 +02:00
penpot-renovate[bot]
c3a23174a1
⬆️ Update DHI base images (#12597)
- dhi.io/debian-base trixie-debian13-dev@9b02576 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13-dev@9b02576 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13@de80b46 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@9b02576 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@9b02576 (docker/devenv/Dockerfile)
- dhi.io/nginx 1.31.7-debian13-dev@940ee35 (docker/images/Dockerfile.frontend)
- dhi.io/node 24.22.1@ (docker/devenv/Dockerfile)
- dhi.io/node 24.22.1@ (.nvmrc)
- dhi.io/node 24.22.1-debian13-dev@20cf36e (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.1-debian13@32a2fa6 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.1-debian13-dev@20cf36e (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.1-debian13-dev@20cf36e (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-10-23 11:50:52 +02:00
Alejandro Alonso
e27c65c77a
✨ Run WASM asset exports locally in the browser (#12515)
When render-wasm is active, shape exports (single file, ZIP, and Export
boards multi-page PDF) run on the shared worker WASM module so the UI
stays responsive. A progressive PDF session (begin/add-page/finish)
updates progress per page; cancel settles on the client. The server
exporter path remains for the SVG renderer.
2026-10-23 11:02:23 +02:00
Alejandro Alonso
6508449fa0
🐛 Rebuild text layout when image fills finish loading (#12564)
Text image shaders are baked into Skia paragraphs at layout time.
touch_shapes_with_image only scanned shape.fills/strokes, so text
spans were never invalidated after store_image. Detect text content
image fills and force a layout rebuild before touching tiles.

Closes #12563
2026-10-23 09:55:23 +02:00
Florian Schrödl
9aa78a536b
✨ Resolve tokens with the tokenscript-interpreter package (#12348)
Replace the @penpot/tokenscript npm package (the TypeScript
interpreter) with penpot/tokenscript-interpreter, a Clojure and
ClojureScript interpreter that now lives in its own repository and
compiles into the frontend build.

The frontend adapter (app.main.data.tokenscript) resolves tokens with
it behind the existing :tokenscript flag and maps its results to the
values and errors the workspace uses:

- Token values are validated by the interpreter's token types, passed
  by their DTCG names: `1foo` is not a dimension, a number has no
  unit, an opacity is between 0 and 1. Penpot only adds its own
  limits (font weight names, text decoration keywords).
- Division by zero, incompatible units, unknown functions and
  function arguments out of range get messages of their own, and
  token pills show the message of their error.
- Font family names are quoted before resolving, as in the adapter
  this replaces (#11750): `Red Hat` stays one name.

The token form's value input passed the token name to the tokenscript
resolver in place of the typed value, and no name at all for a new
token. It now resolves the typed value under the form's token name, as
the StyleDictionary path does.

StyleDictionary stays the default until the interpreter is stable.

frontend/deps.edn pins the interpreter by tag and commit, v0.1.0.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-10-22 17:32:52 +02:00
Andrey Antukh
ba78ccb8c9
✨ Harden file data offload job with lock, summary and metrics (#12403)
Take the file row with FOR UPDATE SKIP LOCKED when the offload job
starts and skip the run when the row is locked or gone, so the job
never runs while the file is being edited. Every live hot-storage
row of the file moves to cold storage inside one all-or-nothing
transaction, and the handler answers a summary map with the
offloaded rows, bytes and per-type stats.

Add bounded metrics for the job: rows and bytes counters labeled by
row type on successful runs only, and a timing histogram with the
ok, skip and error outcomes. The job definition now requires
metrics, the recorders live in app.jobs.metrics, and the existing
ephemeral job timing histogram pins its buckets.

Cover the job with a new test namespace: the happy path, the locked
row, missing and deleted files, the rows and bytes summary with its
no-op rerun, the whole-file abort on storage failure, the metric
definitions and emissions, and the submit-time params validation.
An end-to-end suite reads, snapshots, writes, exports, imports and
deletes offloaded files; concurrency tests pin the stale-revn skip,
the blocked-edit retry and the reclaimable orphans of a partial
failure. The file-gc scheduler test pins one job per eligible file,
and the scheduler private helper drops its bang.

Document the lock invariant, the deliberate no-pacing decision and
the metric label sets in the backend storage memory and the
configuration guide.

Related to #12398.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-22 15:31:16 +02:00
Andrey Antukh
f8f1a668f7
♻️ Use skip-if-active for cron no-overlap in backend worker (#12412)
The cron tick kept its own active-job count beside the shared
submit option. Delegate the no-overlap check to
::jobs/skip-if-active so both paths share one queue-qualified
check served by the partial index.

A tick now skips only when a live job sits on the :cron queue,
and a skip records both the cron counter and the generic skipped
counter.

Closes #12411

AI-assisted-by: muse-spark-1.3-contributor
2026-10-22 15:29:37 +02:00
Belén Albeza
0505eb3b1d
🐛 Fix pasted gradient fills in text editor v3 (#12516)
* 🐛 Keep text gradients in place when pasting at the caret

Text gradients are stored as fractions of the whole text box. Paste
copied those fractions unchanged, so in a box of another size the
handles and colours moved away from the pasted text.

Copy from the v3 editor now records where each span with a gradient or
image fill starts in its text box, and the box size, in the clipboard
payload. A new WASM export, text_editor_get_range_origin, gives the
start of any text range.

Paste at the caret finds where those spans start in the target and
rewrites their gradients so the handles keep the same pixel offset from
the span. Auto-sized texts use the box size they get when the editor
closes.

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

* 🐛 Rebuild text gradient shaders when the text box resizes

Text gradient and image shaders are built into the cached Skia
paragraphs against the text box size. A resize with no content change
reused those paragraphs, so the colours stayed matched to the old box
until a reload.

The text now remembers the box size its shaders were built for and lays
itself out again when that size changes and a span has a gradient or
image fill. Text with only solid fills keeps its cache.

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

* 🐛 Keep text gradients in place when pasting on the canvas

A text selection pasted on the canvas becomes a new text whose box
differs from the source, so its gradients moved like they did for caret
paste.

After adding the new text, lay it out, find where its gradient spans
start and move their gradients to keep the same pixel offset from the
span, in the same undo step. The range origin export now takes a shape
id, so it works on texts that are not being edited.

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

* 🐛 Keep text gradients in place when pasting copied texts at the caret

Text shapes copied whole, rather than as an editor selection, reached the
caret paste without any geometry, so their gradients still moved away from
the pasted text.

A shape copy now records where the gradient and image spans of each
selected text start, and the box size, by shape id. Paste joins them in
the order of the joined paragraphs, with no origin for texts copied
without one.

Copied texts each have their own box, so every span origin now carries
its own box size instead of one box for the whole payload.

AI-assisted-by: claude-opus-5-5
2026-10-22 13:39:26 +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
Alejandro Alonso
347a52d514
🐛 Keep Path out of update-team-photo audit props (#12530)
Nitrate encodes audit chunks as JSON and cannot serialize java.nio.file.Path;
mirror update-profile-photo and replace the upload map with safe props.
2026-10-22 11:07:07 +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
Eva Marco
6acb360f4a
♻️ Use DS icons and an icon button in the undo history panel (#12472)
* ♻️ Use DS icons and an icon button in the undo history panel

The undo history panel (History > Actions) drew its entry type icons
with the deprecated icon set, and the arrow that shows the details of an
entry was a div with a click handler: no button role, no keyboard
access and no accessible name.

- The entry type icons use the DS icon (drop-icon is i/drop in the DS).
- The arrow becomes a DS action icon button labeled "Show details", with
  aria-expanded for its state. It keeps its data attribute, its classes
  and its click handler, and it also shows on keyboard focus.

The DS icon draws 12px inside a 16px box, so the entry icon gets a
negative margin, and the button wrapper a negative end margin: icon,
arrow and text stay where they were. The arrow turns through its own
class instead of an svg rule.

Add the workspace.undo.entry.show-details key (en, es) and a visual test
that draws a rectangle, opens the actions list and expands the entry.

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

* ♻️ Move undo history panel styles off the deprecated styles

Move history.scss to DS tokens, sizes, borders and logical properties,
and drop the refactor import. The entry color aliases become local
variables of the entry, set by its disabled and hover states, and the
arrow opacity goes through one as well.

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

* 💄 Place the undo history icon and arrow without negative margins

Drop the -2px margin of the entry icon and the -6px end margin of the
details button wrapper. Keep text and arrow in place with token spacing
instead: the text after the 16px icon box starts with --sp-xs instead
of --sp-s, and the entry end padding is --sp-xxs, since the 24px button
leaves 6px around its 12px arrow. The icon now sits 2px to the right,
centered in its box.

AI-assisted-by: claude-opus-5-5
2026-10-21 15:59:31 +02:00
Elena Torró
05e1981acc
✨ Support running several exporter replicas (#12487)
* ✨ Support running several exporter replicas

* 📚 Document one exporter per machine instead of compose replicas

* 📚 Trim exporter replica docs to the essentials

* ✨ Cancel an exporter's jobs when it shuts down

* 📚 Replace exporter replica docs with a short beta note
2026-10-21 15:30:46 +02:00