Commit Graph
24130 Commits
Author SHA1 Message Date
alonso.torres cf1fbb4d7c 🐛 Close plugins whose load is cancelled before they run
Unloading or replacing a plugin while its code was still being
fetched could leave a late instance running, or block reopening a
plugin that closed during startup.

Each load now carries an AbortController. Unloading, a newer load
of the same plugin or logout aborts it, and the plugin manager
closes itself on abort, also while it fetches its code.
createPlugin returns nothing for a plugin closed before its
sandbox exists, and failed loads clear their pending state so the
plugin can be loaded again.
2026-10-07 14:58:32 +02:00
alonso.torres c4d2ca9cd1 ✨ Move the dashboard MCP menu to the page headers
Show the MCP menu next to the layout toggle in the header of the
projects, files and deleted pages, instead of in the sidebar. It
uses the secondary button style there and a visible dot colour
while disconnected.

Clicking the MCP button now toggles the menu instead of only
opening it. Dashboard events report "dashboard:header" as their
origin.
2026-10-07 14:58:32 +02:00
Dominik Jain 036f59266b 🐛 Discard cancelled plugin loads after unloading
Cause:
- Disabling MCP during script loading could leave a late plugin instance
  attached to a stopped controller, blocking connection after re-enabling.

Fix:
- Track pending loads and discard instances cancelled by unload.
- Prevent duplicate global loads and preserve newer replacement instances.
- Clear pending state after failures so a subsequent load can retry.

Validation:
- Pass 84 runtime tests, the runtime build, lint and formatting checks.
- Reproduce the race with a delayed script in the browser, then verify
  re-enabling restores the session ID, themed indicator and MCP execution.

AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
2026-10-07 14:58:32 +02:00
Dominik Jain 2bb6cd557e ✨ Add project and file discovery to penpotMgmt
Discovery:
- Add listProjects and project-scoped listFiles with content:read checks.
- Return metadata from existing RPCs in dashboard and workspace contexts.
- Reject invalid IDs and preserve backend access errors.

Documentation:
- Keep usage in API type comments and include regenerated MCP API docs.
- Remove README implementation details and consolidate the changelog.
- Add usage information to the MCP server's initial instructions

Validation:
- Pass 8 frontend tests, 81 runtime tests and the runtime build.
- Verify both APIs through live MCP calls in dashboard and workspace.
- Pass type, lint, formatting and diff checks.

AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
2026-10-07 14:58:32 +02:00
Dominik Jain 54cce66c13 ✨ Share the MCP connection menu with the dashboard
Connection status:
- Replace the dashboard's action-only link with an explicit connected
  label and indicator, using the theme's existing accent colour.
- Reflect the actual connection state independently of connection intent.

Shared controls:
- Extract the toolbar's MCP menu and styles into a shared component used
  by both the dashboard and workspace.
- Provide connection status, session ID, copy, connect and disconnect
  actions in both locations, preserving hover and keyboard behaviour.
- Keep dashboard and toolbar event origins distinct and update the
  dashboard connection instructions.

Validation:
- Confirm dashboard disconnect/reconnect, menu opening, keyboard
  navigation and Escape dismissal in the running app.
- Pass live compilation, targeted ClojureScript and SCSS lint,
  formatting checks and git diff checks.

AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
2026-10-07 14:58:32 +02:00
Dominik Jain be17fabc6a 🎉 Introduce global plugins and move MCP to application scope
Global plugin lifecycle:
- Add optional manifest scope "global" for plugins that remain active
  across dashboard and workspace navigation within one browser tab.
  Existing plugins retain workspace scope by default.
- Preserve global sandboxes when a workspace ends or another plugin
  loads, including when that other plugin fails to initialize.
- Avoid duplicate instances of an already-running global plugin.
- Close global plugins on explicit closure or logout, with idempotent
  cleanup of listeners, timers and UI resources. Global scope does not
  imply automatic startup or persistence across a browser reload.

Plugin API and navigation:
- Keep a permanently live penpot facade whose getters follow the current
  workspace, while user, theme and UI facilities remain available on
  the dashboard.
- Expose penpotMgmt to global plugins with workspace metadata,
  none/loading/ready status and workspacechange subscriptions.
- Add awaitable openFile(fileId, {teamId}) navigation in the current tab,
  defaulting to the current team and resolving only once the requested
  file and its active page are ready.
- Reject invalid identifiers, failed or superseded navigation and waits
  exceeding 30 seconds; release navigation watchers on completion.
- Guard workspace-bound operations while no workspace is ready and
  return null from root, currentFile and currentPage in that state.
- Require content:read for the initial management API. Callers must
  reacquire file, page and shape references after changing files.

MCP lifecycle and connection controls:
- Make MCP a global plugin and initialize its integrated controller per
  authenticated application session instead of per workspace.
- Load the enabled integration with a valid token on the dashboard as
  well as in a file, retaining its existing hidden-iframe implementation.
- Keep enabling and connecting separate: an enabled plugin starts idle,
  and each tab connects explicitly through dashboard or workspace
  controls.
- Preserve the iframe, WebSocket, heartbeat and reconnect machinery
  during navigation; disconnect on request and close on disable/logout.

MCP sessions and execution:
- Derive the short MCP session ID solely from currentUser.sessionId,
  excluding the file ID. Retain the SHA-256-based, 50-bit Base32 encoding
  to produce a stable, copyable 10-character identifier.
- Allow sessions without an open file and update workspace metadata over
  the existing connection without replacing its routing identity.
- Refresh context after the connection handshake so initialization does
  not leave stale file metadata.
- Expose penpotMgmt to execute_code, preserve execution storage across
  navigation and skip layout waits when no workspace is ready.
- Update API types, tool guidance, documentation and the changelog.

Validation:
- Cover authenticated startup, workspace survival, logout cleanup,
  readiness, navigation failure, duplicate loads and session routing.
- Verify that short IDs remain stable across files and reconnects and
  that disconnect cancels pending asynchronous ID initialization.
- Pass 16 focused frontend tests, 73 runtime tests, 30 MCP plugin tests,
  builds, TypeScript checks and frontend/runtime lint.
- Pass all 73 MCP server tests with --test-force-exit; the normal runner
  still stays alive after assertions finish.
- Verify a live dashboard-to-file-to-dashboard round trip without a
  document or iframe reload, preserving facade identity and storage.

AI-assisted-by: gpt-6
2026-10-07 14:58:32 +02:00
Alejandro Alonso 31000c3055 ⚡ Enable rustc wasm SIMD for render-wasm builds (#12163)
Set RUSTFLAGS target-feature=+simd128 on the emscripten
target so our Rust code can autovec; Skia SIMD remains
via the -simd prebuild and EMCC -msimd128.
2026-10-07 14:52:46 +02:00
David Barragán Merino 6473dc85ab 👷 Notify new devenv images in penpotdev
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-07 14:11:21 +02:00
Andrey Antukh c4c1c5a955 🐛 Fix media-processor upload-storage tests
Update media-processor dependencies (multer 2.4, vitest 5
and others) and fix mockFile to emit Buffer chunks so
multer memory storage Buffer.concat works. String chunks
caused TypeError and 5s timeouts in two tests.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-07 11:52:46 +00:00
David Barragán Merino 27ccb32ea1 👷 Fix the devenv image build workflow
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-07 13:17:13 +02:00
Marina López d0cf399807 🐛 Show trial status on subscription page (#12158) 2026-10-07 13:14:58 +02:00
Marina López 65b248eec3 🐛 Preserve team ID in Nitrate membership URL (#12157) 2026-10-07 13:08:02 +02:00
David Barragán Merino edd500302c ⬆️ Update ImageMagick to 7.1.2-32
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-07 13:02:27 +02:00
Belén Albeza 1d7c775f72 🐛 Fix not setting mimetype in image fills (#12136) 2026-10-07 12:26:45 +02:00
Alejandro Alonso cc5f2f57ad ⬆️ Upgrade skia-safe to 0.153.3 with wasm SIMD (#12148)
Point SKIA_BINARIES_URL at the 0.153.3 simd prebuild and adapt
fonts, path stroke expansion, and gradients to the new API.
2026-10-07 12:08:33 +02:00
Andrey Antukh 9abcfebf60 ✨ Run binfile import and export as durable jobs (#12056)
* ✨ Run binfile import and export as durable jobs

Import and export of `.penpot` files become durable jobs: two commands
create them and answer at once with a job id the caller follows, while
a worker of the `binfile` queue does the work. Each job keeps its own
ledger (owner, params frozen at creation, result, progress history and
an expiry), and the legacy commands and their SSE keep working.

Why: both operations ran inside the request. A large import held it
open for minutes, progress only existed while the caller kept the
stream open, and a client that went away left work nobody recorded,
retried or could cancel. The unified `job` substrate already dispatched,
leased, retried and logged events, but it had no user-facing ledger.

How:
- The substrate gains the pieces a user-facing job needs: optional
  job-def metadata (family, resource-role), an expiry on submit, the
  owner in the runner context, and job resources in their own bucket,
  written with the profile of the job and never deduplicated.
- One generic command per family: the caller names the job and sends its
  business params, the registry checks the family, the job-def decodes
  and validates, and permission and quota are checked first.
- Progress taps of the core become milestones of the job: a shared stage
  vocabulary with self-contained counters, throttled heartbeats, and an
  interrupt that stops the run inside its own transaction on cancel.
- Cancel reaches pending and running jobs from both modals, and closing
  a modal mid-run cancels the job it was following.
- The export re-checks the read permission at run time and answers with
  the package descriptor; the import re-checks the edition permission,
  runs the core in a transaction so a cancellation rolls it back, and
  releases the consumed package and its temporary copy.

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

* ♻️ Handle externally-cancelled binfile jobs end to end

The `cancelled` outcome of a job nobody followed no longer hangs its
screens: the data layers emit an explicit terminal message for it.

- The export emits `{:cancelled true}` per file and the modal paints a
  neutral cancelled row instead of success; a message without an
  artifact never reaches the download branch.
- The import closes every entry with a dedicated cancelled message
  through the existing error rendering, so the wizard completes.
- Both flows are pinned by data-layer tests, including the red state
  they replace.

Also in this change: the progress-contract comment of the management
API, early business-params validation in create-import-job (before
assembling the upload), a `release-resource` helper without fabricated
job ids, a note on the heartbeat write cost, and a memory cleanup.

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Pre-translate the cancelled import message

The cancelled entry of the import wizard carried its message as a key
for the UI to translate, which needed a rehash marker comment. Like
its neighboring errors, it now translates eagerly in the data layer:
the literal call keeps rehash clean with no marker, and the tests
assert through the same call instead of the raw key.

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Move cancelled strings under generic labels keys

The dashboard-specific cancelled key never shipped: before use it moves
under `labels.*` next to the generic strings both flows already use,
with Spanish translations following the established wording.

AI-assisted-by: muse-spark-1.3-contributor

* ✨ Add minor improvements

* ♻️ Rename export job command to create-export-binfile-job

Export now has one command per job type, the shape the :export-assets
command needs: create-export-binfile-job freezes exactly the
:export-binfile job, so :name no longer travels in the body and only
:params does. Import keeps the generic envelope.

submit-job reads the queue from the job-def metadata ::jobs/queue-name
(falling back to :binfile), read by the command and never by the
substrate, so a job-def can route its work to another queue without a
change in the command.

The rename reaches the frontend and the tests: the binfile export is
asked for under its own name, and the exporter service keeps
create-export-job.

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

* ✨ Polish the import and export status rows

The status row of a file already carries what the file is doing, so the
generic "Uploading file…"/"Downloading file…" lines are gone: the
import wizard drops its own (the export one went with the jobs
refactor), along with the now-unused style.

In the export, every file is queued up front, even the ones whose job
is created only after the previous one is over, and a file stays queued
until its first milestone: a worker picking the job up no longer ends
the queue. The queued text is a step smaller than the milestone.

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

* 🐛 Show the cancelled export and fix the import error

The cancelled export was unreachable: mark-file-cancelled sets :loading
false and the label lived inside the loading branch, so a cancelled
file only painted a neutral notification. The terminal state now
carries the label, and the export flags lose the `?` suffix
(predicates keep it; a data prop is not a question).

The import wizard rendered (:error entry) through tr, but every
producer writes user-facing text (a hint or a translated label), never
a key: it renders the text as-is now, and the dynamic-key ignores and
the stale comment are gone.

The total-text-limit comment no longer references the 200MiB it
replaced; the value is 500MiB in develop already.

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

* ♻️ Reflect the real job params in the creation commands

The creation commands declared their params as an open empty map, so the
RPC contract and the generated docs said nothing about them. Each command
now imports the schema from the job namespace
(`export-binfile/schema:params`, `import-binfile/schema:create-params`),
and the RPC validates the business params before the command runs. The
job-def still owns the params and re-validates the assembled ones at run
time. The commands no longer decode the params again: the RPC did it.

Import joins export as one command per job type: `create-import-job`
becomes `create-import-binfile-job`, its body no longer carries `:name`
and the job-def is resolved by name like export's. Its creation schema
keeps the version optional, because the command reads it from the package
header when the caller does not say it, and fills the manifest metadata
kept for audit.

A version out of range is now refused by the schema (`:params-validation`)
instead of the command's own `:unsupported-version` check, which is gone.

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

* 🐛 Stop the test system from running job runners

The test system is main/system-config plus main/worker-config with a
few components dissoc'ed. The runners of default, webhook and cron were
listed by hand; the binfile runner added with the durable binfile jobs
was not, so a real penpot/job-runner/binfile/0 thread ran during the
whole suite.

That runner races the tests that drive the loop by hand (run-batch plus
run-worker-loop): it takes the job the test just dispatched and
completes it, and the test then reads a state it did not produce. That
is the reported flake of binfile-jobs-integration-test, whose middle
assertion waits for a runner of another queue to not take the job. It
only reproduced in CI: in the devenv PENPOT_TENANT makes the tenant
baked into main/worker-config differ from the one the test builds, so
the two used different queue keys locally.

Drop every [<profile> :app.worker/runner] by key shape instead of
listing the known queues, so a new queue cannot bring the race back.

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

* ✨ Upload the package one chunk at a time and as a percentage

The upload milestone counts chunks, not units of work, so the row wrote
"Uploading the package 3/9" over a file the user had uploaded once: it
read as if nine packages were on their way. Write the stage as a
percentage of the file instead ("Uploading file… (33%)"), and make the
English text of the stage say file, not package.

The chunks also go one at a time now (concat-all instead of merge-all
with two in flight), so the percentage moves in a straight line instead
of jumping. upload-blob-chunked is the generic uploader, so media and
font uploads become sequential too.

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

* 🌐 Complete the Spanish catalogue

es.po was 25 keys short of en.po: the 15 that the durable binfile jobs
introduced (14 jobs.progress.stage.* and labels.queued) plus 10 gaps
that predate them (errors.connection-error, errors.save-retrying, the
find shortcuts, workspace.header.retrying,
workspace.tokens.stroke-width and the mcp session labels).

sync -l es then sorts the entries, syncs the #: comments from en and
drops the 7 dashboard.import.progress.* keys en no longer has. The
msgid sets of en and es are now identical (0 missing, 0 extra).

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

* ♻️ Move the job status keys into the jobs namespace

The three status words the durable import and export rows render were
filed under labels.*, next to the generic labels bucket, while their
progress stages live under jobs.progress.stage.*: the strings of one
feature sat in two places.

Rename them so the whole job vocabulary is one namespace:

  labels.queued           -> jobs.queued
  labels.export-cancelled -> jobs.export-cancelled
  labels.import-cancelled -> jobs.import-cancelled

No other locale has these keys yet, so the rename costs no translation.
The convention (a key names the feature that renders it; labels.* holds
the generic reusable words) is written down in the frontend/translations
memory.

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

* 🔥 Remove the translation keys nothing references

Six keys of en.po (and of es.po, which now mirrors it) have no (tr ...)
call site left in frontend/src or common/src, and no literal reference
either:

  labels.uploading-file    a duplicate of jobs.progress.stage.upload
                           since the upload stage says "Uploading file…"
  labels.downloading-file  dead since the durable jobs replaced the old
                           import and export progress rows
  inspect.attributes.stroke.alignment.center
  inspect.attributes.stroke.alignment.inner
  inspect.attributes.stroke.alignment.outer
  workspace.toolbar.mcp-connect-here

The first two are leftovers of the durable binfile jobs, the other four
predate them. Both catalogues keep an identical msgid set afterwards.

AI-assisted-by: deepseek-v4.1-flash
2026-10-07 11:52:48 +02:00
Alejandro Alonso accea1abde ⚡ Enable wasm SIMD for Skia render-wasm builds (#12145)
Link with -msimd128 and point SKIA_BINARIES_URL at the pdf+simd prebuilt archive.
2026-10-07 11:30:40 +02:00
Danny ShirelyandEva Marco 051f01d559 🐛 Keep per-side stroke widths out of inspect geometry (#12055)
Remove per-side stroke widths from both Inspect geometry panels so
Stroke is the only section that lists them. Keep dimensions, position,
corner radii, and transforms in Size and position.

Add rendered-component regressions for boards, rectangles, multiple
selection, uniform strokes, and CSS declarations.

Closes #12026

AI-assisted-by: GPT-6

Signed-off-by: Danial Shirali <danialshirali16@gmail.com>
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2026-10-07 11:18:46 +02:00
Marina López a9bf53adbf Revert "🐛 Preserve path segments when joining backend URIs (#12152)" (#12156)
This reverts commit 2a6e92a7d9.
2026-10-07 11:10:02 +02:00
Marina López 2a6e92a7d9 🐛 Preserve path segments when joining backend URIs (#12152) 2026-10-07 10:45:17 +02:00
Andrey Antukh a5df69f60f Merge remote-tracking branch 'origin/staging' into develop 2026-10-07 10:25:01 +02:00
Andrey Antukh 7f0aa4f38e Merge remote-tracking branch 'origin/main' into staging 2026-10-07 10:23:37 +02:00
Andrey Antukh b2d688a04c 💄 Stringify ids in comp-processors log props
Wrap every id prop in the comp-processors warn logs
with str so the log shows plain text instead of
the #uuid reader form. Also use :hint instead of
:msg and lowercase text, as the rest of the system
does. No logic changes, logging only.

AI-assisted-by: muse-spark-1.3
2026-10-07 08:17:33 +00:00
Eva Marco 2f47be6c15 🐛 Show the size badge on paths and straight lines (#12066)
* 🐛 Show the size badge on paths and straight lines

The badge was hidden when the shape was smaller than the badge in
either side. Flat paths have a zero width or height, so they never
showed it, and straight two-point paths hid it on purpose.

Paths now only compare their longest side with the badge width. A
straight line compares its length, and the badge runs along the
line, rotated with it and kept upright. Near the bottom of the
viewport the badge moves to the other side of the line.

The badge now reads the size from the shape :selrect, like the
measures inputs, so a rotated path shows the same values in both
places. The three badge branches share a single render block.

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

* ♻️ Extract the size badge layout and add tests for it

Move the badge geometry out of selection-size-badge* into the pure
size-badge-layout function, so it can be tested without rendering.
The component now only picks the color and draws the result.

Add unit tests for rects, flat paths and straight lines: when the
badge hides, where it sits, how it rotates and how it moves when it
reaches the bottom of the viewport. Keep the line rotation between
-90 and 90 degrees instead of returning 360 for lines drawn leftward.

The e2e test still expected straight lines to hide the badge. It now
checks that the badge shows and follows the line angle.

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

* ✨ Show the length of straight lines in the size badge

A straight two-point path is measured by its length, not by the box
around it, so its size badge now shows the length with two decimals at
most instead of width x height. Other shapes and paths keep width x
height.

The shorter text makes the badge narrower, so it also shows on shorter
lines; the rule is the same, the badge needs the line to be at least as
long as the badge.

Relates to #12064

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

* 🐛 Hide the path size badge when the path is lower than it

Paths other than a straight line only hid the size badge when their
longest side was narrower than it, so a wide but low path, or a narrow
but tall one, still showed it. Flat paths no longer need that special
case, since straight lines get their own length badge. Use the same
rule as any other shape: hide the badge when either the width or the
height of the path is smaller than the badge.

AI-assisted-by: claude-opus-5-5
2026-10-07 09:18:57 +02:00
Andrey Antukh e7041234f3 📎 Set library (sdk) version to 1.2.0 2026-10-07 08:49:32 +02:00
Andrey Antukh fc8a126048 Merge remote-tracking branch 'origin/staging' into develop 2026-10-06 18:59:56 +02:00
Andrey Antukh 84f068aca7 Merge remote-tracking branch 'origin/main' into staging 2026-10-06 18:59:36 +02:00
Andrey Antukh 5b02e4d898 ✨ Add find-project-anomalies skill and helper
Check a milestone against the Main project board with
scripts/project-anomalies.py (check writes
tmp/<MILESTONE>-ANOMALIES.md): open issues with merged PRs,
milestone mismatches either way, needs triage labels and
unassigned non-community issues. Outsiders resolve in bulk;
only a missing milestone auto-fixes, on confirmation.

Teach scripts/gh.py batched issue lookup (issue subcommand),
milestone on prs output, plus assignees and projects fields,
and retry transient HTTP 504s.

AI-assisted-by: muse-spark-1.3
2026-10-06 16:58:08 +00:00
Andrey Antukh 4bdded86be ♻️ Extract changelog checks into scripts/changelog.py
Move the inline programs out of the update-changelog skill into
scripts/changelog.py (check-merged, cross-ref, report) and shrink
the skill to the workflow alone.

Teach scripts/gh.py to retry transient HTTP 504s, resolve lookups
in batches of 50 (new issue subcommand, milestone on prs output),
and report 💥 entries without the breaking change label.

Add the nine missing 2.17.0 changelog entries found while
trying the new flow.

AI-assisted-by: muse-spark-1.3
2026-10-06 16:26:37 +00:00
David Barragán Merino 1afa8b1afa 🔧 Skip triage for Renovate's dashboard and GitHub Actions PRs
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 18:09:57 +02:00
David Barragán Merino 8204e97a98 🔧 Manage GitHub Actions with Renovate
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 18:09:57 +02:00
David Barragán Merino b6a3ab67ef 🔥 Remove Dependabot configuration
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 18:09:57 +02:00
Belén Albeza 7a37cefe4c 🐛 Pass team-id in binfile SVG import tests (#12139)
AI-assisted-by: claude-opus-5-5
2026-10-06 17:33:43 +02:00
David Barragán Merino 472973533e 🐳 Verify the Node.js tarball checksum in the devenv
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 17:14:55 +02:00
Eva Marco d046f6997d 🐛 Keep assets list view scroll position across sidebar tabs (#12115)
In list view the height of each component row depended on its
thumbnail, which renders deferred. When the Assets tab mounted again,
rows were shorter until the thumbnails appeared, so the restored
scrollTop landed several rows further down and scroll anchoring kept
it there. Reserve the thumbnail height in the row grid so rows keep
their size from the first render.

Closes #11971

AI-assisted-by: claude-opus-5-5
2026-10-06 16:55:16 +02:00
Eva Marco fcbb8f1fc0 🐛 Keep derived layout paddings non-negative (#12129)
Adding a flex or grid layout to a board with children derives the
paddings from the distance between the board and its children. A child
that overflows the board, or floating point noise after "Resize board
to fit content", gave negative paddings. The shape schema rejects them
since paddings must be non-negative, so update-file failed and autosave
stopped. Clamp the derived paddings to 0, as the derived gaps already
are.

Closes #12107

AI-assisted-by: claude-opus-5-5
2026-10-06 16:34:28 +02:00
David Barragán Merino f38cb4dc07 🔥 Remove unused package-lock.json from plugins-runtime
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 16:30:30 +02:00
Andrey Antukh 746184caf9 Merge remote-tracking branch 'origin/staging' into develop 2026-10-06 15:58:01 +02:00
Andrey Antukh 51ae83202e Merge remote-tracking branch 'origin/main' into staging 2026-10-06 15:56:55 +02:00
Andrey Antukh 0ccd75487f ⬆️ Update dependencies on devenv 2026-10-06 15:43:38 +02:00
Eva Marco 5981d1ee66 🐛 Open a never scrolled token set at the top of the list (#12119)
The tokens list container stays mounted when another token set is
selected; only the restore key changes. With no saved position for the
new set, use-restore-scroll did nothing, so the list kept the scroll
position of the previous set. Scroll to the top when nothing is saved
for the key. Freshly mounted panels are already at the top, so Layers
and Assets do not change.

Closes #11973

AI-assisted-by: claude-opus-5-5
2026-10-06 15:40:24 +02:00
penpot-renovate[bot] a61a4fceb1 ⬆️ Update dependency pnpm to v12.8.1
- pnpm 12.8.1 (docker/images/Dockerfile.media-processor)
- pnpm 12.8.1 (docker/images/Dockerfile.mcp)
- pnpm 12.8.1 (docker/images/Dockerfile.exporter)
- pnpm 12.8.1 (render-wasm/package.json)
- pnpm 12.8.1 (plugins/package.json)
- pnpm 12.8.1 (plugins/libs/plugins-styles/package.json)
- pnpm 12.8.1 (plugins/libs/plugins-runtime/package.json)
- pnpm 12.8.1 (plugins/libs/plugin-types/package.json)
- pnpm 12.8.1 (plugins/apps/table-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/rename-layers-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/poc-tokens-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/poc-state-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/plugin-api-test-suite/package.json)
- pnpm 12.8.1 (plugins/apps/lorem-ipsum-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/icons-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/example-styles/package.json)
- pnpm 12.8.1 (plugins/apps/e2e/package.json)
- pnpm 12.8.1 (plugins/apps/create-palette-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/contrast-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/composable-test-suite/package.json)
- pnpm 12.8.1 (plugins/apps/colors-to-tokens-plugin/package.json)
- pnpm 12.8.1 (package.json)
- pnpm 12.8.1 (media-processor/package.json)
- pnpm 12.8.1 (mcp/packages/server/package.json)
- pnpm 12.8.1 (mcp/packages/plugin/package.json)
- pnpm 12.8.1 (mcp/packages/common/package.json)
- pnpm 12.8.1 (mcp/package.json)
- pnpm 12.8.1 (library/package.json)
- pnpm 12.8.1 (frontend/text-editor/package.json)
- pnpm 12.8.1 (frontend/packages/ui/package.json)
- pnpm 12.8.1 (frontend/packages/tokenscript/package.json)
- pnpm 12.8.1 (frontend/packages/mousetrap/package.json)
- pnpm 12.8.1 (frontend/packages/draft-js/package.json)
- pnpm 12.8.1 (frontend/package.json)
- pnpm 12.8.1 (exporter/package.json)
- pnpm 12.8.1 (docs/package.json)
- pnpm 12.8.1 (common/package.json)
- pnpm 12.8.1 (backend/package.json)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-10-06 15:12:15 +02:00
Andrey Antukh ff7a66f519 📚 Update changelog 2.18.3 2026-10-06 14:25:50 +02:00
Andrey Antukh 10504ff2bf 🐛 Sanitize SVG on binfile import (#12105)
* 🐛 Sanitize smuggled SVG on binfile import

Route imported SVG storage objects through the SVG sanitizer on
all three binfile paths (v3, v1, v2), closing the second bypass
of GHSA-ffhp-m958-qxvr. Integrity checks still run on the raw
bundle bytes first; only the persisted copy is the sanitized one.

Adds a shared sanitize-imported-svg helper with unit tests and a
v3 export-tamper-import test proving smuggled scripts no longer
survive the import.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Normalize SVG content-type on binfile import

Close the residual GHSA-ffhp-m958-qxvr bypass where a crafted
content-type spelling (uppercase, parameters) skipped the import
sanitizer. Detection now uses a shared case-insensitive predicate,
stored values are canonicalized, and the v3 re-read honors the
import size limit.

Extends the tamper tests to obfuscated spellings and adds
exhaustive v1 coverage, including the tempfile branch.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Gate binfile content-type on known storage types

Define the complete set of storage content-types in
app.common.media and enforce it on the binfile import schema, so
unknown types fail closed instead of passing through. Detection
keeps a single normalization at the boundary with an exact
predicate, and the string helpers use cuerdas.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Keep only produced types in storage-object-types

Drop apng, avif, penpot and plain text from the set: no flow
stores them, they only exist in the extension mapping table.
Every member must have a producing code path; unproduced types
stay out until some flow actually stores them.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Reject unknown content-type on v1 binfile import

Enforce the storage content-type allowlist on the v1 import
path, like v3 already does via schema. Bundles declaring types
outside the set, or none at all, fail closed with the same
media-type-not-allowed error as uploads. The sanitize branch is
explicit: SVG bytes are sanitized, anything else passes through.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-06 14:21:12 +02:00
penpot-renovate[bot] e17c3c0c8f ⬆️ Pin DHI base image digests
- dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/images/Dockerfile.backend)
- dhi.io/debian-base trixie-debian13@20079b5 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/imagemagick/Dockerfile)
- dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/devenv/Dockerfile)
- dhi.io/nginx 1.31.6-alpine3.24@9465275 (docker/images/Dockerfile.storybook)
- dhi.io/nginx 1.31.6-debian13-dev@32e4340 (docker/images/Dockerfile.frontend)
- dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.21.0-debian13@f3575cf (docker/images/Dockerfile.mcp)
- dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-10-06 13:58:17 +02:00
Eva Marco ff74c38bbf 🐛 Keep per-side token attributes when remapping a token (#12091)
Renaming a token with "Remap tokens" grouped the references by container
and wrote the union of their attributes onto every shape in it. A shape
with a stroke width token on one side got the token on all four sides
when another shape in the same container had it on every side.

Group the references by shape, so each shape is only renamed on the
attributes it already had the token on.

Closes #12075

AI-assisted-by: claude-opus-5-5
2026-10-06 13:49:16 +02:00
Eva Marco 582f943228 🐛 Show stroke width tokens in the inspect stroke panel (#12096)
Since stroke widths can be set per side, stroke width tokens are stored
in the four per-side attributes, not in :stroke-width. The inspect stroke
panel still read :stroke-width for Border width and had no token lookup
for the per-side widths, so it showed the raw value instead of the token.

Read the token of each side for the per-side widths, and the token shared
by every side for Border width. Files that are not migrated yet keep the
token in :stroke-width, so Border width still falls back to it.

Closes #12029

AI-assisted-by: claude-opus-5-5
2026-10-06 13:48:56 +02:00
Eva Marco dacfac17bb 🐛 Show the top stroke width in the old render (#12093)
* 🐛 Show the top stroke width in the old render

With the old render, the stroke width input showed "Mixed" when the four
sides had different widths or tokens. The old render only draws the top
side, and the input must show it as the global stroke width.

Move the input value and token logic into two pure functions. When per
side strokes are disabled, both return the top side: its width and its
token, or no token when the top has none.

Closes #12063

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

* 🐛 Add e2e tests for the stroke width input in both renders

Cover the global stroke width input when the four sides differ. With the
new render it shows Mixed. With the old render it shows the top side:
its width, or its token when the top has one.

The old render cannot create per side data from the UI, and switching
the render reloads the page and drops the test state. The tests patch
the existing stroke token fixture instead, appending the per side fields
to a rectangle and a board.

Relates to #12063

AI-assisted-by: claude-opus-5-5
2026-10-06 13:33:36 +02:00
David Barragán Merino bcd4631082 🔧 Add Renovate configuration
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 13:19:13 +02:00
David Barragán Merino d47753dbb3 🔧 Build Docker images on Renovate branches
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-06 13:19:13 +02:00