Operators get a full instance administration panel behind a new
"superuser" access rule. The :app.auth/superusers service resolves
the configured :superusers emails to profile ids once at startup,
so guarded calls are a plain contains? with no query. A token
carrying "superuser" as an operator-granted scope also passes, for
automation without a session; tokens are created with empty perms,
so this opens nothing by itself.
The check is a branch of wrap-authentication, after auth and
before params validation, so outsiders get 403
:superuser-required without leaking expected params. Other token
scopes keep their exact meaning. get-profile and the login
response report the same answer as :is-superuser; the team-scoped
:is-admin is untouched. On devenv any authenticated profile
passes, as the /dbg gate does. The :admins config becomes
:superusers (PENPOT_SUPERUSERS) with no compatibility layer:
operators must rename it or instance-admin access fails closed.
Quote notifications go to the same set.
Admin operations live in app.rpc.admin.* behind
/api/admin/methods, with the same auth wrapping as main but
excluded from the main API docs. Profiles can be listed,
inspected, blocked, unblocked, deleted and restored, teams listed
with members and features, projects and files listed with validate
and repair, error reports listed and shared through JWE links,
and the virtual clock read and set.
Delete and restore run as durable jobs on the unified job table:
the command stamps the object's own row inline so it reads back
instantly, then a delete-object or restore-object job walks the
rest of the tree through app.features.object-cascade. Listing
uses created-at cursors over the new 0157 indexes.
The panel itself is a static SPA served under /admin (public like
any asset; the JS gate reads :is-superuser from get-profile)
with dashboard, users, teams, projects, files and error-report
pages. Devenv points /admin at the backend.
Covered by backend-tests rpc-superuser-test plus
rpc-admin-{users,teams,project,file,delete,restore,misc,
error-reports,error-share} and http-admin tests, with green
regressions on profile, auth and quotes.
Relates to #12015
AI-assisted-by: muse-spark-1.3-contributor
The export flow now reports the artifact size, but the stale
expectations dropped it and failed the frontend suite. Include the
size in both assertions so they match the emitted message.
AI-assisted-by: muse-spark-1.3-contributor
Every exported file is its own job, so each row of the dialog reports
its own state: waiting for a worker, running with the milestone of the
job and an animated bar, exported once the artifact downloads, or failed
in a red row with the hint of the job. The header only counts the files
and subtitles the artifact format and, once it exists, its size.
Closing changes too: the Close button is gone, the X cancels like Cancel
Export and the dialog stays with the terminal states, and Done dismisses
it. Success, failure and cancellation replace each other, so a row never
reports two outcomes at once, and rows are matched by id canonicalized
to string.
The retry button of the finished design is left out: no retry mechanism
exists on the client yet.
AI-assisted-by: muse-spark-1.3
The import wizard now opens with a Check/Upload/Import stepper derived
from the dialog status, plus a phase title and description per stage,
and its file rows follow the new design: folder icon, file name and a
size-plus-extension subtitle.
The step reflects the real state: analyze is Check, a milestone in the
upload stage is Upload, and queue, start and job milestones are Import.
While checking, the dialog shows no rows and no spinner, and the exact
files inside a binfile appear once the analysis resolves.
Each row paints a progress bar: the upload stage drives it with the
real chunk counter, and every import stage keeps the animated bar,
because the client cannot know a global percentage of the work. The
queued label joins the same status line as the milestone text and now
reads "Waiting job slot".
The change also drops the context-notification wrapper and the legacy
loader around file names, and documents the two CSS pitfalls found on
the way: a logical inline-size wins over an inline physical width, and
Rumext does not convert dynamic :style maps for plain elements, so
inline styles leave CLJS as #js objects.
AI-assisted-by: glm-5.3-flash
🔧 Enable NativeMemoryTracking in the backend devenv
Add -XX:NativeMemoryTracking=summary to JAVA_OPTS so the backend and
REPL JVMs expose native memory breakdowns through jcmd.
Useful to diagnose non-heap growth (metaspace, code cache, GC, thread)
while working with imports and other JVM-heavy paths.
AI-assisted-by: deepseek-v4.1-flash
✨ Rework the import wizard progress and the size of its files
The milestone in course is written once, in the header, where the
message of the phase was: every file of a container reports the same
one, so a row repeated it. A row keeps its name and its size, and the
button to drop a file only while the wizard still offers its import
step, which is where dropping one makes sense.
The title counts the files and counts them done with what the job
reports about the file it is working on, so the number moves while a
container rebuilds its files one by one. The bar measures the whole
import: the real bytes of every container while they are uploaded, and
animated while the wizard checks, waits for a slot or rebuilds.
A file of a version 3 package shows what it occupies inside its
container: its own entries plus the storage objects its media and its
thumbnails reference, which the container keeps shared under
`objects/`. What the container itself occupies stays in
`:container-size`, the size the upload of the package is measured on.
The ready step waited for every known file instead of for the end of
the analysis, so Continue left the files that arrived later out of the
import, with a row that never showed anything. The upload step was
recognized only once a row carried a milestone, so the wizard said it
was importing during the first chunk; it now holds until every file
leaves the upload behind.
Also fixes a misspelled design system token that left the success
color unset.
AI-assisted-by: deepseek-v4.1-flash
✨ Cover the import sizes and the done counter with tests
The size a file of a version 3 package shows is its own entries plus
the storage objects its media and its thumbnails reference, and what
the container occupies stays apart: the test drives the analysis with a
fake container that has both, and checks that the manifest is not
charged to any file.
The done counter of the header counts the files the job left behind:
all of them for a container that finished, the ones before the file it
is working on for a container in course, none for one that waits for a
slot, and the files of every container add up.
The test that answers one entry per file also needed the mock of the
entries of a container, which the analysis now reads.
AI-assisted-by: deepseek-v4.1-flash
✨ Say the wait for a slot in the header, not in every row
A row of the import wizard no longer paints a waiting state: while no
job of the import has started, the header says it waits for a free
slot, and the rows keep their name and their size. Only a file whose
container already finished uploading says something of its own
("Ready"), plus the error text when it fails.
The clock icon and the `dashboard.import.status.waiting` key of both
locales go away with it.
The notes of the redesign come along: the description of the wizard in
`mem:frontend/dashboard-viewer-subtleties`, and a new
`mem:clojure/promesa` with the call styles of the library and how they
bridge to rx.
AI-assisted-by: deepseek-v4.1-flash
* ✨ 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>
* ♻️ 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
* ♻️ 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
* ♻️ 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
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
* ♻️ 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
* 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>
* ♻️ 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
* ♻️ 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
* ♻️ 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
* ♻️ 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
* ♻️ 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
* 🐛 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.
* ♻️ 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
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
* 🐛 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
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
* 🐛 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>
`.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
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
Edits wait up to 3s in a buffer before they are sent. Closing the
laptop or switching away right after an edit could leave that batch
unsent, and a crash or a discarded tab then lost it.
The batch is now sent at once when the tab is hidden (another tab, a
minimized window, a closed tab or laptop) or the window loses focus
(another app or a plugin iframe). The 3s window itself is unchanged.
pagehide and beforeunload are left out: they come too late for an
async save, and the tab is already hidden by then.
The .penpot download is built from the server copy. Users who
downloaded a file to keep their work while saves were failing got an
old or empty file, with no warning.
Opening the download dialog now sends the pending changes of the files
and waits up to 10s for them. If some are still not saved, the dialog
says the download will not include them.
A failed export raised the exporter error to the global handler, which
replaced the workspace with the error page. Users then reloaded and
lost the changes that were not saved yet.
- A failed single or multiple export now shows an error toast and
keeps the workspace. Exporter defects still report as unhandled.
- A failed multiple export request ends its progress listeners, so the
leave-page warning it turned on is released.
- Before exporting, the pending changes of the exported files are sent
and the export waits up to 10s for them, instead of 400ms. Exports
are built from the server copy, so if some changes are still unsaved
a toast warns that the export may not include them.
force-persist-files-and-wait waits only for the given files, so a stuck
save of another file does not delay the export.
We only learn how often users lose work from support tickets. Each time
the user is told a save failed, persistence now emits a save-failed
audit event with the cause type, the code, whether the queue is still
retrying and how many changes are waiting.
It follows the same rule as the failure toast: a repeated failure of
the slow retry cycle is not counted again. Failures handed to another
handler, such as an expired session, are counted too, since they also
stop the save.
A save waits for the lock that another save holds on the same file. A
slow save, such as one on a large file, blocks the other saves to that
file. The waiting request also holds the user's only save slot on the
backend, so the user's saves to other files wait behind it too.
Those saves ended in answers that stopped autosave for good:
- a 429 after 60s, when no save slot freed up in time;
- a 504 written by the backend after 300s, with :statement-timeout,
when Postgres cancelled the wait for the lock.
In both cases the backend never applied the save, so sending it again
is safe. transient-error? now treats them as temporary, and the save
takes the retry path. A statement timeout takes 300s per attempt, so
the slow failure limit still bounds it. GET requests and error reports
are unchanged: the check lives in the save pipeline, not in
retryable-types.
Both answers were reproduced in the devenv by holding the file lock
from a separate database session.
* 🐛 Fix silent failure when uploading custom fonts
Custom font upload crashed with re-seq must match
against a string when opentype.js returned nil
subfamily names, leaving the user with no feedback.
Unknown signatures and unparseable files were also
silently dropped.
Make variant parsing nil-safe, harden entry
preparation for missing tables, report every
per-file failure with a bad-font notification, and
show a generic error if the stream itself fails.
Closes#12304
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename font mtype helper and mark helpers private
Avoid confusion with app.worker.import/parse-mtype,
which detects binfile signatures. The font helper now
lives as parse-font-mtype, and the upload helpers only
used internally are private, with tests reaching them
through var references.
AI-assisted-by: muse-spark-1.3-contributor
* 💄 Fix formatting in font upload tests
Realign map keys shifted by the var-quote prefix so
cljfmt check passes again. No behavior change.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix infer warnings in fonts metrics code
Add missing ^js type hints to the hhea and os2 metric lookups in
prepare-font so shadow-cljs can infer the interop targets and the
release build reports zero warnings.
AI-assisted-by: muse-spark-1.3-contributor
The dashboard required app.common.exceptions without ever using it,
and the plugin shape namespace still referred to
token-attr-plugin->token-attr after the per-side stroke width work
replaced its only use with expand-token-attrs. Both are leftovers of
earlier refactors; dropping them leaves clj-kondo with no warnings.
AI-assisted-by: deepseek-v4.1-flash
The libraries modal now opens on the "This file" tab, so the visual test
timed out looking for the shared library. Open the "Libraries" tab first,
as the functional test does, mock the file with the library linked, and
check the "Added" label instead of the old "File library" text.
AI-assisted-by: claude-opus-5-5
* ♻️ Use a DS icon button for flatten in the boolean options
Replace the raw flatten button and its deprecated icon with icon-button*
and the DS boolean-flatten icon. The title text moves to aria-label, so
the DS tooltip shows it. The grid column goes on the tooltip wrapper
through tooltip-class.
Add a visual test that selects two shapes so the boolean options show.
AI-assisted-by: claude-opus-5-5
* ♻️ Remove deprecated button styles from the boolean options
The flatten button is a DS icon button now, so its placeholders, sizes
and icon colors go away; only the grid column stays, on the tooltip
wrapper. Use block-size and drop the refactor/ import.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons for more colors in the selected colors
Replace the three raw "more colors" buttons (library colors, colors and
color tokens) with the DS secondary button.
Add a visual test that selects a board with more than three colors so
the button shows.
AI-assisted-by: claude-opus-5-5
* ♻️ Move selected colors styles to DS tokens
The "more colors" buttons are DS buttons now, so their placeholder and
typography mixin go away; the class only centers the label, since the
column stretches the button. Delete the unused .add-fill rule, move
spacing to DS tokens and logical properties, and drop the refactor/
import.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icon buttons in the align options
Replace the eight raw align and distribute buttons with icon-button*
and DS icons. The title text moves to aria-label, so the DS tooltip
shows the same text and shortcut. Disabled distribute buttons now use
the DS disabled color.
Add a visual test that selects two shapes so the align options show.
AI-assisted-by: claude-opus-5-5
* ♻️ Remove deprecated button styles from the align options
The align buttons are DS icon buttons now, so the .align-button rules
and their placeholders go away. Move the row height to a DS size token
and drop the refactor/ import.
AI-assisted-by: claude-opus-5-5