14452 Commits

Author SHA1 Message Date
Andrey Antukh
70b8c898a1 ✨ Stabilize fragmented file data across snapshots and GC
Snapshots, automatic and user, now store pointer-based rows that share
fragments with the live file instead of duplicating page content.
`get-file-snapshot` resolves those pointers on the server so version
previews keep rendering plain data, and `restore!` reuses the shared
rows. Export still materializes (the package carries no fragments) and
import regenerates them.

`file-gc` treats the main file and every unpurged snapshot as liveness
roots, reads the file unrealized, and reattaches the original pointer
wherever the cleaned value is unchanged, so a pass over an untouched
file writes no fragments and a pass over an edited file keeps the ids
of the pages it did not touch. Snapshot creation and the restore
backup take the file row `FOR SHARE`, which serializes them with the
pass and closes the race that let a new snapshot reference fragments
the pass was about to mark.

All backend pointer bindings go through the `fdata/with-pointers`,
`with-tracked`, `with-tracked-pointers` and `with-untracked` macros.
The loader reads fragments a previous pass marked until `objects-gc`
purges them, and the frontend fails loudly with retry instead of
dropping an unresolved fragment.

Review fixes in this branch: the automatic snapshot attaches before
pending migrations, `realize-pointers` is the single verb,
`reattach-unchanged` takes the identity fast path, the restore guard
takes its revn from the backup read, and `PointerMap.withMeta`
generates a fresh id when metadata changes.

Tests cover both snapshot writers (red without the lock), snapshot
sharing and pin release through the real delete and purge paths,
reattach against orphan marking, preview after offload, the pointer
binding contracts and the with-meta id change.

Closes #12410

AI-assisted-by: muse-spark-1.3-contributor
AI-assisted-by: deepseek-v4.1-flash

♻️ Pin no-churn behavior for untouched pages

Pin the behavior the churn verification measured: a save keeps the
fragment id of every page it does not touch, with a page of eleven keys
(PersistentHashMap), a 46-shape objects map and a nine-page index (hash
map). Also pin the mixed case: a data-level objects-map flip rewrites
the old pointer pages once and the next save keeps the new ids.

The memory records the identity rule behind it: re-assoc with the same
value returns the same map in array and hash maps alike.

Closes #12410

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

⚡ Stop loading untouched pages when enabling objects-map

`enable-objects-map` wrapped every page on every save, and reading a
page's objects forced its PointerMap to load the fragment. A save of
one page in a seven-page file therefore made seven fragment reads.

Skip unloaded pointers: their fragment was already wrapped by the
encode-file that wrote it, so loading it only to wrap it again is a
no-op. The save now loads only the pages it edits. A page with a plain
fragment (a data-level feature flip) keeps its plain fragment until it
is first loaded or edited; a migration that enables the feature must
rewrite the data itself.

Cover the change with load-count tests for save and restore, update the
flip test to the new contract, and drop the stale restore comment.

Closes #12410

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

⚡ Keep page fragment ids through pending migrations

Applying a pending migration on read realized the file, ran the
migration and persisted it again; the persist wrapped every plain page
in a new PointerMap, so every page got a new fragment id even when the
migration did not change it. That leaves the fragment cache cold and
rewrites every fragment on the first read after a deploy.

Capture the original pointers before realizing, reattach the unchanged
positions after migrating (same value comparison file-gc already uses),
persist the reattached file and return the realized one: the read
contract is plain data. Read-only connections skip the persist and the
reattach. A migration that adds objects-map skips the reattach so the
pages get wrapped.

Move the three helpers to fdata so file-gc and the migration path share
them, and cover the path with a real 0031 migration test: the unchanged
page keeps its id, the changed page gets a new one, the caller gets
realized data and a second read does not migrate again.

Closes #12410

AI-assisted-by: deepseek-v4.1-flash
2026-10-23 17:20:13 +02:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +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
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
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
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
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
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
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
Eva Marco
d6e850e917
♻️ Use DS icon buttons for the bottom palette toggles (#12443)
* ♻️ Use DS icon buttons for the bottom palette toggles

The color palette and typographies toggles of the bottom palette were raw
buttons with deprecated icons. Replace them with DS ghost icon buttons
with small icons, as before, that show the open palette through
aria-pressed. The layout flags are keywords, so they are coerced to a
boolean for aria-pressed. Their title moves to the label, shown in the DS
tooltip with the shortcut.

The palette actions button spans the palette height, not a DS button
size, so it stays raw; only its deprecated icon moves to the DS icon.

Add a visual test that compares the palette with each toggle selected
with no tolerance.

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

* ♻️ Move bottom palette styles off the deprecated styles

The palette toggles are DS icon buttons, so their placeholders go away.
The actions button keeps the tertiary button look through local state
variables, and its icon takes the color from the button. Spacing, sizes,
borders, opacity and the color aliases move to DS tokens and logical
properties, except right and width of the palette, which are the
transitioned properties. The grid template is split into areas, rows and
columns, since Sass reads its slash as a division with a size variable.

The z-index values came from the legacy global scale. The resize area,
the toggles and the actions button had their own (4, 2 and 2), plus 0
overrides while collapsed, but they sit in separate grid areas and the
palette content does not reach them: removing them changes no pixel with
the text or color palette, the actions menu open, the 80px size, or the
palette collapsed and hidden. Only the palette's own z-index stays, with
a comment: it keeps the palette above the viewport overlays (z-index 1),
and drops to their level while collapsed.

The help button styles are not used in palette.cljs, but the external
chat widget injected into the palette wrapper may use them, so they stay,
inlined on DS tokens.

palette.scss no longer imports refactor/.

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

* ♻️ Use a DS icon button for the bottom palette actions

The palette actions button (⋮) stayed a raw button because it spans the
palette height, not a DS button size. The DS icon button takes its
height from --button-height, so a class now stretches it to the palette
and the hand-made colors and states go away. It uses the ghost variant,
like the palette toggles next to it.

The button had no accessible name and did not say that it opens a menu:
it now has the "Palette options" label (new en and es keys) with its
tooltip, aria-expanded and aria-haspopup. The class given to its icon
did not exist in palette.scss and is gone.

Only the button corners change in the palette screenshots: the raw
button painted its own rounded background, the ghost button is
transparent at rest.

AI-assisted-by: claude-opus-5-5
2026-10-21 14:35:12 +02:00
Eva Marco
1d54d170cf
♻️ Move layers panel styles off the deprecated button styles (#12478)
The layers panel already uses DS icons and buttons; its two raw buttons
(the layer filter pill and the focus mode title) stay raw, since they are
custom widgets. Their styles still used the deprecated %button-tag
placeholder and color aliases.

- Inline %button-tag in the filter pill: border, cursor, centering, and
  its focus and pressed states through local variables. Its hover rule
  set the icons to the color they already had, so it goes away.
- Resolve the icon, title, pill and menu color aliases to DS tokens. The
  filter menu item texts are the same color in every state, so only the
  background changes, through a local variable.

The refactor import stays for the shared %menu-dropdown placeholder.
Add a visual test of the focus mode title, a filter pill and the filter
menu; the panel looks the same as before, pixel for pixel.

AI-assisted-by: claude-opus-5-5
2026-10-21 14:11:15 +02:00
Eva Marco
ce84fce0dc
💄 Remove the border from the boolean operation buttons (#12510)
The boolean operation buttons at the top of the Design tab showed the
2px border of the legacy radio buttons. The design team is reworking
this section; for now only the border goes away, in normal and selected
state, by making the border color variables transparent there.

AI-assisted-by: claude-opus-5-5
2026-10-21 14:10:53 +02:00
Eva Marco
5259b4b2d5
♻️ Use DS buttons in the workspace right header (#12454)
* ♻️ Use DS buttons in the workspace right header

The comments and history buttons were raw buttons with deprecated icons;
they become DS ghost icon buttons that show their state through
aria-pressed, with the unread dot as a child of the comments button. The
view mode link had a click handler and no href; it becomes a DS icon
button too, so it gets a button role and keyboard activation. Their
tooltips open below and to the left, at the top right corner of the
screen. The zoom menu's Reset is a DS ghost button.

The share link stays raw with its deprecated icon until the DS has a
share icon; it now uses its own class instead of the view mode one.

Add a visual test that captures the header at rest, with comments
active, and with the zoom menu open.

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

* ♻️ Move workspace right header styles off the deprecated styles

The comments, history and view mode buttons are DS icon buttons, so their
placeholders go away; the comments class only holds the unread dot. The
zoom widget color goes through a local variable, and spacing, sizes,
borders and the color aliases move to DS tokens and logical properties.

Delete the save status rules: the status widget moved to the left header
in #5773 and nothing here uses them anymore.

refactor/ stays only for the shared menu placeholders and for the
tertiary button and icon placeholders of the share link.

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

* 🐛 Give the action icon button a color while pressed or active

The action variant of the DS icon button set the active background but
not the active foreground, so it kept the base value, initial. A custom
property set to initial is invalid at computed time, so the icon took
the color of its container while the button was pressed (:active) or
toggled on (aria-pressed): yellow in the workspace header, for example.

Use the accent color, as the ghost and secondary variants do.

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

* 💄 Use the action variant for the right header icon buttons

The comments, history and view mode buttons had no background at rest,
on hover or while active. The DS action variant keeps that: no
background at rest and on hover, accent icon on hover. It is 24px
instead of the ghost 32px, and shows a small background while comments
or history is on.

The unread dot moves to 4px from the button corner, so it stays over the
top right corner of the icon in the smaller button.

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

* 💄 Drop the background of a pressed action icon button

A toggled-on action icon button (aria-pressed) showed the quaternary
background next to the accent icon. Keep only the accent icon, as the
workspace header buttons looked before the DS migration. The background
stays for the click feedback (:active).

Only the right header comments and history buttons use the action
variant with aria-pressed today.

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

* 💄 Widen the gap between the right header actions

The action icon buttons are 24px wide, smaller than the old ones, so
the comments, history and view mode buttons sat closer together than
before the DS migration. Raise the header gap from --sp-s to --sp-m to
keep the old spacing between the icons without resizing the buttons.
The separator absorbs the extra space, so the header width stays the
same.

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

* ♻️ Make the right header zoom widget a button

The zoom percentage of the right header opened its menu from a div with
a click handler: no button role and no keyboard access. Make it a
button with aria-expanded, keeping its classes, title and handler; its
visible percentage is its accessible name. Reset the browser padding of
the button so the percentage stays in place.

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

* ✨ Toggle the zoom dropdown from the zoom widget

Clicking the zoom widget while its dropdown was open left it open: the
handler always set the open state, and it stops the click before the
dropdown sees it. The widget now toggles the dropdown, as its
aria-expanded already suggests. Closing from outside or with Escape
works as before.

AI-assisted-by: claude-opus-5-5
2026-10-21 14:10:34 +02:00
Eva Marco
5dbf0731e5
♻️ Use DS buttons and icons in the plugins dialogs (#12470)
* ♻️ Use DS buttons and icons in the plugins dialogs

The plugins manager and the permissions, permissions update and try-out
dialogs used raw buttons, input elements styled as buttons and
deprecated icons. Replace them with the DS components:

- The four close buttons become ghost icon buttons labeled "Close", shown
  in their DS tooltip. The class that places them in the corner goes on
  the tooltip wrapper, so the tooltip anchors to the button.
- Install, and the cancel and accept buttons of the three dialogs, become
  DS primary and secondary buttons.
- The empty list puzzle and the external link icons use the DS icon.

The permission icons (oauth-*) stay: they are brand art.

Add a visual test with fixtures for a team with the plugins feature and
a profile with one installed plugin. It shows the empty manager, the
manager with a plugin and the permissions dialog.

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

* ♻️ Move plugins dialog styles off the deprecated styles

Remove the styles the DS buttons and icons no longer need (close-icon,
primary-button, cancel-button, search-icon and the svg rules of the
migrated icons) and move the rest of plugins.scss to DS tokens, DS
typographies and logical properties.

- The close button wrapper keeps only its position; the deprecated
  tertiary button style was landing on the tooltip wrapper.
- The full-width cancel and accept buttons center their label again: the
  DS button does not center it when it grows.
- The external link icon gets a class instead of an svg rule.
- The deprecated body-small used line-height 1.4 and the DS one 1.3, so
  the permissions dialog is a few pixels shorter.

The refactor import stays for the modal placeholders until the modals
move to the DS modal.

AI-assisted-by: claude-opus-5-5
2026-10-21 13:11:04 +02:00
Eva Marco
858d5dad9c
♻️ Use DS buttons in the debug shape panel (#12484)
* ♻️ Use DS buttons in the debug shape panel

The object and tree dump buttons of the debug shape panel were raw
buttons with the browser default look. Use DS secondary buttons. The
DS buttons are taller, so the title row centers the shape name.

Add a visual test that turns on the shape-panel debug option and shows
the title row of the selected shape.

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

* ♻️ Move debug shape panel styles off the deprecated styles

Move debug_shape_info.scss to DS spacing tokens, borders and logical
properties, resolve the panel background alias, and drop the refactor
import. The two font sizes match no DS typography, so they stay with a
TODO, and the unused shape-info-title class is commented out for a
manual check. The panel looks the same as with the previous commit.

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

* 🐛 Let the debugging tools options scroll

The debugging tools panel lists 32 options in the left sidebar, whose
content area hides what overflows. On a short window the last options
were cut off with no way to reach them. Make the options list fill the
space under the title and scroll on its own.

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

* ♻️ Move debugging tools panel styles to DS tokens

Move debug.scss spacing, size and the panel background alias to DS
tokens and logical properties. The refactor import stays only for the
checkbox placeholders, until the checkboxes move to the DS. The panel
looks the same as before.

AI-assisted-by: claude-opus-5-5
2026-10-21 12:32:09 +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
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Elena Torró
a1606c42ff
🐛 Export bitmaps over the GPU surface limit through the exporter (#12455) 2026-10-20 15:37:07 +02:00
Shreyash Agare
ecfa350470
✨ Auto-scroll sortable lists while dragging near their edges (#11595)
* ✨ Auto-scroll sortable lists while dragging near their edges

Native drag and drop swallows wheel events, so a long list could only be
scrolled mid-drag by hitting the browser's narrow auto-scroll strip at the
very edge of the container.

use-sortable now scrolls the item's nearest scrollable ancestor on every
animation frame while the pointer sits within 56px of its top or bottom
edge, ramping the speed up to 18px per frame as the pointer gets closer.
Every list built on the hook (layers, sitemap, option rows, gradients,
token sets) gets it without any change on their side.

* ✨ Cap auto-scroll zones and test the scroll loop

Each edge zone now takes at most a quarter of the container, so the
middle half of a short list, like the pages panel, never scrolls.

The scroll container is found once when the drag starts. The speed is
scaled by the time between frames, so 120 Hz screens no longer scroll
twice as fast. Starting a drag also stops any loop left over from a
drag whose end event was lost.

auto-scroll-speed is now private. New tests cover the running loop:
only one loop starts, it stops on drag end, when the speed goes back
to zero and after 500ms without dragover, and the frame time scaling.

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

---------

Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-10-20 11:27:31 +02:00
Eva Marco
47d7ad2de1
♻️ Explain layout_container's deprecated import and format visual spec (#12449)
* ♻️ Explain why layout_container.scss keeps the deprecated import

The TODO at the top said to remove the refactor/ import once the buttons
were replaced, and sat above the DS imports instead of that one. The
buttons are now DS buttons (#12428, #12431), but the import is still
needed by the numeric inputs (%input-element, %disabled-input) and by the
two raw track header buttons, which keep the secondary and tertiary
placeholders. Replace the TODO with a comment on the import that says so.

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

* 📎 Format the workspace visual spec with Prettier

Three expectations added in #12423, #12428 and #12431 broke a call chain
in a way Prettier rewrites. check-fmt:js did not catch it: its
playwright/**/*.js glob is expanded by the shell without globstar, so it
does not reach visual-specs/.

AI-assisted-by: claude-opus-5-5
2026-10-20 09:19:17 +02:00
Eva Marco
9904e50889
♻️ Use DS icon buttons in the component options (#12394)
* ♻️ Use a DS icon button for the component pill menu

Replace the raw three dots button of the component pill with the DS
secondary icon button. It had no label, so it reuses the "Options" one,
shown in the DS tooltip above and to the left (it sits at the right edge
of the sidebar), and aria-pressed while the menu is open instead of the
selected class.

Its class drops the button placeholders and only keeps the pill segment
shape: full height and square corners on the left. The tooltip wrapper
fills the segment too.

Add a visual test that selects a component copy so the pill shows.

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

* ♻️ Use DS icon buttons for the component annotation actions

The save/create, discard, edit and delete actions of a component
annotation were divs with a click handler: no keyboard focus and no button
role. Replace them with DS ghost icon buttons. Their title moves to the
label, shown in the DS tooltip above and to the left, and save is disabled
while the text is blank, which it already ignored.

The class that hides them until the title is hovered, or while editing or
creating, now goes on the tooltip wrapper. The green and red hover colors
and the fixed size go away with the DS look. While editing or creating,
the actions get a small padding so their hover background does not touch
the annotation border; the title row grows from 32 to 36px there.

Add a visual test that creates an annotation on a main component and
shows both sets of actions.

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

* 💄 Open component options tooltips above and to the left

The variants help, create variant, add property and remove property icon
buttons sit at the right edge of the sidebar: open their tooltips above
and to the left, like the other actions of the component options.

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

* ♻️ Move component options styles off the deprecated styles

Inline the dropdown placeholders of the component pill menu with DS
tokens and a local variable for the item colors that change on hover, and
give the item label its own class instead of styling the span. Resolve
the annotation title border alias to its DS token and delete the unused
.component-title-actions rule.

The item label uses the DS body-small typography; its line-height is 1.3
instead of 1.4, with no visible change since the item centers it.

component.scss no longer imports refactor/.

AI-assisted-by: claude-opus-5-5
2026-10-20 09:18:49 +02:00
Eva Marco
7e5e589744
♻️ Use a DS icon button for the guide defaults menu (#12391)
* ♻️ Use a DS icon button for the guide defaults menu

Replace the raw three dots button that opens the use default / set as
default menu of a guide with the DS ghost icon button. It had no label, so
it gets a new "Guide defaults" one, shown in the DS tooltip, and
aria-pressed while the menu is open instead of the selected class.

The add, show/hide and remove guide buttons, and the new one, sit at the
right edge of the sidebar: open their tooltips above and to the left.

The advanced options toggle stays a raw button: it is one segment of the
guide type control and an icon button would break that shape.

Add a visual test that adds a guide to a board and opens its advanced
options.

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

* ♻️ Remove guide defaults button styles

The three dots guide defaults button is a DS icon button now: delete the
.show-more-options rule, its state variables and its svg rule. The rest of
frame_grid.scss already uses DS tokens.

AI-assisted-by: claude-opus-5-5
2026-10-20 09:18:24 +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
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +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
Eva Marco
cdb7dc2977
♻️ Use a DS button for edit grid in the grid cell options (#12385)
* ♻️ Use a DS button for edit grid in the grid cell options

Replace the raw "Edit grid" button of the grid cell options with the DS
secondary button. Drop its alt attribute, which is not valid on a button;
the label already names it.

The column and row icons label the numeric inputs, so they move with the
numeric inputs migration.

Add a visual test that selects a cell in grid edit mode so the options
show.

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

* ♻️ Move grid cell options styles to DS tokens

The edit grid button is a DS button now, so its button placeholder and
typography mixin go away; the class only stretches it and centers its
label. Merge the two .grid-cell-menu-container rules, inline the flex
mixins, and move spacing, borders and color aliases to DS tokens and
logical properties.

Use the DS body-small typography in the inputs; its line-height is 1.3
instead of 1.4.

The refactor/ import stays only for %input-element and %button-icon, used
by the inputs and their legacy icons, which move with the numeric inputs
migration.

AI-assisted-by: claude-opus-5-5
2026-10-19 17:05:22 +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
Eva Marco
1c70b0ce2d
♻️ Use the DS icon in the workspace left header (#12432)
* ♻️ Use the DS icon in the workspace left header

The shared library badge of the left header used a deprecated icon; use
the DS library icon instead.

Drop the deprecated icons inside the save status dot. They rendered as
500px SVGs with no stroke, so they never showed: the dot is only its
background color, and its title keeps the status text. The Penpot logo is
brand art and stays.

Add a visual test that opens a shared file and shows the header with the
badge, then the saved status after drawing a shape.

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

* ♻️ Move workspace left header styles off the deprecated styles

Inline the 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 status dot takes its color from a local variable. The
badge icon gets its color through a class instead of an svg selector.

The project name uses the DS headline-small typography: 12px instead of
11px. The file name matches title-small exactly.

Left with a TODO: the rename input font size and z-index have no DS
match, and the pending status orange (#f5a91b) has no DS color.

left_header.scss no longer imports refactor/.

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

* ♻️ Resolve the TODOs left in the workspace left header styles

The status dot used the legacy warning orange #f5a91b, which drifted from
the DS one when both replaced #fe4811 a few days apart in early 2025. Use
the DS --color-accent-warning (#fe9c07), the orange of every other DS
warning, so the dot looks a little more orange while saving.

The rename input had z-index 20, which did nothing: the input is neither
positioned nor a flex or grid item. Remove it.

The rename input only set its font size and got the rest from the global
reset; it now uses the DS title-small typography, like the file name. Its
line-height goes from normal to 1.2, so it grows from 16 to 16.8px.

AI-assisted-by: claude-opus-5-5
2026-10-19 15:52:48 +02:00
Eva Marco
e4da0527fc
♻️ Use DS buttons and icons in the grid layout options (#12431)
* ♻️ Use DS buttons and icons in the grid layout options

The "Edit grid" button of a grid layout and the "Exit" button of the grid
editor were raw buttons with the deprecated secondary style. Replace them
with DS secondary buttons; their text and actions stay the same. Drop the
"alt" attribute of "Edit grid": it is not valid on a button.

The expand and add buttons of the column and row headers are 52px parts of
a custom header, not DS buttons, so they stay raw; only their deprecated
icons move to the DS icon, as does the direction icon of each track.

Add a visual test that shows the grid layout options and the grid editor
with the column tracks expanded.

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

* ♻️ Remove deprecated button styles from the grid layout options

"Edit grid" and "Exit" are now DS buttons, so their secondary placeholder
and typography go away; the classes only place them in the grid and center
their label. The track header and direction icons drop the deprecated icon
placeholder and get their colors through classes. The header buttons keep
their secondary and tertiary placeholders, since they stay raw.

AI-assisted-by: claude-opus-5-5
2026-10-19 15:49:14 +02:00
Eva Marco
3d6823eacd
♻️ Use DS icon buttons for the flex wrap and padding mode (#12428)
* ♻️ Use DS icon buttons for the flex wrap and padding mode

The wrap button of a flex layout and the button that shows the four
padding sides (flex and grid layouts) were raw buttons with deprecated
icons. Replace them with DS ghost icon buttons that show their state
through aria-pressed, with the tooltip above and to the left since they
sit at the right edge of the sidebar.

The wrap label is now always "Wrap": its title switched to "No wrap"
while pressed, which conflicts with the pressed state.

Add a visual test that selects a flex board and shows both buttons at
rest and pressed.

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

* ♻️ Remove deprecated wrap and padding toggle styles

The wrap and padding mode buttons are now DS icon buttons, so their
tertiary button, icon and selected placeholders go away. The tooltip
wrappers take the same grid cells the buttons had.

AI-assisted-by: claude-opus-5-5
2026-10-19 15:48:59 +02:00
Eva Marco
398678e54c
♻️ Use a DS icon button for the layout element margin mode (#12423)
* ♻️ Use a DS icon button for the layout element margin mode

The button that switches a flex or grid element between simple and
multiple margins was a raw button with a deprecated icon. Replace it with
a DS ghost icon button that shows the multiple state through aria-pressed.
Its title moves to the label, shown in the DS tooltip above and to the
left since the button sits at the right edge of the sidebar. The grid
column class goes on the tooltip wrapper.

The legacy margin icons label the numeric inputs and stay for the numeric
inputs group; the radio buttons are out of scope.

Add a visual test that selects a flex element and shows both margin modes.

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

* ♻️ Remove deprecated button styles from layout element options

The margin mode button is now a DS icon button, so its tertiary button,
icon and selected placeholders and the deprecated 32px height go away.
The class only places the tooltip wrapper in the third grid column.

The refactor/ import stays for %input-element, still used by the legacy
numeric inputs.

AI-assisted-by: claude-opus-5-5
2026-10-19 15:48:43 +02:00
Eva Marco
c345c7cbaa
♻️ Use a DS icon button to remove imported SVG attributes (#12382)
* ♻️ Use a DS icon button to remove imported SVG attributes

Replace the raw remove button of each imported SVG attribute with the DS
ghost icon button and the DS remove icon. The button had no title, so it
gets a new "Remove attribute" label, which the DS tooltip shows. Drop the
wrapper div that only held the button.

Add a visual test that selects a group imported from SVG so the section
shows.

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

* ♻️ Move imported SVG attributes styles to DS tokens

The remove button is a DS icon button now, so .attr-actions and
.attr-action-btn with their button placeholders go away. Inline the flex
and ellipsis mixins, move spacing to DS tokens and logical properties,
sizes to px2rem and the title color alias to its DS token.

Use the DS body-small typography; its line-height is 1.3 instead of 1.4.
Drop the line clamp rules of the attribute name: its later display:
inline-block already overrode the -webkit-box they need.

The refactor/ import stays only for %input-element on the value input,
which moves with the inputs migration.

AI-assisted-by: claude-opus-5-5
2026-10-19 15:35:44 +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
18ca25518e
♻️ Use a DS icon button to close the nudge amount modal (#12439)
* ♻️ Use a DS icon button to close the nudge amount modal

The close button of the nudge amount modal (main menu, Preferences) was a
raw button with a deprecated icon and no accessible name. Replace it with
a DS ghost icon button labeled "Close", shown in its DS tooltip. The class
that places it in the corner goes on the tooltip wrapper, so the tooltip
anchors to the button. The numeric inputs stay for the inputs group.

Add a visual test that opens the modal from the main menu.

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

* ♻️ Move nudge modal styles off the deprecated styles

The close button is a DS icon button, so its placeholder goes away; it is
now 32px wide instead of 28px, and sits 4px from the edge instead of 6px
so the icon stays in the same place. Spacing, sizes and the color aliases
move to DS tokens and logical properties, the flex mixin is inlined, and
the label rule targets its class instead of the label element.

The title matches the DS headline-medium typography exactly. The content
and labels use body-large and the wrapper body-small, whose line-heights
differ by 0.1 from the legacy ones; the labels keep their 1.5 override,
so nothing visible changes.

refactor/ stays only for the shared modal and labeled input placeholders.

AI-assisted-by: claude-opus-5-5
2026-10-19 14:28:03 +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
Stephen Sagarino Jr.
8787937b7c
🐛 Truncate long labels in the shadow options panel (#12414)
Long translations of "Blur" and "Spread" ran over the number input in the
advanced shadow options. The label inherited display: flex from the legacy
%input-label placeholder, where text-overflow has no effect, and it could not
shrink below its text.

Make the label a block with the DS text-ellipsis mixin, size it to its text,
and cap it at the field width minus room for the number. Labels that fit look
the same as before; longer ones end in an ellipsis and keep their tooltip.

Closes #4456
2026-10-19 10:24:11 +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
fcbc4f1769 🐛 Remove duplicate declarations in the typography sample
`.typography-sample-input` declared `font-size` and `line-height`
twice with the same values, so stylelint failed the frontend lint
with `declaration-block-no-duplicate-properties`.

The duplication came from an earlier staging into develop merge: one
side moved the pair next to its comment, the other kept it below
`block-size`, and the merge kept both. Drop the second pair.

No behaviour change: both pairs held the same values, and only the
last declaration of a block takes effect.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 12:53:13 +00:00
Andrey Antukh
8c5113b815 Merge remote-tracking branch 'origin/staging' into develop 2026-10-17 14:44:44 +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