Migrate the remaining legacy components in the inspect module to the
modern rumext syntax: typography-block, text-block, svg-attr, svg-block,
selection-rect, selection-feedback, layer-item and left-sidebar.
Rename the ?-suffixed props of layer-item (component-child? to
is-component-child, hide-toggle? to hide-toggle) and drop the unused
sortable? and filtered? props passed from left-sidebar.
Relates to #9260
AI-assisted-by: claude-opus-5-5
Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The svg-with-images case asserted that the stubbed rp/cmd! saw only the
URL upload RPC. clear-thumbnail sends :delete-file-object-thumbnails after
a 200 ms timer, so on slow CI runners that call was recorded too and the
assertion failed.
Count only the :create-file-media-object-from-url calls and expect one,
so unrelated RPCs no longer break the test.
AI-assisted-by: claude-opus-5-5
* ♻️ Give the workspace logo a real link and the DS logo asset
The Penpot logo in the workspace header was an <a> without href, so the
keyboard could not reach it and it could not open in a new tab. It now
links to the dashboard Recent page; a plain click still runs the in-app
navigation (prevent default), and Ctrl/Cmd-click opens the link in a
new tab.
The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing), with its own 32px class. The legacy svg kept its 500px height
attribute, so it overflowed the header by 222px above and below. This
was the last legacy icon in left_header.cljs.
New visual test "Shows the Penpot logo link": 0 px of difference at rest
and on hover.
AI-assisted-by: claude-opus-5-5
* ♻️ Make the workspace project name a real link
The project name in the workspace header was a div with on-click that
opened the project files in a new tab, so the keyboard could not reach
it. It is now a link to the same page with target "_blank": the browser
opens the new tab, and Tab, Enter and "Open link" work as for any link.
The left header visual test gives 0 px of difference.
AI-assisted-by: claude-opus-5-5
The Penpot logo in the viewer header was an <a> without href. For team
members it now links to the dashboard Recent page of the file's team: a
plain click still runs the in-app navigation (prevent default), and
Ctrl/Cmd-click opens the link in a new tab. Without team permission it
stays as before: no href and no click.
The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing, same 28px class). This was the last legacy icon in
viewer/header.cljs.
The "Viewer header" visual test gives 0 px of difference.
AI-assisted-by: claude-opus-5-5
A template card was an <a> without href inside a focusable div: Tab
reached the div, and neither Enter nor Space imported the template, so
the cards could not be used from the keyboard. Importing is an action,
so the card is now a <button> with the same look; it takes the tab
index the div had, and the manual Enter handler goes away.
Keyboard focus looks like hover (accent border and "Add to your
project").
The templates visual test gives 0 px of difference at rest, on hover and
for the scroll and collapse buttons; a new shot covers the focus.
AI-assisted-by: claude-opus-5-5
"Change email" and "Want to remove your account?" were <a> without href
that open modals: the keyboard could not reach them. They are now
<button type="button"> that keep their text-link look (accent color,
underline on hover), so Tab reaches them and Enter or Space opens the
modal.
The profile visual test gives 0 px of difference, now also with a shot
of the hover on "Want to remove your account?".
AI-assisted-by: claude-opus-5-5
On the internal error page and the feedback page, "Download report.txt"
was an <a> without href, so Tab skipped it. It is an action, so it becomes
a <button type="button"> with the same look, underline on hover included.
The type keeps it from sending the feedback form.
A new visual test reaches both pages through a mocked server error and
checks 0 px of difference and the download with Enter.
AI-assisted-by: claude-opus-5-5
"Forgot password?", "Create an account" and "Login here." switch the
section of the login dialog (viewer and MCP authorize) and of the dialog
on the not found page. They were <a> without href, so Tab skipped them.
They become <button type="button"> with the same look; a class replaces
the bare a selector in static.scss.
A new visual test opens both dialogs, uses the keyboard to switch
sections and checks 0 px of difference at rest and on hover.
AI-assisted-by: claude-opus-5-5
An operator writing `PENPOT_TENANT=ACME` and one writing `acme` name the
same tenant: the value is compared as an exact string in the keys and
rows it lands in, and one uppercase letter would otherwise split one
tenant into two.
The value is normalized when the configuration is read, so nothing
downstream has to care about case, and the validated form accepts only
lowercase labels from then on.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix ChatGPT tool discovery with the submit_feedback tool
ChatGPT compiles the regex patterns advertised in tool input schemas
with Python, which rejects Unicode property escapes such as \P{Cc}. The
submit_feedback subject pattern used one, so ChatGPT dropped the whole
tool list ("action discovery failed") on deployments that register the
tool.
The subject pattern now spells out the Cc range as a character class,
which accepts the same input. A test asserts that no advertised pattern
uses \p{...} or \P{...}.
AI-assisted-by: claude-opus-5-5
* 🐛 Fix MCP plugin test stub for the version check
plugin.ts checks version compatibility with extractMinorVersion, but the
tests loaded it with a require stub that returned the same dummy object
for every import, so the check threw once the UI initialized.
The plugin.ts tests now share a stub that supplies the real
VersionCompatibility, ErrorUtils and TaskHandler modules, stubs only the
code execution handler and rejects unexpected imports. TaskHandler is
transpiled for the sandbox because Node's type stripping cannot load its
parameter properties.
AI-assisted-by: claude-opus-5-5
* 🐛 Fix Management tests for backend-confirmed setters
Management setters save to the backend first and update the object only
when the save succeeds, so seven live tests failed:
- setter tests now wait for the new value before reading it
- createProject checks fileCount by listing the project again, as
fileCount is the count from when the project was listed
- project moveTo no longer calls remove on the target team, which teams
do not have; the live CI driver deletes temporary teams
- renaming the open file waits for its restore, which made the next
navigation test fail
The README notes on setters, fileCount and team cleanup match this.
AI-assisted-by: claude-opus-5-5
* 🐛 Create Plugin API shapes at the page root
Create component instances and SVG shapes at the page root so a board at
the page origin cannot capture them. Continue using the existing forced
frame option for component instances and an explicit parent frame for
SVG imports. Interactive placement retains its current behavior.
Cover component instances, SVG imports and SVG imports with images on
ordinary, flex and grid origin boards, including undo, redo, position
changes and explicit reparenting.
Closes#12086
AI-assisted-by: deepseek-v4.1-flash
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
* 🐛 Preserve interactive creation placement
Cover component and SVG placement inside and outside an origin board.
Exercise the sidebar move lifecycle and verify parent preservation.
Correct the Plugin API changelog category.
AI-assisted-by: gpt-6
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
* 🔧 Cover page-root creation in the Plugin API test suite
Check that LibraryComponent.instance(), createShapeFromSvg() and
createShapeFromSvgWithImages() create shapes at the page root when a
board covers the page origin. Each test removes the shape it creates,
since it lives outside the scratch board.
AI-assisted-by: claude-opus-5-5
---------
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* ✨ Add skills for the DS migration epic
Two skills that drive the DS migration (#12375) one area at a time, so
anyone working with an agent follows the same flow.
refactor-scss moves one .scss off frontend/resources/styles/common/
refactor/ to the DS tokens, mixins and px2rem as a pure refactor, with
a self-review for the issues reviewers flag and a zero-tolerance visual
check.
migrate-ds-area takes one area of a group issue (buttons and icons,
selects, ...): claim it in the group's PRs table, add a visual test,
change the controls, run refactor-scss, compare before and after with
the bundled diff scripts, then open the draft PR and post the QA
comment. It also lists devenv traps seen while migrating the first
areas.
AI-assisted-by: claude-opus-5-5
* ✨ Add the claim rules to the migrate-ds-area skill
The group issue body is saved whole, so two people claiming at the same
time can lose a row. The skill now rereads the body after saving and
puts the row back if it is gone. A claim with no PR for 7 days is free
once its owner has been asked in a comment and did not answer.
AI-assisted-by: claude-opus-5-5
Generate plugin.js before Angular builds each of the eight Angular plugins.
The previous order wrote the script to src/assets after Angular had copied
assets to dist, so clean deploys published manifests without their code.
Verified clean builds include all declared manifest assets for all eight
plugins. Plugin lint, format checks and all 101 tests pass.
AI-assisted-by: gpt-6.1-sol
Refresh the pinned DHI digests used by Penpot's container builds.
Stale Debian bases conflict with current APT package versions.
AI-assisted-by: gpt-6
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
* ✨ Migrate legacy file payloads to file_data and delete orphans
Copy file.data and labelled file_change.data to file_data on boot
(type main/snapshot, backend db, deleted_at preserved), then delete
file_change rows with data but no label, which no read path reaches.
Large instances pre-migrate with batched statements and fake this
step. Closes#12784.
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Add 2.19.0 operations runbook for legacy file_data migration
Manual pre-migration procedure for big instances, same statements as
migration 0157 in batched transactions, following the convention of
the file-change partition runbook. Closes#12784.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Address review feedback on the legacy file data migration
Fix the issues raised in the review of the migration that moves the
legacy file payloads to file_data.
The migration deletes the unlabelled file_change rows that carry data,
but restore-files still cleared their date, and the retention test
asserted they came back. A row without a date never reaches objects-gc,
which is the leak fixed in #12419. Restore now unmarks only snapshots,
the rows that carry a label, and the retention test checks that the
legacy payload keeps its date instead of coming back.
Cover the conflict path that had no test: a pre-existing snapshot that
collides with a labelled change row, and a second run that must leave
the legacy columns null and the orphan row absent.
The runbook analyses file_data after the inserts, so the planner does
not wait for autovacuum.
Drop app.srepl.procs.fdata-storage: the copy it performed is what the
migration replaces and nothing requires it.
Closes#12784
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add unit tests for the token form and combobox helpers
Several helpers of the token forms and the token value combobox were
already pure but had no tests. Cover the reference under the cursor,
the order of the token tree, moving through the combobox options, the
shadow and typography form values and their validation, which value a
form tab validates, and that a token import needs a json file.
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
* ✨ Test the shadow and typography form rules
Add tests for the shadow and typography form schemas: a negative blur
is rejected, a negative spread and a blank blur or spread are accepted,
every shadow needs its offsets and color, a line height needs a font
size, and a token can't reference itself. A typography token submitted
without any field is rejected.
They replace the tests of the validator wrappers, the combobox focus
order and the import file check.
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
---------
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
The Integrations settings page labelled the MCP server as Beta, while
the MCP docs give no maturity status. Drop the badge, its style and the
unused translation key so the UI no longer presents it as beta.
Closes#12800
AI-assisted-by: claude-opus-5-5
The token pill worked out inside the component whether its token is
applied to the selection, whether it can apply to it at all and why
the token is invalid, so no test could reach that logic. Move it into
`pill-state`, and split the tooltip into `tooltip-info`, which decides
what it says, and `render-tooltip`, which renders it. Both are pure
and get unit tests for the applied, partial, disabled and error states
and each tooltip case.
The two chained `on-click` handlers become one. Nothing else changes
in behavior.
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
At some point the code around this changed, I believe this used to reduce over
file ids. However today it reduces over entire library maps, so it passes a map
instead of an id to `dm/get-in files`. This then hashes the library which
forces it to decode all of its shapes, and then it fails to lookup the data. So
the component/colors/typography counts were always returning 0.
Using reduce-kv fixes this, and we can just get the library data directly with
`(:data library)`.
On the backstage file in penpot-files this brings the time to the telemetry
snapshot down from 3.8s to 1.5s
AI-assisted-by: claude-5.5-sonnet
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The "watch a video explanation" link pointed to the how-to-use section
of penpot.app/libraries-templates, which is broken and will change.
Point it to the YouTube video instead.
AI-assisted-by: claude-opus-5-5
Add a `boom` subcommand that generates the `💥` section of CHANGES.md
from the closed, non-excluded milestone issues labelled `breaking change`,
using the last `## Breaking change note` comment of each issue. The whole
comment is copied (headings demoted to `#####` outside code fences), and
each issue becomes a `#### <title>` block with the issue and merged
closing PR references on their own line. `--write` replaces only that
subsection and is idempotent.
Add a `comments` subcommand to gh.py (REST, paginated) so the generator can
read issue comments, and report anomaly type G (`breaking change` issue
missing from `💥`), mirroring the `release highlight` check.
Document the flow in `mem:workflow/updating-changelog` (also readable by
release managers, like `mem:audit-log`), turn the `update-changelog` skill
into a router to it, and update the references in `critical-info`,
`creating-issues` and the scripts memories.
AI-assisted-by: deepseek-v4.1-flash
The writing principles of the PR memory carried the no-wrap rule inside
the PR description section, so it read as description-only and comments
kept getting hard-wrapped. Say that it applies to PR and issue comments
and to review replies too.
AI-assisted-by: deepseek-v4.1-flash
* ⚡ Avoid decoding the opened page twice for its frame index
The frame index was built over a with-meta copy, so pages got decoded again.
Read it from the page objects themselves and fix the with-meta identity check.
* ⚡ Intern keywords when decoding transit on the frontend
Each shape is decoded on its own, so every shape had its own key keywords.
Cache them in the read handler so all decoded data shares one instance.
* ⚡ Use an array map as the wasm shape proxy delegate
A hash map with ~35 keys costs ~3 KB of nodes per shape on large pages.
An array map holds the same entries in one array, at a small lookup cost.
The MCP endpoint parsed request bodies with express.json() and its
default 100kb limit, so large tool calls such as execute_code failed
with HTTP 413 (PayloadTooLargeError). Read the limit from the new
PENPOT_MCP_MAX_BODY_SIZE env var, defaulting to 10mb, and document it.
Closes#12080
Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Most browsers return "ja" rather than "ja-JP" in navigator.language.
Because parse-locale expands "ja-JP" to ["ja_jp" "ja"] but keeps "ja"
as ["ja"], using "ja_jp" causes autodetection to fall back to English.
Normalize the Japanese locale identifier to "ja" and map jpn_JP.po to
translation.ja.js, matching other single-region locales like Ukrainian.
AI-assisted-by: gemini-3.8-flash
The plugin compared the full major.minor.patch of the running Penpot
version with the MCP package version. The MCP package version is only
set per minor release (2.15.0, 2.16.0, 2.17.0), so every Penpot patch
release (for example 2.16.2 or 2.17.2) showed the "Version mismatch
detected" warning although nothing changed for the MCP.
Compare only major.minor, move the check into a small module with
unit tests, and update the README versioning note to match.
AI-assisted-by: claude-opus-5-5
Signed-off-by: robertkodra <36516516+robertkodra@users.noreply.github.com>
manage.sh has kept the ws1+ workspace clones in
~/.penpot/penpot_workspaces since parallel instances landed (#9906).
That directory sits outside the XDG base directory layout. The default
is now $XDG_DATA_HOME/penpot-devenv/workspaces, with ~/.local/share
standing in when the variable is unset or empty. It is the data
directory and not the cache, because a clone can hold commits that
exist nowhere else.
A directory that already exists at the former default keeps being
used, so existing clones are neither orphaned nor seeded again. An
exported PENPOT_WORKSPACES_DIR still overrides both.
AI-assisted-by: claude-opus-5-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
A token cannot contain tokens: next to a token `a`, the tokens `a.b`
and `a.b.c` are now invalid with tokenscript, whatever the order of
their sets. As with StyleDictionary, their pills and the token form
show "Group name of a.b conflicts with a token of the same name in
another active set." and `{a.b}` is a missing reference.
The interpreter reports them as :processor/token-name-collision, which
the adapter maps to :error.token/name-collision. frontend/deps.edn
updates penpot/tokenscript-interpreter to v0.2.0, which adds the check.
Since v0.1.0 its errors carry codes and data only, which the adapter
already relies on.
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>