Compare commits

...
Author SHA1 Message Date
Pablo Alba fb6ece7a7e Revert "🐛 Enforce SSRF checks and add timeouts to HTTP client (#11474)" (#11556)
This reverts commit ff63668c1e.
2026-09-08 13:38:00 +02:00
Elena Torró 18e641d79a 🐛 Fix selrect auto-width on selrect click and selrect resize (#11541)
* 🐛 Fix text edge double click needing two undos

* 🐛 Fix text font change needing two undos
2026-09-08 13:32:37 +02:00
Elena TorróandBelén Albeza af5b767933 🐛 Fix microinteractions on text shape selrects for autowidth/autoheight (#11068) (#11545)
Co-authored-by: Belén Albeza <belen.albeza@kaleidos.net>
2026-09-08 10:29:15 +02:00
Andrey Antukhandalonso.torres 937b3fc65f 🐛 Add missing permission checks to plugin API (tokens, shapes, variants, flows, layouts, user identity) (#11139)
* 🐛 Add content:write permission checks to Design Tokens plugin API

The Design Tokens API (tokens.cljs) had zero permission checks, allowing
any plugin to create, modify, and delete tokens, sets, and themes
regardless of granted permissions.

Add r/check-permission checks to all 22 write operations across:
- token-proxy: name, value, description, duplicate, remove, applyToken
- token-set-proxy: name, active, toggleActive, addToken, duplicate, remove
- token-theme-proxy: group, name, active, toggleActive, addSet, removeSet,
  duplicate, remove
- tokens-catalog: addTheme, addSet

Follows the established pattern from comments.cljs, file.cljs, page.cljs.

Closes #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add permission checks to shape proxy interactions, detach, export, and variants

The shape proxy (shape.cljs) had multiple operations missing permission
checks, plus a cond ordering bug that bypassed the existing content:write
check for text shapes in commit-fills!.

Fix commit-fills! cond ordering: move permission check before the
text-shape branch so text shapes are also protected.

Add content:write permission checks to:
- interaction-proxy: :trigger, :delay, :action setters, :remove method
- shape-proxy: :addInteraction, :removeInteraction, :detach
- shape-proxy: :applyToken, :switchVariant, :combineAsVariants

Add content:read permission check to:
- shape-proxy: :export (read/extraction operation)

Follows the established pattern from :resize, :rotate, :blocked setters.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add library:write permission checks to variant plugin API

The library.cljs variant operations (variant-proxy and
lib-component-proxy) had seven mutating operations that
did not check the library:write permission, allowing
any plugin to create, modify, and delete component
variants regardless of granted permissions.

Add r/check-permission checks to all 7 operations:
- variant-proxy: addVariant, addProperty,
  removeProperty, renameProperty
- lib-component-proxy: transformInVariant, addVariant,
  setVariantProperty

Follows the established pattern from the :name and
:path setters in the same file.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add content:write permission checks to flow and flex layout plugin API

Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.

Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
  startingBoard setters, remove) and page-proxy (createFlow,
  removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
  (remove, appendChild)

Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add user:read permission checks to plugin API

Add permission checks to user identity accessors that were bypassing
the consent model, allowing plugins to access user data regardless
of whether the user granted user:read permission.

Changes:
- api.cljs: Add user:read checks to getCurrentUser and getActiveUsers
- comments.cljs: Add user:read checks to comment-proxy and
  comment-thread-proxy owner/user getters
- file.cljs: Add user:read check to file-version-proxy createdBy getter

When user:read permission is not granted:
- getCurrentUser() returns null
- getActiveUsers() returns empty array
- owner/user/createdBy getters return null

Follows the established pattern from other permission checks in the
plugin API.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Fix problem with token API

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-08 09:24:45 +02:00
Andrey Antukhandalonso.torres 5f1e151e84 🐛 Validate plugin UI URLs cannot target Penpot's own domain (#11273)
* 🐛 Validate plugin UI URLs cannot target Penpot's own domain

The plugin UI iframe combines allow-scripts and allow-same-origin in
its sandbox. While necessary for plugins to use their own cookies and
storage, this creates a risk if a plugin's UI URL resolves to Penpot's
own origin, potentially allowing the iframe to escape sandbox isolation.

Add validateUIUrl() that checks the resolved URL against Penpot's
origin (from penpotPublicURI or location.origin) and throws if they
match. Called in openModal() after prepareUrl() resolves the URL.

Closes #11271

AI-assisted-by: qwen3.7-plus

* 💄 Fix prettier formatting in plugin-manager.spec.ts

Apply prettier formatting to fix format:check failure.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix problem with penpot origin plugins

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-08 09:23:20 +02:00
Pablo Alba ff63668c1e 🐛 Enforce SSRF checks and add timeouts to HTTP client (#11474) 2026-09-07 15:59:35 +02:00
Elena Torró 7b135b80b2 🐛 Fix background blur clipping (#11532) 2026-09-07 15:51:27 +02:00
Andrey Antukhandniwinz acc078064b 🐛 Do not register developer tools in multi-user MCP mode (#11310)
Prevent developer tools from being exposed when the MCP server runs in multi-user mode. Keep them available for local devenv usage and document the mode restriction. Add regression coverage for the registration policy.

Closes #11291

AI-assisted-by: gpt-5.6-luna

Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
2026-09-07 13:19:32 +02:00
María Valderrama 77bf3ea419 🐛 Fix sso expiration time (#11528) 2026-09-07 11:56:25 +02:00
Eva Marco 5f6169e1d3 🐛 Fix typography sample errors (#11515)
* 🐛 Fix font-family sample not showing for numeric font names

Setting style.fontFamily to a raw, unquoted family name (e.g. "Micro
5") parses it against CSS's <family-name> grammar: a whitespace-
separated sequence of <custom-ident>s. "Micro" tokenizes fine, but a
bare "5" isn't a valid CSS identifier (idents can't start with a
digit) — it tokenizes as a number instead, so the whole property is
invalid CSS and the browser silently drops it. Every other font in
the list happened to avoid this because none of their names have a
token that's purely numeric.

Quote the family name, matching what font-item-preview* (the font
selector's own preview, a few lines down in the same file) already
does, so it's parsed as a CSS string instead of unquoted identifiers.

Also falls back to the live fontsdb entry's family when the
typography record's own :font-family is blank — a font that was
unloaded when a typography's font/variant was last changed can leave
that field nil (the same failure mode remove-nil-style-attrs already
repairs for shape text spans) — and loads the font unconditionally in
the collapsed asset row, matching the expanded editor, since the
optical-offset cache can otherwise skip loading it entirely.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix flaky typography sample position in automated tests

The optical-centering offset for the "Ag" sample (and the font
selector's fallback name label) resolves asynchronously: first paint
is unshifted, then an idle-scheduled Canvas measurement lands and the
sample jumps to its final position. Any test that checks position or
takes a screenshot shortly after paint races that jump — whether it
runs before or after is a timing accident, not a deterministic
outcome, which is exactly the "sometimes a few pixels up, sometimes
down" flakiness QA hit.

use-optical-offset now returns [offset ready?], with both lazily
initialized from the cache so a cache hit needs no async round-trip
at all. sample-text-style hides the glyphs until ready?, so the
sample only ever appears already in its final, correct position
instead of visibly moving there after the fact.

The font selector's own name label uses the same hook but always
shows real text content rather than a decorative sample, so it keeps
the old behavior instead: hiding it would blank out font names while
scrolling, worse than the minor positional nicety it's fixing.

AI-assisted-by: claude-sonnet-5
2026-09-07 11:29:36 +02:00
Andrey Antukh 9462543fb5 Add skip-onboarding option to create-demo-profile (#11526)
Add optional skip-onboarding param to create-demo-profile. When true, the demo profile is created with onboarding-viewed and release-notes-viewed set, so it skips the onboarding flow. Default keeps the current behavior. Cover both cases with RPC tests. AI-assisted-by: muse-spark-1.3-contributor
2026-09-07 11:15:17 +02:00
Andrey Antukh 5c10ea5bd6 📚 Document branch naming convention
The creating-prs memory described a branch format (issue/... with a
<type>/<short-description> fallback) that does not match actual repo
practice, where issue-driven branches are issue-NNNN. Replace it with
a Branch Naming section: issue-NNNN as primary, descriptive name
without slashes as fallback.

Add the matching public convention to CONTRIBUTING.md under Pull
Requests, which previously defined no branch naming at all.

AI-assisted-by: omen-alpha
2026-09-07 08:41:54 +00:00
Andrey Antukh 7ebd7cc0d5 Merge remote-tracking branch 'origin/staging' into staging 2026-09-07 09:52:07 +02:00
Andrey Antukh 2100ed29ea Merge remote-tracking branch 'origin/main' into staging 2026-09-07 09:45:06 +02:00
Andrey Antukh c8691d0cde 🐛 Fix crash when a nil shape id reaches WASM modifiers (#10896)
* 🐛 Fix crash when a nil shape id reaches WASM modifiers

A modif-tree containing a nil shape id crashed the WASM heap write
with "Cannot read properties of null (reading '__u32_buffer')" in
propagate-modifiers. Production builds elide the upstream asserts,
so a nil id (e.g. update-dimensions called with a missing shape's
parent id in the variants flow) reached the uuid heap write.

Drop nil-keyed modif-tree entries in apply-wasm-modifiers and
set-wasm-modifiers before they reach propagate-modifiers and
set-structure-modifiers, filter nil ids at the parse level, and
guard the variant reposition/creation events against missing shapes.

AI-assisted-by: hy3

* 🐛 Fix let scoping bugs in modifiers.cljs

Two let forms in set-wasm-modifiers and apply-wasm-modifiers
had their bindings vectors closed prematurely, causing undeclared
var warnings for snap-pixel?, translation?, ids, update-shape,
options, bool-ids, undo-id, and other bindings.

In set-wasm-modifiers: removed premature ) after ] on the let
bindings so the let body wraps snap-pixel? and translation?.

In apply-wasm-modifiers: the cond form was not explicitly closed,
so all subsequent bindings (ignore-tree, options, modif-tree, ids,
update-shape, bool-ids, undo-id) were parsed as cond clauses
instead of let bindings. Added ) to close cond after :else, and
moved ] to close the bindings vector after undo-id.

AI-assisted-by: mimo-v2.5-pro
2026-09-07 08:34:06 +02:00
Marina López 5452eb5c8a 🐛 Fix email sales (#11514) 2026-09-04 13:36:10 +02:00
Eva Marco df48c834e3 🐛 Fix create nil typography token (#11489)
* 🐛 Fix stroke width token dropdown closing mid-interaction

The stroke-row* key included a hash of applied-tokens, forcing a full
remount whenever the async token-resolution pipeline updated that
state (e.g. after a detach action settles). If the remount landed
while the width dropdown was open, it destroyed the dropdown and
reset its open state, permanently closing it before the user could
pick a token.

Drop the hash from the key so the row only remounts on actual
structural changes (add/remove/reorder), not on every token
resolution tick.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix crash when a typography token has a nil value

A composite typography token saved with no fields filled in stores a
nil :value. That value reached StyleDictionary's tokens-studio
font-styles preprocessor, which assumes a typography value is never
null and threw, crashing token resolution for every other token in
the file.

Reject the nil value at the source in the typography token form
validation, and as defense in depth, filter nil-valued tokens out of
every resolve-tokens* entry point before they reach StyleDictionary,
tagging them with the existing empty-input error instead.

Also remove a stray debug prn left in find-refs.

AI-assisted-by: claude-sonnet-5

* 🔧 Gitignore local CLAUDE.md

Keep the commit-conventions instructions file local to each
contributor's checkout instead of tracking it in the repo.

AI-assisted-by: claude-sonnet-5

*  Use a single transducer to tag invalid-value tokens

merge-invalid-value-tokens ran three separate passes over the token
map (remove, map, into) and then merged the result back in. Combine
the remove/map steps into one ns-level transducer, defined once
instead of rebuilt on every call, and pass resolved as the seed to
into so the trailing merge isn't needed either.

AI-assisted-by: claude-sonnet-5

* ♻️ Drop redundant t/testing wrapper in nil-value token test

The outer t/testing just repeated the deftest's own name and added
nothing the two inner t/testing blocks (each covering one concrete
assertion group) don't already say.

AI-assisted-by: claude-sonnet-5

* 🐛 Fail the nil-value token test on a resolution error

rx/sub! only handles the success case, so if token resolution ever
errors instead, done is never called and the async test hangs
instead of failing. Switch to rx/subs! with an error handler that
reports the failure and calls done, matching the pattern already
used elsewhere in the tokens test suite.

AI-assisted-by: claude-sonnet-5
2026-09-04 12:31:26 +02:00
Elena Torró fdcc4666e3 🐛 Fix multiple shapes export on files rendered with webgl (#11506) 2026-09-04 11:00:35 +02:00
Eva Marco 064f200774 🐛 Fix crash pressing Ctrl+D with no shape selected (#11491)
AI-assisted-by: claude-sonnet-5
2026-09-04 10:21:31 +02:00
Eva Marco 214dbd0c8b 🐛 Stop opening Comments section from persisting display-comments (#11492)
AI-assisted-by: claude-sonnet-5
2026-09-04 10:15:38 +02:00
Marina López f2f352d06c Add props to invitations and organizations events (#11508) 2026-09-04 10:13:25 +02:00
Elena Torró 80dea409c6 🐛 Fix share-link viewers unable to load file fragments (#11484) 2026-09-03 12:15:35 +02:00
María Valderrama 7c762d8a98 🐛 Fix recommended plan (#11488) 2026-09-03 11:59:20 +02:00
Pablo Alba 15dff4a9e1 💄 Fix sales email (#11478) 2026-09-03 08:46:52 +02:00
Eva Marco c6a32a2f5a 🐛 Fix list on registration toast notification (#11479) 2026-09-02 17:19:37 +02:00
David Barragán Merino f633d82f51 ♻️ Switch penpot images to sha-<commit> tagging
Replaces the content-hash build key (bundle_version + docker/images tree hash) used to tag and dedupe the backend/frontend/exporter/storybook/mcp image set with sha-<commit>, matching the scheme already used by admin-console, licenses-manager and payments across the org. The check→build→promote pattern with the S3 marker is unchanged; only the key used for the marker, the immutable tag and the local bundle cache filename moves from the composite build key to the git commit sha (the bundle cache now keys on bundle_version alone, which is what it actually caches). devenv is intentionally left out of this pass, it has no versioned tagging today.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-02 17:14:31 +02:00
andrés gonzález 88d715938f 📚 Add migration guide page (#11454)
Point First Steps at the enterprise migration PDF with a short
summary, without duplicating the Community post.
2026-09-02 14:06:06 +02:00
Eva Marco 7620386215 🐛 Fix font family preview sample (#11473)
* 🐛 Fix font family preview sample

* 🐛 Fix font line height inside font selector
2026-09-02 13:55:38 +02:00
andrés gonzález b6b1a47a7b 📚 Update MCP Quick demo video (#11453)
Replace the outdated Quick demo embed on the MCP docs with the
new recording.
2026-09-02 13:54:01 +02:00
Alejandro Alonso b46ed37141 🐛 Fix size-limiting-stream read arity on v3 binfile import (#11468)
The FilterInputStream proxy only implemented read() and
read(byte[], int, int). Buffered reads call read(byte[]) (and
read(byte[], int) via Clojure interop), causing ArityException
while hashing storage objects and breaking v3 imports.

Implement all read overloads and extract shared byte-count logic.
2026-09-02 10:28:16 +02:00
Luis de DiosandEva Marco 99378dc02d 🐛 Fix font preview in assets breaks the font row (#11428)
* 🐛 Fix font preview in assets breaks the font row

* 🐛 Fix font height problem also in the font dropdown

* 🐛 Fix a small bug within the changes

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-09-01 15:48:09 +02:00
Andrey Antukh 41ec134ea2 📚 Update changelog 2026-09-01 12:02:57 +02:00
Andrey Antukh 9051b87f0e 📎 Update the update-changelog skill 2026-09-01 12:02:19 +02:00
Andrey Antukh a1079cf788 ⬆️ Update JVM, pnpm and node dependencies (#11404)
* ⬆️ Update pnpm and its deps

* ⬆️ Update JVM dependencies in backend and common

Update several JVM dependencies across backend and common:

- passay 1.6.6 -> 2.0.0 (package reorg, ctor-based rules)
- siphash 2.0.0 -> 3.0.0 (SipHasher* renamed to SipHash*)
- lettuce-core, guava, sqlite-jdbc, jsoup, lz4-java, markdown-clj,
  awssdk s3/sts, selmer, jackson-core/databind, shadow-cljs

Adapt passay validation to the new API (moved packages, constructor
configuration) and siphash to the renamed classes. Add tests for
password validation and UUID advisory-lock hashing.

AI-assisted-by: deepseek-v4-flash

* ⬆️ Update node on docker images

* 📎 Minor fixes related to pnpm12 compatibility
2026-09-01 12:01:35 +02:00
Eva Marco 5c4b35e350 🐛 Fix top spacing on left sidebar (#11446) 2026-09-01 10:56:53 +02:00
Andrey Antukh 23869f3b1b 🐛 Gate MCP REPL server behind isDevEnv check (#11282)
* 🐛 Gate MCP REPL server behind isDevEnv check

The ReplServer was starting unconditionally on every MCP server
instance, regardless of configuration. This exposed an
unauthenticated POST /execute endpoint that forwarded arbitrary
JavaScript to connected Penpot plugins.

Gate ReplServer creation, startup, and shutdown behind isDevEnv(),
consistent with how CljsReplTool and other dev tools are already
protected. Log an info message when the REPL server is disabled.

Consolidate the dev-env check into a single static isDevEnvEnabled()
method that isDevEnv() delegates to, avoiding duplicate logic. Add
PluginBridge.close() for proper WebSocket server cleanup on shutdown.

Add regression tests that construct PenpotMcpServer and verify
hasReplServer() returns the correct value based on the dev-env flag.

AI-assisted-by: mimo-v2.5-pro

*  Add PENPOT_MCP_REPL_ENABLE env var for explicit REPL control

Allow the REPL server to be enabled independently of the devenv
setting via a new PENPOT_MCP_REPL_ENABLE environment variable.
When set to "true", the REPL server starts regardless of
PENPOT_MCP_DEVENV; when set to any other value, it is disabled.
When unset, the previous isDevEnv fallback applies.

Addresses review feedback on PR #11282.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:50:05 +02:00
Andrey Antukh 810f8ef671 🐛 Cascade profile deletion in objects-gc task (#11395)
The objects-gc task was performing a hard delete on profiles
without cascading the soft-delete to owned teams, projects, and files.
This left orphaned objects that were never cleaned up.

Now the task invokes delete-object before the hard delete, ensuring
all owned resources are properly marked for deletion and cleaned up
in subsequent GC iterations.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:49:18 +02:00
Andrey Antukh 6d9f411fab 🐛 Enforce share-link comment permissions and page scope (#11371)
Fix two security vulnerabilities in comment RPCs when accessed
via share-links:

- GHSA-4p97-v4wg-jxfx: Share-link holders with who-comment=team
  could bypass the restriction and comment. The check-comment-permissions!
  function treated can-read as sufficient, but share-links always set
  can-read=true.

- GHSA-fwm4-hm9f-rmcp: Comment query RPCs returned threads from all
  pages, ignoring the share-link's :pages restriction.

Changes:
- files.clj: Differentiate :membership vs :share-link in
  check-comment-permissions!. For share-links, require
  has-comment-permissions? only (who-comment=all).
- comments.clj: Filter threads by (:pages perms) for share-link
  access in get-comment-threads, get-comment-thread, and get-comments.

Closes #11370

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:55 +02:00
Andrey Antukh 45f0153e8f 🐛 Prevent admin from removing team owner in delete-team-member (#11368)
Add owner protection to ::delete-team-member RPC command.
Previously, a team admin could remove the team owner, permanently
locking them out of their team and all resources.

Changes:
- Fetch target member data before deletion
- Validate member exists (return :not-found if not)
- Reject removal if target is owner and caller is not owner

This mirrors the existing protection in update-team-member-role.

Closes #11367

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:09 +02:00
Andrey Antukh 326d83e780 🐛 Block IPv6 transition addresses in SSRF guard (#11320)
* 🐛 Block IPv6 transition addresses in SSRF guard

The outbound HTTP SSRF blocklist did not classify NAT64
(64:ff9b::/96), 6to4 (2002::/16) or Teredo (2001:0000::/32)
addresses, whose embedded IPv4 target is invisible to the JVM
InetAddress predicates, so URLs resolving to them could reach cloud
metadata, loopback or RFC 1918 hosts from webhook delivery and media
import.

Transition ranges are now rejected outright and any embedded IPv4 is
re-checked against the full blocklist, including operator-supplied
extra blocked CIDRs.

Closes #11319

* ♻️ Remove dead embedded-IPv4 re-check from SSRF guard

The previous commit added a recursive re-check of the IPv4 embedded in
NAT64/6to4/Teredo addresses, but the `or` in `blocked-address?`
short-circuits on the truthy keyword returned by `transition-prefix`,
so the embedded-IPv4 branch was unreachable. The transition ranges are
already rejected outright (fail-closed), making the re-check both
unnecessary and untested.

Remove `transition-embedded-ipv4`, simplify the IPv6 branch to a plain
prefix check, and correct the docstrings and tests to match what the
code actually does.

AI-assisted-by: glm-5.3-flash
2026-09-01 08:47:46 +02:00
Andrey Antukh 15195b3bbb 🐛 Filter share-link tokens in get-view-only-bundle response (#11286)
* 🐛 Filter share-link tokens in get-view-only-bundle response

The get-view-only-bundle RPC command returned all share-link tokens for a file, allowing an anonymous holder of a restrictive share-link to enumerate and use more permissive tokens.

When authenticating via a share-link, the response now only includes the share-link used for authentication, preventing token disclosure and scope escalation.

Implemented using TDD:
- RED: Test demonstrates vulnerability (all tokens visible)
- GREEN: Filter share-links when (:type perms) = :share-link
- Verified all existing tests still pass

Closes #11285

AI-assisted-by: qwen3.7-plus

* 🐛 Add membership-side test for share-link token visibility

Add test coverage for the allow side of the share-link token filtering:
team members and file owners should still see all share-links, while
anonymous share-link holders only see their own token.

This protects the (:type perms) = :share-link guard from accidental
regression that could break the owner's share-link management dialog.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:46:59 +02:00
Andrey Antukh 3df039abc3 Add several improvements for demo profile creation mechanism (#11257)
*  Optimize demo user setup for performance tests

Use UUID-based demo emails to prevent concurrent profile collisions.\nUse fast PBKDF2 hashing for demo profiles while keeping regular user hashing unchanged.\nAdd focused coverage for hashing, email uniqueness, and the feature flag.\n\nAI-assisted-by: gpt-5.6-luna

* 🐛 Harden font upload test setup

Report upload-session errors before chunk validation.
Skip chunk uploads when the session ID is invalid.
Remove unnecessary Mockery state from the foreign-font test.

AI-assisted-by: gpt-5.6-luna

*  Add demo profile purge task

Schedule delayed deletion for demo profiles through the worker system.
Restore normal profile filtering and cover the purge handler with tests.

AI-assisted-by: gpt-5.6-luna
2026-09-01 08:46:28 +02:00
Andrey Antukh 92c2079ae2 🐛 Add configurable limits for ZIP entry count and object size in v3 import (#11022)
* 🐛 Add configurable limits for ZIP entry count and object size in v3 import

Add binfile-import-max-zip-entries (default 500,000) and
binfile-import-max-object-size (default 100 MiB) config entries.
Both are configurable via PENPOT_BINFILE_IMPORT_MAX_ZIP_ENTRIES and
PENPOT_BINFILE_IMPORT_MAX_OBJECT_SIZE env vars.

Entry count is checked before processing begins. Per-object size is
checked after each storage object content is resolved.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Enforce actual decompressed byte limits on v3 import

The previous object-size check trusted the ZIP entry header's declared
size (ZipEntry.getSize()), which a malicious zip-bomb can forge. The
check would pass, then the full decompressed payload would be read
anyway during hashing and storage persistence.

Add size-limiting-stream, a FilterInputStream wrapper that counts
actual bytes read and raises :validation :max-file-size-reached when
the configured limit is exceeded. Wire it into zip-entry-storage-content
so both the hash calculation and storage write paths are bounded by
real decompressed bytes, not declared header size.

Also wire import limits into management.clj (clone-template) and
debug.clj (import-handler + clone path) for defense-in-depth, and
add a test that exercises the object-size limit with a real storage
object in the exported ZIP.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:44:06 +02:00
Andrey Antukh 73d3d63616 Enable a way to provide custom opencode config on starting devenv 2026-08-31 16:31:24 +02:00
Luis de Dios fc207a1768 🐛 Fix shapes flyout in toolbar only opens with the space key, and doesn't close when pressed again (#11331) 2026-08-31 14:00:14 +02:00
Pablo Alba 93ac6d8338 Add a new nitrate endpoint to check the airgapped flag (#11434) 2026-08-31 13:31:17 +02:00
Luis de Dios 806f947360 🐛 Fix avoid empty space at the bottom when there are many layers (#11427) 2026-08-31 12:38:20 +02:00
Luis de DiosandEva Marco bfb5cae9f4 🐛 Fix viewer login modal lacks spacing between SSO buttons and work email field (#11312)
* 🐛 Fix spacing between SSO buttons and email field

* ♻️ Update scrollbar on register page

* ♻️ Translate hardcoded string

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-08-31 12:17:57 +02:00
Luis de Dios 2ed0981e8a 🐛 Fix disabled numeric input with token applied is broken (#11324) 2026-08-31 11:51:42 +02:00
Luis de Dios 169e6fcbca 🐛 Fix comments not close main menu (#11347) 2026-08-31 11:50:59 +02:00
Andrey Antukh 995a5460e5 📎 Update agents and opencode on devenv 2026-08-28 21:20:17 +02:00
Andrey Antukh 4c9aeab65e 📚 Add issue/PR CLI auto-trigger to agent guide
When an issue or PR is mentioned in conversation, fetch details via gh
CLI or scripts/gh.py instead of WebFetch. Use gh issue view for issues,
gh pr view for single PRs, and scripts/gh.py prs for multiple PRs.

AI-assisted-by: glm-5.3-flash
2026-08-28 14:55:30 +00:00
María Valderrama c01306734a 🐛 Fix default team and UI labels to use Personal Projects (#11418)
* 🐛 Fix default team and UI labels to use Personal Projects

* 📎 Code review
2026-08-28 14:04:56 +02:00
Pablo Alba 10e40e62a2 Add airgapped check to nitrate (#11421) 2026-08-28 13:58:18 +02:00
Marina López f06d1df5be 🐛 Align management API with payments service (#11414) 2026-08-28 11:54:02 +02:00
María Valderrama eb0c51ed74 📚 Rename personal space label from Your Penpot to Personal Projects (#11399) 2026-08-28 10:41:25 +02:00
Pablo Alba 18ebf5a92d 📚 Add highlights file (#11406) 2026-08-28 10:40:25 +02:00
María Valderrama cdfdbf9adc 🐛 Fix labels for teams navigation and personal files section (#11398) 2026-08-28 09:49:13 +02:00
Andrey AntukhandSumit Ridhal 1baa8bb29f 🐛 Center canvas and select layer when navigating layer search results (#10422) (#11391)
When using previous/next buttons in the layer search panel, the canvas
now centers on the matched layer and selects it in the layers panel.
Previously only the internal search cursor was advanced without any
visible canvas or selection update.

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-27 18:33:48 +02:00
Luis de Dios 11224940ab 🐛 Fix wrong selection color on inputs (#11400) 2026-08-27 18:32:22 +02:00
Andrey AntukhandLuis de Dios 43bbf756b0 🐛 Fix font change regression and activate font preview by default (#11341)
*  Materialize font-preview flag to make it permanent

* 🐛 Fix font change regression when WebGL is disabled

* 📎 Add fixes for e2e tests

* 🐛 Fix font change from legacy render

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-27 18:30:33 +02:00
María Valderrama 6bedd5ac76 🐛 Fix move organization modal (#11390)
* 🐛 Fix move organization modal

* 📎 Code review
2026-08-27 12:41:32 +02:00
Andrés MoyaandAndrey Antukh c9a2b8f12f 🔧 Add TokensStatus data type for forward compatibility (#11314)
* 🔧 Add TokensStatus data type for forward compatibility

* 📚 Add Transit round-trip tests for TokensStatus type

Add serialization tests validating TokensStatus survives Transit
encode/decode and Fressian round-trip (JVM). Clarify tokens-source
field comment as forward-compatibility placeholder.

AI-assisted-by: mimo-v2.5-pro

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-27 11:51:47 +02:00
Andrés Moya 582187dd03 🎉 Allow to validate and repair files from /dbg (#11335) 2026-08-27 11:50:14 +02:00
David Barragán Merino 6f35348c7c 🐳 Pin docker images to 2.17
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-08-27 10:51:08 +02:00
Andrey Antukh b4dc8207ff Merge remote-tracking branch 'origin/main' into staging 2026-08-27 10:05:27 +02:00
Andrey Antukh 1d2c37e52c 📎 Update changelog 2026-08-27 10:00:13 +02:00
Andrey Antukh f7fd3e1cd5 📚 Update the update-changelog skill 2026-08-27 10:00:00 +02:00
Andrey Antukh 06491a2d24 🐛 Fix svg-raw child ids stored as strings in existing files (#11387)
* 🐛 Fix svg-raw child ids stored as strings in existing files

Before schema:svg-raw-attrs declared :shapes as a vector of uuid, the
JSON decoder had no type information for those child ids and left them
as plain strings on any round trip, so they got persisted as strings.
Once the schema was tightened those files fail schema validation on
load.

Add migration 0026-fix-svg-raw-shapes-uuids that coerces the :shapes
entries of svg-raw shapes back to uuid instances, in both pages and
components.

AI-assisted-by: deepseek-v4-flash

* ♻️ Drop redundant vector check in svg-raw shapes migration

:shapes is always a vector when present, so the guard is unnecessary.

AI-assisted-by: deepseek-v4-flash
2026-08-27 09:49:22 +02:00
Alejandro Alonso f7bdc9786c 🐛 Compare text numeric attrs with float tolerance (#11366)
Editor/WASM round-trips can truncate line-height strings
(e.g. 1.3333333333333333 → 1.33333). Exact string compare
treated that as a style change and detached typography tokens.
2026-08-27 09:41:50 +02:00
Andrey Antukh 88a52d1098 📎 Update changelog 2026-08-27 09:24:53 +02:00
Luis de Dios 2bce69f927 🐛 Fix increasing value clicking and dragging in a numeric input (#11334) 2026-08-26 21:35:07 +02:00
Andrey Antukh 6e173a02fb 📚 Split backend testing memory and link from testing skill
Extract the backend Testing section from backend/core into a dedicated
backend/testing memory, following the pattern of common, frontend, and
exporter. Update the testing skill and root testing memory to point at
the new location, and add exporter/testing to the skill's required
reading list.

AI-assisted-by: deepseek-v4-flash
2026-08-26 18:11:40 +00:00
Andrey Antukh 4be749d45f Add minor improvements to scripts/gh.py 2026-08-26 19:19:22 +02:00
Pablo Alba e1a2d0b932 🐛 Fix nitrate sso failure message (#11214) 2026-08-26 17:24:59 +02:00
Andrey Antukh 33e39bc7ed 🔧 Fix backend format check script in CI
The backend format check was using 'check-fmt' instead of 'check-fmt:clj',
causing CI to always fail on the fmt step.

Closes #11358

AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 33f13f9bfd 🐛 Fix test formatting for get-team-invitation-token
AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 4adfa5d2f2 🐛 Require edition permissions for get-team-invitation-token
The handler previously allowed any team member (including viewers) to
generate invitation tokens. Now requires at least edition-level
permissions (can-edit, admin, or owner).

Closes #11358

AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 7079d33ae1 🐛 Enforce ownership check on tempfile bucket access (#11270)
* 🐛 Enforce ownership check on tempfile bucket access

The upload-tempfile RPC stores profile-id with tempfile objects, but
objects-handler never verified the requester was the owner. Any
authenticated user who knew the UUID could access the tempfile.

Add ownership check: tempfile bucket now requires the request's
profile-id to match the stored profile-id. Returns 404 on mismatch
(not 403) to avoid leaking object existence.

Legacy tempfiles without stored profile-id remain accessible to any
authenticated user for backward compatibility.

Closes #11269

AI-assisted-by: qwen3.7-plus

* ♻️ Extract tempfile-bucket constant and fix docstring indentation

Extract the 'tempfile' bucket string literal into a named constant
(sto/tempfile-bucket) to prevent typos and make future bucket renames
trivial. Updated 9 occurrences across 7 files.

Also fixed minor docstring indentation inconsistency in authenticated?
function.

AI-assisted-by: qwen3.7-plus

* ♻️ Refactor process-bucket! and authenticated? helpers

Replace case with cond in process-bucket! to properly resolve
sto/tempfile-bucket var from another namespace (case does not
evaluate qualified vars at compile time).

Redefine authenticated? in terms of request-profile-id to remove
duplicated lookup logic.

Closes #11269

AI-assisted-by: mimo-v2.5-pro
2026-08-26 13:05:36 +02:00
Andrey Antukh b33213787e 🐛 Add accumulated storage byte quota for media uploads (#11038)
* 🐛 Add accumulated storage byte quota for media uploads

Add media-storage-bytes-per-team quote to prevent persistent DoS via
repeated uploads. The quota sums storage_object sizes from both
file_media_object (media + thumbnails) and team_font_variant
(otf/ttf/woff1/woff2). Default limit is 20 GiB per team, configurable
via PENPOT_QUOTES_MEDIA_STORAGE_BYTES_PER_TEAM.

The check is invoked in upload-file-media-object before processing,
looking up the team-id via file -> project -> team_id join.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Fix deduplicated storage overcounting in media-storage-bytes-per-team quote

The SQL query sql:get-media-storage-bytes-per-team used UNION ALL
across six SELECT branches that each produce a so_id reference.
When deduplication causes multiple file_media_object or
team_font_variant rows to point at the same storage_object, UNION ALL
counts that objects size once per reference — inflating "used bytes"
and causing false :max-quote-reached rejections.

Change all five UNION ALL to UNION so that duplicate so_id values are
collapsed before the JOIN storage_object / SUM(so.size).

Add a test (media-storage-bytes-quote-deduped) that creates one
storage_object referenced by two file_media_object rows and asserts
the computed usage reflects the deduplicated physical size, not 2x.

AI-assisted-by: mimo-v2.5-pro
2026-08-26 08:33:20 +02:00
María Valderrama 5e1ced03ea 🐛 Fix missing warning when moving a team (#11357) 2026-08-26 08:08:40 +02:00
Luis de DiosandAndrey Antukh 44dfc04300 🐛 Fix font selector dropdown takes noticeably long to open when changing font (#11073)
* 🐛 Fix font selector dropdown takes noticeably long to open when changing font

* ♻️ Refactor detach-preview-sprite! to use atomic swap

Use idiomatic atomic swap! update instead of non-atomic
read-then-write pattern. The new implementation computes
the decremented refs inside swap! and only removes the
node when the result reaches zero.

AI-assisted-by: mimo-v2.5-pro

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-25 16:55:34 +02:00
Andrey Antukh 85a68ea3b2 🐛 Normalize toast HTML prop to boolean
Ensure toast components always receive a boolean `is-html` prop so nil or
truthy notification values do not violate the Rumext schema.

AI-assisted-by: gpt-5.6-luna
2026-08-25 13:47:37 +00:00
Andrey Antukh b79680eeb7 🐛 Fix asset 404 for unauthenticated share-link viewers (#11342)
PR #11036 added a per-request permission check to the file-media asset
endpoints (/assets/by-file-media-id/:id and the /thumbnail variant) using
bfc/get-file-permissions. Anonymous share-link viewers were then rejected
because they have neither a session nor an access token, the asset URL
carries no share context, and the 2-arg get-file-permissions short-circuits
to nil when profile-id is nil.

Make the asset endpoints share-link aware, mirroring how get-view-only-bundle
already authorizes the same scenario: read the share-id from the query
string, validate it as a UUID, and call the 3-arg perms/get-file-read-permissions
(which chains the existing 2-arg bfc lookup, the bfc share-link fallback,
and the Nitrate org-owner fallback). On the frontend, extend
cf/resolve-file-media with an optional share-id arg and pass it from the
WASM viewer render path using the share-id already present in
[:viewer-local :share-id]. Non-viewer call sites (workspace, clipboard,
code-gen) keep the original URL shape because the new arg defaults to nil.

Closes #11338

AI-assisted-by: minimax-m3
2026-08-25 13:10:55 +02:00
Luis de Dios c31236947c 🐛 Fix skipped-tokens warning details show raw HTML tags instead of a bulleted list (#11325) 2026-08-25 09:37:54 +02:00
Andrey Antukh 52573be074 🐛 Enforce share-link page scope in get-page and get-file-fragment RPC commands (#11284)
The get-page RPC command did not validate that the requested page-id was within the share-link's authorized :pages set, allowing share-link holders to read out-of-scope pages. The get-file-fragment command had the same issue.

This fix adds page scope validation to get-page, rejecting requests for pages not in the share-link's :pages set with a :not-found error. For get-file-fragment, share-link access is denied entirely as fragments lack direct page-id mapping.

The fix aligns these commands with the existing correct behavior in get-view-only-bundle, which already filters pages based on share-link scope.

Closes #11281

AI-assisted-by: qwen3.7-plus
2026-08-25 09:37:42 +02:00
María Valderrama 316b58bc75 🐛 Fix team dropdown not refreshing after organization deletion (#11336) 2026-08-24 20:02:59 +02:00
María Valderrama e93c419785 🐛 Fix wrong copy on disabled invite button tooltip (#11311) 2026-08-24 15:25:31 +02:00
Pablo Alba 7732c93541 💄 Update nitrate activation code link (#11330) 2026-08-24 14:04:22 +02:00
Andrey Antukh 47d599fe34 Persist binfile manifest and emit workspace audit events (#11106) (#11138)
Persist binfile manifest metadata in file_data on import so file
statistics are available at open-workspace time. Emit a new
open-workspace-file audit event enriched with file statistics:
page count, shape count, component count, linked libraries,
design tokens, and whether the file is a shared library.

Closes #11106

AI-assisted-by: mimo-v2.5-pro
2026-08-21 14:37:21 +02:00
Andrey Antukh 0cacf9bd99 ♻️ Rename code-review-and-quality skill to code-review
Rename skill directory and update all references to follow the
same naming pattern as plan-review.

Simplify review.md command from 138 to 25 lines — remove redundant
content that duplicated what the skills already define. The command
now acts as a thin router; the skills own the methodology.

AI-assisted-by: mimo-v2.5-pro
2026-08-21 12:11:11 +00:00
Andrey Antukh 7c85837290 🐛 Fix session invalidation on logout to prevent token replay (#11317)
Logout only cleared the auth-token cookie but never deleted the
server-side row because delete-fn read ::id which wrap-authz no longer
sets since 363b4e3778. Make delete-fn delete via ::session/:id attached
by wrap-authz so replayed tokens are rejected (CWE-613, GHSA-mj9f-5cwq-7p3q).

Add regression tests covering invalidation, idempotency and isolation
of other sessions. Fix verified with Red→Green TDD and full backend
suite (677 tests).

Closes #11316

AI-assisted-by: muse-spark-1.2-contributor
2026-08-21 12:55:24 +02:00
Andrey Antukh dd4a163217 🐛 Remove internal error details from HTTP error responses (#11288)
* 🚑 Remove internal error details from HTTP error responses

PostgreSQL exceptions, I/O exceptions, and unhandled errors were
leaking raw database messages (table names, constraint names,
SQLSTATE codes), filesystem paths, and internal exception details
to API clients via :hint, :state, and :path response fields.

Remove these fields from server-error responses while keeping
full error context in server-side logs for operators.

Closes #11287

AI-assisted-by: mimo-v2.5-pro

* 🚑 Strip internal fields and map PG errors to safe messages

Complete the security fix for GHSA-r8wx-23q6-w3gf by addressing
the incomplete redaction found in code review.

Add strip-internal-fields helper to dissoc :hint, :state, :path,
and :context from error response data in three handlers that
previously passed raw ex-data through to clients:

- handle-error :internal
- handle-exception :default (else branch)
- handle-error :assertion (else branch)

Add pgsql-state->message to map PostgreSQL SQLSTATE codes to safe,
client-facing messages (e.g. 23505 → "A conflicting entry already
exists") instead of returning raw PG error text. Include :message
in all PSQLException response branches.

Add regression tests asserting :hint, :state, :path, :context are
absent from responses for :internal and unhandled ex-info errors.

Closes #11287

AI-assisted-by: mimo-v2.5-pro

* 🚑 Keep :hint in error protocol, fix unsafe sources

Refine the security fix based on code review feedback.

Keep :hint as part of the error protocol — it is essential for
controlled error communication. Remove it from strip-internal-fields
(which now only strips :state, :path, :context).

Fix the actual sources of unsafe :hint values:

- http/middleware.clj: replace (ex-message cause) with safe static
  strings for IllegalArgumentException, RequestTooBigException, and
  EOFException. These :validation errors return ex-data verbatim
  to clients, so raw exception messages were leaking internals.

- PSQLException handler: use :hint instead of :message for the
  SQLSTATE-mapped messages, staying consistent with the error
  protocol.

Update tests to assert :hint is present (with safe static values)
in :internal and unhandled ex-info responses, and absent only from
bare RuntimeException and IOException responses.

Closes #11287

AI-assisted-by: mimo-v2.5-pro
2026-08-21 12:43:23 +02:00
Andrey Antukh 9fa07e7468 ⬆️ Update opencode on devenv dockerfile 2026-08-21 12:18:33 +02:00
Andrey Antukh 77971740e6 Merge remote-tracking branch 'origin/main' into staging 2026-08-21 12:13:22 +02:00
Alejandro AlonsoandAndrés Moya 2318866f8d 🎉 Add repair functions for variant validation errors (#10768) (#11309)
* 🎉 Add repair functions for variant validation errors

* 📚 Fix copyright notice

Co-authored-by: Andrés Moya <andres.moya@kaleidos.net>
2026-08-21 11:17:25 +02:00
María Valderrama 9f6878d118 🐛 Fix disabled invitation explanatory title (#11295) 2026-08-20 13:19:35 +02:00
Andrey Antukh 209aea8365 🐛 Add proper ownership check on managing/deleting shared link on a file (#11290)
* 🐛 Add ownership check to share-link deletion

The delete-share-link RPC command only verified file-level edit
permission but did not check if the caller owned the share-link.
This allowed any file editor to delete share-links created by
other users, disrupting collaborative workflows.

The fix adds an ownership check that allows deletion only by:
- The share-link creator (owner-id matches profile-id)
- File admins (is-admin permission)
- File owners (is-owner permission)

Implemented using TDD:
- RED: Test demonstrates IDOR vulnerability (editor can delete)
- GREEN: Ownership check prevents unauthorized deletion
- All existing tests continue to pass

Closes #11289

AI-assisted-by: qwen3.7-plus

* 🐛 Add test coverage for share-link deletion escape hatches

Address code review feedback for PR #11290:

- Add test for editor deleting their own share-link
- Add test for admin deleting editor's share-link
- Add test for owner deleting editor's share-link
- Remove redundant :is-owner check (already included in :is-admin)
- Add clarifying comment about :is-admin including :is-owner

Closes #11289

AI-assisted-by: qwen3.7-plus
2026-08-19 18:26:35 +02:00
Andrey Antukh c200a4d777 🐛 Fix HTML escaping in notification pill detail section (#11275)
The notification pill component now properly respects the `is-html`
flag when rendering the detail section, matching the behavior of the
children section. Token import error messages now escape HTML
characters in user-provided values like token names and type names
before displaying them in notifications.

AI-assisted-by: qwen3.7-plus
2026-08-19 18:21:55 +02:00
Elena Torró a91c796b0e 🐛 Fix missing zip export on tempfile types (#11292) 2026-08-19 16:19:15 +02:00
Marina López c378ec9218 🐛 Avoid swallowing fatal errors in organization sso telemetry (#11279) 2026-08-19 14:23:40 +02:00
Andrey AntukhandSumit Ridhal 4da6499197 🐛 Fix linear gradients in SVG text exports (#11272)
* 🐛 Use gradient type instead of export type in SVG renderer

data->gradient-def was comparing the render `type` parameter (:svg,
:png, :pdf) against "linear" to decide between linearGradient and
radialGradient elements. Since the export type is never "linear",
the comparison always fell through to radialGradient, causing all
linear gradients to be exported as radial in SVG output.

Read the gradient type from the data map instead:
(get-in data ["gradient" "type"])

Closes #5972

* 🐛 Add SVG gradient export regression test

Extract SVG gradient definition generation from the renderer so it can
be tested directly. Add exporter test build wiring and cover both
linear and radial gradient output.

AI-assisted-by: gpt-5.6-luna

*  Standardize exporter testing workflow

Align exporter scripts with the frontend testing pattern. Add a
dedicated GitHub Actions workflow and document the canonical exporter
commands in Serena memories.

AI-assisted-by: gpt-5.6-luna

*  Add focused exporter test execution

Mirror frontend test-runner behavior for focused namespaces and test
vars. Support --focus, --log-level, and --help, and document the
commands.

AI-assisted-by: gpt-5.6-luna

* 🐛 Replace shell exec with execFile in exporter

Replace child_process.exec with execFile to eliminate shell
interpretation. Add hex color validation in exporter and frontend
to reject malformed input before command construction.

This fixes GHSA-4f36-m4hj-cv86 (CVSS 9.9 Critical), an authenticated
OS command injection vulnerability where malicious fill-color values
could execute arbitrary commands in the exporter container.

Defense in depth:
- Layer 1: execFile passes arguments directly without shell parsing
- Layer 2: Exporter validates colors with strict hex regex
- Layer 3: Frontend filters invalid colors before DOM emission

All three independent reporters' attack vectors are addressed:
- Quote breakout (lyhtheori)
- Command substitution (B1gN0Se)
- Path traversal (KimiSecurityTeam)

AI-assisted-by: qwen3.7-plus

* 🐛 Use existing hex-color-string? and fix test path mismatch

Address code review feedback:

- Replace duplicated hex-color-rx and valid-hex-color? with existing
  hex-color-string? from app.common.types.color
- Fix RCE test to use marker path in payload instead of hardcoded /tmp/pwned

AI-assisted-by: qwen3.7-plus

---------

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-19 13:57:14 +02:00
Andrey AntukhandSumit Ridhal aa3bc1ae98 🐛 Fix linear gradients in SVG text exports (#11272)
* 🐛 Use gradient type instead of export type in SVG renderer

data->gradient-def was comparing the render `type` parameter (:svg,
:png, :pdf) against "linear" to decide between linearGradient and
radialGradient elements. Since the export type is never "linear",
the comparison always fell through to radialGradient, causing all
linear gradients to be exported as radial in SVG output.

Read the gradient type from the data map instead:
(get-in data ["gradient" "type"])

Closes #5972

* 🐛 Add SVG gradient export regression test

Extract SVG gradient definition generation from the renderer so it can
be tested directly. Add exporter test build wiring and cover both
linear and radial gradient output.

AI-assisted-by: gpt-5.6-luna

*  Standardize exporter testing workflow

Align exporter scripts with the frontend testing pattern. Add a
dedicated GitHub Actions workflow and document the canonical exporter
commands in Serena memories.

AI-assisted-by: gpt-5.6-luna

*  Add focused exporter test execution

Mirror frontend test-runner behavior for focused namespaces and test
vars. Support --focus, --log-level, and --help, and document the
commands.

AI-assisted-by: gpt-5.6-luna

* 🐛 Replace shell exec with execFile in exporter

Replace child_process.exec with execFile to eliminate shell
interpretation. Add hex color validation in exporter and frontend
to reject malformed input before command construction.

This fixes GHSA-4f36-m4hj-cv86 (CVSS 9.9 Critical), an authenticated
OS command injection vulnerability where malicious fill-color values
could execute arbitrary commands in the exporter container.

Defense in depth:
- Layer 1: execFile passes arguments directly without shell parsing
- Layer 2: Exporter validates colors with strict hex regex
- Layer 3: Frontend filters invalid colors before DOM emission

All three independent reporters' attack vectors are addressed:
- Quote breakout (lyhtheori)
- Command substitution (B1gN0Se)
- Path traversal (KimiSecurityTeam)

AI-assisted-by: qwen3.7-plus

* 🐛 Use existing hex-color-string? and fix test path mismatch

Address code review feedback:

- Replace duplicated hex-color-rx and valid-hex-color? with existing
  hex-color-string? from app.common.types.color
- Fix RCE test to use marker path in payload instead of hardcoded /tmp/pwned

AI-assisted-by: qwen3.7-plus

---------

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-19 13:53:40 +02:00
Andrey Antukh fda6d56139 📚 Update AGENTS.md file 2026-08-19 13:00:01 +02:00
Pablo Alba 5080a90f76 💄 Change nitrate activation code texts (#11237) 2026-08-19 12:33:33 +02:00
Andrey Antukh 4d90fe9126 Add advisories access helper to gh tool 2026-08-19 12:20:37 +02:00
Marina López ddc98bdd47 Add sso events (#11265) 2026-08-19 07:58:44 +02:00
David Barragán Merino d826c7ac13 📚 Remove architectural constraints related to MCP Server HA 2026-08-18 19:18:06 +02:00
Andrey Antukh 4339d8d244 📎 Update serena documentation about backend storage 2026-08-18 18:37:58 +02:00
Andrey Antukh df664fe96b 📎 Add improvement for review command 2026-08-18 18:10:57 +02:00
Luis de Dios 7061ecae0a 🐛 Fix gitch of placeholder when switching between teams on dashboard (#10922)
* 🐛 Fix use single point for retrieving state and propagate it

* 🐛 Fix use loading message instead of placeholder when loading files
2026-08-18 17:50:15 +02:00
Alonso TorresandAndrey Antukh 4ac14cfd08 Add component synchronization to waitForLayoutUpdate (#10964)
*  Add component synchronization to waitForLayoutUpdate

* 🐛 Fix async mock leak in workspace-reflow-test

Use mock/with-mocks instead of with-redefs for http/send! mock in
failed-google-font-css-does-not-abort-shared-consumers test.

with-redefs restores bindings when the block exits synchronously,
but the RxJS subscription fires asynchronously. This caused the mock
to leak into subsequent tests (workspace-media-test), producing 3
spurious failures.

AI-assisted-by: mimo-v2.5-pro

* ♻️ Replace async with-redefs with mock/with-mocks in frontend tests

with-redefs restores bindings when the block exits synchronously,
which is too early for async code (t/async, rx/subs!, promises).
mock/with-mocks uses set! and restores in the done callback, keeping
mocks alive across async boundaries.

Converted 15 with-redefs usages across 4 test files:
- workspace_reflow_test.cljs: 2 genuinely async tests (P1)
- routes_test.cljs: 3 SSO caching tests (P2)
- main_errors_test.cljs: 8 expired-org SSO tests (P2)
- comments_test.cljs: 2 comment thread tests (P2)

36 purely sync with-redefs usages left unchanged — with-redefs
is correct for synchronous code.

AI-assisted-by: mimo-v2.5-pro

* 📎 Fix fmt issues

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 17:45:42 +02:00
Andrey Antukh 1671cc4fcc 🐛 Escape markdown in Mattermost error notifications (#11034)
Add escape-markdown to common/data.cljc that escapes Markdown
special characters (*, _, ~, `, [, ], >, #, @, etc.) by prefixing
them with backslash. Apply it to user-controlled fields (:hint,
:href) in the Mattermost error reporter before constructing the
notification message.

This is an internal-only feature not accessible to end users.

AI-assisted-by: mimo-v2.5-pro
2026-08-18 17:43:25 +02:00
Andrey Antukh e72c1869eb 🐛 Validate version parameter in import-binfile (#11107)
Restrict version parameter to supported values (1 or 3) via schema
validation instead of accepting any integer. Add content-based format
detection when version is not provided, using bfc/parse-file-format
to inspect file magic bytes.

Closes #11105

AI-assisted-by: qwen3.7-plus
2026-08-18 15:13:49 +02:00
Andrey Antukh 3be07ccced 🐛 Add minimum validation for total-chunks in upload session (#11104)
The create-upload-session RPC method accepted total-chunks values of 0
or negative numbers without validation, creating inconsistent session
state. Add {:min 1} constraint to the schema to reject invalid values
at input validation.

Closes #11103

AI-assisted-by: qwen3.7-plus
2026-08-18 14:37:51 +02:00
Andrey Antukh 73c0668877 🐛 Verify read access on source file in clone-file-media-object (#11090)
The clone-file-media-object RPC command only checked edit permissions
on the destination file. The source media object was fetched directly
by UUID without verifying the caller had access to the file that owns
it.

This fix adds a read permission check on the source file before
cloning. If the caller lacks read access to the source file, the
operation fails with :not-found to avoid leaking information about
the existence of files/media the caller cannot access.

Closes #11087

AI-assisted-by: qwen3.7-plus
2026-08-18 14:36:50 +02:00
Andrey Antukh 367e4d534c 🐛 Scope assemble-chunks session lookup to profile-id (#11012)
Prevent BOLA in chunked upload assembly by verifying session
ownership. The assemble-chunks function now requires a profile-id
parameter and scopes the upload_session lookup accordingly, matching
the pattern already used by upload-chunk.

All three callers (assemble-file-media-object, create-font-variant,
import-binfile) updated to pass the authenticated profile-id.

AI-assisted-by: mimo-v2.5-pro
2026-08-18 14:36:29 +02:00
Andrey Antukh 3f09f161ae Merge remote-tracking branch 'origin/main' into staging 2026-08-18 14:34:22 +02:00
Andrey Antukh 5b4a5776cb 🐛 Prevent nil theme in profile updates
Omit nil optional profile fields before frontend schema validation and RPC persistence. Preserve omitted language and theme values in backend updates, and add regression coverage for partial profile saves.

AI-assisted-by: gpt-5.6-luna
2026-08-18 11:18:44 +00:00
0xTHAC0andAndrey Antukh d745dc4a3c 🐛 Fix grid item date tooltip showing deletion text on non-deleted files (#11161)
The grid-item-metadata* component always used :will-be-deleted-at (falling
back to :modified-at) and always showed the "Will be deleted %s" tooltip,
even for files in the Recent tab that have no deletion date.

Now the component branches on the presence of :will-be-deleted-at:
- Deleted files: show the deletion timeago with the existing
  "Will be deleted %s" tooltip.
- Regular files: show :modified-at timeago with a new
  "Last modified %s" tooltip key (dashboard.grid.last-modified-at).

Closes #10873

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 12:44:53 +02:00
María Valderrama c72bb331ef 🐛 Fix nitrate advanced permissions error (#11255) 2026-08-18 11:20:02 +02:00
Filip SajdakandAndrey Antukh 9311737f66 🐛 Do not cache the environment generated config.js (#11146)
On self hosted installs /js/config.js is regenerated from PENPOT_FLAGS
on every container start, but nginx served it with the same
`public, max-age=604800` used for build assets, and index.html versions
it only by the build. A flags only change therefore leaves the URL
untouched, so a browser that had already loaded the app kept using its
cached copy for up to a week: enabling a flag such as
enable-login-with-google had no visible effect for returning users
until the cache expired or they cleared their site data.

Serve that one file with the same no-store headers already used for
index.html, which is the other file whose contents change without its
URL changing. Every other static asset keeps the long lived cache.

Fixes #10556.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-17 22:35:10 +02:00
Sebastien MALOT 7ac61e0597 🐛 Fix typo in auto-file-snapshot timeout setting (#10909)
Corrected a typo in the configuration documentation regarding the auto-file-snapshot timeout setting.

Signed-off-by: Sebastien MALOT <sebastien.malot@pm.gouv.fr>
2026-08-17 22:14:01 +02:00
Andrey Antukh 8acb92b782 🐛 Normalize fractional rate-limit reset durations (#11254)
Round bucket reset intervals up to whole milliseconds before adding them to an instant. This prevents Clojure ratios from reaching duration conversion and disabling rate limiting for the request.

Add a regression test for a refill rate that produces fractional milliseconds.

Closes #11253

AI-assisted-by: gpt-5.6-luna
2026-08-17 15:27:06 +02:00
Andrey Antukh c797656d17 🐛 Fix crash when pasting into an empty or element-focused caret (#11150)
Pasting text could throw "Unknown node type" and lose the paste. The
insertion paths assume the caret sits on a text node or a <br>, but the
browser can report it on a container element (the offset being a child
index, common in Firefox) or, for an empty text shape that was just
focused, on nothing at all: selectAll() returned early without ever
setting a selection.

Add resolveTextNodePosition(), which walks a (node, offset) pair down to
the addressed text node or line break and returns null instead of
throwing when it cannot. The selection controller normalizes the caret
with it before inserting text or a pasted fragment, and selectAll() now
collapses on the line break of an empty editor so the caret is always
usable.

Closes #11149

AI-assisted-by: longcat-2.0-free
2026-08-17 13:32:12 +02:00
Andrey Antukh f96d850049 📎 Add ste skill to opencode 2026-08-17 13:31:24 +02:00
Pablo Alba ed04d509ed 🐛 Fix bad managed error on backend sso failure (#11247) 2026-08-17 12:40:18 +02:00
María Valderrama 57c9c3f6a4 🐛 Fix sso error message (#11252) 2026-08-17 12:26:49 +02:00
Andrey Antukh 29dbf9ab12 🐛 Validate content-type on management upload endpoints (#11026)
Add media type validation to upload-tempfile and upload-org-logo
management endpoints. Both stored user-supplied mtype without
checking against an allowlist. Only image types and PDF are
permitted. Non-public bucket assets now also carry
Content-Disposition: attachment to prevent inline rendering.

AI-assisted-by: mimo-v2.5-pro
2026-08-17 12:13:56 +02:00
Andrey Antukh 509f5395cb 📎 Update changelog 2026-08-17 12:08:38 +02:00
Andrey Antukh 4ecd8ffb89 🐛 Fix crash when editing tokens with group nodes (#11144)
Make sd-token-uuid nil-safe when accessing .original.id to prevent
crashes when StyleDictionary emits group nodes alongside real tokens.

Group nodes have an original object but no id property, causing
undefined is not an object errors during interactive token resolution
in the edit modal.

Closes #11143

AI-assisted-by: qwen3.7-plus
2026-08-17 11:53:48 +02:00
Andrey Antukh 0797d7235a 🐛 Fix workspace crash on rapid sidebar measures input changes (#10793) (#10794)
The sidebar measures panel numeric inputs (X, Y, width, height,
rotation) emitted one full apply-modifiers commit per DOM event with
no throttle: every arrow key-repeat, wheel tick and scrub pointermove
became update-positions / update-dimensions / increase-rotation. A
sustained gesture starved the React renderer and crashed the
workspace with error #185 (Maximum update depth exceeded).

Coalesce those bursts at the data layer (potok), following the
update-position-data debounce pattern in texts.cljs:

- update-positions is now burst-coalesced in place (its only caller
  is the measures panel); new update-dimensions-coalesced and
  increase-rotation-coalesced variants are used by the measures
  panel, while the immediate events keep serving plugins, variants
  and token application (including the delta? rotation path).
- The first event of a burst commits immediately (leading edge, so
  single edits stay synchronous); further ticks commit at most once
  per 50 ms (throttle); a trailing debounced flush guarantees the
  exact final value lands. All payloads are absolute values, so
  keeping the latest queued value per shape/attribute is lossless.
- Pending payloads are drained atomically and stale shape ids
  (deleted mid-burst) are skipped. The drain stream lives until the
  workspace is finalized, so bursts reuse a single subscription.
- Fewer commits per burst also means fewer undo entries; scrub drags
  still produce a single entry via the input's outer transaction.

Tests: new frontend-tests.logic.sidebar-transform-coalescing-test (8
tests, legacy SVG and WASM renderer branches) guards the invariant
that a 20-event burst commits the exact final value in a handful of
commits. The previously unregistered update-position-test is wired
into the runner with WASM mock fixtures (it fails in full-suite
context without them due to a pre-existing global mock-state issue).

AI-assisted-by: kimi-k3
2026-08-17 11:51:24 +02:00
Andrey Antukh 5efd9cc3c5 🐛 Prevent admins from granting owner role in team invitations (#11099)
Add role-ceiling check to create-team-invitations and
update-team-invitation-role methods. These RPC methods allowed
team admins to grant or elevate invitations to :owner role,
bypassing the protection that exists in update-team-member-role.

The fix replicates the existing check from update-team-member-role:
reject promotion to :owner when the caller is not an owner.

Closes #11098

AI-assisted-by: qwen3.7-plus
2026-08-17 11:37:51 +02:00
Andrey Antukh e219ce20eb ⬆️ Update opencode version on devenv 2026-08-17 11:26:23 +02:00
Andrey Antukh 9e97477a98 ⬆️ Update to latest nodejs lts 2026-08-17 11:26:23 +02:00
Andrey Antukh c688cba8d8 🐛 Mock DNS resolution in SSRF tests for environments without public DNS (#11040)
The validate-url-allows-public-{https,http} tests relied on real DNS
resolution of example.com, which fails in containers without public
DNS access. Mock resolve-host to return a known public IP, consistent
with the pattern used by other tests in the same file.

AI-assisted-by: mimo-v2.5-pro
2026-08-17 11:21:25 +02:00
Andrey Antukh 68e1db984d Add resolve-git-conflicts opencode command 2026-08-17 11:20:04 +02:00
Andrey Antukh 3033da4409 🐛 Add concurrency limit to import-binfile RPC handler (#11024)
Apply climit with 4 global permits and 1 per-profile permit (queue 2)
to prevent connection pool exhaustion from concurrent imports. Each
import holds a DB connection for its entire duration with idle
transaction timeout disabled, so unbounded concurrency could exhaust
the pool (default 60 connections).

AI-assisted-by: mimo-v2.5-pro
2026-08-17 11:08:07 +02:00
David Barragán Merino aecfee0f02 🔧 Align MCP workflow name with the rest of CI workflows
The MCP workflow was named "MCP CI" while every other tests-*.yml
workflow uses the "CI: <Component>" pattern. Rename it to "CI: MCP"
for consistency in the GitHub Actions listing.
2026-08-14 20:15:57 +02:00
Eva Marco a3bc4b0e3a 🐛 Fix text alignment on libraries (#11243) 2026-08-14 10:56:48 +02:00
Pablo Alba 350dc14632 🐛 Show a specific error on nitrate reused activation code (#11236) 2026-08-14 09:33:58 +02:00
María Valderrama c7f036bed0 🐛 Fix organization invitation schema validation for logo URI (#11238) 2026-08-13 13:15:59 +02:00
Pablo Alba 3db7548c19 💄 Change nitrate error message (#11232) 2026-08-13 13:06:26 +02:00
Yamila MorenoandSebastien MALOT 9528400c6e 🐛 Forward internal Host in nginx proxy_pass to backend/exporter (#11233)
The global `proxy_set_header Host $http_host;` forwarded the client-facing
Host to internal proxy_pass calls (backend/exporter), breaking mTLS routing
in service-mesh setups (e.g. Istio STRICT mode), which match outbound
requests to a cluster based on Host/:authority.

Explicitly set `Host $proxy_host` on /api, /assets, /api/export, /readyz
and /ws/notifications so these calls always target the correct internal
service host, independent of the client's original Host header.

Fixes #10835

Signed-off-by: Sebastien MALOT <sebastien.malot@pm.gouv.fr>
Co-authored-by: Sebastien MALOT <sebastien.malot@pm.gouv.fr>
2026-08-13 12:03:27 +02:00
Pablo Alba f7fc869e52 📚 Unify doc added for nitrate apis (#11231) 2026-08-13 11:37:06 +02:00
Pablo Alba ef26231b8f 🐛 Fix nitrate organization sso expiration (#11227) 2026-08-12 17:53:02 +02:00
María Valderrama 93f02ea0b4 🐛 Fix send-invitations policy not enforced in backend RPC (#11206) 2026-08-12 16:55:20 +02:00
María Valderrama 6d49fb2be0 🐛 Fix organization dropdown alignment (#11216) 2026-08-12 16:52:27 +02:00
Belén Albeza fee416d275 🐛 Fix crash after changing typography options (v2 and v3) (#11221) 2026-08-12 15:10:23 +02:00
Eva Marco 986ee60cad 🐛 Fix invitation loop (#11223) 2026-08-12 13:20:21 +02:00
Eva Marco e5c80edbf3 🐛 Fix libraries grid layout (#11226) 2026-08-12 13:20:07 +02:00
David Barragán Merino 732162e720 🔧 Report flaky e2e tests in integration workflow
Enable Playwright's JSON reporter alongside `list` and publish a
summary of flaky tests to the job step summary. The JSON report is
kept as an artifact for 30 days so flakiness rates can be aggregated
over time.

CI already runs with `retries: 2`, so unstable tests have been passing
silently on retry. This only surfaces what the suite already absorbs;
no test behaviour changes.

The reporter in `frontend/scripts/test-e2e` becomes overridable via
`PLAYWRIGHT_REPORTER` so the local developer default stays untouched.
2026-08-12 12:52:30 +02:00
María Valderrama 985d219810 🐛 Fix confusing copy for feams in organizations (#11213) 2026-08-11 14:36:07 +02:00
Eva Marco 53985dc630 🐛 Fix setting dark theme on onboarding (#11212)
* 🐛 Fix setting dark theme on onboarding

* 🎉 Add test
2026-08-11 13:49:48 +02:00
Marina López 02c31e7348 🐛 Cache Nitrate SSO checks during navigation (#11209) 2026-08-11 13:30:04 +02:00
Eva Marco 69ef7e86cd 🐛 Fix colorpicker z-index (#11207) 2026-08-11 13:06:30 +02:00
Eva Marco d7daefafe2 🐛 Fix select shape after enter path edition (#11205) 2026-08-11 13:05:47 +02:00
Eva Marco c4d1a1bc94 🐛 Fix node deleting (#11126)
* 🐛 Fix delete path node

* 🐛 Fix typography on shortcuts list
2026-08-11 13:03:29 +02:00
María Valderrama 1e6d438257 🐛 Fix SSO failure logging user out instead of showing error page (#11129)
* 🐛 Fix SSO failure logging user out instead of showing error page

* 📎 Code review
2026-08-11 09:14:29 +02:00
María Valderrama d4294bbf1e 🐛 Fix missing membership check in create-team (#11166) 2026-08-11 09:13:13 +02:00
Filip SajdakandClaude Opus 5 83efa28b12 🐛 Keep comment bubbles from painting over the rulers (#11168)
The comments layer lives in the viewport overlays, which are absolutely
positioned above the canvas, and the container itself carries a high
z-index. A comment bubble panned into the ruler bars therefore painted
on top of them, covering the ticks and numbers.

Clip the comments container to the area outside the ruler bars while
the rulers are visible, the same thing the `clip-handlers` clip path
already does so the selection handlers stay off the rulers. Clipping
only the comments container leaves the text editing overlay, which
shares the viewport overlays, untouched.

Fixes #11163.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:18:58 +02:00
Luis de Dios 16e52b0494 🐛 Fix error page logo not visible in dark mode (#11167) 2026-08-10 15:51:31 +02:00
Marina López 0fd2a9d26f 🐛 Secure organization invitation creation (#11164) 2026-08-10 13:45:15 +02:00
Marina López 5d2cb22966 Fetch team organization in a single batch (#11140) 2026-08-10 13:43:27 +02:00
Marina López 900a7ef498 ♻️ Show subscription section to everybody (#11007) 2026-08-10 13:42:28 +02:00
Luis de Dios 86c563f11f 🐛 Fix font family typography asset persist across files in new created text layers (#11134) 2026-08-10 11:35:51 +02:00
Jules d63d6370c0 🐛 Fix stale DNS caching in frontend nginx MCP proxy (#10947)
The generated /etc/nginx/overrides/server.d/mcp-locations.conf used a
plain proxy_pass target (e.g. `proxy_pass http://penpot-mcp:4402;`)
where $PENPOT_MCP_URI/$PENPOT_MCP_URI_WS are shell variables substituted
once by envsubst in nginx-entrypoint.sh at container startup, not nginx
variables. nginx resolves a literal proxy_pass hostname once when the
config loads and never re-checks it, so the existing
`resolver 127.0.0.11 valid=10s;` directive in
overrides/http.d/resolvers.conf has no effect on these three locations
- it only applies to nginx variables evaluated per-request.

In multi-container deployments where the penpot-mcp container restarts
or is recreated independently of penpot-frontend (image update, OOM,
orchestrator reschedule), it gets a new IP from Docker's/the
orchestrator's DNS, and the frontend's nginx keeps forwarding to the
old, now-dead address until penpot-frontend itself is restarted. This
surfaces to users as `wss://<host>/mcp/ws` failing to connect from the
browser after enabling the MCP plugin, with
`connect() failed (111: Connection refused)` in the frontend's nginx
logs.

Route each location through a `set $var ...; proxy_pass $var;` pair so
proxy_pass evaluates a real nginx variable, letting the pre-existing
resolver directive re-resolve penpot-mcp within its 10s TTL instead of
caching the address for the container's lifetime.

For /mcp/stream and /mcp/sse, the set value also appends
$is_args$args explicitly: when proxy_pass targets a variable AND that
variable's value includes a URI/path component, nginx does not
automatically forward the original request's query string the way it
does for a static proxy_pass target - it must be appended by hand, or
the userToken query parameter used for multi-user authentication is
silently dropped before reaching the MCP server. /mcp/ws has no path
component in its target so it isn't affected by this and needed no
such change.

Verified locally: force-recreated the penpot-mcp container onto a
different IP while leaving penpot-frontend untouched; the /mcp/ws
WebSocket upgrade kept returning 101 Switching Protocols throughout,
both immediately and after the resolver's TTL window. Separately
verified /mcp/stream: a POST with ?userToken=... now shows up
server-side as userTokenFp=<redacted first 8 chars> instead of <none>,
and an actual MCP client (Claude Code) using this proxy can now call
authenticated tools like execute_code successfully.

Signed-off-by: Jules LaPrairie <jules@lucidbox.ca>
2026-08-10 11:34:52 +02:00
Luis de Dios b9c92496f1 🐛 Fix overrides lost after switch (#10619) 2026-08-10 10:56:04 +02:00
Andrey Antukh e01b36b841 🐛 Add project-id guard to use-plugin-register layout effect (#10859)
Add `project-id` to the guard condition in `use-plugin-register`'s layout effect so the plugin "Try out" flow waits until projects have loaded.

Previously, only `plugin-url` was checked, which allowed the fetch to fire
before projects were available, sending a nil `project-id` and causing a 400
validation error from the backend.

AI-assisted-by: mimo-v2.5-pro
2026-08-07 19:21:32 +02:00
Andrey Antukh 2f04fcddbf 🐛 Invalidate all sessions on profile deletion (#11115)
When a profile is deleted, only the current session was being
invalidated. Other active sessions on different devices remained
functional until the background cleanup task completed.

Add session/invalidate-all helper that deletes all sessions for
a profile by profile_id, and call it from delete-profile before
the response transform. This ensures immediate access revocation
across all devices when an account is deleted.

Closes #11114

AI-assisted-by: qwen3.7-plus
2026-08-07 13:44:13 +02:00
Andrey Antukh e2d429d283 🐛 Add timeout to plugin manifest fetch (#11120)
The fetch-manifest function previously had no timeout, causing the
plugin installation flow to hang indefinitely if the server accepted
the connection but never completed the response.

Added a 15-second timeout using rx/timeout to abort the request
automatically.

Closes #11119

AI-assisted-by: qwen3.7-plus
2026-08-07 13:37:00 +02:00
Eva Marco bf9825fcfe 🐛 Fix close modal with esc (#11131) 2026-08-07 12:33:11 +02:00
Andrey Antukh a131e40a6d Add proper rlimit config and propagate limit timestamp
Replace the placeholder rlimit.edn with a real per-endpoint
configuration covering auth, SSRF, search, email, media and project
operations. The previous file only had a commented-out example, so
all limits fell back to the 200k/h default window.

Also propagate the evaluated `now` timestamp into both bucket and
window result maps, so consumers (e.g. soft-mode reports) can know
exactly when the limit was checked.

AI-assisted-by: minimax-m3
2026-08-07 11:35:05 +02:00
Andrey Antukh 5571c53502 🐛 Use random UUIDs for share link IDs (#11117)
Share link IDs function as capability secrets — anyone possessing
the ID can read a file without authentication. The previous UUIDv8
scheme is predictable (56 bits fixed per process + 48-bit timestamp).

Changed to uuid/random (UUIDv4) for genuine unpredictability.

Closes #11116

AI-assisted-by: qwen3.7-plus
2026-08-07 11:27:12 +02:00
Andrey Antukh 6951876c13 🐛 Use constant-time comparison for shared key authentication (#11122)
Replace standard '=' operator with MessageDigest/isEqual to prevent
timing attacks on shared key authentication middleware.

Closes #11121

AI-assisted-by: qwen3.7-plus
2026-08-07 11:25:15 +02:00
Andrey Antukh 399b00b86d 🐛 Add permission checks to WebSocket subscription handlers (#11054)
* 🐛 Add permission checks to WebSocket subscription handlers

Check file and team read permissions before allowing WebSocket
subscriptions to prevent resource enumeration via presence
notifications.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Fix random backend test failure
2026-08-07 11:24:24 +02:00
Álvaro Tejero Cantero bc9319eac5 🐛 Port the foreign-font-id test to the uploads API
`create-font-variant-rejects-foreign-font-id` sends `:data`, which
`schema:create-font-variant` no longer accepts: the same commit that
added the test documents that param as removed in 2.18 in favour of
`:uploads`. Both of the test's requests are therefore rejected by params
validation before they reach `check-font-team-ownership!`, which is the
thing the test exists to check. It asserted nothing about ownership and
failed three assertions.

Upload the font through `upload-font-chunked!`, the helper the other
tests in this namespace already use, and pass the session id in
`:uploads`.

`backend-tests.rpc-font-test` is 16 tests, 172 assertions, 0 failures
with this applied.

AI-assisted-by: mixed models
2026-08-07 09:10:04 +02:00
Álvaro Tejero Cantero 5359ff04cf 📎 Drop an unused binding in create-font-variant
`create-font-variant` destructures `uploads` and never reads it: the
handler passes the whole `params` map to `prepare-font-data-from-uploads`.

`clj-kondo` reports it as an unused binding and exits 2, which fails the
Lint step of the Backend workflow, and the Lint step runs before the
tests, so no branch based on `develop` can run the backend suite at all.

AI-assisted-by: mixed models
2026-08-07 09:10:04 +02:00
Andrey Antukh 9875db2f82 🐛 Fix media-remote-test falling back to real config in REPL
The info-service-uri-not-configured test used config-get-mock with an
empty map, which falls back to cf/config for missing keys. In a REPL
with real config, media-processing-service-uri is set, causing the code
to attempt an HTTP call instead of raising the expected error.

Use (constantly nil) to ensure cf/get always returns nil, matching the
test intent of simulating an unconfigured service URI.

AI-assisted-by: mimo-v2.5
2026-08-06 20:53:58 +02:00
Andrey Antukh 1548748aed ♻️ Reuse organization schema in create-organization-invitation
Replace the inline organization map in schema:create-organization-invitation with cto/schema:organization-with-avatar, eliminating schema duplication and fixing mismatched validation rules for :logo and :sso-active fields.

AI-assisted-by: mimo-v2.5
2026-08-06 20:53:58 +02:00
Andrey Antukh 0702363b5c 🐛 Validate font-id team ownership in create-font-variant (#11014)
Prevent cross-team font injection by checking that when a font-id
already has variants, they belong to the same team. This closes a
BOLA gap where a user with team edit permissions could create a
font variant referencing a font-id from another team.

AI-assisted-by: mimo-v2.5-pro
2026-08-06 18:32:00 +02:00
Elena Torró 688c69b478 🐛 Fix import-binfile schema test (#11118) 2026-08-06 17:05:41 +02:00
Andrey Antukh 229d24e8f2 🐛 Fix regression on uploading binfile with incorrect schema 2026-08-06 13:30:59 +02:00
Eva Marco fdf1684565 🐛 Fix plugin modal z index (#11109)
* 🐛 Fix z-index on plugin modals

* 🐛 Fix CI
2026-08-06 12:49:52 +02:00
Andrey AntukhandLuis de Dios 81e44afbe3 🐛 Add backend password validation with complexity rules and dictionary check (#11059)
* 🐛 Add backend password validation with complexity rules and dictionary check

Enforce minimum 8-character password length, require at least 1 lowercase
letter, 1 uppercase letter, 1 digit, and 1 special character, and reject
common passwords using Passay library with a 10k-entry wordlist from
SecLists during registration and password change flows.

AI-assisted-by: mimo-v2.5-pro

*  Improve user feedback

When the password is invalid, the user now gets extra indications to make it stronger, so it can be valid.

* 🐛 Fix remove unneeded common password check

The dictionary check is only relevant for passwords that meet all other requirements, but all 10,000 common passwords would fail the character requirements, so this check is not needed

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-06 10:17:14 +02:00
Andrey Antukh 495e9f059e 🐛 Add minor fix on token tests 2026-08-06 09:35:21 +02:00
Andrey Antukh a60b648c6c 🐛 Fix issues with draft-js tests 2026-08-06 08:55:58 +02:00
Andrey Antukh b6656ee8dd 🐛 Enable SSRF check for organization SSO provider (#11064) (#11065)
Remove :skip-ssrf-check? true from prepare-organization-sso-provider so
SSRF protection is active when validating organization SSO configs.

The endpoint is already protected by shared-key authentication
(admin-console), but enabling SSRF protection prevents potential misuse
of internal network resources if the shared key were ever compromised
(defense-in-depth).

Add test prepare-organization-sso-provider-does-not-skip-ssrf-check to
verify the SSRF check is not skipped.

AI-assisted-by: qwen3.7-plus
2026-08-05 21:54:33 +02:00
Andrey Antukh 86aaf642b6 🐛 Fix scripts/ci issue with backend lintig 2026-08-05 21:52:59 +02:00
Andrey Antukh c4dd04353f 🐛 Sanitize SVG files on upload to prevent XSS
Add sanitize-svg function that removes dangerous elements and attributes:
- script tags
- foreignObject elements
- Event handler attributes (onload, onmouseover, etc.)
- javascript: URLs from href/xlink:href attributes

Apply sanitization in process-main-image before storing SVG files.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 21:52:59 +02:00
Andrey Antukh 0ac711aa68 🐛 Normalize string inputs to prevent unfiltered echo (#11061)
Add normalize-string helper in app.common.data that trims whitespace
and returns empty string for nil input. Apply to profile, team, and
project string fields (fullname, lang, theme, name) before storage.

AI-assisted-by: qwen3.7-plus
2026-08-05 17:54:07 +02:00
Andrey Antukh bf62e59f73 🐛 Add cooldown to prevent duplicate invitation emails (#11063) 2026-08-05 17:53:15 +02:00
Andrey Antukh 5906312dff 🐛 Normalize error response on duplicate file ID (#11050)
Capture unique constraint violation in insert-file! and return
generic :not-found error instead of propagating raw PostgreSQL
exception, preventing file existence oracle.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:52:14 +02:00
Andrey Antukh 25066c2f46 🐛 Require file read permissions for asset endpoints (#11036)
Add authorization check to generic-handler in assets.clj so that
/assets/by-file-media-id/:id and its /thumbnail variant verify the
requesting profile has read access to the parent file. Return 404
(not 403) when access is denied to avoid confirming existence.

Also switch get-file-media-object from db/get to db/get* so that
non-existent media objects return nil instead of raising.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:44:01 +02:00
Andrey Antukh 3d176d5390 🐛 Restrict webhook creation/edit/delete to team members only (#11029)
* 🐛 Restrict webhook edit/delete to team members only

Remove the creator-id fallback from get-webhooks-permissions.
Previously, the webhook creator could always edit/delete their
webhook even after being removed from the team. Now can-edit
comes from team role only — removed users get :not-found.

Webhooks are NOT deleted on member removal; the team owns them
and team admins/owners manage them.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Restrict webhook creation to team editors

Use team role check (check-edition-permissions!) for create-webhook
instead of the custom check that allowed any team member to create
webhooks via creator-id self-match override.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:42:49 +02:00
Andrey Antukh 0481408531 🐛 Add recursion depth limit to Fressian reader (#11020)
Bound read depth at 128 levels to prevent StackOverflowError from
crafted deeply-nested payloads. All recursive read handlers go
through read-object!, so a single depth check covers all paths.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:41:59 +02:00
Andrey Antukh 689d3a1be2 🐛 Add max-object-size guard to read-obj! in v1 parser (#11018)
Prevent unbounded memory allocation when a crafted binfile specifies
an excessively large object size. Apply the same 100 MiB limit that
read-stream! already enforces.

AI-assisted-by: mimo-v2.5
2026-08-05 17:40:58 +02:00
Andrey Antukh fb07273897 🐛 Validate library belongs to same team in link/unlink/sync handlers (#11016)
Add check-library-team-ownership! helper that verifies both the file
and library share the same team before creating or modifying library
relations. This prevents cross-team library injection where a user
with edit permissions on files in different teams could link them
across team boundaries.

Applied to link-file-to-library, unlink-file-from-library, and
update-file-library-sync-status handlers.

AI-assisted-by: mimo-v2.5
2026-08-05 17:40:04 +02:00
Andrey Antukh 9242556da6 🐛 Close import-binfile schema and remove file-id parameter (#10994)
Add :closed true to schema:import-binfile to reject unknown keys.
Remove file-id from handler destructuring, config binding, and audit
props to prevent specifying a target file on import.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:37:27 +02:00
Andrey Antukh 4f7bb94bb1 🐛 Add size limit and rate limiting to send-user-feedback (#10979) (#10990)
Prevent email bombing attacks on the send-user-feedback endpoint by
limiting the error-report field to 1MiB and adding climit rate limits:
by-profile (1 permit, queue 3) and global (4 permits), configured in
climit.edn. Make the schema public so it can be exercised by tests,
and add schema validation tests covering the new size limit.

AI-assisted-by: qwen3.7-plus
2026-08-05 17:35:59 +02:00
Andrey Antukh 49276886f3 🐛 Fix some issues with immutablejs incompatibility 2026-08-05 17:28:58 +02:00
Andrey Antukh a2968defbe 🐛 Add bounding box dimension limit to prevent export DoS (#11042)
Add max-export-dimension constant (100000 units) and validate in
calculate-dimensions. Reject exports when bounding box width, height,
or position exceeds the limit to prevent resource exhaustion in the
Chromium export pool.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 14:18:14 +02:00
Marina López 6628f0a134 🐛 Adjust button icon visibility (#11070) 2026-08-05 11:30:13 +02:00
Andrey Antukh 636bc22cc4 Add Node.js E2E API tests for backend (#10787)
Add end-to-end HTTP tests under backend/test/e2e/ using Node.js built-in
test runner (node:test) and native fetch. Tests run through the devenv
nginx proxy on port 3450.

Test suites (19 tests total):
- auth-flow: demo profile creation, login, session cookies, access tokens
- export-binfile: file creation, export to asset URL via SSE
- asset-download: download with cookie/token auth, 401 without auth,
  S3 redirect behavior, full export-to-download flow

Key findings documented in tests:
- nginx @handle_redirect intercepts backend 307 and proxies to S3 directly,
  stripping the client Authorization header (bug does not reproduce in devenv)
- SSE end event uses ~#uri tagged format for URLs
- Unauthenticated RPC returns uuid/zero profile (not null)

AI-assisted-by: mimo-v2.5-pro

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-08-05 10:15:41 +02:00
Andrey Antukh aeedb96260 Add media-processor service for image and font processing (#10767)
*  Add media-processor service for image and font processing

Externalizes ImageMagick and FontForge subprocess invocations into a
separate Node.js HTTP service (media-processor/). Backend dispatches
via feature flag :use-remote-media-processing.

Key changes:
- media-processor module (TypeScript, Express 5, Sharp, FontForge/woff)
  - POST /api/image/info, /api/image/thumbnail, /api/font/generate
  - Resource limits: 128MP rejection, prlimit (512MB + 30s CPU)
  - Streaming multipart via SequenceInputStream
- app.media split into validation (leaf), local (shell impls), remote (HTTP)
- Schema enforcement: :upload and :input schemas in validation namespace
- Configurable timeout (PENPOT_MEDIA_PROCESSING_SERVICE_TIMEOUT)
- 78 tests across 4 files (image, font, middleware, config)
- FontForge path escaping for command injection prevention
- Parallel font variant conversions with Promise.all

AI-assisted-by: mimo-v2.5-pro

* 🐳 Revert docker-compose changes from media-processor commit

Remove docker-compose.yaml modifications that were part of the media-processor
service commit. The media-processor service definition, flags, and environment
variables are reverted to their previous state.

AI-assisted-by: qwen3.7-plus

* ⬆️ Update dependencies

* 🐛 Fix PR review issues in media-processor

- Font path bug: sfntToWoff and woff2ToSfnt now copy input to temp dir
  when input is a file path, ensuring output lands in expected location
- Error preservation: execCommand preserves killed/signal/code properties
  from child process errors for OOM detection
- Content-Length: service-multipart-request calculates and includes
  Content-Length header for streaming multipart requests

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor

- Rename PENPOT_MEDIA_PROCESSOR_SECRET_KEY to PENPOT_MEDIA_PROCESSOR_SHARED_KEY
  in devenv to match backend config key
- Fix timeout middleware to destroy request AFTER response finishes,
  preventing truncated 504 responses
- Fix quality=0 parsing to preserve explicit zero (was silently overridden to 85)
- Replace require('fs') with proper ES module import in upload-storage.ts
- Refactor font conversion temp-dir boilerplate into withTempInput helper
- Document FontForge escaping limitations (single quotes only)
- Fix misleading comment in image.ts about sharp metadata decoding

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 2)

- Fix queue middleware to skip next() when response already ended,
  preventing orphaned work after timeout
- Fix hybrid storage to use disk when Content-Length is absent (chunked
  transfer), preventing unbounded memory allocation
- Add source image format validation in generateThumbnail to reject
  unsupported formats (TIFF, BMP, etc.) with 400 instead of 500
- Remove dead code in convertFont for unreachable woff→woff path
- Remove unused isEnabled() method from LokiLogTransport
- Fix sfntToWoff to use correct extension (.ttf/.otf) based on source type
- Extract queue middleware to separate file for testability
- Add comprehensive tests for queue middleware and upload storage

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 3)

- Fix disk-backed upload cleanup after successful requests by adding
  cleanup middleware that removes temp files on response finish/close
- Wrap sharp metadata/decoding errors as 400 validation errors instead
  of 500 internal errors
- Only apply flatten() for JPEG output to preserve alpha channel in
  PNG and WebP outputs

AI-assisted-by: qwen3.7-plus

*  Add comprehensive tests for media-processor

Phase 1 - Cleanup verification:
- Add cleanup middleware unit tests (6 tests)
- Add HTTP upload cleanup integration tests (5 tests)

Phase 2 - Error handling & alpha preservation:
- Add sharp error wrapping tests (4 tests)
- Add HTTP malformed image tests (2 tests)
- Add alpha preservation tests (3 tests)

Phase 3 - Edge cases:
- Add upload storage edge case tests (3 tests)
- Add queue middleware edge case tests (4 tests)

Phase 4 - Backend mock verification:
- Fix backend mocks to include :mtype field in image info responses
- Verify all error codes match actual service behavior

Total: 27 new tests added (160 tests passing)

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 4)

- Add Zod validation constraints for config values (int, positive, min)
- Fix auth middleware to compare Buffer byte lengths instead of string lengths
- Validate requested output dimensions in generateThumbnail (crop mode)
- Change queue middleware to release slot via callback in finally block
- Add comprehensive tests for all fixes

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response streams in backend media remote

- Wrap stream consumption in try/finally with .close() calls
- Add tests to verify stream closure for info, font-convert, and thumbnail

AI-assisted-by: qwen3.7-plus

* 🐛 Fix queue slot leak on upload failures

Make releaseQueue idempotent and attach fallback listener to release
slot when response finishes. This covers Multer errors that bypass
the route handler's finally block, preventing permanent queue stall.

AI-assisted-by: qwen3.7-plus

* 🐛 Cancel processing on timeout

Create AbortController in timeout middleware and abort signal when
timeout fires. Pass signal to Sharp and FontForge to cancel ongoing
processing and release resources when request is cancelled.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 6)

- Error handler: check headersSent before writing response to prevent
  ERR_HTTP_HEADERS_SENT when timeout already sent 504
- Timeout config: increase default requestTimeout from 60s to 180s to
  match font processing timeout (120s) and backend request timeout
- Image processing: check abort signal before starting Sharp operations
  to cancel processing when timeout fires
- Queue lifecycle: remove res.on('close', release) fallback to hold
  queue slot until processing completes, preventing concurrency limit
  violation when client disconnects

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response stream in download-image

Wrap response body in with-open to ensure stream is closed after
writing to temp file, preventing HTTP connection leaks on repeated
URL imports.

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response stream on validation errors in download-image

Move with-open to wrap the entire validation and processing block,
ensuring the response body stream is closed even when validation fails
(non-2xx status, missing size, invalid media type). This prevents
HTTP connection leaks on repeated failed downloads.

Add test to verify stream closure on validation errors.

AI-assisted-by: qwen3.7-plus

* 🐛 Pass abort signal to Sharp toBuffer for timeout cancellation

Wrap Sharp's toBuffer() with Promise.race to check abort signal during
processing. This ensures large thumbnails stop processing when the
request times out, preventing wasted CPU/memory and queue capacity.

Add test to verify abort during toBuffer operation.

AI-assisted-by: qwen3.7-plus

* 🐛 Hold queue slot until Sharp completes and handle client disconnect

- Remove Promise.race from generateThumbnail — Sharp processing now
  completes fully before queue slot is released, preventing concurrency
  limit violations under timeout conditions
- Remove res.on("finish", release) fallback from queue middleware —
  error handler now explicitly calls releaseQueue in all error paths
- Add res.on("close") handler in timeout middleware to abort signal
  when client disconnects, ensuring processing stops early
- Add tests for client disconnect handling and queue slot lifecycle

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 9 review findings

- Document Sharp 0.35.3 cancellation limitation in image.ts
- Add integration test for timeout cleanup with large images
- Fix font tools (sfntToWoff, woffToSfnt, woff2ToSfnt) to throw
  ProcessingError on resource limit kills instead of returning null
- Validate font signatures for same-format conversions to prevent
  arbitrary files from being persisted as valid fonts
- Fix concurrent mkdtemp race in upload-storage by using shared
  initialization promise

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 10 review findings

- Add tmpdir assertion in font.ts to prevent path injection
- Preserve original error in queue middleware catch handler
- Change auth middleware response type from "internal" to "authorization"
- Add cleanup flag to prevent double cleanup in cleanup middleware
- Move quality clamping into parseQuality function for consistency
- Add integration tests for quality parameter clamping at route level
- Update existing tests to match new auth response type

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 11 review findings

- Extract releaseSlot helper in error-handler to reduce duplication
- Remove redundant try/catch in font.ts withTempDir cleanup
- Improve font path validation error message for clarity
- Move path validation before try/catch to prevent swallowing
- Add debug logging for cleanup failures in cleanup middleware
- Inline TransportTargetSpec type alias in logger.ts
- Extract logging middleware to separate file for consistency
- Remove duplicate MIME validation in image thumbnail route
- Add test for font path validation (outside tmpdir rejection)
- Add tests for error handler queue release across all branches

AI-assisted-by: qwen3.7-plus

* 🐛 Remove Content-Length header from multipart requests

The JDK's HttpClient rejects Content-Length as a restricted header,
causing IllegalArgumentException when sending multipart requests to the
media-processor. Remove the explicit Content-Length header and let the
JDK use chunked transfer encoding. The media-processor will use disk
storage for all multipart requests (safe default behavior).

Remove unused size computations (file-size, header-bytes, footer-bytes,
total-size) that were only used for Content-Length.

Update test to verify Content-Length is not present in request headers.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix pino ESM bundling for media-processor

Mark pino and its transports (pino-pretty, pino-loki) as external to
avoid bundling issues with worker thread modules that reference
__dirname (not available in ES modules).

AI-assisted-by: qwen3.7-plus
2026-08-05 09:41:48 +02:00
Andrey Antukh 3e59754a25 Add optional max-size param to blob decode functions
Accept an optional :max-size keyword argument in blob/decode and
blob/decode-str. When provided, the uncompressed size declared in the
blob header is validated before allocating memory, raising an error if
it exceeds the limit. Callers that do not pass :max-size are unaffected.

AI-assisted-by: deepseek-v4-pro
2026-08-05 08:38:55 +02:00
David Barragán Merino c16b7919f9 🐳 Remove the configuration of the admin-console from Nginx if it is not enabled 2026-08-04 20:33:31 +02:00
Andrey Antukh 83a3d099f6 🐛 Mock RPC and timer in composable test interpreter to fix network errors
The `SyncFromLibrary` op dispatches a real `sync-file` event that
schedules `rx/timer 3000` + an RPC call to
`update-file-library-sync-status`. In the headless test runner
(no backend), this produces a network error that leaks into test
output.

Wrap the `check` function in `mock/with-mocks` to mock `rp/cmd!`
(returning success) and `rx/timer` (firing instantly). This
eliminates the 3200ms grace period in `op-grace-ms` and prevents
the network error from appearing in test output.

AI-assisted-by: mimo-v2.5
2026-08-04 15:55:47 +00:00
Andrés Moya 8e713df5f0 🎉 Add repair functions for variant validation errors (#10768)
* 🎉 Add repair functions for variant validation errors

* 📚 Fix copyright notice
2026-08-04 16:53:36 +02:00
Filip SajdakandAndrey Antukh 3fba272848 🐛 Keep svg-raw children as uuids on binfile import (#10837)
Importing a .penpot file left every svg-raw subtree broken: the parent's
:shapes vector came back holding plain strings instead of uuids, so the
child ids no longer resolved against the page objects map. The next
persisted change touching that page then failed referential integrity
validation with :child-not-found, surfaced to the client as an HTTP 400
:referential-integrity error, which in practice bricks the file.

An svg-raw shape can be a container: importing an SVG builds a tree of
svg-raw shapes, and cfh/group-like-shape? explicitly treats an svg-raw
with children as group-like. But schema:svg-raw-attrs was an empty map.
Frame, group and bool all declare :shapes as a vector of uuid; svg-raw
did not, so the JSON decoder used by binfile had no type information for
those ids and left them as strings.

Declare :shapes on schema:svg-raw-attrs, optional because a leaf svg-raw
shape has no children, so the child ids decode back to uuids.
Closes #10496.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-04 16:49:33 +02:00
Andrey Antukh 23ea2bbad6 📎 Update creating-commits serena workflow file 2026-08-04 15:36:54 +02:00
Andrey Antukh b507a6b667 ♻️ Convert commiter agent to create-commit skill
Replace the commiter subagent with a create-commit skill,
consistent with the create-pr and create-issue skill patterns.

- Remove .opencode/agents/commiter.md
- Add .opencode/skills/create-commit/SKILL.md
- Update implement-plan.md to use the skill instead of
  subagent delegation
- Document commit body line wrapping at 72 chars in
  creating-commits memory and skill

AI-assisted-by: mimo-v2.5
2026-08-04 13:08:22 +00:00
Andrey Antukh 3865e29b65 ⬆️ Update pnpm dependencies across all modules 2026-08-04 14:15:35 +02:00
Luis de Dios 43e05c38bf 🐛 Fix error when clicking 'Start' button to finish onboarding after creating a new account (#11058) 2026-08-04 12:44:25 +02:00
María Valderrama edbe9f8215 🐛 Fix nitrate related literals (#11047) 2026-08-04 11:55:01 +02:00
Andrey Antukh 319a2185c9 🐛 Fix crash on drag-and-drop of selected text in Draft.js editor (#10959)
After `Modifier.removeRange` modified the block map, the `targetRange` from
the drop event still referenced stale block keys from the pre-removal DOM
state, causing `TypeError: Cannot read properties of undefined`.

- Added a guarded `moveText` export in `frontend/packages/draft-js/index.js`
  that validates block keys exist before and after removal. Falls back
  gracefully when target range references stale keys.
- Added a `handle-drop` callback in
  `frontend/src/app/main/ui/workspace/shapes/text/editor.cljs` that returns
  "handled" for internal drag operations, preventing Draft.js from calling its
  default (crash-prone) handler.

AI-assisted-by: mimo-v2.5
2026-08-03 17:31:55 +02:00
Andrey Antukh 1136e5eda5 🐛 Restrict update-profile-props to documented keys only (#10992)
Close the profile props schema to reject undocumented keys and add a
denylist for system-managed props like :subscription that should not be
user-writable via RPC.

Changes:
- Add system-managed-props denylist (#{:subscription})
- Close schema:props with :closed true
- Add tests for subscription rejection and valid key acceptance

AI-assisted-by: qwen3.7-plus
2026-08-03 17:27:09 +02:00
Andrey Antukh 49119e0339 ♻️ Rename nitrate config to admin-console (#10929)
* ♻️ Rename nitrate config to admin-console

Rename user-facing configuration from 'nitrate' to 'admin-console':
- Feature flags: :nitrate -> :admin-console, :nitrate-bulk-create-profiles -> :admin-console-bulk-create-profiles
- Config keys: :nitrate-shared-key -> :admin-console-shared-key, :nitrate-backend-uri -> :admin-console-uri
- Shared-keys map entry: :nitrate -> :admin-console (setup.clj + main.clj)
- Env vars: PENPOT_NITRATE_SHARED_KEY -> PENPOT_ADMIN_CONSOLE_SHARED_KEY, PENPOT_NITRATE_BACKEND_URI removed (consolidated into PENPOT_ADMIN_CONSOLE_URI)
- Docker/nginx: PENPOT_NITRATE_URI -> PENPOT_ADMIN_CONSOLE_URI

Code namespaces, file paths, CSS classes, and i18n keys stay as-is.

AI-assisted-by: mimo-v2.5-pro

* ♻️ Rename initialize-user-in-nitrate-organization to initialize-user-in-organization

Part of the nitrate -> admin-console rename series. The function and all 9 references across 6 files have been renamed.

* ♻️ Rename :nitrate-bulk-create-profiles-not-allowed to :bulk-create-profiles-not-allowed

* ♻️ Inline nitrate-permissions into app.common.types.organization

- Delete app.common.types.nitrate-permissions and its test
- Move permission rules (allowed?, can-send-invitations?, etc.) into organization.cljc
- Harmonize all consumers to use alias cto for app.common.types.organization
- Update test runner and create organization_test.cljc
2026-08-03 17:23:55 +02:00
Andrey Antukh 79da4d274d 📎 Fix linter issues 2026-08-03 14:40:36 +02:00
Andrey Antukh 2f535c3f3f 🐛 Prevent unexpected exception in swatch* component 2026-08-03 14:25:56 +02:00
Andrey Antukh c320cecf15 📎 Add better prompt for review opencode command 2026-08-03 14:25:19 +02:00
Luis de Dios 141cf7f79f 🐛 Fix main menu does not keep alignment when left sidebar is expanded (#10986) 2026-08-03 10:18:06 +02:00
Luis de Dios 767f90282c 🐛 Fix shape size badge is displayed twice when user with viewer permissions selects a shape (#10985) 2026-08-03 10:16:01 +02:00
Andrey Antukh eacdca3c0a Merge remote-tracking branch 'origin/develop' into staging 2026-08-03 09:16:26 +02:00
Andrey Antukh b72536d312 Merge remote-tracking branch 'origin/staging' into develop 2026-08-03 09:16:09 +02:00
Andrey Antukh d835baefec Merge remote-tracking branch 'origin/staging' 2026-08-03 09:15:03 +02:00
Andrey Antukh f023c09018 Add testing skill with TDD workflow enforcement
Minimal skill referencing mem:testing for full guidance, covering
TDD Red-Green-Refactor cycle, Prove-It pattern for bug fixes,
and key testing principles.

AI-assisted-by: mimo-v2.5-pro
2026-08-01 11:32:14 +00:00
Andrey Antukh 32520e66e5 Allow stopping ws0 independently of other workspaces
Remove the guard in stop-devenv that refused to stop ws0 while any
ws1+ instance was running. Each workspace is now fully independent
and can be started/stopped in any order. Shared infra shuts down
only when no instances remain running.

Updated docs (devenv.md, agentic-devenv.md) and devenv memory to
reflect the new behavior.

AI-assisted-by: mimo-v2.5-pro
2026-08-01 11:29:42 +00:00
Andrey Antukh 5ad0ff752e 📎 Add minor changes on backend package.json 2026-08-01 11:20:31 +02:00
David Barragán Merino 3fc5360df5 🔧 Optimize the bundle and docker images build process 2026-07-31 20:39:51 +02:00
Andrey Antukh b907bc4cd3 📚 Add hard rule for test output handling to serena memories
Never pipe test output directly to filters (head, tail, grep).
Always redirect to a file first to prevent hiding test failures.

AI-assisted-by: mimo-v2.5
2026-07-31 19:45:06 +02:00
Andrey Antukh 73519778ae 📎 Update serena memories 2026-07-31 19:31:45 +02:00
David Barragán Merino 97a833f1e7 🐳 Migrate penpot images to DHI (#10734)
Move Dockerfile.frontend, .backend, .exporter, .mcp and .storybook
under docker/images/ from ubuntu:26.04 / nginx-unprivileged / a
manual Node tarball install to Docker Hardened Images (Debian 13,
or Alpine for storybook). storybook and mcp get a true non-dev
runtime; frontend, backend and exporter keep the -dev tag as their
final image, since each needs a shell and/or package manager at
container runtime (nginx templating, fontforge/python3, and a
headless-browser stack, respectively).
2026-07-31 19:27:15 +02:00
Andrey Antukh e741313add Merge remote-tracking branch 'origin/staging' into develop 2026-07-31 19:21:07 +02:00
Andrey Antukh 896aef8c1b 📎 Update imagemagick version on manage.sh 2026-07-31 19:04:33 +02:00
David Barragán Merino 328fa0559a 🐳 Migrate imagemagick and devenv images to DHI
Move docker/imagemagick/Dockerfile and docker/devenv/Dockerfile from
ubuntu:26.04 to Docker Hardened Images (Debian 13 / trixie).
imagemagick gets a true non-dev runtime with its shared libraries
vendored via ldd; devenv keeps the -dev tag as its final image since
it's an interactive development container, not a production
artifact.
2026-07-31 19:04:33 +02:00
Anonymous 863671135d 🌐 Add translations for: French (Canada)
Currently translated at 95.7% (2329 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-31 19:03:01 +02:00
Alexis Morin 657b5bd94e 🌐 Add translations for: French (Canada)
Currently translated at 95.7% (2329 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-31 19:03:00 +02:00
Anonymous 5ea3a2efb8 🌐 Add translations for: Hindi
Currently translated at 81.1% (1974 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-07-31 19:02:58 +02:00
VKing9 94cfd6e60d 🌐 Add translations for: Hindi
Currently translated at 81.1% (1974 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-07-31 19:02:58 +02:00
Anonymous 600c573ccd 🌐 Add translations for: Swedish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-07-31 19:02:56 +02:00
AntonPalmqvist 4c388f6d8b 🌐 Add translations for: Swedish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-07-31 19:02:55 +02:00
Anonymous 4d07510581 🌐 Add translations for: Dutch
Currently translated at 94.4% (2297 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-31 19:02:53 +02:00
Stephan Paternotte 5e8a2b0a03 🌐 Add translations for: Dutch
Currently translated at 94.4% (2297 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-31 19:02:53 +02:00
Anonymous 588222d409 🌐 Add translations for: Latvian
Currently translated at 75.7% (1842 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-07-31 19:02:51 +02:00
Anonymous 0139c0f0e3 🌐 Add translations for: Korean
Currently translated at 81.5% (1984 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-07-31 19:02:49 +02:00
Denys Kisil 1b69d8c6a2 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 83.5% (2032 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-07-31 19:02:48 +02:00
Anonymous d7b55a245a 🌐 Add translations for: Italian
Currently translated at 86.5% (2105 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-07-31 19:02:46 +02:00
Anonymous fdeff910fe 🌐 Add translations for: Persian
Currently translated at 31.4% (765 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2026-07-31 19:02:44 +02:00
Anonymous 3fa21180fd 🌐 Add translations for: Hebrew
Currently translated at 83.0% (2020 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-31 19:02:42 +02:00
Yaron Shahrabani ecaf63b204 🌐 Add translations for: Hebrew
Currently translated at 83.0% (2020 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-31 19:02:42 +02:00
Anonymous 1074cb376d 🌐 Add translations for: Romanian
Currently translated at 78.3% (1906 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-07-31 19:02:40 +02:00
Anonymous 0aff047301 🌐 Add translations for: German
Currently translated at 79.8% (1942 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-07-31 19:02:38 +02:00
Anonymous 02b0f18a83 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 56.4% (1372 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-07-31 19:02:36 +02:00
DoubleCat 13b2954cbf 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 94.4% (2298 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-31 19:02:35 +02:00
Anonymous 0703a3fcc1 🌐 Add translations for: Turkish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-07-31 19:02:33 +02:00
Oğuz Ersen b294aecb7c 🌐 Add translations for: Turkish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-07-31 19:02:32 +02:00
Anonymous 620c26b304 🌐 Add translations for: Russian
Currently translated at 68.4% (1664 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-07-31 19:02:30 +02:00
Anonymous 1ac000bc27 🌐 Add translations for: French
Currently translated at 82.1% (1999 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-07-31 19:02:28 +02:00
Ingrid Pigueron 9042bd5129 🌐 Add translations for: French
Currently translated at 82.1% (1999 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-07-31 19:02:27 +02:00
Anonymous 905c230e17 🌐 Add translations for: Spanish
Currently translated at 95.4% (2321 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-07-31 19:02:25 +02:00
Hosted Weblate 552cda9aae 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-07-31 18:57:18 +02:00
Andrey Antukh 89029060e0 Merge remote-tracking branch 'weblate/develop' into develop 2026-07-31 18:56:46 +02:00
Andrey Antukh 07efd8fb99 ⬆️ Update dependencies (#10982)
* ⬆️ Update root pnpm depdendencies

* ⬆️ Update common pnpm dependencies

* ⬆️ Upgrade docs pnpm dependencies

* ⬆️ Upgrade pnpm deps on library

* ⬆️ Upgrade plugins pnpm dependencies

* ⬆️ Update pnpm dependencies on mcp

* ⬆️ Update pnpm dependencies on frontend

* ⬆️ Update exporter pnpm dependencies

* ⬆️ Update jvm/clojure deps

* ⬆️ Update docker dependencies (jvm and node)

* 📎 Add minor fixes
2026-07-31 18:53:05 +02:00
David Barragán Merino 31158c319a 🔧 Optimize the Docker image build process (avoid rebuilds, make better use of the cache) 2026-07-31 16:57:04 +02:00
Andrey Antukh ab1c70115b 🐛 Prevent MCP tokens from being used as access tokens (#10962)
MCP tokens now use a separate JWT issuer claim (`urn:penpot:mcp-token`) instead of `access-token`, preventing them from being validated as API access tokens.

Fixes #10960

AI-assisted-by: qwen3.7-plus
2026-07-31 13:19:45 +02:00
Eva Marco 9bb5861322 🐛 Fix undo delete pages change order (#10969) 2026-07-31 13:09:18 +02:00
Andrey Antukh d04cbf175e 🐛 Fix nil dereference crash during flex layout drag operations (#10845)
Production crash where @(get bounds id) threw
"No protocol method IDeref.-deref defined for type null"
when a shape ID had no corresponding entry in the bounds map
during layout calculations.

Added defensive nil guards (when-let / when) to all unprotected
bounds dereference sites:

- flex_layout/bounds.cljc: layout-content-points (parent + child)
  and layout-content-bounds
- grid_layout/bounds.cljc: layout-content-points and
  layout-content-bounds
- min_size_layout.cljc: child-min-width grid branch (3 sites) and
  child-min-height grid branch

Added 7 new tests in geom_bounds_layout_nil_test.cljc covering all
nil-bounds edge cases for flex, grid, and min-size layout paths.
Registered in runner.cljc.

Closes #10843

AI-assisted-by: qwen3.7-plus
2026-07-31 12:51:35 +02:00
Eva Marco 36553a7c6f 🐛 Fix reset cancel shortcut (#10980) 2026-07-31 12:51:01 +02:00
Luis de Dios ff32f104b9 🐛 Fix main menu is covered by the toolbar (#10926)
* 🐛 Fix main menu is covered by the toolbar

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of workspace context menu

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of tokens context menu

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of old context menu

* 📎 PR improvements
2026-07-31 12:48:59 +02:00
María Valderrama 1744d07731 Allow nitrate onboarding to be handled by admin-console (#10936)
*  Allow nitrate onboarding to be handled by admin-console

* 📎 Code review
2026-07-31 12:24:30 +02:00
Andrey Antukh 764b62906b 🐛 Handle unrecognized JSON escape sequences as malformed-json (#10808)
* 📎 Update serena documentation about creating-prs workflow

* 🐛 Handle unrecognized JSON escape sequences as malformed-json

When clojure.data.json's read-escaped-char encounters an unrecognized
escape sequence (e.g. a backslash followed by '}', or other case
fall-throughs in the parser) in a JSON request body, it throws a bare
IllegalArgumentException. Previously this fell through to the generic
RuntimeException branch in wrap-parse-request's handle-error, which
unwrapped and recurred without matching, eventually reaching the
internal-error handler and producing HTTP 500 + an error report — even
though the root cause was malformed client input, not a server bug.

The fix converts any IllegalArgumentException raised in the JSON parse
path into a `:validation`/`:malformed-json` error by raising a new
ex-info (which is caught by the top-level error handler in
`app.http/router-handler`). The result is an HTTP 400 response with a
descriptive hint, and no error report is generated. This addresses
~10% of all error reports received.

The new IAE branch is placed before the RuntimeException branch in
the cond (since IllegalArgumentException IS-A RuntimeException) and
uses the throw-style (ex/raise) to match the existing
RequestTooBigException / EOFException branches. A comment above the
handle-error cond documents why raising is intentional and is caught
by the top-level app.http error handler, not by the per-route
wrap-errors middleware.

Test suite changes:

- Extend the existing `DummyRequest` defrecord in
  `http_middleware_test.clj` from 2 fields to 12 fields, implementing
  every IRequest method, and add a private `make-dummy-request`
  constructor that accepts an options map with every key optional and
  sensible `:or` defaults. Future fields added to DummyRequest won't
  break existing call sites as long as the `:or` defaults are kept in
  sync.

- Remove the now-redundant `JsonRequest` defrecord and migrate all 11
  `->DummyRequest` call sites to `make-dummy-request`.

- Add 6 new deftest cases:
  - parse-request-illegal-argument-exception: malformed JSON body
    (containing `\}`) is converted to `:malformed-json`.
  - parse-request-request-too-big-exception: RequestTooBigException
    is converted to `:request-body-too-large`.
  - parse-request-eof-exception: java.io.EOFException is converted
    to `:malformed-json`.
  - parse-request-runtime-exception-with-cause: a wrapped
    RuntimeException recurses on ex-cause and dispatches to the
    matching specific branch.
  - parse-request-runtime-exception-without-cause: a bare
    RuntimeException falls through to errors/handle, returning 500
    with :type :server-error :code :unexpected.
  - parse-request-non-runtime-throwable: java.io.IOException (a
    non-RuntimeException Throwable) is handled by the dedicated
    handle-exception method, returning 500 with :code :io-exception.

Together, the new tests cover all 6 branches of wrap-parse-request's
handle-error cond.

Refs #10804.

AI-assisted-by: minimax-m3
2026-07-31 12:06:19 +02:00
Marina López 94f51afb20 ♻️ Normalize organization naming across Penpot (#10977) 2026-07-31 12:04:10 +02:00
Elena Torró a2231a8bf9 🐛 Fix text align shown as left in v3 editor (#10973) 2026-07-31 11:48:31 +02:00
Luis de Dios f5b17a5c75 🐛 Fix toolbar keyboard navigation (#10939)
* 🐛 Fix flyouts are not expanded using just the keyboard

* 🐛 Fix escape key does not close the flyouts
2026-07-31 11:25:46 +02:00
Luis de Dios e952d9c70a 🐛 Fix draw a line/arrow with a single click (#10974) 2026-07-31 11:25:20 +02:00
Luis de Dios 3fc1e6aadd 🐛 Fix image swatches displays a wrong format in list view (#10975) 2026-07-31 11:24:55 +02:00
Filip SajdakandAndrey Antukh 0b5db57def 🐛 Highlight first search result in font picker and fix Enter confirm (#10450)
When the user types a search term that filters out the currently-active
font, the picker showed no selection at all and Enter would close without
applying any font. Fix:

- Compute effective-selected (render-only, no state mutation) as the
  first filtered font when the current font is absent from the results.
  The row renderer and recent-fonts list use this for the tick mark, so
  the top match is visually pre-selected while the user types.
- Enter key applies first-result (via on-select then on-close) when the
  current selection is not in the filtered list; otherwise closes as
  before. No font is live-applied on every keystroke.
- on-key-down deps extended to on-select/on-close; effect deps include
  on-key-down so the global listener is always current.

Avoids the live-apply side-effect that caused the revert of #9512.
Fixes #3204.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-31 10:46:53 +02:00
Belén Albeza 7fa6631005 🐛 Fix collapsed fills section on text shapes (#10972) 2026-07-31 10:29:26 +02:00
Elena Torró 9b0be1c750 🐛 Fix crash when editing justified text in wasm editor (#10945) 2026-07-31 10:20:42 +02:00
Eva Marco fe85a5717e ♻️ Fixes on shortcuts (#10906)
* 🎉 Improve reset to default flow

* 🎉 Add diff modal

* 🐛 Fix measurement shortcuts

* 🎉 Separate open-sections for each tab

* 🐛 Align button link icon

* 🎉 Reset only works after press save

* 🐛 Fix little things

* 🎉 Make the import export row to be fixed

* ♻️ Fix CI

* 🐛 Cancel shortcut is not appearing on disabled tab

* 🐛 Fix tests

* 🐛 Fix loop on personalized

* 🐛 Fix show measurements shortcut
2026-07-30 16:56:10 +02:00
Pablo Alba 370e91c3e3 🐛 Fix bad error callback on nitrate checkout (#10886) 2026-07-30 16:50:47 +02:00
Marina López 31ccfa546f Add nitrate audit events 2026-07-30 15:32:45 +02:00
Belén Albeza 91d4b16171 🐛 Fix selrect not being recalculated after pasting properties (#10963) 2026-07-30 15:01:50 +02:00
Eva Marco c8867f8c3a 🐛 Fix registration mail color (#10965) 2026-07-30 14:51:32 +02:00
Dominik Jain 30943f1074 Make MCP tool call timeout configurable, raising default
The timeout for tool calls (which is trictly relevant for plugin tasks only)
is now configurable via env. var PENPOT_MCP_TOOL_TIMEOUT_S.

The default was raised from 30 to 120, because 30 seconds was not enough for
some calls, especially in larger Penpot files. #10953
2026-07-30 14:21:42 +02:00
Dominik Jain b4659df5b2 🐛 Preserve established plugin connection when rejecting a duplicate #10961
In multi-user mode, rejecting a second plugin WebSocket connection for an
already-registered user token performed the full removeConnection cleanup
for the newcomer. Since the token-keyed cleanup is keyed by token rather
than by socket, this deleted the clientsByToken entry and the Redis
request-channel subscription of the established, healthy connection. That
connection then remained open and heartbeating but was unroutable, so every
subsequent MCP tool call for the user failed although a valid plugin
connection existed.

removeConnection now performs the token-keyed cleanup only if the removed
connection actually owns the token registration, so rejecting a duplicate
releases only the resources the newcomer itself registered.

AI-assisted-by: claude-fable-5
2026-07-30 14:21:42 +02:00
Dominik Jain 1ae9334064 🐛 Fail fast on Redis task dispatch when no instance is connected #10958
In multi-user mode, plugin task requests are published to a Redis channel
keyed by user token. When no MCP server instance held a plugin connection
for that token (e.g. after the user navigated away from the workspace),
the publish reached zero subscribers and the request was silently dropped,
so every tool call stalled until the 30-second task timeout instead of
failing with a meaningful error.

RedisBridge.sendTaskRequest now returns the PUBLISH receiver count and
releases its response-channel subscription when the request reached no
receiver (or publishing failed), since no response can arrive. PluginBridge
uses the count to reject the pending task immediately with the multi-user
connection error message; publish failures likewise reject the task instead
of surfacing as an unhandled rejection followed by a timeout. The pending-
task settlement logic shared with the timeout handler is extracted into a
rejectPendingTask helper.

AI-assisted-by: claude-fable-5
2026-07-30 14:21:42 +02:00
014ec34249 🐛 Fix system crash on grid element deletion of a component (#10956)
* 🐛 Add regression test: main-side component edits break copy swap slots

Reproduces the :missing-slot referential-integrity failure ("Shape has been
swapped, should have swap slot") that crashes files with component copies.

Root cause: reordering or deleting a nested sub-head IN THE MAIN of a component,
while copies exist, does not propagate swap slots to the copies. find-near-match
matches a copy's sub-heads to the main's children by POSITION, so once the main's
order changes the copies' shape-refs no longer match their position and, lacking a
swap slot, fail referential-integrity validation.

- Copy-side edits are handled correctly (characterization tests, pass today).
- The two main-side tests fail today with :missing-slot and go green once the
  sync assigns swap slots to copies on a main reorder/delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 Fix integrity crashes from copy/main child-order divergence

Copy sub-heads were matched to their main's children purely by position
(find-near-match), while several code paths reorder or mutilate one side
only. Any of them made file validation fail with :missing-slot ("Shape
has been swapped, should have swap slot"), crashing the workspace on the
next validated commit, or persisting a corrupt file whose later edits
crash. Reproduced live: deleting a sub-head inside a copy (which only
hides it) and then reflowing the copy's grid moved the hidden (cell-less)
child to the front of :shapes, knocking every sibling out of its
positional slot.

Four fixes, one per divergence path:

- validate/check-required-swap-slot: a sub-head whose shape-ref is still
  a child of the near main parent is a REORDER (the component sync
  realigns it), not a swap; a swap slot is required only when the ref
  points outside the near main parent (a real swap). This matches how
  the sync engine itself pairs children (by shape-ref, not by position).
  comp-processors/fix-missing-swap-slots (migration 0019) is aligned:
  adding slots to merely-reordered sub-heads would freeze them out of
  normal synchronization.

- changes/:reorder-children now refuses to alter the child structure of
  component copies unless allow-altering-copies is set, mirroring the
  is-valid-move? rule of :mov-objects; that structure is owned by the
  component sync engine. Grid reflows emitted this change type with no
  guard. pcb/reorder-grid-children also skips copy grids producer-side.

- layout/reorder-grid-children keeps children that participate in no
  cell (hidden or absolute positioned) at their original index instead
  of lumping them at the front: moving them gratuitously changed their
  z-order and, in copies, broke the positional matching. Note :shapes
  stays reversed relative to the sorted cell order for in-cell children.

- logic/generate-delete-shapes: deleting shapes from inside a main
  (without deleting the main root, whose copies keep working against the
  deleted component) now also deletes the copy shapes that reference
  them, transitively (copies of copies) and across all pages of the
  file, so no dangling shape-refs remain. Skipped for
  allow-altering-copies flows (component swap replaces the shape and the
  sync reconciles copies via swap slots). Cross-page removals build
  redo/undo changes against that page's objects directly, since the
  changes-builder mounts only the current page; their undo mov-objects
  carry allow-altering-copies so restoring inside a copy is not rejected
  by the new guard.

The regression tests assert the fixed semantics: main-side reorders and
deletes keep copies valid, the previously crashing full chain (unvalidated
main reorder + later copy edit) stays healthy, :reorder-children cannot
scramble copies, and reorder-grid-children keeps cell-less children in
place. The namespace is now also registered in the JS test runner.

AI-assisted-by: Claude Opus 4.8 (1M context)

*  Extend composable slot cases to the grid-reflow crash sweep

Case D (CopySubheadDeletePreservesSlots) now sweeps which copy sub-head
is deleted (first or last) and whether the copy root is resized
afterwards, forcing a grid reflow: the reflow used to move the hidden
(cell-less) child to the front of the copy's children, shifting every
sibling out of its positional slot. Deleting the FIRST sub-head masked
the bug (moving it to the front is a no-op), which is why the case
passed before this sweep. The foundation layout is grid accordingly.

Case E (MainReorderKeepsCopySlots) no longer hangs the app now that a
main-side reorder leaves a valid file, so its warning docstring is
replaced: it runs as a routine test (verified headless, passing) and is
safe in a "run all".

SlotIntegrity's doc is updated to the new validator semantics (a slot is
required only for real swaps, not reorders); the positional alignment it
asserts remains the correct, stronger steady-state invariant for these
cases. The lockfile change materializes the playwright devDependency
already declared in package.json.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 📚 Record copy/main order-divergence invariants in memories

Swap-slot semantics (membership, not positional; slots only for real
swaps), the copy-structure guards on :mov-objects/:reorder-children, the
grid reorder stability for cell-less children, and the main-side delete
propagation in generate-delete-shapes.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 🐛 Add adjustements to the code

---------

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 13:25:03 +02:00
Eva MarcoandAndrey Antukh e705b6e12c 🎉 Add components library (#10675)
* 🎉 Install react-aria-components

* 🎉 Create modal component in TS using react-aria-component

* 🎉 Create modal ds component

* 🎉 Separate header content and footer components

* 🐛 Remove mf/html macros when not needed

* 🎉 Solve little problems

* ♻️ Format files

* ♻️ Remove ModalCloseBtn

* 🐛 Fix CI

* ♻️ Remove unused files

* 🎉 Make close button not dependant on the modal header

* 🎉 Add footer with two slots

* 🎉 Improvements on modal

* 🐛 Fix package imports and remove login example code

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-30 13:05:28 +02:00
972353eccb 🐛 Fix layout padding persisted as string after invalid input (#10758)
Combined fixes from PR #10656 (numeric-input redesign) and PR #10696
(global finite? guard) for issue #10638.

- Add (number? v) guard to cljs mth/finite? so strings are rejected
- Redesign numeric-input last-value* to store number, not formatted string
- Invalid-input fallback restores display without emitting on-change
- Esc now fully discards typed text (resets raw-value* + dirty flag)
- Token dedup by name instead of resolved value
- Defense-in-depth: d/parse-double at 4 padding/gap handlers
- Math tests (common), unit tests, Storybook play tests, Playwright E2E

AI-assisted-by: deepseek-v4-flash

Co-authored-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Ulises Millán <ulises.millanguerrero@gmail.com>
2026-07-30 12:51:44 +02:00
Andrey Antukh ef593514f2 🐛 Add nil guards on viewport-node in pixel overlay component (#10812)
* 🐛 Add nil guards on viewport-node in pixel overlay component

Add nil checks for viewport-node in process-pointer-move, viewport->canvas-coords, process-pointer-move-wasm, pick-color-at-wasm, and handle-draw-picker-canvas.

Fixes a crash ("can't access property 'getBoundingClientRect', ... is null")
when the viewport DOM node is unmounted while the color picker eyedropper
is active and pointer move events are still firing.

Fixes #10811

AI-assisted-by: mimo-v2.5

* 🐛 Remove unused app.common.pprint require from errors.cljs

Fixes clj-kondo warning: namespace app.common.pprint is required but never used.

AI-assisted-by: mimo-v2.5
2026-07-30 12:50:46 +02:00
Andrey Antukh 040080749b 🐛 Fix shape export failures when export name is nil or empty (#10852)
Use cuerdas blank-name handling directly when normalizing frontend export
payloads. Replace nil or blank export names with the object-id string in
request-simple-export, request-multiple-export, clipboard export, and plugin
direct export payloads so that the backend always receives a valid name. Add
focused frontend tests for nil/blank name normalization and normalized request
params.

AI-assisted-by: nex-n2-pro
2026-07-30 12:49:19 +02:00
Andrey Antukh fadb3124a0 🐛 Clamp gradient stop offsets to valid range (#10881)
* 🐛 Clamp gradient stop offsets to valid [0, 1] range

Fixed a bug where gradient stop offsets outside the valid [0, 1] range were being sent to the server, causing schema validation errors ('invalid shape found').

Changes:
- Viewport gradient handler: clamp offset in points-on-pointer-down before creating new stops
- Colorpicker gradient preview: clamp offset in handle-preview-down before adding stops
- Data layer: clamp offset parameter in update-colorpicker-add-stop and all stop offsets in update-colorpicker-stops; added app.common.math require

All clamping follows the existing pattern used in handle-marker-pointer-move.

AI-assisted-by: deepseek-v4-flash

* 💄 Fix formatting in gradient handlers

Fix cljfmt formatting issues in gradient handler functions.

AI-assisted-by: qwen3.7-plus
2026-07-30 12:49:00 +02:00
Pablo Alba 3eb50ef50e 🐛 Fix nitrate ignores sso token expiration (#10921) 2026-07-30 12:40:47 +02:00
Elena Torró a722a33503 🐛 Fix gradients and text selection (#10941)
* 🐛 Add missing text fills

*  Keep text leaf selected while editing
2026-07-30 12:11:53 +02:00
Pablo Alba 03f9393200 🐛 Fix nitrate sso doesn't notify workspace on activation (#10955) 2026-07-30 11:56:06 +02:00
Belén Albeza be5d9f9e9c 🐛 Fix invalid shape due to missing fonts (#10942)
* 🐛 Fix setting a font-related properties to nil

*  Repair already-corrupted text nodes

* 🐛 Fix deleted fonts appearing in Recents and as the auto-selected font for new text shapes
2026-07-30 11:18:19 +02:00
Luis de Dios 2cb8813862 Remember expanded/collapsed state of token sets in the colorpicker (#10864) 2026-07-30 10:47:11 +02:00
Luis de Dios 69649cd608 🐛 Fix tick icons are not aligned in font selector (#10774) 2026-07-30 10:00:52 +02:00
Luis de Dios e5d588ca54 🐛 Fix inputs broken when there are tokens on them (#10905) 2026-07-30 09:59:11 +02:00
Alejandro Alonso 485cb8e5ec Merge remote-tracking branch 'origin/staging' into develop 2026-07-30 09:04:31 +02:00
Andrey Antukh 25618febcd Merge remote-tracking branch 'origin/main' into staging 2026-07-30 08:46:52 +02:00
Andrey Antukh 5e5465a0fe 🐛 Fix audit event validation for error reports with string profile-id (#10898)
The audit event validation was failing when processing error reports that
contain string profile-id values. The error report storage converts
profile-id to string format, but the audit schema expects a UUID.

Changes:
- Modified prepare-rpc-event to convert string profile-id to UUID using
  uuid/parse* (exception-safe parsing)
- Updated access token middleware to set ::id and ::type on request so
  audit context includes token identification
- Added tests for profile-id conversion and token context population

Closes #10897

AI-assisted-by: qwen3.7-plus
2026-07-30 07:32:02 +02:00
Andrey Antukh 0ed151d2ca Merge remote-tracking branch 'origin/staging' into develop 2026-07-29 19:55:33 +02:00
Andrey Antukh adda7e6645 📚 Update testing serena memories 2026-07-29 19:55:12 +02:00
Belén Albeza 3e7fb07931 🐛 Fix undo crash after deleting a word (#10862) 2026-07-29 15:53:41 +02:00
Alejandro AlonsoandCursor c8d1f5b397 🐛 Guard finalize-view-interaction! against spurious pointerup events (#10917)
Every pointerup unconditionally fires finish-panning and finish-zooming,
which call finalize-view-interaction!. This triggered internal-render
(and reset_canvas) on plain clicks — causing a visible white flash on
large viewports or weak GPUs.

Add a guard so finalize-view-interaction! only runs when a view
interaction (pan/zoom) is actually active.

Fixes #10915

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 11:43:29 +02:00
Marina López 056cd3d379 🐛 Fix SSO review 2026-07-29 11:26:16 +02:00
Alejandro Alonso 4cd34b22e2 🐛 Fix wasm context restore guards (#10824)
* 🐛 Prevent WASM panics during WebGL context restore

Delay context-restored until reload finishes and no-op app
mutations while reloading so resize/modifiers cannot panic
mid-teardown.

* 🐛 Fall back to CLJS bool content when WASM is not ready

Returning nil from calculate-bool would persist empty path content
during context loss/reload; use path/calc-bool-content instead.
2026-07-29 10:06:52 +02:00
Eva Marco e14e7bb616 ♻️ Refactor and remove deprecated css (#10698)
* ♻️ Fix Alert component

* ♻️ Fix auth files

* ♻️ Fix comment files

* ♻️ Fix confirm files

* ♻️ Fix dashboard files

* ♻️ Fix settings files

* ♻️ Fix static files

* ♻️ Fix color bullet file

* ♻️ Fix color workspace file

* ♻️ Fix color common file

* ♻️ Fix color recovery request file

* ♻️ Fix color recovery file

* ♻️ Fix disabled login buttons

* 🐛 Fix button link page

* ♻️ Fix format
2026-07-29 09:54:32 +02:00
David Barragán Merino 66201ac437 🐳 Add --tag/--push flags to docker image build commands (#10732)
Local-only builds by default for build-devenv and
build-imagemagick-docker-image; --push is now required to build
multi-platform and push to the registry. All release-image build
commands (frontend, backend, exporter, mcp, storybook) now accept
--tag to override the image tag. Adds a shared DEVENV_TAG variable
threaded through pull-devenv, the production build function and
docker-compose.main.yml so a custom devenv tag can be used
end-to-end.
2026-07-29 08:47:08 +02:00
Pablo Alba 70675919c7 Add link to download a nitrate activation code request (#10900) 2026-07-29 08:29:05 +02:00
David Barragán Merino 9f92206a1c 🔧 Synchronise Admin Consoel's Docker image build process 2026-07-28 18:43:28 +02:00
Belén Albeza 704cd40182 🐛 Fix caret not mimicking text color (#10866) 2026-07-28 16:25:47 +02:00
Alonso Torres cb21f6401a Add waitForLayoutUpdate plugin method (#9898)
*  Add waitForLayoutUpdate plugin method

*  Refactor internal wait for tasks
2026-07-28 14:44:30 +02:00
Luis de Dios 0999bc31b2 🐛 Fix disabled/mixed-value state when per-side stroke is expanded/collapsed (#10874) 2026-07-28 13:28:16 +02:00
Andrey Antukh 343865cf27 🐛 Fix issues with ci script 2026-07-28 13:08:29 +02:00
Andrey Antukh e9603f00b0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-28 12:50:41 +02:00
Andrey Antukh bbc7e9bee9 Add a script for run ci-like tasks 2026-07-28 12:47:33 +02:00
Andrey Antukh 65111195a9 📎 Fix missing require 2026-07-28 12:39:31 +02:00
Andrey Antukh 6bbd3fa364 📎 Fix syntax error introduced in prev merge 2026-07-28 12:18:06 +02:00
Elena Torró d4d22ff1a4 🔧 Prepare wasm render for headless exporter (#10795)
* 🔧 Support background blur on PDF render for strokes and text shapes

*  Add LRU eviction to the wasm image store

*  Add RasterFormat to encode png, jpeg and webp from wasm

* 🐛 Fix client-side wasm export encoding jpeg and webp as png
2026-07-28 11:54:22 +02:00
Marina López 0c02121d5d 🐛 Fix alignment from organization and team selector 2026-07-28 11:16:48 +02:00
Marina López 7674229a5b 🐛 Fix bullets position for subscription list 2026-07-28 11:16:48 +02:00
Dr. Dominik Jain af00860c13 🐛 Report the effective UI theme to plugins (#10677)
Penpot itself and plugins used different default theme selection
strategies when no theme is explicitly selected in Penpot by the user:

- Penpot itself defaults to dark.
- Plugins defaulted to whatever is configured in the user's
  system/browser, which could be light.

So with no explicit theme chosen in the profile — the common state —
every plugin could be told the theme was light while Penpot itself
showed dark, both on open (penpot.theme) and on themechange. For every
explicit choice (light, dark, system, and the legacy default) the two
resolutions already agreed; only the unset case diverged.

Extract the app's resolution into app.util.theme/resolve-theme (system
follows the system theme; default and unset mean dark) as the single
source of truth, and use it in the app's own use-initialize as well as
in the plugin runtime's getTheme and themechange handling. The
themechange handler's now-dead default-to-dark remapping is removed.

Fixes #10676

AI-assisted-by: claude-fable-5
2026-07-28 11:09:17 +02:00
Andrey Antukh 4b994d20aa 🐛 Fix leaked focus timers in dashboard sidebar navigation (#10715)
* 🐛 Fix leaked deferred DOM ops on dashboard navigation and template clone

The React reconciliation "removeChild" error surfaced during rapid
dashboard navigation because several effects scheduled deferred DOM
operations (focus, CSS positioning) without returning a cleanup that
cancelled them. When the component unmounted before the callback
fired, it ran against stale DOM and desynchronized React fiber tree
from the actual DOM.

- context_menu_a11y.cljs: replace tm/schedule-on-idle (30s idle
  window) with tm/schedule (setTimeout 0) and return a rx/dispose!
  cleanup.
- dropdown.cljs: capture the tm/schedule handle and dispose it in
  the effect cleanup.
- tooltip.cljs: capture the ts/raf handle and cancel it on cleanup.

AI-assisted-by: opencode-go/mimo-v2.5-pro

* 🐛 Fix leaked focus timers in dashboard sidebar navigation

Six sidebar navigation handlers scheduled setTimeout callbacks to mutate
tabindex/focus on React-managed title elements without cancelling prior
pending callbacks. During rapid keyboard navigation (Projects→Fonts→Libs→Drafts)
the stale callbacks fired against unmounted DOM, desyncing React fiber tree
and triggering "removeChild" NotFoundError.

- sidebar-project*: cancel prior timer in on-key-down
- sidebar-search*: cancel prior timer in on-key-press
- sidebar-content*: cancel prior timer in go-projects-with-key,
  go-fonts-with-key, go-drafts-with-key, go-libs-with-key

Each handler now stores the timer handle in a component-level ref and
disposes any pending handle before scheduling a new one.

AI-assisted-by: opencode-go/mimo-v2.5-pro

* ♻️ Refactor sidebar focus timer handling into helpers

Extract the repeated dispose-before-schedule focus idiom into
schedule-focus-by-id! (sidebar.cljs) and focus-and-untabbable!
(app.util.dom). Replaces the six duplicated blocks and adds
mf/use-effect unmount cleanup to dispose any pending timer in the
three sidebar components, closing the remaining leak noted in the
original fix.

AI-assisted-by: opencode/hy3-free

* ♻️ Extract use-focus-timer-ref hook for sidebar components

Replace the duplicated mf/use-ref + mf/use-effect cleanup pairs in
sidebar-project*, sidebar-search*, and sidebar-content* with a shared
use-focus-timer-ref hook (app.main.ui.hooks). The hook creates the ref
and disposes any pending timer on unmount via mf/with-effect, reading the
ref with mf/ref-val instead of deref. mf/use-effect is now a body-level
hook call rather than a let binding.

AI-assisted-by: opencode/hy3-free

* 📎 Add pr feedback fix
2026-07-28 11:06:00 +02:00
Andrey Antukh 7eb188b077 🐛 Fix text content repair and remove blast-radius containment (#10731)
Repair text shapes with empty/broken content at all three levels
(root, paragraph-set, paragraph) in migration 0025 to prevent
workspace update failures. Handle all corner cases: nil/empty/non-
vector children, non-map items, wrong types. Remove geometry-only
validation skip in changes.cljc so all shapes are validated.

AI-assisted-by: qwen3.7-plus
2026-07-28 11:00:53 +02:00
Andrey Antukh 535ccfb930 Merge remote-tracking branch 'origin/staging' into develop 2026-07-28 11:00:47 +02:00
Andrey Antukh 6c2b61e1ad 🐛 Fix several issues in RPC command handlers (#10670)
* 🐛 Fix several issues in RPC command handlers

- Reject circular library references in link-file-to-library
- Add explicit team permission check in search-files
- Constrain search-term max length to 250 chars
- Include :deleted-at in file ETag for COND caching
- Move storage I/O outside DB transaction in create-file-thumbnail

AI-assisted-by: deepseek-v4-pro

* 📎 Check perms before circular link checks

* 🐛 Handle circular library reference error

Catch :circular-library-reference error from backend when linking
files to libraries. Show user-friendly toast notification instead of
propagating unhandled error. Add English and Spanish translations.

AI-assisted-by: qwen3.7-plus
2026-07-28 10:59:16 +02:00
Andrey Antukh 458fa41036 Merge remote-tracking branch 'origin/main' into staging 2026-07-28 10:42:03 +02:00
Alejandro Alonso 6063a45c3c 🐛 Fix area selection aborted by select-shapes interrupt (#10870)
* 🐛 Fix area selection aborted by select-shapes interrupt

Only emit :interrupt from select-shapes when edition mode is active.
Unconditional :interrupt (from #10798) made drag-stopper cancel the
marquee mid-drag.

* 🔧 Fix text editor v2 fill e2e test on develop
2026-07-28 10:26:31 +02:00
Juanfran 68dccd6ac2 Degrade removed org owners to viewer instead of kicking them out
An organization owner keeps read-only access to the teams of their
organization even when they are not a member, so removing them from a
team was navigating them away from content they are still allowed to
see, and they could walk right back in through the URL.

Publish a :team-role-change to :viewer for them instead of a
:team-membership-change, which reuses the existing real-time role
transition on both the dashboard and the workspace. Any other member is
notified as before.

Signed-off-by: Juanfran <juanfran.ag@gmail.com>
2026-07-28 08:26:08 +02:00
María Valderrama 678e206fe6 🐛 Fix team last_activity_at calculation (#10854) 2026-07-27 15:31:56 +02:00
Andrey Antukh 63e0c536f0 Align event names column in format-last-events output
The third column (event name) in error report "last events" now starts at
a consistent position regardless of the delta value, by right-padding the
delta string to 10 characters. The first event always shows (+0ms).

Adds tests for empty, single, multi-event, and column alignment cases.

AI-assisted-by: deepseek-v4-flash
2026-07-27 13:24:11 +00:00
Alejandro Alonso ff16b8bbef 🔧 Fix text editor v2 fill e2e test on develop (#10848) 2026-07-27 13:25:35 +02:00
Andrey Antukh af120feb1f 🐛 Fix workspace crash and cleanup viewport_ref event/resize handling (#10721)
Replace `globals/document` and `globals/window` with
`js/document` and `js/window` in workspace.cljs, removing
the unused `app.util.globals` import. This avoids "can't
access dead object" errors in Firefox when navigating between
pages/files, matching the existing pattern used in
viewport/hooks.cljs.

Fix a leaked MOUSELEAVE listener in viewport_ref.cljs — the
ref callback added a new listener on every mount but never
unregistered the previous one. Now uses standard
.addEventListener/.removeEventListener with a React ref to
track the handler for proper cleanup.

Fix ResizeObserver cleanup in viewport_ref.cljs —
`init-observer` is now a private function that only creates
an observer when a node is provided, and cleanup is handled
via the ref callback on unmount.

AI-assisted-by: mimo-v2.5-pro
2026-07-27 12:11:12 +02:00
Andrey Antukh 5a0cee44b1 🐛 Harden frontend .getData call sites against undefined receivers (#10718)
* 🐛 Fix nil getData crash dropping ZIP without manifest.json

Add nil-guard in read-as-text to raise typed :invalid-entry error instead of calling (.getData nil writer) which produced a raw TypeError.

Made read-zip-manifest public (was defn-) with explicit detection of missing manifest.json, raising typed :invalid-penpot-file validation error. The existing catch path surfaces this hint as a friendly user error instead of the raw TypeError text.

Add regression tests for both paths. 374 users were affected, 704 occurrences across 2.17.0-RC2/RC3/RC4.

Fixes #10709.

AI-assisted-by: minimax-m3

* 🐛 Harden dnd/get-data against missing dataTransfer

When the sortable hook or any caller passes a synthetic event
without a dataTransfer property (e.g. a dragend fired after a drop
that has already cleared the transfer), the previous implementation
called .getData directly on the nil/undefined result and threw
"Cannot read properties of undefined (reading 'getData')".

Wrap the body in when-let so get-data returns nil cleanly when
dataTransfer is missing. All three current callers
(hooks.cljs:164, viewport/actions.cljs:531 and :562) already treat
the return value as optional via when-let / when, so no caller
breaks.

Add a regression test covering both the missing-dataTransfer case
and a real dataTransfer roundtrip.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in forms

When a paste event arrives without a clipboardData property (e.g.
a programmatically dispatched ClipboardEvent in some browsers, or
edge cases like dragging a file with no text content), the previous
implementation called .getData directly on the nil/undefined
clipboardData and threw "Cannot read properties of undefined
(reading 'getData')".

Wrap the body in when-let so the paste logic is skipped entirely
when clipboardData is missing. The existing (string? paste-data)
guard in the inner when already tolerates nil; no other caller
behavior changes.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in components/forms

Same defensive pattern as the main/ui/forms.cljs paste handler: wrap
the body in when-let so the .getData call is skipped when the
clipboardData property is missing on the paste event. Prevents the
raw "Cannot read properties of undefined (reading 'getData')"
TypeError for programmatic / edge-case paste events.

AI-assisted-by: minimax-m3

* 🐛 Harden v3 text editor paste and styles-fn against undefined receivers

Two related fixes for the "Cannot read properties of undefined
(reading 'getData')" family of bugs in the workspace text editor:

- v3_editor.cljs: wrap the paste body in when-let on clipboardData
  so .getData("text/plain") is never called on a nil receiver. The
  existing (when (and text (seq text))) guard already tolerates nil
  text; only the outer .getData call was unprotected.

- editor.cljs: add (and content ...) to the if branch in styles-fn
  so .getText and .getData are never called on a nil content. The
  else branch (legacy.txt/styles-to-attrs) is already the correct
  fallback for missing content.

Add a regression test that mirrors the fixed patterns and verifies
they no longer throw on synthetic events with no clipboardData or
nil content.

AI-assisted-by: minimax-m3

* 🐛 Harden get-editor-block-data and get-editor-block-type against nil block

getCurrentBlock from Draft.js can return undefined for an empty
selection (e.g. before any block is created). The previous
implementations called .getData / .getType directly on the result
and threw "Cannot read properties of undefined (reading
'getData')" / "...reading 'getType')".

Wrap both functions in (when (some? block) ...) so they return nil
cleanly. Callers in editor.cljs and text_editor.cljs already handle
nil results (render-block short-circuits via the case on type; the
text-data caller in text_editor.cljs lets nil flow up), so no
upstream change is required.

Add a regression test covering both functions with nil and
js/undefined input.

AI-assisted-by: minimax-m3

* 🐛 Harden draft-js block-data helpers against nil block

Three related fixes in the vendored draft-js package:

- mergeBlockData: early-return undefined when block is falsy.
  Without this, the first line (block.getData()) throws for callers
  that pass a nil block.

- splitBlockPreservingData: guard the blockMap.get(...) lookup. If
  the start key is stale (e.g. after a Modifier.splitBlock that
  doesn't actually produce the expected key), .get() returns
  undefined and the subsequent .getData() throws. Fall back to an
  empty Immutable Map for the block data.

- updateBlockData: short-circuit (return state unchanged) when
  mergeBlockData returns undefined. Without this, the chain
  newBlock.getData() would throw on the same nil-block case that
  mergeBlockData now guards.

These match the defensive nil-handling pattern used elsewhere in
the frontend (.getData callers) and protect against stale
selection keys in the Draft.js content state.

AI-assisted-by: minimax-m3
2026-07-27 10:56:27 +02:00
Luis de Dios 5ce206f904 🐛 Fix margin input order is inconsistent with padding input order (#10797) 2026-07-27 10:52:14 +02:00
Luis de Dios af51c897a8 🐛 Fix change defective onboarding image (#10783) 2026-07-27 10:51:26 +02:00
Andrey Antukh 44c15480f2 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2026-07-27 10:48:54 +02:00
Andrey Antukh f60c82300a Merge remote-tracking branch 'origin/staging' into develop 2026-07-27 10:46:25 +02:00
Andrey Antukh aad9bc8f65 📎 Fix playwright version 2026-07-27 10:45:59 +02:00
Elena Torró e17f0ea7ee 🐛 Fix selrect misplacement on grid and flex layouts (#10832)
* 🐛 Fix selrect misplacement on grid and flex layouts

* ♻️ Refactor clear-transform-preview to use it from both ui and data layers
2026-07-27 10:25:05 +02:00
María Valderrama bb8f965cf0 🐛 Hide Organizations header/divider when org list is empty (#10833) 2026-07-27 09:54:04 +02:00
Dr. Dominik Jain a749763fb1 ⬆️ Upgrade plugin-types and plugin-styles to v1.5.0 in MCP plugin (#10830)
The upgrade allows ts-ignore annotations and manually declared type information
to be removed.
2026-07-27 09:10:04 +02:00
Andrey Antukh 2f8d71e143 Merge remote-tracking branch 'origin/staging' into develop 2026-07-27 09:02:56 +02:00
Andrey Antukh 0b072b22b0 📚 Update changelog 2026-07-27 09:01:43 +02:00
Andrey Antukh 170d129a5a Merge remote-tracking branch 'origin/staging' 2026-07-27 08:52:50 +02:00
Andrey Antukh eda5aa76f0 Merge remote-tracking branch 'origin/main' into staging 2026-07-27 08:52:31 +02:00
Andrey Antukh b54c1f316a Add minor improvements for error report script 2026-07-25 09:56:38 +02:00
María Valderrama 66bb6a11df 🐛 Make organization logos publicly accessible for invitation emails (#10831) 2026-07-24 14:08:41 +02:00
María Valderrama 62880cb58c 🐛 Fix downgrade to Professional bypassing warning modal (#10814)
* 🐛 Fix downgrade to Professional bypassing warning modal

* 📎 Code review
2026-07-24 14:08:11 +02:00
Elena Torró 8142f4949e 🔧 Revert stroke to path flag deletion and enable it by default (#10827) 2026-07-24 13:40:47 +02:00
c10c7b08aa 🐛 Fix radial gradient handles on rotated ellipses (#10666)
The handle transform composed rotation incorrectly, so handles blew out in size when an ellipse was rotated.

Fixes #10069

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-24 13:29:37 +02:00
Belén Albeza 56c3e9777e 🐛 Fix blinking when zooming in / out in the text editor (#10816) 2026-07-24 13:10:31 +02:00
8b485b4a8b 🐛 Preserve token references when copying and pasting properties (#10665)
Copy/paste of properties resolved tokens to their values, dropping the reference. Carry the token with the value it resolves, at sub-attribute granularity for map-valued attrs.

Fixes #9582

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-24 13:08:01 +02:00
Eva Marco d94b139071 🐛 Fix text edition state when relesecting (#10798)
* 🐛 Fix text edition state when relesecting

* 🐛 Fix CI
2026-07-24 12:15:16 +02:00
Marina López 3610b81e4b 🌐 Localize Nitrate subscription flows 2026-07-24 11:02:58 +02:00
Eva Marcoandalonso.torres 5de06e8f77 🎉 Add customizable shortcuts (#10237)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 10:36:32 +02:00
Dr. Dominik Jainandalonso.torres caacd482af Add composable test framework plugin (applied to component tests, runs in CI) (#10679)
*  Add plugin with composable test framework and component tests

The plugin provides a framework for writing composable tests against
the Plugin API, and applies it to systematic end-to-end testing of
component semantics.

The framework's core ideas: a test is written once as a composition of
operations over a starting configuration; choice points among the
operations (optional steps, alternatives) expand the composition into
a full sweep of test variants, so a single case definition yields
broad combinatorial coverage; and the operations drive the real Plugin
API with real change propagation, testing the full production
implementation.

The initial application is a suite of component test cases covering
synchronization, overrides, swap slots and variants — the
TypeScript/e2e continuation of the ClojureScript composable test suite
(frontend_tests.composable_tests). Several cases originate from
reproducing real defects (e.g. #10109 and the swap-slot corruptions).

Tests run from an interactive panel in Penpot: cases are listed with
plain-language descriptions, tests can be run selectively, results
stream in live, and every checkbox carries a stable DOM id — so the
panel can equally be driven programmatically (the basis for running
the suite in CI), as documented in the plugin's README.

Lives at plugins/apps/composable-test-suite as a regular member of the
plugins workspace (init script, start:plugin:composable-test-suite,
shared dev port 4202, covered by build:plugins via the new
./apps/*-test-suite filter).

Related to #10584.

AI-assisted-by: claude-fable-5

*  Run the composable test suite headlessly in CI

Adds a headless run mode for the composable test suite, following the
plugin-api-test-suite's CI architecture, and a workflow that runs it as
a per-PR gate.

An in-sandbox entry (src/ci/headless.ts) runs the suite without the
panel UI — the framework's runner was UI-free by construction, so no
refactoring was needed — and streams each result through console
markers, addressed by the same composite identifiers the panel uses
(e.g. MainEditSyncs-2), with durations and, on failure, the error and
the applied-steps transcript. It is built as a single self-executing
bundle and evaluated directly inside a real Penpot plugin sandbox by
the driver (ci/run-ci.ts), so no plugin dev server or port is involved.

The driver needs no backend and no login: it serves the prebuilt
frontend bundle via the frontend e2e static server and intercepts every
backend RPC with Playwright fixtures. The mocked backend is not a
limitation for this suite — everything it asserts is frontend store
logic executed in memory — which the full run confirms: all 48 tests
behave identically to the interactive panel, including variants and
swap slots, with the single (currently expected) failure of
MainEditSyncs-2 reproducing bug #10109 under the mock.

TEST_FILTER selects tests by identifier substring; CI_TIMEOUT_MS bounds
the run. The mock harness mirrors the frontend e2e harness (see the
provenance note in the driver).

Related to #10584.

AI-assisted-by: claude-fable-5

* 📚 Restructure the composable-tests memory around both suites

Present the composable component tests top-down: the shared framework
principles upfront, then the two implementations — the ClojureScript
suite in the frontend test tree and the TypeScript suite in the plugin,
which tests fully end-to-end with a slightly more elaborate set of
abstractions — and the plugin's headless CI run, pointing to the
plugin's README for operational details. Also records this session's
additions (geometry operations, case N, the CI harness).

AI-assisted-by: claude-fable-5

* 📎 Refine the PR-description conventions in the creating-prs memory

Encourage digestible descriptions: bullet items over prose (grouped by
area with bold lead-ins for larger PRs) and no manual line wraps, since
the rendered markdown adapts to the viewport. Also drop the outdated
'MCP' from the standard Note line.

AI-assisted-by: claude-fable-5

* 🔧 Set Prettier endOfLine to auto in plugins workspace

Prettier defaults to endOfLine "lf", which is incompatible with
checkouts on Windows that use core.autocrlf=true

* 🐛 Fix problems with suite

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 09:59:18 +02:00
Belén Albeza 4f46700c94 🐛 Fix text editor v3 blinks (#10654) 2026-07-24 09:14:59 +02:00
Andrey Antukh c467d98c9e Merge remote-tracking branch 'origin/staging' into develop 2026-07-23 17:06:56 +02:00
Andrey Antukh d5d6c61ba4 💄 Remove unused app.common.pprint import in errors.cljs
AI-assisted-by: deepseek-v4-flash
2026-07-23 15:05:18 +00:00
ParinithandAndrey Antukh 6e85ce5d7a 🐛 Center canvas and select layer when navigating search results (#10453)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 15:47:57 +02:00
Andrey Antukh f7c312021b Improve error-reports CLI with streaming, time-range, and stats
Server changes:
- Switch list ordering from DESC to ASC (oldest first)
- Flip cursor direction to > for forward pagination
- Add 'until' param for server-side upper-bound filtering

CLI changes:
- Add --from/--to flags mapping to server's since/until
- Streaming output for --all and --format ndjson
- Add --format ndjson option (one JSON object per line)
- Add --normalize-hints flag to strip dynamic values
- Add --output flag to write list results to file
- Add 'stats' subcommand with aggregations (signature, host,
  tenant, version, source, kind, hour) reading from API, file, stdin
- stats input supports JSON, JSON array, and NDJSON formats

Test changes:
- Fix pagination assertions for ASC ordering

AI-assisted-by: mimo-v2.5-pro
2026-07-23 13:35:09 +00:00
Lucas OzdemirandAndrey Antukh 3fa7943344 🐛 Allow SVG files for image fills (#10771)
Use Penpot's shared image type list for image fill uploads and add an integration test covering SVG file selection.

AI-assisted-by: gpt-5.6-sol

Signed-off-by: Lucas Ozdemir <lulu.58@outlook.fr>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:32:06 +02:00
19bdd7081b 🐛 Do not apply the new-password policy to the old-password field (#10661)
The change-password form validated the existing password against the 8-character policy, locking out accounts whose current password predates it.

Closes #10626

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:31:38 +02:00
Andrew CunliffeandAndrey Antukh ed85d1d1af 🐛 Fix color token sets rendering in ascending-precedence order (#10658)
The Color tokens picker showed token sets in their raw definition
order (ascending precedence), so the lowest-precedence set appeared
first and the highest-precedence (last-defined, winning) set
appeared last. This is the opposite of what's useful: users care
most about which set is currently winning, so that one should be at
the top.

get-sets returns sets in definition order and the picker's
grouped-tokens-by-set pipeline (add-tokens-to-sets ->
filter-active-sets -> filter-non-empty-sets -> group-sets ->
combine-groups-with-resolved) preserves that order at every step, so
the picker just rendered get-sets' raw order. Reverse the set seq
once, before it enters the pipeline, so the highest-precedence set
renders first.

group-sets groups sets by parent path via group-by, which risked
restoring definition order within a subgroup independent of the
reversed input order. Added tests covering a flat set list, a
reversed subgroup (to confirm group-by does not silently re-sort
subgroup members), and a mixed flat/subgrouped list.

Closes #10552

Signed-off-by: Andrew Cunliffe <cunliffeandrewc@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:30:32 +02:00
d42f78b80e 🐛 Fix clipboard crash when copying as SVG (#10663)
* 🐛 Fix clipboard crash when copying as SVG

clipboard.write with an image/svg+xml payload throws an unhandled DOMException on browsers that do not support the type. Fall back to writeText for that specific failure.

Fixes #10596

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>

* 📎 Update Kaleidos Copyright

Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>

---------

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:29:59 +02:00
Andrey Antukh 45405a018b 📎 Add minor changes on error reports 2026-07-23 13:06:46 +02:00
Alejandro Alonso 8bf411c347 🐛 Fix viewer wasm position data init (#10805) 2026-07-23 12:58:20 +02:00
Andrey Antukh 9b88d35664 Merge remote-tracking branch 'origin/main' into staging 2026-07-23 11:02:56 +02:00
Andrey Antukh e4d88b3ab4 🐛 Show proper version on error report api 2026-07-23 11:02:27 +02:00
Andrey Antukh 66b978c01e Add wall-clock timestamps to last-events buffer
Each event entry in `last-events` is now wrapped as
`{:name <event-type> :t (app.common.time/now)}` so every event carries a
wall-clock timestamp. A new helper `format-last-events` renders the
buffer as a multi-line string with ISO time and delta-since-previous-
event in ms, replacing the previous pprint dump in error reports.

This lets support/devs tell whether the events leading up to a crash
were spaced out (user action) or jammed together (runaway loop).

AI-assisted-by: minimax-m3
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 10:50:06 +02:00
Andrey Antukh 52e5e0bec6 🐛 Add more fields on table format on errors report cli client 2026-07-23 10:46:17 +02:00
Andrey Antukh 7430ffe718 ⬆️ Update opencode on devenv 2026-07-23 10:46:05 +02:00
Elena Torró 09730d10d0 Make stroke to path available without config flag (#10792) 2026-07-23 10:27:14 +02:00
Andrey Antukh b3091399cd Add timestamp to the frontend error report (#10772) 2026-07-23 10:26:41 +02:00
Andrey Antukh b6629c0034 🐛 Mark non-Penpot zip files as unknown in import worker (#10782)
Non-export zip files were tagged as :legacy-zip with the body attached,
causing downstream parsing to crash on unrecognized zip content. Now
they are marked :unknown, matching how other unrecognized formats are
handled, so the import fails gracefully.

AI-assisted-by: deepseek-v4-flash
2026-07-23 10:26:22 +02:00
Luis de Dios c57f90d4a2 🐛 Fix main toolbar overlaps grid edition bar (#10789) 2026-07-23 10:25:47 +02:00
Andrey Antukh 707cfac375 🐛 Throttle nudge stream to cap re-renders under fast key-repeat (#10736)
Holding an arrow key on a selection with a fast OS key-repeat rate
crashed the workspace with React error #185 (Maximum update depth
exceeded): each OS key-repeat event was converted 1:1 into a
`set-modifiers`/`set-wasm-modifiers` store write inside
`nudge-selected-shapes` with no throttle, starving the renderer.

The sibling mouse-driven resize/rotate/move paths got an `rx/sample`
throttle in PR #10560; the keyboard-nudge path was the only transform
stream left un-throttled. This change applies the same `rx/sample`
throttle to the nudge stream, mirroring the drag-path structure, and
adds a regression test guarding the final committed position
invariant under a burst of 20 `move-selected` events for both the WASM
and legacy (non-WASM) branches.

Closes #10726

AI-assisted-by: glm-5.2
2026-07-23 09:57:28 +02:00
Juanfran 4900d4b24a Gate nitrate bulk-create-profiles behind a flag (#10785)
The bulk profile creation endpoint creates already active profiles that
skip email verification and onboarding, so it should not be reachable on
production deployments. Add the `nitrate-bulk-create-profiles` flag,
disabled by default, and reject the call when it is not enabled.

Signed-off-by: Juanfran <juanfran.ag@gmail.com>
2026-07-23 09:33:32 +02:00
Andrey Antukh 702a435569 📎 Add token id to the dom for more easy identify the token id 2026-07-23 09:29:45 +02:00
Andrey Antukh bac739717c 📎 Do not print exeption when cant setup reloading
happens only when production jar is executed
2026-07-23 09:29:09 +02:00
Andrey Antukh 4c222c469a 🐛 Move to runtime the repl reploading config 2026-07-23 09:04:57 +02:00
Andrey Antukh b1ccb252fd 📎 Update review command and code-quality skill 2026-07-23 08:31:36 +02:00
Elena Torró 4383cf183a Add background blur to strokes (#10716) 2026-07-22 17:07:16 +02:00
Andrey Antukh eb1e0ad186 🐛 Guard team-container* when team-id is not a uuid (#10645)
The dashboard route can be reached without a `:team-id` query parameter
(e.g. `/#/dashboard/recent`). When that happened, `team-container*` was
emitting `dtm/initialize-team` with a `nil` team-id, which set
`:current-team-id` to `nil` in the application state. The dashboard
and workspace initialize events then built `df/fetch-fonts` with a
`nil` team-id, producing a `:get-font-variants` RPC with empty params
`{}` that the backend rejected with HTTP 400.

Guard `team-container*` so it does not emit `initialize-team` /
`finalize-team` and does not render the children when `team-id` is
not a uuid. The `with-effect` body and the render are guarded
independently; the cleanup closure captures the same `team-id` as the
setup, so the finalize still fires correctly when transitioning between
valid teams.

AI-assisted-by: minimax-m3
2026-07-22 16:57:46 +02:00
Belén Albeza 7b9896ab32 🐛 Fix tabindex error (#10780) 2026-07-22 16:31:20 +02:00
Andrey Antukh 2523a72c32 :boolk: Update changelog 2026-07-22 16:26:14 +02:00
Andrey Antukh e076443eca Merge remote-tracking branch 'origin/main' into staging 2026-07-22 16:23:48 +02:00
Elena Torró ef3511e519 Add basic individual strokes implementation (#10648) 2026-07-22 16:13:38 +02:00
Alejandro Alonso f21bd45893 🐛 Fix paths and layout performance and rendering on boolean exclusions (#10778)
* 🐛 Skip identity transforms in layout reflow propagation

Layout reflow emitted identity transforms for unchanged children, which
fanned out through the whole subtree on every drag frame and froze large
files in the WASM renderer.

* 🐛 Fix exclude boolean rendering in render WASM
2026-07-22 15:47:43 +02:00
Andrey Antukh 40ab48ea01 🐛 Fix inconsistencies on serenea memories 2026-07-22 15:23:25 +02:00
Marina López 396d799c71 🐛 Redesign my penpot review 2026-07-22 14:45:14 +02:00
María Valderrama f20e4280fc 🐛 Fix theme-aware illustration in nitrate modal (#10773) 2026-07-22 14:44:26 +02:00
Andrey Antukh 2344ba22a6 🎉 Add error reports API and CLI tool
Implement RPC methods for querying server error reports with pagination
and filtering. Add CLI tool (tools/error-reports.mjs) for convenient
access with table and JSON output formats. Extract profile-id from audit
events and logging context for better error categorization. Build
improved HREF using request path when available.

AI-assisted-by: qwen3.7-plus
2026-07-22 14:18:51 +02:00
Andrey Antukh 018d840bab ♻️ Refactor internal organization of system initialization
Add the ability to suspend and add nrepl to the whole system

AI-assisted-by: qwen3.7-plus
2026-07-22 14:18:51 +02:00
Andrey Antukh e1f976aa2f Merge remote-tracking branch 'origin/staging' 2026-07-22 14:01:42 +02:00
Andrey Antukh 80c84a3331 🐛 Strip Authorization header when proxying asset redirects to S3 (#10777)
When nginx follows a backend 307 redirect to a presigned S3 URL, it was
forwarding the client's Authorization header to S3. Production S3 rejects
this because it sees two auth mechanisms (presigned URL signature +
Authorization header). MinIO in devenv is more lenient and ignores the
extra header.

Fix: add proxy_set_header Authorization "" in the @handle_redirect block.

Fixes #10776

AI-assisted-by: mimo-v2.5-pro
2026-07-22 14:00:35 +02:00
Andrey Antukh 9cb039070f ♻️ Refactor nitrate audit method and tests
Keep the audit refactor lint-clean after rebasing onto develop.

AI-assisted-by: gpt-5
2026-07-22 13:11:57 +02:00
Marina López 9f029de578 Handle penpot events from nitrate 2026-07-22 13:11:57 +02:00
Pablo Alba 3a0aca52c3 Add new debugger tool components-debugger (#10757) 2026-07-22 10:49:18 +02:00
Andrey Antukh f9439d2942 Merge remote-tracking branch 'origin/staging' into develop 2026-07-22 10:48:40 +02:00
Andrey Antukh e4cddd8536 📚 Update serena memories 2026-07-22 10:45:18 +02:00
Andrey Antukh 72f3165341 📎 Update changelog 2026-07-22 10:45:00 +02:00
Andrey Antukh f98b1ddb8e Merge remote-tracking branch 'origin/main' into staging 2026-07-22 10:36:47 +02:00
Elena Torró 08e42687d1 ♻️ Extract GPU-free RenderResources; add headless wasm exports (#10653) 2026-07-22 09:56:21 +02:00
Dr. Dominik Jain d4e87ec59d ⬆️ Update Serena to 1.6.1 in agentic devenv (#10770)
Update project files accordingly
2026-07-22 09:35:50 +02:00
AKandAkshit Nassa b8e3089ae7 Make plugin API validation errors precise and crash-safe (#10667)
Schema validation reported a generic "Invalid data" message. Report the expected schema and the received value, with a bounded cycle-safe renderer so the error path cannot itself crash.

Fixes #10072

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
2026-07-22 09:29:19 +02:00
Andrey Antukh 1c917951b6 Merge remote-tracking branch 'origin/staging' into develop 2026-07-22 09:18:25 +02:00
Andrey Antukh f3bf24b4f6 ♻️ Consolidate dev tooling into scripts/ and reorganize docs
Move all development tools from tools/ to scripts/ for consistency.
Rename lint/fmt/check-fmt to lint-clj/fmt-clj/check-fmt-clj to clarify
they target Clojure specifically. Remove unused scripts (attach-opencode,
start-opencode, start-opencode-server) and the backport-commit skill.

Update all internal references across .serena/, AGENTS.md, and
CONTRIBUTING.md to point to the new script locations. Simplify
CONTRIBUTING.md by delegating module-specific fmt/lint instructions
to the respective serena memories.

AI-assisted-by: deepseek-v4-flash
2026-07-22 09:18:06 +02:00
Andrey Antukh 73bfc0dc15 🐛 Guard workspace-page* when file-id is not a uuid (#10655)
The workspace route can be reached without a `:file-id` query parameter
(e.g. `/#/workspace?team-id=...`). When that happened, `workspace*` was
emitting `dw/initialize-workspace` with a nil file-id, which stored nil
in `:current-file-id`. The `fetch-profiles` event then read nil from
state and called `:get-profiles-for-file-comments` with `{:file-id
nil}`, producing a 400 response.

Move the `use-equal-memo` calls for `file-id` and `page-id` from
`workspace*` up to `workspace-page*`, and guard the render with
`(when (uuid? file-id) ...)` so `workspace*` only mounts when `file-id`
is a valid uuid. Since `workspace*` never mounts with a nil `file-id`,
`initialize-workspace` is never emitted with nil, and the 400 is
prevented at the source.

AI-assisted-by: minimax-m3
2026-07-22 09:00:43 +02:00
Andrey Antukh bdce5817ea 📚 Update changelog 2026-07-22 08:43:47 +02:00
Andrey Antukh 4ecbac896f Merge remote-tracking branch 'origin/staging' 2026-07-22 08:43:03 +02:00
Juan de la CruzandLuis de Dios 5dff551f31 🎉 Add page multi-selection in the workspace sitemap (#10581)
* 🎉 Add page multi-selection in the workspace sitemap

* ♻️ Simplify page selection state updates with single assoc

* 🐛 Fix SCSS issue

* ♻️ Update some components to new syntax

* ♻️ Adapt SCSS to the new guidelines

* ♻️ SCSS cleanup

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-07-21 14:26:03 +02:00
Juan de la CruzandLuis de Dios fa996ab240 Add list view toggle for dashboard files (#10692)
*  Add list view toggle for dashboard files

*  Add drop files visual feedback

* ♻️ Use radio buttons component from DS

* ♻️ Use hook to keep layout status

* ♻️ Refactor code and SCSS

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-07-21 13:16:10 +02:00
Pablo Alba 0ff73ae711 Add prune-unrelated-items debug utility (#10687) 2026-07-21 12:31:23 +02:00
Alonso Torres 6c5d01283a 🐛 Add more test for reported bugs (#10765) 2026-07-21 12:18:40 +02:00
Andrey Antukh 64026fc1f6 📎 Update changelog 2026-07-21 11:11:21 +02:00
María Valderrama 2de08f00e8 🐛 Fix teams' inactive status for nitrate (#10763) 2026-07-21 10:23:03 +02:00
Dr. Dominik Jainandalonso.torres 4ea56e0b89 🐛 Fix propagation of geometric changes to rotated component copies (#10574)
* 🐛 Fix geometry sync between mains and rotated component copies

Rotating a copy instance as a whole marked every shape inside it as
touched for geometry, so later geometric changes in the main (e.g. a
resize) were no longer propagated to that copy, while non-geometric
ones (e.g. fills) still were. And on paths where geometry did get
written to a rotated copy (e.g. resetting overrides), the sync engine
compensated only the roots' position delta, so the written values wiped
the copy's rotation back to 0.

Model the instance root's transformation as inherited, overridable
content, asymmetric to position (which remains free per-instance
placement):

- An untouched copy follows the main's transformation verbatim,
  including rotation and flips (preserving the BUG #13267 semantics
  that rotating a main propagates to its copies).

- Transforming a copy as a whole overrides only its ROOT: check-delta
  compares the root's rotation/flips absolutely, but the descendants
  relative to their root, so they merely follow and stay untouched.

- When a copy root's geometry is overridden, update-attrs expresses the
  main's geometry in the copy's own frame: reposition-shape applies the
  roots' relative transformation (rotation/flips) around the dest root
  center in addition to the position delta. Geometric changes from the
  main then keep propagating to the rotated copy, landing correctly in
  its rotated frame instead of destroying its placement.

Covered by the new composable test case
case-n-geometry-sync-with-rotated-instances: an 8-variant sweep over
optional copy rotation, optional main rotation, and one of a fills or
height edit on the main child, asserting the whole model through the
real workspace events (the new rotate operation dispatches
dwt/increase-rotation, whose apply-modifiers step runs the check-delta
classification under test; change-height dispatches
dwt/update-dimensions and implements IPropertyCheck so one-of sweeps
can mix property and geometry edits). Verified by temporarily reverting
the fix: the case then fails with 6 assertion failures and passes again
with the fix restored.

Fixes #10109

AI-assisted-by: claude-fable-5

* 🐛 Fix synchronization problems

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-21 10:05:31 +02:00
Marina López 27392abd49 🐛 Increase team name abbreviation limit in invitation emails 2026-07-21 09:54:06 +02:00
Alejandro Alonso 6c5618025d Merge pull request #10712 from penpot/elenatorro-10706-apply-background-blur-to-text-shapes
 Add background blur to text shapes
2026-07-21 09:53:01 +02:00
Andrey Antukh 4dc59ede50 Merge remote-tracking branch 'origin/main' into staging 2026-07-20 21:49:46 +02:00
Andrey Antukh 378d97df93 📎 Update refine-prompt skill 2026-07-20 21:49:19 +02:00
Alonso Torres 79a471a6f1 🐛 Fix default exporter config (#10761) 2026-07-20 21:28:24 +02:00
f2a9dd1a08 🐛 Reject invalid formulas on numeric inputs (#10659)
Relative operators were accepted in operand position, so "10+*3" silently evaluated to 310 instead of being rejected. Make negation a first-class operand so legitimate negative operands ("10 + -3") keep working.

Fixes #9581

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 16:15:29 +02:00
María Valderrama f06339fb87 Update enterprise modals illustration (#10759) 2026-07-20 16:03:40 +02:00
Andrey Antukh c1d09c80a6 Merge remote-tracking branch 'origin/staging' 2026-07-20 14:49:21 +02:00
Andrey Antukh e69fdef1dc Merge remote-tracking branch 'origin/main' into staging 2026-07-20 14:48:39 +02:00
Andrey Antukh 4c7863d6c8 📚 Add .penpot format docs and inspector tool (#10674)
* 📚 Add comprehensive documentation for .penpot file format (v3)

Create user-facing documentation for the .penpot binfile format to help
developers and power users understand and inspect the ZIP+JSON structure.

- Add technical specification with complete schema reference for all JSON
  files (manifest, file metadata, pages, shapes, library assets, storage
  objects, plugin data)
- Add user-friendly overview explaining the format structure, inspection
  methods, and version history
- Update export-import-files.njk to clarify current format is not deprecated
  and note that v2 was never released
- Add cross-links between documentation pages
- Include source code references to authoritative malli schemas

AI-assisted-by: qwen3.7-plus

* 📎 Add playwright dependency to the root package.json

* 📚 Add browser based .penpot file inspector to docs

Add a client-side, no-build inspector for .penpot (v3) files. The tool
runs entirely in the browser: JSZip is loaded lazily from a CDN, the
file is never uploaded. It provides a collapsible file tree, syntax-
highlighted JSON viewer with search, image previews, a summary panel
with file/shape/storage counts, shape summary cards with color swatches,
and clickable UUID cross-references with backlinks.

The inspector is added at
docs/technical-guide/developer/data-model/penpot-file-inspector.njk
and linked from the format spec and the user-facing format page.

AI-assisted-by: minimax-m3

* 📚 Use full page width and 2-column layout for inspector

The inspector previously sat inside the docs site's 42rem content
column, which forced a stacked layout and wasted the wide viewport.

- Hide the docs page-navigation sidebar on this page via :has()
- Use a 2-column CSS grid: file tree (280px, sticky) on the left,
  content (flex: 1) on the right
- Restore the directory tree as a permanent left-rail navigation
  (it was removed when the picker was first introduced, then added
  back as a collapsible panel; a sticky rail is a better fit now
  that horizontal space is plentiful)
- Remove the tree toggle button — the tree is always visible
- Collapses to 1 column on viewports below 900px

AI-assisted-by: minimax-m3

* 💄 Indent nested JSON values in inspector

The JSON tree view in the inspector was rendering all properties at
the same indent level, making it hard to see which values were
nested inside objects or arrays. Root-level properties and deeply
nested ones looked identical.

Add padding-left and a subtle vertical guide line to .jchildren so
each nesting level is visually distinct. Bump the toggle column to
1.2em and add a touch of vertical padding to .jrow for breathing
room.

AI-assisted-by: minimax-m3
2026-07-20 14:45:59 +02:00
Andrey Antukh 766368567e 🐛 Ensure text position-data always includes :fills key (#10650)
Position-data entries for text shapes could omit the :fills key when the
element had no explicit fills, causing backend malli validation failures.

- Use get-default-text-attrs as base in WASM calculate-position-data
- Default fills when CSS --fills property is absent in DOM calc path

AI-assisted-by: deepseek-v4-pro
2026-07-20 14:45:36 +02:00
Andrey Antukh c6de3ce47b 📎 Do not drop uuid-ossp extension on migrations
Requires specific permissions
2026-07-20 14:05:04 +02:00
Luis de Dios a0b5c23f99 🐛 Fix close comments panel button does not work (#10651) 2026-07-20 13:10:27 +02:00
Dr. Dominik JainandMichael Panchenko 51c0129485 Simplify MCP client setup, adding client-setup command (#10604)
Add a `client-setup` command to the published `@penpot/mcp` package.
When invoked as `penpot-mcp client-setup`, the bin delegates to the `add-mcp`
CLI against the local MCP server URL (from `PENPOT_MCP_SERVER_PORT`, default
4401), run via npx.

Update docs on MCP client configuration.
Recommend calling `add-mcp` directly for now, since the `client-setup` command
only becomes available once a new `@penpot/mcp` release is published to npm.

AI-assisted-by: Claude

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
2026-07-20 13:08:51 +02:00
Belén Albeza 4c21923c4a 🐛 Fix accent menu (MacOS) inserting an extra character (#10637) 2026-07-20 12:16:52 +02:00
Andrey Antukh 5f35fdf217 ♻️ Replace uuid-ossp defaults with gen_random_uuid() and add missing :id on insert (#10591)
* 📎 Add postgresql client tool wrapper for devenv

* ♻️ Replace uuid-ossp defaults with gen_random_uuid() and add missing :id on insert

- Switch all DEFAULT uuid_generate_v4() to gen_random_uuid()
  (built-in PG 13+, no extension required)
- Add explicit :id (uuid/next) to 4 db/insert! calls that were
  relying on the DB default (team-profile-rel, project-profile-rel,
  team-project-profile-rel)
- Drop uuid-ossp extension (no longer needed)
- Add missing uuid require to projects.clj and srepl/binfile.clj

AI-assisted-by: deepseek-v4-flash

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 12:14:44 +02:00
Andrey Antukh 26c4ec18fe 🐛 Return 400 instead of 500 when ImageMagick fails on invalid images (#10643)
When ImageMagick fails to process an uploaded image (e.g., corrupted PNG
with invalid IHDR data), the backend was raising :type :internal with
:code :imagemagick-error, which mapped to HTTP 500. The frontend treated
this as a server error and displayed the full error page.

Changed exec-magick! to raise :type :validation with :code :invalid-image
instead. This flows through the existing :invalid-image handler in
errors.clj which returns HTTP 400. The frontend's handle-media-error and
process-error now catch this code and show a notification banner.

AI-assisted-by: qwen3.7-plus
2026-07-20 11:56:07 +02:00
Sangeeth Thilakarathnaandsanmaxdev 5dadda0f2b 📚 Improve plugin deployment documentation (#10544)
Co-authored-by: sanmaxdev <sanmaxdev@users.noreply.github.com>
2026-07-20 11:55:36 +02:00
Andrey Antukh 5acc3aac99 Merge remote-tracking branch 'origin/staging' into develop 2026-07-20 11:48:26 +02:00
Andrey Antukh fc6b3ee7f0 🐛 Demote OIDC userinfo 401 errors to warning and add comprehensive test coverage (#10636)
* 🐛 Demote unable-to-retrieve-user-info OIDC error to warning level

401 responses from the OIDC userinfo endpoint (e.g. expired/revoked GitHub
token) are normal auth failures, not server errors. Logging at :error level
triggers the database and Mattermost error reporters unnecessarily.

AI-assisted-by: deepseek-v4-flash

*  Add pure function tests for OIDC auth module

Add tests for: int-in-range?, valid-info?, qualify-prop-key, qualify-props,
provider-has-email-verified?, profile-has-provider-props?, redirect-response,
redirect-with-error, redirect-to-verify-token, and build-redirect-uri.

AI-assisted-by: deepseek-v4-flash

*  Add HTTP-mock tests for fetch-user-info and fetch-access-token

Replace with-redefs with binding (cf/config is ^:dynamic).
Add tests for: fetch-user-info (success, 401, 500, request structure),
fetch-access-token (success, 400 error).

AI-assisted-by: deepseek-v4-flash

*  Add get-info integration tests with partial mocking

Test all branches: token/userinfo/auto info sources, incomplete info,
role checks (satisfied and insufficient), state props merge,
sso-session-id from claims, and sso-provider-id for uuid providers.

AI-assisted-by: deepseek-v4-flash

*  Add callback-handler integration tests with real tokens and session

Tests all main branches: error param, no profile (registration disabled),
profile blocked, provider mismatch, inactive profile, success flow,
and graceful handling of unable-to-retrieve-user-info exception.
Uses real tokens/generate, tokens/verify, and session/inmemory-manager.

AI-assisted-by: deepseek-v4-flash
2026-07-20 11:46:20 +02:00
Elena Torró 8d98877ba7 🐛 Fix using SVG image file as shape fill (#10707) 2026-07-20 10:41:07 +02:00
Shlok GoyalandAndrey Antukh 826072c65d Remove misleading MCP config from success modal (#10415)
Signed-off-by: Shlok1729 <shlokgoyal1279@gmail.com>
Signed-off-by: Shlok Goyal <shlokgoyal1279@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 10:07:26 +02:00
Juan de la Cruzandalonso.torres cddbd8e897 Add font family preview in typography selector (#10411)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-20 10:06:02 +02:00
Yaron Shahrabani 29ed79fcdc 🌐 Add translations for: Hebrew
Currently translated at 85.9% (2036 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-20 10:01:22 +02:00
Andrey Antukh a96001894c 📚 Add explicit AI-assisted-by format rules to commit memory
AI-assisted-by: mimo-v2.5
2026-07-20 07:43:27 +00:00
Andrey Antukh fd5d72cedd 📎 Update common test script to match package.json changes
Replace removed test:js and test:jvm npm scripts with pnpm run test
and direct clojure -M:dev:test invocation.

AI-assisted-by: mimo-v2.5
2026-07-20 07:42:27 +00:00
Andrey Antukh b2140d5ff1 📎 Update opencode on devenv 2026-07-20 09:38:18 +02:00
jonnysemon 9aac607552 🌐 Add translations for: Arabic
Currently translated at 47.4% (1124 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-07-18 05:01:31 +02:00
Mahmoud A. Rabo 4ea83d31fc 🌐 Add translations for: Arabic
Currently translated at 47.4% (1124 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-07-18 05:01:24 +02:00
Marina López 33d478b532 ♻️ Rename org to organization 2026-07-17 11:36:08 +02:00
Dr. Dominik Jain d9511db585 🔧 Auto-confirm pnpm modules purge in MCP bootstrap #10680 (#10681)
pnpm occasionally detects an incompatible node_modules directory (e.g.
after a store location or pnpm major version change) and interactively
asks whether to remove and recreate it, blocking the MCP bootstrap in
the devenv tmux pane. Set confirmModulesPurge: false in
mcp/pnpm-workspace.yaml so the purge is auto-confirmed; this file is
included in the npm pack tarball (unlike .npmrc) and applies to all
install invocations from a single place.

AI-assisted-by: claude-fable-5
2026-07-16 17:00:20 +02:00
Andrey Antukh a4347451d0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-16 16:57:32 +02:00
Andrey Antukh 779983d38e 📎 Standardize test scripts and add execution discipline docs
- Remove conditional build from test scripts (frontend, common)
- Remove test:jvm from common package.json (JVM tests via clojure directly)
- Remove test from backend package.json (JVM tests via clojure directly)
- Unify common/scripts/test-quiet.js with frontend's BUILD_STEPS pattern
- Add execution discipline section to mem:testing (no piping, tee to file)
- Add READ mem:testing FIRST directives to module testing docs

AI-assisted-by: deepseek-v4-flash
2026-07-16 14:57:05 +00:00
Kobi Hikri 7e36192034 🔧 Pin mattermost-notify action to its v2.1.0 commit SHA
The notify steps referenced mattermost/action-mattermost-notify@master, a
mutable branch that runs in CI with access to the MATTERMOST_WEBHOOK_URL
secret. Pinning to the immutable commit of the latest release (v2.1.0,
ae31bb6) keeps the exact reviewed code from executing, per GitHub third-party
action hardening guidance, while staying easy to bump.
2026-07-16 14:59:08 +02:00
Andrey Antukh 7f60e3735d 🐛 Remove hardcoded metadata URI default on webhook form (#10723)
The webhook creation form used a hardcoded AWS instance metadata
endpoint (http://169.254.169.254/...) as the default :uri value for
new webhooks. This leaked an internal cloud credential endpoint into
the UI defaults and could expose it to users.

Remove the default :uri so new webhooks start with an empty URI
instead of a sensitive hardcoded value.
2026-07-16 14:04:06 +02:00
Andrey Antukh 5cc6477e3f Merge remote-tracking branch 'origin/staging' into develop 2026-07-16 13:52:41 +02:00
Andrey Antukh 8e8fb67793 Merge remote-tracking branch 'origin/main' into staging 2026-07-16 13:52:28 +02:00
Andrey Antukh 258dd6ad6c 🐛 Handle text node targets in closest-text-editor-content (#10641)
Event targets can be DOM text nodes (nodeType 3) which lack the
.closest() method. Add a get-element helper that normalizes text
nodes to their parent Element before calling .closest(), matching
the existing pattern in dom/get-parent-with-data.

Fixes #10640

AI-assisted-by: mimo-v2.5-pro
2026-07-16 12:19:50 +02:00
David Barragán Merino 17c344b8f5 🐛 Fix auto-label action failing on PRs from forked repos 2026-07-16 10:15:22 +02:00
Elena Torro 001dba1bef Add background blur to text shapes 2026-07-16 08:33:36 +02:00
Andrey Antukh c50ec233ae Merge remote-tracking branch 'origin/staging' into develop 2026-07-15 21:09:12 +02:00
Andrey Antukh b3105e6b82 Backport github workflows from develop 2026-07-15 21:01:55 +02:00
Andrey Antukh e498dd2382 📎 Update commiter opencode agent 2026-07-15 21:01:33 +02:00
Andrey Antukh e48f374984 ⬆️ Update opencode on devenv dockerfile 2026-07-15 17:32:16 +02:00
María Valderrama bdc078d5ea 🐛 Fix mismatched subscription in social login (#10703) 2026-07-15 14:47:44 +02:00
María Valderrama 792d88dc4f 🐛 Fix invalid org invitation show toast (#10693)
* 🐛 Fix invalid org invitation show toast

* 📎 Code review
2026-07-15 12:30:20 +02:00
Alejandro Alonso ec4c5a75a7 Merge pull request #10697 from penpot/elenatorro-10537-fix-mask-position-within-layout
🐛 Fix masked group position in flex layout on child visibility change
2026-07-15 11:05:41 +02:00
Elena Torro 6285b1de60 🐛 Fix masked group position in flex layout on child visibility change 2026-07-15 08:58:48 +02:00
DoubleCat 2203482531 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 100.0% (2369 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-14 15:01:44 +02:00
María Valderrama 167aa7410f 🐛 Fix modals for nitrate subscription when unlimited (#10690) 2026-07-14 14:15:29 +02:00
Marina López c119622ad9 🐛 Changed color and text from nitrate banners 2026-07-14 12:59:16 +02:00
Pablo Alba 95cfbf5f7c Improve workspace debug sidebar workflow (#10678) 2026-07-14 12:42:19 +02:00
Elena Torró c11d3aaaa7 🐛 Add :stroke-image support to plugins API (#10683) 2026-07-14 10:26:19 +02:00
Elena Torró ab58d00d66 Improve bool intersection perfomance (#10671) 2026-07-14 07:43:52 +02:00
Andrey Antukh 85dbf14344 📎 Add better planner skill and improve testing doc 2026-07-13 11:42:26 +02:00
Andrey Antukh 33e18c72e2 📎 Add minor improvements to opencode setup 2026-07-11 10:12:07 +02:00
Elena Torró a006a12ab6 🐛 Fix render stroke caps on drag (#10634) 2026-07-10 12:33:59 +02:00
Dr. Dominik JainandClaude 2c15dcdb84 Add systematic component tests via a composable test model (#10529)
*  Add systematic component tests via a composable test model

Introduce a framework for systematically testing Penpot component behaviour
(synchronisation/propagation, swaps, variant switches, nesting), plus a first
suite of cases built on it.

A test is expressed as a COMPOSITION OF OPERATIONS over a "situation" (an
in-memory file value plus named role bindings). Operations are reified as data
and composed by two combinators — `in-sequence` (threads the situation) and
`one-of`/`optional` (alternatives, enumerated into concrete variants). So one
written case stands for a whole matrix of variants, and coverage grows by
composition rather than by copying tests. Operations drive the REAL production
change pipeline, and event-operations dispatch the REAL workspace events and
await settlement, so the production watcher's automatic propagation is what is
exercised — the tests reflect genuine app behaviour, not a reimplementation.

Structure (frontend/test/frontend_tests/composable_tests/):
  - core            — the domain-agnostic engine: situation, the operation and
                      enumeration protocols, the combinators, and the runners.
  - comp/nodes      — the component operations (create/instantiate/reset, nesting,
                      swap, the variant ops, child add/remove/move, change, undo,
                      library sync).
  - comp/setups     — component-shaped starting configurations.
  - interpreter     — runs a case against the real frontend store: sync-ops apply
                      directly, event-ops dispatch real events and await
                      settlement (absorbing sync-file's delayed status RPC, which
                      would otherwise leak an error into subsequent tests).
  - comp/sync-test  — the cases (B-F, H, I, K, L, M).

This is test-only code with a single consumer — the frontend test suite (the
layer that runs the real app) — so it lives entirely under the frontend test
tree as .cljs, not under app/common.

The framework and its cases are documented in the project memory
frontend/composable-component-tests, added alongside.

Co-authored-by: Claude <noreply@anthropic.com>

* 🐛 Guard WASM mock teardown against an empty snapshot

`teardown-wasm-mocks!` unconditionally restored from the `originals` atom.
When run without a matching setup (double teardown, or `with-wasm-mocks*`
misused around an async test body), the snapshot is empty and every WASM API
function was `set!` to nil — permanently, for the remainder of the test run.
Any later code calling one of them (e.g. a leaked debounced resize-wasm-text
event firing during a subsequent test namespace) then crashed with
"initialized? is not a function".

Make the restore a no-op when there is nothing to restore.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 12:17:43 +02:00
Belén Albeza 176a813fb9 🐛 Fix selected text background color in light theme (#10614) 2026-07-10 12:02:30 +02:00
Andrey Antukh 3add7211a5 Merge remote-tracking branch 'origin/staging' 2026-07-10 11:35:04 +02:00
Andrey Antukh fd8cb957d1 Merge remote-tracking branch 'origin/staging' into develop 2026-07-10 11:23:42 +02:00
Andrey Antukh 1f4b85209e 📚 Simplify the ia asistance note on creating-prs serena workflow 2026-07-10 11:22:44 +02:00
AKandAndrey Antukh 89551b2415 🐛 Fix dashboard user menu submenu not closing on hover out (#10639)
The dashboard profile menu submenus (Help & Learning, Community &
Contributions, About Penpot) opened on pointer enter but nothing closed
them when the pointer left the option, leaving a stale submenu visible
until the whole menu closed.

Close the open submenu when the pointer leaves an expandable option or
its submenu, with a 200ms grace period (same approach as the workspace
context menu) so the submenu survives the pointer crossing the gap
between the parent menu and the floating submenu. Keyboard navigation
is unchanged and now covered by tests.

Closes #10549

AI-assisted-by: claude-fable-5

Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-10 11:21:21 +02:00
Belén Albeza 3469867cf5 🐛 Fix text editor not auto-selecting all text on mount (#10573) 2026-07-10 11:08:10 +02:00
Eva Marco 3708cf31d4 🐛 Fix stroke cap selects (#10631) 2026-07-10 11:05:09 +02:00
Andrey Antukh e73aa9e981 Add PENPOT_INTERNAL_URI to exporter for separate internal and public URI handling (#10630)
Add PENPOT_INTERNAL_URI environment variable to the exporter. This allows
separating the URI used for internal communication (headless browser to
frontend) from the public URI used for resource references in exported SVGs.

Previously, PENPOT_PUBLIC_URI served both purposes, which caused exported
SVGs to contain broken font URLs when the internal Docker address was used.

Changes:
- Add :internal-uri to exporter config schema with fallback to :public-uri
- Add get-internal-uri helper function
- Use internal-uri for browser navigation in SVG/PDF/bitmap renderers
- Post-process SVG output to replace internal URI with public URI
- Use internal-uri for backend API calls in resource handler
- Log both URIs on startup
- Update docker-compose.yaml to use both variables
- Document the new variable in configuration.md

Closes #10627

AI-assisted-by: mimo-v2.5-pro
2026-07-10 10:41:49 +02:00
Alonso Torres 3ee5b50007 🐛 Fix problems with padding multiple values in plugins and UI (#10602)
* 🐛 Fix problem with padding types in plugins

* 🐛 Fix problem with multiple selection paddings
2026-07-10 09:13:20 +02:00
Whos DeezandAndrey Antukh 508569437a 📚 Fix flex word repetition and correct modifier key in docs (#10610)
* 📎 Update version on mcp package.json

* Update flexible-layouts.njk : fixed repetition and changed shortcut

Fixed 'Flex' word repetition; made the Toggle Flex instruction reflect the correct keyboard modifier key.

Signed-off-by: Whos Deez <134500894+Whosdeez@users.noreply.github.com>

* Update package version to 2.17.0

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Whos Deez <134500894+Whosdeez@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-10 09:01:43 +02:00
Andrey Antukh 1311bafec4 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 19:41:04 +02:00
Andrey Antukh ad4dae5f28 📚 Add testing principles to serena doc 2026-07-09 19:40:45 +02:00
Andrey Antukh f84a8687f6 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 19:37:16 +02:00
Andrey Antukh 23a9b4bdd9 🐛 Fix backend util shell tests 2026-07-09 19:34:15 +02:00
Andrey Antukh 3400d6afbb 🐛 Fix too much recursion when clicking shape in comments mode (#10622)
* 🐛 Fix too much recursion when clicking shape in comments mode

Remove `deselect-all` from `handle-interrupt` in comments mode. The
`select-shape` event emits `:interrupt` which the comments stream
watcher routes to `handle-interrupt`. In comments mode, calling
`deselect-all` cleared the selection and emitted a competing
`rt/nav`, creating a synchronous recursion cycle in the potok store
that overflowed the JS call stack.

Fixes #10620

AI-assisted-by: opencode

* 🐛 Add unit tests for comments handle-interrupt

Make `handle-interrupt` public (defn- → defn) and add 4 tests covering
each branch: draft thread, open thread, comments mode, and noop.

AI-assisted-by: opencode
2026-07-09 18:04:05 +02:00
Alonso Torres d6c50cc40b 🐛 Fix problems with plugins api setPluginData (#10632) 2026-07-09 18:00:26 +02:00
María Valderrama ebfcb716ac 🐛 Fix permission to add a team to an organization (#10623) 2026-07-09 14:12:41 +02:00
Andrey Antukh 64b0bff7bd 📚 Update serena creating-prs workflow documentation 2026-07-09 13:12:41 +02:00
Alonso Torres 80d688be93 🐛 Fix problems with comments clusters (#10543) 2026-07-09 11:37:45 +02:00
Andrey Antukh 28fd798c52 🐛 Fix crash with degenerate selrect in text pipeline (#10618)
Guard remaining text pipeline locations that accessed :selrect directly
against nil/zero-dimension selrects by using safe-size-rect, which
provides a 4-level fallback chain (selrect -> points -> shape fields ->
empty 0.01x0.01 rect).

Fixes:
- fix-position in viewport_texts_html.cljs: replaced dm/get-prop
  :selrect with ctm/safe-size-rect for both old and new shape
- assoc-position-data in modifiers.cljs: replaced (:selrect ...)
  with ctm/safe-size-rect for delta computation
- change-orientation-modifiers in modifiers.cljc: replaced raw
  :selrect access with safe-size-rect for scale and origin computation

Closes #10617

AI-assisted-by: mimo-v2.5-pro
2026-07-09 11:03:06 +02:00
Andrey Antukh 47a3158602 🐛 Fix component variant panel crash with mismatched property counts (#10616)
Use `get` instead of `nth` to avoid index-out-of-bounds when a selected
component copy has fewer variant properties than the first component in
the selection.

Closes #10615

AI-assisted-by: deepseek-v4-pro
2026-07-09 11:01:08 +02:00
Andrey Antukh 41ebb8e80b 🐛 Fix crash when converting SVG-raw shape to path (#10613)
Guard the content-to-PathData coercion on whether
stp/convert-to-path actually produced a new value, so
SVG-raw shapes (whose :content is a hiccup map) pass through
unchanged instead of crashing.

Closes #10612

AI-assisted-by: deepseek-v4-pro
2026-07-09 11:00:18 +02:00
Andrey Antukh f2460eee29 🐛 Fix Draft.js selection offset DOMException on text editor re-render (#10608)
Clamp selection offset to node length in setDraftEditorSelection to
prevent DOMException when Draft.js SelectionState offset exceeds the
actual DOM text node length. This can happen during spellcheck, IME
composition, or race conditions during React re-renders.

Updates @penpot/draft-js to commit 09a33e0a which includes the fix
in addPointToSelection and addFocusToSelection.

Fixes #10607

AI-assisted-by: mimo-v2.5-pro
2026-07-09 10:59:40 +02:00
Andrey Antukh 8e9df0c515 Add insert-multi chunked variant to app.db 2026-07-09 09:25:59 +02:00
Andrey Antukh 8fcd8a63b4 ⬆️ Update opencode dependency on devenv 2026-07-09 09:25:19 +02:00
Andrey Antukh 455c7f5cae 📚 Update serena doc related to creating issues 2026-07-09 08:48:00 +02:00
Andrey Antukh 54bef496f1 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 08:34:27 +02:00
Andrey Antukh b3d1a7aa8b 📚 Add better dev tools doc to serena 2026-07-09 08:33:46 +02:00
Andrey Antukh fb2ec4c617 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 08:14:15 +02:00
Andrey Antukh 10f240ce78 Merge remote-tracking branch 'origin/main' into staging 2026-07-09 08:13:18 +02:00
Elena Torró 8ac2e8c8a8 🐛 Fix sidebar scroll to shape (#10600) 2026-07-08 20:38:16 +02:00
Andrey Antukh a004f7cd84 Merge remote-tracking branch 'origin/staging' into develop 2026-07-08 15:44:37 +02:00
Andrey Antukh bcf77767b2 📖 Update ai agents documentation 2026-07-08 15:44:18 +02:00
María Valderrama 5d933fd770 🐛 Organizations list sorted by name (#10589) 2026-07-08 15:43:36 +02:00
Andrey Antukh 8b734d9844 📎 Add postgresql client tool wrapper for devenv 2026-07-08 14:22:34 +02:00
Pablo Alba cc454de9ce 🐛 Fix wrap nitrate sso when there is no profile (#10592) 2026-07-08 14:21:34 +02:00
Elena TorróandBelén Albeza d656d342fe 🐛 Fix text selrect size (#10566)
* 🐛 Fix text selrect size

* 🐛 Fix blinks on complex files

---------

Co-authored-by: Belén Albeza <belen@hey.com>
2026-07-08 13:56:58 +02:00
alonso.torres 88186eaec1 ⬆️ Release MCP 2.17.0 2026-07-08 12:57:45 +02:00
alonso.torres b153748866 ⬆️ Release plugins 1.5.0 2026-07-08 12:57:45 +02:00
alonso.torres f0cf8dca2a 🐛 Fix the release scripts 2026-07-08 12:57:45 +02:00
Andrey Antukh e61c55e53c 📚 Add no text-wrapping rule to the creating-issue workflow 2026-07-08 12:53:11 +02:00
Pablo Alba 0de8bce895 Add ignore sso flag to nitrate management api endpoints (#10579) 2026-07-08 12:51:03 +02:00
Andrey Antukh aadae99b91 📎 Add create-issue skill 2026-07-08 12:43:51 +02:00
Andrey Antukh 83ce70d02d 📎 Update version on mcp package.json 2026-07-08 12:07:40 +02:00
Andrey Antukh 1b00ca8cb9 Merge remote-tracking branch 'origin/staging' 2026-07-08 12:03:49 +02:00
Andrey Antukh fdb5291384 📚 Update changelog 2026-07-08 12:03:02 +02:00
Andrey Antukh d4aa512247 📚 Update changelog (add 2.18.0 section) 2026-07-08 11:57:27 +02:00
Dominik JainandMichael Panchenko b9e0421f79 📚 Update and improve devenv documentation
* Fix stale references to `run-devenv-agentic` and `start-devenv`
* Improve clarity in description of workspaces
* Improve section on agentic devenv

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
2026-07-08 11:28:55 +02:00
Dominik Jain 93b5baa9fc 🐛 Relocate MCP run directory out of the npm-managed install directory
For the npm MCP package, adjust the launcher to specifically prepare
the runtime directory: The package contents are copied once per version
to a runtime directory owned by the launcher.

The npm-owned package directory cannot be used. While installing there
worked for older npm/npx version, we have to assume it is read-only.

Fixes #9947
2026-07-08 11:28:17 +02:00
Dominik Jain aee555ff6e 📚 Update API types for MCP server for new release 2026-07-08 11:27:46 +02:00
Andrey Antukh 0e25f7d8ad Merge remote-tracking branch 'origin/staging' into develop 2026-07-08 10:25:25 +02:00
Andrey Antukh 50a98c35bf 📎 Update creating-commits serena workflow 2026-07-08 10:22:40 +02:00
Andrey Antukh 1d97ae5b46 📎 Simplify create-pr skill 2026-07-08 10:22:40 +02:00
Eva Marco ff0d56a86e 🐛 Fix blur menu width (#10575) 2026-07-08 10:06:23 +02:00
Luis de Dios 4eaefec43e 🐛 Fix glitch when using drawing tool in toolbar (#10447) 2026-07-08 09:55:50 +02:00
Juan de la CruzandAndrey Antukh 8b16bb8874 Add new slides content for 2.17 release (#10577)
*  Add new slides content for 2.17 release

* 📎 Fix linter issues

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-08 09:39:38 +02:00
Alonso Torres 7655f854a4 Improved api test suite (#10533)
* 🐛 Fix plugins api issues

* 🐛 Fix problem with text weight

* 🐛 Fix api problems

*  Updated plugins test suite

* 🐛 Referential integrity tests

*  Add resetOverrides method

* 🐛 Fix plugins components structure poluting
2026-07-07 16:12:12 +02:00
Aitor Moreno f51db39b8d Merge pull request #10563 from penpot/ladybenko-gh-10467-fix-autowidth
🐛 Fix autowidth in text editor v3
2026-07-07 14:24:19 +02:00
Belén Albeza a58c0e1407 🐛 Fix autowidth in text editor v3 2026-07-07 13:20:03 +02:00
Marina López 3e2826de9e Redesign my penpot to my files 2026-07-07 12:22:58 +02:00
Juanfran ac31edab14 🐛 Skip end-user SSO gate on nitrate org management endpoints (#10559)
get-teams-detail, get-org-invitations and delete-org-invitations lacked
::rpc/auth false, so wrap-nitrate-sso ran on them whenever params carried
an organization-id. For SSO-active orgs this rejected the org owner's
admin-console reads with a 401, since their Penpot session has no SSO
grant for the org. These are shared-key protected management calls made
on the org owner's behalf and never use profile-id, so they should not
require an end-user SSO session — matching their sibling endpoints.
2026-07-07 12:22:17 +02:00
Belén Albeza 0a2570c9eb 🐛 Fix text editor not repainting text if there was not a geometry change (#10553) 2026-07-07 11:18:44 +02:00
Dr. Dominik Jain c88d9f568b 🐛 Preserve component order when combining components as variants (#10562) 2026-07-06 16:49:29 +02:00
Belén Albeza 83d2edf256 🐛 Fix empty text shape not being removed (#10541) 2026-07-06 15:42:42 +02:00
Alonso Torres 2dfb1046f7 🐛 Fix problem with rotations crashing (#10560) 2026-07-06 11:56:46 +02:00
Luis de Dios e5d3ea14d1 🐛 Fix size badge shows "x" instead of dimensions when creating a path (#10550) 2026-07-03 14:58:43 +02:00
Andrey Antukh 618ec57b6f 📎 Update changelog 2026-07-03 11:10:53 +02:00
Andrey Antukh a4ba5fc2e0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-03 10:41:21 +02:00
Andrey Antukh 1228b9aa08 Merge remote-tracking branch 'origin/main' into staging 2026-07-03 10:41:01 +02:00
Andrey Antukh 62a9053422 📎 Add minor agents and skills restructuration for opencode 2026-07-03 10:39:42 +02:00
Marina López 04f6549e82 🐛 Replace hyphens with bullets in subscriptions benefits 2026-07-03 10:35:23 +02:00
Andrey Antukh 50c0299e7f ⬆️ Update devenv dockerfile 2026-07-03 09:49:31 +02:00
Juanfran 0182f239ae Add Nitrate bulk profile creation endpoint (#10491)
Add a Nitrate-only management RPC method to create active Penpot profiles in
bulk through the existing shared-key protected management API.
2026-07-03 09:45:44 +02:00
Luis de Dios 2c4a9e0f82 Improve dashboard invitations modal (#10459) 2026-07-03 09:28:52 +02:00
Eva Marco 841b47736b 🐛 Fix token pill border color on invalid pills when not selected (#10535)
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
2026-07-03 09:28:27 +02:00
Eva Marcoandsxxtony 0387cdf7e4 ♻️ Migrate shared ancillary components to modern syntax (#10527)
* ♻️ Migrate shared ancillary components to modern syntax

* ♻️ Address shared ancillary syntax review

* ♻️ Apply Step 3 props destructuring on numeric-input*

Replaces the legacy ?-suffixed prop aliases with clean :keys destructuring
and updates internal references, addressing review on PR #9406.

* 🐛 Fix some problems on refactored components

---------

Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
2026-07-03 09:26:16 +02:00
Belén Albeza 2e32a8743e ♻️ Refactor rulers into ui modules (#10461)
* ♻️ Move rulers rendering to ui submodule

* ♻️ Refactor RulerState into UIState
2026-07-03 08:37:22 +02:00
Luis de Dios d795a1f110 Manage rotation and color of selection size badge (#10393)
*  Rotate and move size badge when shape is rotated

* 🐛 Fix wrong text color in selection size badge

*  Add tests

* ♻️ Extract common constants
2026-07-02 14:59:33 +02:00
Alejandro Alonso b82ab0c830 Merge remote-tracking branch 'origin/staging' into develop 2026-07-02 13:52:25 +02:00
alonso.torres f1426cb3bb 🐛 Add test of fixes in the plugins api suite 2026-07-02 13:32:17 +02:00
RenzoMXDandAlonso Torres 1c9ab691e6 🐛 Plugin API theme.addSet/removeSet accept proxy or set ID
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

🐛 Preserve validation errors for theme set APIs

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

Co-authored-by: Alonso Torres <alonso.torres@kaleidos.net>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 5b7447fbe4 🐛 Accept delay 0 in plugin API interaction setter
The InteractionProxy `delay` setter rejected `0` via `(not (pos? value))`,
so an immediate after-delay interaction (`interaction.delay = 0`) was
refused. The model allows it: `set-delay`
(common/.../shape/interactions.cljc) only asserts `check-safe-int`, with
no positivity constraint, and `safe-int` permits 0.

Replace the guard with `(or (not (sm/valid-safe-int? value)) (neg? value))`:
it allows 0 and positive integers, rejects negatives, and rejects
non-integers cleanly (the previous `number?` check let fractional values
through, which then failed the model's `check-safe-int` assertion
downstream).

Adds a regression test (delay round-trips a positive value, then 0).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 1c5c6ee072 🐛 Accept fractional border radius in plugin API shape setters
The ShapeProxy `borderRadius` setters (`borderRadius` and the four
per-corner variants) validated with `sm/valid-safe-int?`, so a fractional
radius (e.g. `shape.borderRadius = 7.5`) was rejected as invalid. The data
model types `:r1`-`:r4` as `::sm/safe-number` (shape.cljc), the radius
sidebar input only constrains `min 0` (not integer), and `set-radius-*`
store the value verbatim — so the plugin API was stricter than the model.
Same class of defect as the merged #9780.

Switch those 5 guards to `sm/valid-safe-number?`, keeping the existing
non-negative guard on the all-corners setter. Integer-only setters in the
file (`getRange` bounds, `setParentIndex`) keep `valid-safe-int?`.

Adds a regression test asserting fractional radius values are accepted
(with throwValidationErrors enabled).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 c7ee54e849 🐛 Accept fractional gap and padding in plugin API layout setters
The flex and grid layout proxies validated `rowGap`, `columnGap` and the
four padding setters with `sm/valid-safe-int?`, so a fractional value
(e.g. `flex.rowGap = 10.5`) was rejected as invalid. The data model types
`:row-gap`/`:column-gap` and `:p1`-`:p4` as `::sm/safe-number`
(layout.cljc), and the sidebar accepts decimals, so the plugin API was
stricter than the model — the same class of defect as the merged #9780.

Switch those 16 gap/padding guards (8 in flex.cljs, 8 in grid.cljs) to
`sm/valid-safe-number?`, matching the model and the predicate already used
by the flex-element setters in the same file. Integer-only setters
(`zIndex`, grid track indices/counts and cell positions/spans) keep
`valid-safe-int?`. Also fixes a `:righPadding` typo in two grid
rightPadding error branches.

Adds a regression test asserting fractional gap/padding values are
accepted (with throwValidationErrors enabled) for both flex and grid.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
mvanhorn 355f7acb66 🐛 Make animation optional in plugin CloseOverlay type and align overlay typings 2026-07-02 13:32:17 +02:00
mvanhorn ab623cc02d 🐛 Make overlay position optional in plugin open-overlay/toggle-overlay interactions 2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 76070008c2 🐛 Fix plugin API board.guides invalid-schema on set/clear
Setting or clearing `board.guides` from a plugin threw a malli
`:malli.core/invalid-schema` for every value, including `[]`. Causes:

- `shape.cljs` validated against `[:vector ::ctg/grid]`, but the
  `:app.common.types.grid/grid` reference is no longer registered
  (direct schemas replaced the namespaced-keyword registrations). Use
  the direct var `ctg/schema:grid`, matching every other setter.
- `parser.cljs` `parse-frame-guide` returned the `parse-frame-guide-column`
  / `parse-frame-guide-row` fns instead of calling them with the guide,
  so column/row guides parsed to a vector containing a function.
- `parse-frame-guide-square` used `parse-frame-guide-column-params`
  instead of the dedicated `parse-frame-guide-square-params`.

Adds a regression test parsing column/square guides and validating the
result (and an empty vector) against `ctg/schema:grid`.

Fixes #9773

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 8fe835a9cc 🐛 Surface real plugin errors instead of bare "Value not valid"
The plugin proxy `:on-error` handler funneled every throwable through
`handle-error`, which only produced a message for malli ExceptionInfo
carrying `::sm/explain`. Two paths lost the real cause and rendered the
unhelpful "[PENPOT PLUGIN] Value not valid. Code: :error":

- A plain JS error (TypeError, etc.) is not an ExceptionInfo, so
  `(ex-data cause)` is nil and the message bound to nil; the real
  `.-message` only reached the host-page console, invisible to the
  plugin sandbox.
- A malli explain whose errors flatten to nothing made `error-messages`
  return "", which rendered as "Value not valid: . Code: :error".

`error-messages` now returns nil (not "") when there is nothing to
render, and `handle-error` falls back to the raw explain, then to the
throwable's `ex-message`/`str`, so a useful message always surfaces.
Working cases (well-formed explain) are unchanged.

Adds regression tests for the plain-JS-error path, the empty-explain
path, and the `error-messages` nil contract.

Fixes #9692

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Eva Marco f1f5a30624 🐛 Fix alignment selector for strokes (#10532) 2026-07-02 13:01:58 +02:00
andrés gonzález 40a79c2b5d 📚 Update user guide for 2.17 release (#10518) 2026-07-02 09:06:44 +02:00
Alexis Morin 30e17eb076 🌐 Add translations for: French (Canada)
Currently translated at 99.0% (2347 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-02 03:01:19 +02:00
Stephan Paternotte 40091924e2 🌐 Add translations for: Dutch
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-02 03:01:18 +02:00
DoubleCat b3585428bd 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 79.4% (1882 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-02 03:01:18 +02:00
Belén Albeza 6d458c80a1 🐛 Fix font and variant dropdowns on mixed text styles (#10520)
* 🐛 Fix dropdown shown Mixed Font Families for same family with different variant

* 🐛 Fix variants dropdown appearing blank on mixed variants but same family

*  Add playwright test for mixed font families/variants
2026-07-01 20:15:20 +02:00
Elena Torró 28b5371901 🔧 Improve text strokes in texts with emoji (#10519) 2026-07-01 18:10:46 +02:00
Alejandro Alonso f25317ac47 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn (#10509)
* 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn

* 🔧 Update e2e Playwright screenshots
2026-07-01 16:24:23 +02:00
Andrey Antukh 6cc2c403c4 ⬆️ Update dependencies 2026-07-01 14:45:41 +02:00
Marina López 64ba70e6f3 🐛 Fix border color for selected subscription 2026-07-01 14:42:06 +02:00
Andrey Antukh 44d33a0c80 Merge remote-tracking branch 'origin/staging' into staging 2026-07-01 14:41:01 +02:00
Andrey Antukh 77a8677b60 Merge remote-tracking branch 'origin/main' into staging 2026-07-01 14:40:36 +02:00
Eva Marco e6a341ff30 🐛 Fix text layout item menu (#10516) 2026-07-01 14:39:34 +02:00
Andrey Antukh 1006efc571 🐛 Fix translation strings 2026-07-01 14:38:08 +02:00
Eva Marco 04254c9fd1 🐛 Fix token tooltips (#10515) 2026-07-01 14:30:18 +02:00
Aitor Moreno 851fde01c1 🐛 Fix SVG Raw not being saved (#10492) 2026-07-01 14:08:54 +02:00
Eva Marco 665e8e018e 🐛 Fix overlay position grid (#10512) 2026-07-01 13:40:12 +02:00
Eva Marco abbd28d101 🐛 Fix long text inside color tooltip (#10511) 2026-07-01 13:38:51 +02:00
Luis de Dios 7e8590b04c 🐛 Fix inspect in workspace displays shape size twice when selecting a shape (#10507) 2026-07-01 13:08:35 +02:00
Belén Albeza 215de966ae 🐛 Fix extra linebreak on MacOS IME (#10498)
* 🐛 Fix extra linebreak in Japense IME (MacOS)

* ♻️ Remove composing? from the state and query event instead
2026-07-01 12:40:25 +02:00
Elena Torró c8f586a197 🐛 Fix async text rendering on tiles (#10504) 2026-07-01 11:09:57 +02:00
Belén Albeza 51877c4bca 🐛 Fix text editor cursor being too thin on dpr > 1 (#10501) 2026-07-01 10:43:59 +02:00
Alejandro Alonso 05e9c68a74 🐛 Fix preserving empty text fill when editing without changes (#10483) 2026-06-30 17:59:54 +02:00
Eva Marco 920c6d4e76 🐛 Fix token tooltips on numeric-input, color and typography (#10480) 2026-06-30 16:26:49 +02:00
Andrey Antukh 782adffec8 Merge remote-tracking branch 'origin/staging' into develop 2026-06-30 16:26:26 +02:00
Andrey Antukh 8087f2e42b 📎 Update .nvmrc 2026-06-30 16:25:24 +02:00
Andrey Antukh c4e72fd7f9 Merge remote-tracking branch 'origin/staging' into develop 2026-06-30 16:23:23 +02:00
Andrey Antukh 88961c6fd3 🔧 Add recursive install to plugins test workflow 2026-06-30 16:15:30 +02:00
Andrey Antukh 5bae6bd078 📎 Fix mcp deprecation warning 2026-06-30 16:15:30 +02:00
Andrey Antukh 1c02810b83 📎 Fix fmt issues on plugins 2026-06-30 16:15:30 +02:00
Andrey Antukh 92035ebd94 ⬆️ Update and fixed warnings on plugins module 2026-06-30 16:15:30 +02:00
Andrey Antukh f38d6cf0f2 ⬆️ Update nodejs version across docker images 2026-06-30 16:15:30 +02:00
Andrey Antukh e2b52d88fc ⬆️ Update dependencies 2026-06-30 16:15:30 +02:00
ParinithandAndrey Antukh 8d5b16295f 💄 Center libraries empty state placeholder vertically (#10452)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-30 16:14:24 +02:00
Luis de Dios 28afe223f4 🐛 Invitations to a team can be sent to existing members without displaying any error (#10489)
* 🐛 Fix team invitations can be sent to existing members without displaying any error

*  Add tests
2026-06-30 15:39:59 +02:00
Alonso Torres 8823f7ac4d Make v2 plugins default throw on error (#10433) 2026-06-30 14:50:50 +02:00
Luis de Dios 46f5346045 ♻️ Merge :thumbnails and :thumbnails-meta into single state key (#10021)
* ♻️ Merge :thumbnails and :thumbnails-meta into single state key

♻️ Unify thumbnail refs in a single ref

🐛 Fix test

* ♻️ Update tests
2026-06-30 14:37:27 +02:00
Andrew Barnes b3b3ea97db 📚 Fix Angular plugin usage doc link (#10349) 2026-06-30 14:35:55 +02:00
Alonso Torres e0be9f7ade Plugin for api testing (#10410) 2026-06-30 14:01:38 +02:00
Belén Albeza e6a49adfbc 🐛 Fix crash on composition update when pressing Esc on a IME (#10479) 2026-06-30 13:55:06 +02:00
Alonso Torres ca81776d04 🐛 Fix problems when dragging frame with comments (#10460)
* 🐛 Fix undo frame position not undoing comments

* 🐛 Fix problem with hover capturing dragging event

* 🐛 Fix watch updates for comment bubbles

* 🐛 Fix "Maximum update depth" crash on SVG shape transforms

* 🐛 Fix comment geometry problems
2026-06-30 13:33:47 +02:00
Elena Torró 21f646aeee 🐛 Fix decimal rounding and format in WebGL rulers (#10487) 2026-06-30 10:35:03 +02:00
VKing9 557a267176 🌐 Add translations for: Hindi
Currently translated at 83.8% (1987 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-06-30 07:01:22 +02:00
AntonPalmqvist f05fb70a8a 🌐 Add translations for: Swedish
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-30 07:01:19 +02:00
Alonso Torres f993f203bd 🐛 Fix problems with plugins API (#10412)
*  Adds static dispatch safe stubs in tests

* 🐛 Fix shapesColors metadata key to match ColorShapeInfo

* 🐛 Fix CommentThread.remove rejecting the owner's own threads

* 🐛 Fix page.removeCommentThread throwing on a spurious Promise

*  Implement ShapeBase.swapComponent in the plugin API

*  Expose File.revn in the plugin API

* 🐛 Fix FileVersion.createdAt calling Luxon method on a js/Date

* 🐛 Fix plugin font/typography application to text and ranges

* 🐛 Default plugin overlay interaction position for non-manual types

* 🐛 Fix plugin interaction setters passing an id-only shape

* 🐛 Fix grid addColumnAtIndex rejecting valid track types

* 🐛 Expose libraryId on library color/typography/component proxies

*  Implement LibraryTypography.setFont in the plugin API

* 🐛 Fix typography.applyToTextRange reading unexposed range bounds

* 🐛 Fix utils.geometry.center argument mismatch

* 🐛 Fix localStorage.removeItem calling getItem

* 🐛 Fix shape backgroundBlur proxy key casing

* 🐛 Report boolean shape type as 'boolean' in the plugin API

* 🐛 Return the resulting paths from plugin flatten

* 🐛 Make plugin z-order methods act on the target shape

* 🐛 Make is-variant-container? return a boolean

*  Implement Group.isMask in the plugin API

* 🐛 Return a shape proxy from TextRange.shape

* 🐛 Return the duplicated set from TokenSet.duplicate

* 🐛 Fix theme addSet/removeSet reading set name with a keyword

* 🐛 Accept string fontFamilies token value in the plugin API

* 🐛 Fix combineAsVariants ignoring the passed component ids

* 🐛 Fix board removeRulerGuide ignoring its argument

* 🐛 Fix board guides setter schema and parser

* 🐛 Avoid 0-byte allocation when syncing empty grid tracks

* 🐛 Validate grid track indices in the plugin API

* 🐛 Return null for empty input in group() and centerShapes()

* 🐛 Return TokenTypographyValue[] from a typography token's resolvedValue

* 🐛 Return TokenShadowValue[] from a shadow token's resolvedValue

* 🐛 Return string[] from a fontFamilies token's resolvedValue

* 🐛 Clear mutually-exclusive reps when setting LibraryColor gradient/image

* 🐛 Add readonly tags to types, deprecate Image type

* 📚 Update plugins changelog
2026-06-29 17:32:15 +02:00
Alejandro Alonso 99638fa60c 🐛 Fix wasm render playwright tests (#10462)
* 🐛 Fix playwright wasm test for updating canvas background

* 🐛 Fix playwright wasm test for rendering blurs

* 🐛 Fix invisible emoji texts in render-wasm playwright data
2026-06-29 17:03:45 +02:00
Aitor MorenoandElena Torró 2d4a24bf97 🐛 Fix stroke to path extra points (#10190)
* 🐛 Fix stroke to path extra points

* 🐛 Set evenodd when needed on stroke to path (#10446)

---------

Co-authored-by: Elena Torró <elenatorro@gmail.com>
2026-06-29 10:34:44 +02:00
Pablo Alba a9f3949abc Avoid going to last team on login if it is protected by sso (#10442) 2026-06-29 09:44:16 +02:00
Alonso Torres a47b0122c7 🐛 Fix problem with measures menu (#10445) 2026-06-29 09:24:37 +02:00
DoubleCat 785c5ffdfc 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 79.4% (1881 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-28 12:01:25 +02:00
Stephan Paternotte 3369abd988 🌐 Add translations for: Dutch
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-27 09:01:22 +02:00
Belén Albeza e9c0982a94 Disable hot swap of render engines (#10444) 2026-06-26 15:04:52 +02:00
Andrey Antukh 5212e2202b Merge remote-tracking branch 'origin/staging' into develop 2026-06-26 14:34:00 +02:00
Andrey Antukh a1352fc79e Merge remote-tracking branch 'origin/main' into staging 2026-06-26 14:33:45 +02:00
Andrey Antukh 0dee6e3cb0 📚 Add let binding algnment info to serena 2026-06-26 14:32:32 +02:00
Andrey Antukh 925dca35ab 📚 Update common testing doc on .serena 2026-06-26 14:28:49 +02:00
Andrey Antukh 736a22ab1d 📚 Add paren-repair script for automatic parentheses repair 2026-06-26 14:28:15 +02:00
Alejandro Alonso 44e39a1008 🐛 Sync WASM viewport when locating board in grid layout editor (#10443) 2026-06-26 14:24:44 +02:00
Eva Marco 6a79383082 🐛 Blur info doesn't show on inspect in certain shapes (#10427)
* 🐛 Blur info doesn't show on inspect in certain shapes

* 🎉 Add test
2026-06-26 14:10:41 +02:00
Belén Albeza 10147b6abd 🐛 Fix pixel grid and board pixel grid shown on top of rulers (#10430)
* 🐛 Fix pixel grid shown on top of rulers

* 🐛 Fix board pixel grid being rendered above rulers
2026-06-26 11:53:11 +02:00
Luis de Dios 8e9fb91959 🐛 Fix view mode is not persisted in color picker (#10369) 2026-06-26 11:38:51 +02:00
Pablo Alba 6e61e3304b Add and endpoint for nitrate to check the SSO configuration for an organization (#10432) 2026-06-26 11:38:18 +02:00
Elena Torró 89f882ecda 🐛 Fix viewer rendering on Firefox+NVIDIA setup (#10385) 2026-06-26 10:59:22 +02:00
Luis de Dios d16a2c93e0 🐛 Fix long typography token name in tooltip in design tab (#10387) 2026-06-26 10:50:19 +02:00
Luis de Dios 66719a14f5 🐛 Fix assets typography container is longer than others (#10406)
* 🐛 Fix assets typography container is longer than others

* ♻️ Use new SCSS guidelines
2026-06-26 09:42:22 +02:00
Denys Kisil f8974288e7 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 87.1% (2064 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-06-25 23:01:20 +02:00
Alejandro Alonso 345affc687 🐛 Fix premature WASM view-interaction end during pan (#10425) 2026-06-25 15:07:37 +02:00
Elena Torró 67386a0358 🐛 Fix missing tiles on the left (#10421) 2026-06-25 13:30:53 +02:00
Juanfran d328cb4a9e Enable org owners to view organization teams (#10388) 2026-06-25 13:07:20 +02:00
David Barragán Merino 14fb211733 🐛 Set org as owner for auto-label workflow 2026-06-25 12:41:18 +02:00
David Barragán Merino ce1191d86f 🐛 fix missing app-id in auto-label workflow 2026-06-25 11:53:07 +02:00
Belén Albeza d323aa9693 🐛 Fix guides not clipping (#10423) 2026-06-25 11:50:58 +02:00
Andrey Antukh 8645801eed 📎 Update changelog 2026-06-25 11:33:59 +02:00
David Barragán Merino f996ef372d 🔧 Migrate auto-label workflow from PAT to GitHub App toke 2026-06-25 11:26:15 +02:00
Andrey Antukh adbb5a8b5f Merge remote-tracking branch 'origin/staging' into develop 2026-06-25 10:44:07 +02:00
Andrey Antukh a668702982 Merge remote-tracking branch 'origin/main' into staging 2026-06-25 10:43:33 +02:00
Andrés Moya 4d1706f71c 🐛 Fix error when sending user feedback form (#10419)
If the smtp-default-from is something like "Penpot
<no-reply@penpot.app>", the schema validation fails because
it expects a simple email without < >
2026-06-25 10:37:50 +02:00
Pablo Alba 2a5b6a69ad Send warning for email about nitrate orgs with sso (#10413) 2026-06-25 09:53:07 +02:00
Andrey Antukh 2eb9423963 Merge remote-tracking branch 'origin/staging' into develop 2026-06-25 09:33:15 +02:00
Andrey Antukh f50d8edb13 Merge remote-tracking branch 'origin/main' into staging 2026-06-25 09:32:49 +02:00
María Valderrama 15a336a249 Allow nitrate to view org teams (#10365)
*  Allow nitrate to view org teams

* 📎 Code review

* 📎 Code review 2
2026-06-25 09:28:23 +02:00
Alonso Torres 28f3b8048a Improve MCP handling when tab is frozen in the browser (#10392) 2026-06-25 08:27:33 +02:00
David Barragán Merino d58d816310 🔧 Define concurrency policy to cancel in progress build workflows (#10409) 2026-06-25 08:22:41 +02:00
Elena Torró 149caaf292 🐛 Fix rulers values visibility (#10408) 2026-06-25 07:20:41 +02:00
Jack StormentandLuis de Dios aedb7f9195 Add dedicated Line and Arrow drawing tools (#9146)
*  Add dedicated Line and Arrow drawing tools

Introduce a Line/Arrow toolbar option and a click-drag drawing
interaction that matches Figma's workflow: select the tool, press and
drag to define the line in one gesture, with Shift snapping to 15°
increments. Arrowhead style can be toggled on either endpoint via the
existing stroke-cap controls.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Fix formatting error

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Translate line and arrow tooltips in top toolbar

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Add missing namespace

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 📚 Update copyright notice

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* Add translations (EN) for toolbar elements

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* Add translations (ES) for toolbar elements

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* ♻️ Improve stroke-cap-end update for arrow handling

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Fix shortcuts select tool but do not replace it in the toolbar

Refactor tool selection logic in top_toolbar.cljs

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* ♻️ Remove unnecessary blank line

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-24 21:32:13 +02:00
Alejandro Alonso b096832bf5 🐛 Fix v2 text editor detaching typography tokens (#10402) 2026-06-24 18:20:21 +02:00
Aitor Moreno 888f6798cf Merge pull request #10407 from penpot/superalex-render-from-cache-4
🐛 Fix render from cache
2026-06-24 16:32:26 +02:00
Alejandro Alonso 9b22d96553 🐛 Fix render from cache 2026-06-24 14:00:57 +02:00
Miguel de Benito DelgadoandAndrey Antukh a6c7bd28e8 💄 Fix some malli schemas (#7733)
* 🐛 Add missing fields to schema:profile

* 🐛 Add missing fields to schema:dissolve-animation

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 13:16:25 +02:00
Stephan Paternotte b9136e6fda 🌐 Add translations for: Dutch
Currently translated at 87.0% (2062 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-24 13:01:25 +02:00
Andrey Antukh 3036ef473e Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:31:03 +02:00
Andrey Antukh 4687b52bf3 📎 Fix fmt issues introduced in previous commits 2026-06-24 11:30:32 +02:00
Andrey Antukh 673c87cf33 Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:18:23 +02:00
Andrey Antukh bfa65547f8 Add more testing related improvements 2026-06-24 11:17:47 +02:00
Andrey Antukh 403e1ec604 Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:03:18 +02:00
Andrey Antukh 0d3a174f13 Merge remote-tracking branch 'origin/main' into staging 2026-06-24 11:03:05 +02:00
Andrey Antukh 27274a56fd 📎 Update changelog 2026-06-24 11:01:38 +02:00
Andrey Antukh 9391535f48 📎 Add minor enhancement to update-changelog skill 2026-06-24 11:01:14 +02:00
Dr. Dominik Jain 0270c2a90f 🐛 Fix description of TokenCatalog.addTheme in high-level overview (#10359)
Fixes #10074
2026-06-24 10:27:28 +02:00
Luis de Dios 5ef8d35683 🐛 Fix avoid flashing the move tool before activating the selected tool (#10291) 2026-06-24 10:25:37 +02:00
DexterityandAndrey Antukh a530cf0dec ♻️ Migrate render-wasm api object-svg to modern component syntax (#9459)
* ♻️ Migrate render-wasm api object-svg to modern component syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 10:24:53 +02:00
DexterityandAndrey Antukh d757f96633 ♻️ Migrate svg filter components to modern syntax (#9448)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 10:21:18 +02:00
Pablo Alba 8fa15f240f Check for nitrate sso on move team to another organization (#10379) 2026-06-24 10:02:13 +02:00
Andrey Antukh fa9012e55f Merge remote-tracking branch 'origin/staging' into develop 2026-06-23 15:28:42 +02:00
Eva Marco 1a9a831e72 🎉 Activate by default token combobox flag (#10378) 2026-06-23 15:16:51 +02:00
Andrey Antukh 06e6671813 Merge remote-tracking branch 'origin/staging' into develop 2026-06-23 12:31:25 +02:00
Andrey Antukh 9259b596dc Merge remote-tracking branch 'origin/main' into staging 2026-06-23 12:26:17 +02:00
Andrés Moya 5042a34e3c 🔧 Normalize text nodes comparison, to be used in tokens detach
🔧 Add more tests for all cases and fix text token application in tests
2026-06-23 12:24:30 +02:00
Stephan Paternotte 1a88864cbb 🌐 Add translations for: Dutch
Currently translated at 86.6% (2053 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-23 10:01:33 +00:00
Yaron Shahrabani 30be0cae71 🌐 Add translations for: Hebrew
Currently translated at 85.0% (2016 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-06-23 10:01:30 +00:00
Oğuz Ersen cf4fb8d4a2 🌐 Add translations for: Turkish
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-23 10:01:27 +00:00
Ingrid Pigueron af83a0c7f7 🌐 Add translations for: French
Currently translated at 84.8% (2010 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-23 10:01:21 +00:00
Andrey Antukh 1b3a3b4cdb 📎 Add auto-label and auto-project github workflow 2026-06-23 11:27:38 +02:00
Andrey Antukh f967a0fc83 Add improvements for frontend tests (#10380) 2026-06-23 11:21:53 +02:00
Ricardo Sawir 121c76235f 🐛 Fix zh-CN boolean intersection label (#10381)
Update the Simplified Chinese translation for the boolean intersection menu item so it no longer duplicates the difference label.
2026-06-23 11:12:05 +02:00
Elena Torró 07de0e92d5 Fix slow zoom/edit on Firefox+NVIDIA WebGL renderer (#10371) 2026-06-23 10:58:14 +02:00
Juanfran fc07a6467b 🐛 Fix organizations dropdown clipped by sidebar overflow (#10362)
Render the organizations selector dropdown in a portal anchored to the
trigger button, so a long list is no longer clipped by the
sidebar-content-wrapper overflow.
2026-06-23 10:37:59 +02:00
Elena Torró 8e548c8c54 🐛 Fix blank tiles and atlas crash on render-wasm zoom/pan (#10367)
* 🐛 Fix missing tiles on page switch and pan/zoom end

* 🐛 Fix blank tiles and atlas crash on render-wasm zoom/pan
2026-06-23 10:25:45 +02:00
since-2017-hubandAndrey Antukh 46abf1c968 ♻️ Migrate auth flow pages to modern component syntax (#9469)
Refactor the 6 mf/defc components across the auth flow
(login, password recovery, recovery request) to modern
mf/defc name* syntax. Modern syntax avoids per-render
JS->CLJS prop-conversion overhead.

- Rename login-page -> login-page* in
  frontend/src/app/main/ui/auth/login.cljs.
- Rename 
ecovery-form -> 
ecovery-form* and
  
ecovery-page -> 
ecovery-page* in
  frontend/src/app/main/ui/auth/recovery.cljs.
- Rename 
ecovery-form -> 
ecovery-form*,
  
ecovery-request-page -> 
ecovery-request-page*,
  
ecovery-sent-page -> 
ecovery-sent-page* in
  frontend/src/app/main/ui/auth/recovery_request.cljs
  (drop redundant {::mf/props :obj} marker since the *
  suffix already implies it).
- Drop unused :as props destructure bindings.
- Update internal forwarding callsites (2) and external
  callers in auth.cljs (3 callsites + 3 :refer imports),
  viewer/login.cljs (1 callsite + 1 :refer), and
  static.cljs (2 callsites + 1 :refer).

Refs #9260

Signed-off-by: since-2017-hub <since2017hub@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-23 09:38:07 +02:00
Elena Torró dd353a8121 🔧 Update design-tab tests for default background-blur flag 2026-06-23 09:28:28 +02:00
Filip SajdakandClaude Opus 4.8 66c8ebf198 🐛 Accept negative letterSpacing in plugin API text setters
The plugin text API rejected negative letter-spacing even though the
product UI allows -200..200 (typography.cljs). Two defects in
frontend/src/app/plugins/text.cljs:

- `letter-spacing-re` (`#"^\d*\.?\d*$"`) had no provision for a leading
  minus, so any negative value failed validation.
- The text-range `:letterSpacing` setter inverted its guard: it used
  `(or (empty? value) (re-matches ...))` to mean "invalid", which
  rejected matching values and let non-numeric input through. The
  text-shape setter and the sibling `lineHeight` range setter both
  correctly use `(not (re-matches ...))`.

Fix the regex to allow an optional leading minus and add the missing
`not` so the range setter matches the shape setter. Adds regression
coverage for the regex accept/reject contract.

Fixes #9780

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-06-22 22:40:44 +02:00
DexterityandAndrey Antukh ea20291d2a ♻️ Migrate v1 text-editor to modern component syntax (#9446)
* ♻️ Migrate v1 text-editor to modern component syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 18:40:01 +02:00
Krishna zadeandAndrey Antukh 0dbc2c54d6 🐛 Fix spacebar activating pan mode in editable fields as Comment Input box (#10287)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 17:32:12 +02:00
Alejandro Alonso e9410dce6b Merge pull request #10368 from penpot/elenatorro-fix-guides-pill-on-drag
🐛 Fix guides pill on drag
2026-06-22 15:58:34 +02:00
Elena Torro 20e90078db 🐛 Fix guides pill on drag 2026-06-22 15:40:12 +02:00
Andrey Antukh 4c5991514a 🐛 Fix syntax issues introduced in prev commit 2026-06-22 15:36:42 +02:00
Aitor Moreno a0d9603243 Merge pull request #10340 from penpot/hiru-fix-text-change-detection
🔧 Normalize text nodes comparison, to be used in tokens detach
2026-06-22 15:33:04 +02:00
DexterityandAndrey Antukh 22cf4917d8 ♻️ Migrate shapes/export components to modern syntax (#9449)
* ♻️ Migrate shapes/export components to modern syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Remove whitespace

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 15:28:00 +02:00
Luis de Dios aec56be9f5 🐛 Fix inspect in View Mode displays shape size twice when selecting a shape (#10364) 2026-06-22 14:46:48 +02:00
Eva Marco 3aa46379a1 🎉 Activate background-blur flag by default (#10366) 2026-06-22 14:46:01 +02:00
Andrey Antukh 4bbf9a6617 Merge remote-tracking branch 'origin/staging' into develop 2026-06-22 14:40:40 +02:00
Andrey Antukh 623a80ca00 🔧 Expose storybook port on devenv
Using 3451 port instead of the previous 6006
2026-06-22 14:35:10 +02:00
Andrey Antukh d8434cbffb 🐛 Add missing migrations (#10363) 2026-06-22 13:26:21 +02:00
Eva Marco 4e33ce7c46 🐛 Fix very long token names on remap modal (#10356) 2026-06-22 13:12:41 +02:00
Eva Marco e495e0ac59 🐛 Allow negative value on margins (#10353) 2026-06-22 13:12:30 +02:00
Andrey Antukh 5a82a38c9c 📎 Update changelog 2026-06-22 12:55:51 +02:00
Andrés Moya f2bf5a3111 🔧 Add more tests for all cases and fix text token application in tests 2026-06-22 12:54:40 +02:00
Alejandro Alonso 3a9be0b1d8 Merge pull request #10358 from penpot/niwinz-bugfix-1
🐛 Fix incorrect events handling on webgl render toggle
2026-06-22 12:54:13 +02:00
Andrey Antukh 3b9a895f62 🐛 Fix incorrect events handling on webgl render toggle
From the workspace main menu
2026-06-22 11:29:57 +02:00
Luis de Dios 7c19ace0f0 Reapply "🎉 Add flyout and semantic improvements to main toolbar (#9480)" (#10354)
This reverts commit 94119159d8.
2026-06-22 10:39:29 +02:00
Marina López e8e0d68019 Changed org avatar shape from rounded to squared 2026-06-22 09:59:43 +02:00
Andrey Antukh 19a851aacb Merge branch 'develop' into staging 2026-06-22 09:52:28 +02:00
Andrey Antukh a627d5c4d1 Merge remote-tracking branch 'origin/staging' into develop 2026-06-22 09:52:06 +02:00
Luis de Dios a74aa10dc1 🐛 Fix correction of some margins, alignment and ensuring consistency (#10351) 2026-06-22 09:49:33 +02:00
Andrey Antukh e3325dd3eb 🌐 Validate and rehash translation files 2026-06-22 09:33:10 +02:00
Alexis Morin 12fa25edf1 🌐 Add translations for: French (Canada)
Currently translated at 98.1% (2326 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:30:44 +02:00
VKing9 aeb63765c6 🌐 Add translations for: Hindi
Currently translated at 83.1% (1969 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-06-22 09:30:43 +02:00
Henrik Allberg 823bef44e0 🌐 Add translations for: Swedish
Currently translated at 98.3% (2330 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:30:42 +02:00
Црнобог 9bd0cd47a7 🌐 Add translations for: Serbian
Currently translated at 56.6% (1342 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2026-06-22 09:30:41 +02:00
Alejandro Alonso c9144a18fb 🌐 Add translations for: Yoruba
Currently translated at 48.4% (1148 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2026-06-22 09:30:39 +02:00
Alejandro Alonso a5cb53fcdf 🌐 Add translations for: Igbo
Currently translated at 20.6% (490 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2026-06-22 09:30:38 +02:00
Revenant afd2b75a25 🌐 Add translations for: Malay
Currently translated at 27.2% (646 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2026-06-22 09:30:38 +02:00
Alejandro Alonso 50f6ce1c0a 🌐 Add translations for: Hausa
Currently translated at 51.2% (1214 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2026-06-22 09:30:37 +02:00
Sebastiaan Pasma af415dbc4d 🌐 Add translations for: Dutch
Currently translated at 85.1% (2017 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-22 09:30:36 +02:00
Stephan Paternotte 5953e1d48b 🌐 Add translations for: Dutch
Currently translated at 85.1% (2017 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-22 09:30:35 +02:00
Edgars Andersons 98084f55b3 🌐 Add translations for: Latvian
Currently translated at 77.9% (1846 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-06-22 09:30:34 +02:00
deveronica 117600fc7a 🌐 Add translations for: Korean
Currently translated at 83.9% (1989 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-06-22 09:30:33 +02:00
Denys Kisil eb006d2bf2 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 84.4% (2001 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-06-22 09:30:31 +02:00
Zvonimir Juranko 44f26b93a8 🌐 Add translations for: Croatian
Currently translated at 66.2% (1570 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-06-22 09:30:30 +02:00
al0cam aad8ec280e 🌐 Add translations for: Croatian
Currently translated at 66.2% (1570 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-06-22 09:30:29 +02:00
Dário 6609018842 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 65.0% (1542 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-22 09:30:28 +02:00
TheScientistPT b644563e2a 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 65.0% (1542 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-22 09:30:27 +02:00
Amerey.eu dbc13f4cc4 🌐 Add translations for: Czech
Currently translated at 66.0% (1565 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-06-22 09:30:26 +02:00
Mikel Larreategi 7c87e65577 🌐 Add translations for: Basque
Currently translated at 48.7% (1155 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2026-06-22 09:30:24 +02:00
Radek Sawicki 3e7d52a709 🌐 Add translations for: Polish
Currently translated at 47.6% (1130 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-06-22 09:30:23 +02:00
Jacopo Lodovico Trabia 26291aa007 🌐 Add translations for: Italian
Currently translated at 89.1% (2111 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:30:23 +02:00
Nicola Bortoletto 09a00edeb6 🌐 Add translations for: Italian
Currently translated at 89.1% (2111 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:30:22 +02:00
william chen 8cc38f2ba9 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 66.2% (1569 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-06-22 09:30:21 +02:00
Yaron Shahrabani f2f98d43d3 🌐 Add translations for: Hebrew
Currently translated at 84.9% (2013 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-06-22 09:30:19 +02:00
Linerly f2400ab5df 🌐 Add translations for: Indonesian
Currently translated at 70.4% (1668 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-06-22 09:30:18 +02:00
Youkho c6c288033a 🌐 Add translations for: Arabic
Currently translated at 47.3% (1121 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-06-22 09:30:17 +02:00
AlexTECPlayz 41ac7e3ec3 🌐 Add translations for: Romanian
Currently translated at 80.6% (1911 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-06-22 09:30:12 +02:00
Stas Haas 4f1eca2500 🌐 Add translations for: German
Currently translated at 82.2% (1948 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-06-22 09:30:11 +02:00
Renan Mayrinck 3b47d88111 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 58.1% (1377 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-06-22 09:30:10 +02:00
Jun Fang d9a255f6de 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:09 +02:00
Beeby Xia 0679f2164f 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:08 +02:00
IsCycleBai f590485c77 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:07 +02:00
Oğuz Ersen ba16d28be1 🌐 Add translations for: Turkish
Currently translated at 98.3% (2330 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:30:06 +02:00
The_BadUser a99ef52311 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:04 +02:00
Vin ffbbb56a5f 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:03 +02:00
Egor Filatov 21657f100d 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:02 +02:00
Unreal Vision 057478ddf1 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:30:01 +02:00
GradelerM 4bc57619d8 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:30:00 +02:00
Corentin Noël 45f9f3e08f 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:29:59 +02:00
Anonymous f4a643e099 🌐 Add translations for: Spanish
Currently translated at 95.3% (2259 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-06-22 09:29:58 +02:00
Hosted Weblate 1952de884d 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-06-22 09:25:31 +02:00
Alexis Morin c087a45424 🌐 Add translations for: French (Canada)
Currently translated at 99.6% (2330 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 40247c6d6e 🌐 Add translations for: French (Canada)
Currently translated at 96.1% (2248 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 8eb20f1b05 🌐 Add translations for: French (Canada)
Currently translated at 93.0% (2177 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 796f95348a 🌐 Add translations for: French (Canada)
Currently translated at 90.8% (2126 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Nicola Bortoletto ad963831f7 🌐 Add translations for: Italian
Currently translated at 90.4% (2115 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:25:28 +02:00
AntonPalmqvist eb71b05d29 🌐 Add translations for: Swedish
Currently translated at 99.7% (2334 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin c42c2f6e84 🌐 Add translations for: French (Canada)
Currently translated at 88.5% (2072 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 66dcff290c 🌐 Add translations for: Swedish
Currently translated at 97.0% (2270 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Joseph V M ef8ddaca4f 🌐 Add translations for: Malayalam
Currently translated at 2.9% (69 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2026-06-22 09:25:28 +02:00
Alexis Morin 532f8410ab 🌐 Add translations for: French (Canada)
Currently translated at 88.1% (2062 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Oğuz Ersen 6f58bffd83 🌐 Add translations for: Turkish
Currently translated at 99.7% (2334 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 26def5424e 🌐 Add translations for: Swedish
Currently translated at 96.6% (2260 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin 92ff7f779a 🌐 Add translations for: French (Canada)
Currently translated at 87.3% (2044 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist d93e7157cd 🌐 Add translations for: Swedish
Currently translated at 93.6% (2191 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin ef6b1ff9a6 🌐 Add translations for: French (Canada)
Currently translated at 86.4% (2023 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Oğuz Ersen 57073da5b9 🌐 Add translations for: Turkish
Currently translated at 97.6% (2283 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:25:28 +02:00
Alexis Morin b8a2c5d34e 🌐 Add translations for: French (Canada)
Currently translated at 85.5% (2001 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 84151bd62b 🌐 Add translations for: Swedish
Currently translated at 87.8% (2054 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Nicola Bortoletto d5eee48c68 🌐 Add translations for: Italian
Currently translated at 89.9% (2105 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:25:27 +02:00
AntonPalmqvist c14ca30707 🌐 Add translations for: Swedish
Currently translated at 86.3% (2019 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:27 +02:00
Andrey Antukh 960b90253f ⬆️ Update root deps 2026-06-22 09:04:13 +02:00
Andrey Antukh d8894abcbc 📎 Update changelog 2026-06-22 09:04:01 +02:00
Andrés Moya 5f8d9740d6 🔧 Normalize text nodes comparison, to be used in tokens detach 2026-06-19 14:58:20 +02:00
Belén Albeza 5775e947ad 🐛 Fix page blur disappearing early 2026-06-19 13:41:42 +02:00
Andrey Antukh 2c5aaaa3c6 Revert "🐛 Highlight first matching font when searching the font picker (#9512)"
This reverts commit aba6e214ed.
2026-06-19 13:25:09 +02:00
Belén Albeza c4115a6143 🐛 Fix eyedropper not taking into account changes in dpr 2026-06-19 13:16:49 +02:00
Elena Torró b91eece5bf 🐛 Fix rulers shown on thumbnails 2026-06-19 13:01:26 +02:00
Alejandro Alonso b9ebe9f4ee 🐛 Fix viewer tile artifacts 2026-06-19 12:51:17 +02:00
Alejandro Alonso 6bc94ad6c6 Merge pull request #10333 from penpot/ladybenko-gh-10321-fix-guides-hover
🐛 Fix hover bugs in wasm guides
2026-06-19 12:30:56 +02:00
Eva Marco 59f9f2e163 🐛 Fix position of font selector on font family token modal 2026-06-19 12:10:47 +02:00
Pablo Alba b984e7bbe8 Add nitrate sso wards to organization navigation 2026-06-19 12:03:21 +02:00
Belén Albeza 2fbff5816c 🐛 Fix hover bugs in wasm guides 2026-06-19 11:47:33 +02:00
Krishna zadeandAndrey Antukh 08721127a3 🐛 Fix incorrect color count in color libraries dropdown (#10281)
* 🐛 Fix incorrect color count in color libraries dropdown

* 📎 Add minor formatting changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 11:32:14 +02:00
9e52bb40d0 Add process-level resource limits to font processing tools (#10274)
*  Add font processing resource limits via prlimit

Font processing tools (fontforge, sfnt2woff, woff2sfnt, woff2_decompress)
were invoked via clojure.java.shell/sh with no timeouts or resource limits.
This adds process-level resource limits using prlimit(1) and the shell/exec!
infrastructure from the ImageMagick hardening work.

shell/exec! changes:
- Add :prlimit parameter that prepends prlimit(1) to the command
- :prlimit takes {:mem <MiB> :cpu <seconds>} for address space and CPU time
  limits, enforced by the kernel's RLIMIT subsystem
- prlimit-cmd builds the prlimit command prefix (private helper)

Font processing changes:
- Replace all clojure.java.shell/sh calls with shell/exec! via exec-font!
- exec-font! applies font-prlimit (512 MiB, 30s CPU, 60s wall-clock)
- All 5 conversion functions (ttf->otf, otf->ttf, ttf-or-otf->woff,
  woff->sfnt, woff2->sfnt) use try/finally for explicit temp file cleanup
- Remove clojure.java.shell require from media.clj

Tests:
- Add exec-prlimit-normal, exec-prlimit-cpu, exec-prlimit-memory tests

Closes #10234

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Make font processing resource limits configurable

Replace hardcoded font-prlimit map and wall-clock timeout with
config-driven values under the PENPOT_FONT_PROCESS_* namespace.
The prlimit implementation detail is not exposed in config keys.

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>

---------

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>
Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-19 11:30:48 +02:00
RenzoandAndrey Antukh aba6e214ed 🐛 Highlight first matching font when searching the font picker (#9512)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 11:29:45 +02:00
David Barragán Merino 1b84655fb4 📚 Add the emoji code to Commit types table 2026-06-19 11:24:09 +02:00
Alejandro Alonso 2a5c29421f 🐛 Cap GPU max texture size at 4096 2026-06-19 09:38:38 +02:00
Andrey Antukh d3bec95860 🐛 Allow pasting comma-separated emails in multi-input (#10186)
The multi-input component did not handle paste events for
comma-separated values. When users pasted emails like
'qa@example.com, test@example.com', the entire string was
inserted as-is, triggering validation errors.

The on-key-down handler already split text on commas/spaces
when typing, but paste events bypassed this logic.

Added an on-paste handler that:
- Detects if pasted text contains commas or whitespace
- Splits the text by commas and/or whitespace
- Validates each part individually
- Adds valid items to the items list
- Prevents default paste behavior
- Resets input state after processing

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 09:38:00 +02:00
andrés gonzálezandCursor 564cd1b528 Show and manage comments while designing in the workspace (#10275)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 09:30:56 +02:00
0xRapzzand0xRapzz 1c8d26faaf 🐛 Fix swapped tooltip messages for token deletion states (#10316)
The tooltip messages for 'not-active' and 'has-errors' conditions
were swapped in both the typography row and color row components.

When a token is deleted (not-active), the tooltip should show the
'deleted-token' message, and when a referenced token has errors
(has-errors), it should show the 'not-active-token' message.

Fixes #10296
Fixes #10299

Signed-off-by: 0xRapzz <oxrapzz@rapzzclip.win>
Co-authored-by: 0xRapzz <oxrapzz@proton.me>
2026-06-19 08:18:25 +02:00
Pablo Alba 038ab5e1f7 🐛 Fix go to your penpot on error page (#10322) 2026-06-19 01:06:36 +02:00
Alonso Torres bbf63e1136 🐛 Fix array format in plugins properties (#10246) 2026-06-19 00:59:47 +02:00
Alonso Torres ecabe7ec32 🐛 Fix token creation fail when set inactive (#10297)
* 🐛 Fix token creatin fail when set inactive

* 🎉 Add a enable flag to addSet to enable the token set
2026-06-19 00:58:45 +02:00
Luis de Dios 4a41b2e5e0 ♻️ Update in-app onboarding slides (#10086)
* ♻️ Modify social media icons in verification email

* ♻️ Update verify email

* ♻️ Update copies in 'check your email'

* ♻️ Update onboarding images

* ♻️ Refurbish create team slide

* ♻️ Refactor SCSS for in-app onboarding

* 🐛 Fix replace old uxbox with penpot image for all email HTMLs

* 🐛 Fix use of link component
2026-06-18 22:49:29 +02:00
Alejandro Alonso c6ecfb7794 Merge pull request #10320 from penpot/superalex-fix-performance-regression-3
🐛 Fix performance regression
2026-06-18 18:37:40 +02:00
Alejandro Alonso 01fc3c3e7d 🐛 Defer tile atlas composition to full frames only 2026-06-18 18:33:10 +02:00
Alejandro Alonso f6f716de3a 🐛 Fix performance regression 2026-06-18 18:30:51 +02:00
Miguel de Benito Delgado 93b9f5c567 🐛 Follow 302 redirects when downloading templates (#10308)
* 💄 Update URIs for templates to avoid redirects

* 🐛 Follow 302 redirects when downloading templates
2026-06-18 18:14:56 +02:00
196e47fa93 Backport MCP related changes from develop (#10315)
* ♻️ Add mcp integration state management refactor (#10226)

* ♻️ Add mcp integration state management refactor

* 🐛 Fix access tokens do not appear

* ♻️ Refactor some names

* ♻️ Refactor token deletion

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Fix stale MCP token data after create/regenerate (#10280)

Fix the root cause in profile.cljs: remove the optimistic conj from
access-token-created and instead chain a fetch-access-tokens after the
create-access-token API call succeeds. This ensures all callers get a
fresh, server-consistent token list automatically.

Suggested-by: niwinz

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>

*  Remove non-recoverable mcp key warning from regenerated modal (#10298)

---------

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
Co-authored-by: kapil971390 <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-18 18:08:40 +02:00
David Barragán MerinoandAndrey Antukh 235f1137f1 ⬆️ Updgrade base image for penpot docker images to ubuntu 26.04 (#10031)
* ⬆️ Updgrade base image for penpot docker images to ubuntu 26.04

* ⬆️ Update playwright

* 🐳 Use dist-upgrade to update all system packages

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-18 18:07:46 +02:00
Alonso Torres 75e23cb9a3 🐛 Fix problem with plugins creating interactions always added a new flow (#10231) 2026-06-18 18:00:19 +02:00
Alonso Torres 6b50e2d822 🐛 Fix errors with code generation (#10217)
* 🐛 Fix errors with code generation

* 🐛 Add cancelation of effects in inspect code
2026-06-18 17:55:55 +02:00
Andrey Antukhandmimo-v2.5-pro b4532486e3 Add configurable resource usage limits for imagemagick (#10240)
* 🐳 Add ImageMagick policy.xml resource limits to backend Docker image

Add a restrictive policy.xml to the backend Docker image that caps
ImageMagick resource usage: 256MiB memory, 512MiB map, 128MP area,
30s time limit, 16KP max dimensions. Blocks PS/EPS/PDF/XPS coders
to prevent Ghostscript attack surface.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Add timeout support to shell/exec!

Add optional :timeout parameter (in seconds) that uses
Process.waitFor(long, TimeUnit). On timeout, the process is
destroyed forcibly and an :internal/:process-timeout exception
is raised. Stdout/stderr readers handle IOException from closed
streams when the process is killed.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* ♻️ Rename ::wrk/netty-executor to ::wrk/executor with cached pool

Replace DefaultEventExecutorGroup (fixed Netty thread pool) with a
cached thread pool (px/cached-executor) for general async task
offloading. The cached pool creates threads on demand and reuses
idle ones, which is more appropriate for blocking I/O workloads
(shell commands, message bus, rate limiting, etc.).

Changes:
- Rename ::wrk/netty-executor to ::wrk/executor in worker/executor.clj
- Switch implementation from DefaultEventExecutorGroup to px/cached-executor
- Update all ig/ref wiring in main.clj (msgbus, tmp cleaner, climit, rlimit, rpc)
- Remove ::wrk/netty-executor from redis.clj (let lettuce create its own
  eventExecutorGroup instead of sharing a Netty executor)
- Assert executor is present in shell/exec! to prevent silent nil usage
- Remove executor-threads config (no longer needed for cached pool)

The ::wrk/netty-io-executor (NioEventLoopGroup) remains unchanged as it
handles actual non-blocking network I/O for Redis and S3.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove im4java dependency and replace with direct ImageMagick CLI calls

- Replace im4java Java library with direct 'magick' CLI calls via shell/exec!
- Add PENPOT_IMAGEMAGICK_* config env vars for resource limits (thread, memory, map, area, disk, time, width, height)
- Use configurable ImageMagick environment with sensible defaults matching policy.xml
- Remove -Dim4java.useV7=true JVM flag from startup scripts
- Remove org.im4java/im4java from deps.edn
- All ImageMagick commands now use shell/exec! with 60s timeout and resource limits

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 💄 Rename imagemagick env functions and optimize config reads

- Rename imagemagick-defaults -> imagemagick-default-env
- Rename imagemagick-env -> get-imagemagick-env
- Optimize to avoid double cf/get calls per config key

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Add tests for shell/exec! timeout and media processing

- Add shell_test.clj: tests for exec! timeout, env vars, stdin, stderr
- Add media_test.clj: tests for info, generic-thumbnail, profile-thumbnail
- Fix generic-process to prefer explicit format over input mtype
- Fix shell/exec! to use cached executor when system has no executor
- Fix reduce-kv accumulator in set-env (must return penv)

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* ♻️ Refactor media/process to take system as first argument

- Change (defmulti process :cmd) -> (defmulti process (fn [_system params] (:cmd params)))
- Change (run params) -> (run system params)
- All process methods now receive [system params]
- Update all callers: rpc/commands/media, profile, auth, fonts
- Revert shell/exec! to require system with executor (no fallback)
- Fix lint warnings and formatting

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove unused app.svgo namespace

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove Node.js from backend Docker image

- Delete unused svgo-cli.js script
- Remove Node.js installation from Dockerfile.backend
- Remove svgo-cli.js copy from backend build script

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove unused process-error multimethod

- Remove process-error multimethod and its default handler
- Simplify media/run to directly call process
- Fix alignment in main.clj

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 📚 Add ImageMagick resource limits configuration to technical guide

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

---------

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>
2026-06-18 17:52:01 +02:00
Alejandro Alonso fe942f9780 Enable viewer WASM when render-wasm/v1 is active (#10314) 2026-06-18 17:47:46 +02:00
Andres Gonzalez 27c6761597 🐛 Emit create-shape-layout telemetry when adding grid layout via plugins or MCP 2026-06-18 16:38:00 +02:00
alonso.torres f81d4af05e 🐛 Changes after review 2026-06-18 16:35:07 +02:00
alonso.torres 823f2107cd 🐛 Add create variant util function 2026-06-18 16:35:07 +02:00
alonso.torres 445d14293e 🐛 Fix issue with padding and margin tokens in plugins 2026-06-18 16:18:46 +02:00
Alejandro Alonso 6f558bad2a Merge pull request #10312 from penpot/elenatorro-load-rulers-fast
 Improve rulers loading time
2026-06-18 16:12:36 +02:00
Eva Marco 955da2a9c2 🐛 Fix circular reference error on token edition (#10185)
* 🐛 Fix circular reference error on token edition

* ♻️ Move the fn to the helpers page

* 🎉 Add comment
2026-06-18 15:46:07 +02:00
Alonso Torres 0ad2864ebe 🐛 Fix problem with flow starting board (#10244) 2026-06-18 15:35:06 +02:00
Alonso Torres 5eb9753278 🐛 Fix problem with empty strings on createText plugins method (#10219) 2026-06-18 15:30:53 +02:00
Alonso Torres 68dd8ecdf5 🐛 Add fixedWhenScrolling to API (#10218) 2026-06-18 15:29:47 +02:00
Elena Torro bfef6ea089 Improve rulers loading time 2026-06-18 14:38:30 +02:00
alonso.torres b9dfa0c607 🐛 Fix problem when adding a variant children 2026-06-18 14:24:40 +02:00
Eva Marco 390a031099 🐛 Fix background blur on frame shapes 2026-06-18 14:23:14 +02:00
Eva Marco 9805d97e45 🐛 Fix font-selector position (#10302) 2026-06-18 13:36:15 +02:00
Andrey Antukh 0aca418007 Remove non-recoverable mcp key warning from regenerated modal (#10298) 2026-06-18 13:26:16 +02:00
Andrey Antukh 94119159d8 Revert "🎉 Add flyout and semantic improvements to main toolbar (#9480)"
This reverts commit 9a3023e5d0.
2026-06-18 12:37:23 +02:00
Belén Albeza d56c9f7bf6 🐛 Fix color picker (wasm) reading colors with disordered bytes 2026-06-18 12:11:06 +02:00
Marina López 785ab53f8c Cancel subscription when user deletes account 2026-06-18 12:09:12 +02:00
Alejandro Alonso 3d2a5a2957 Merge pull request #10216 from penpot/ladybenko-gh-10213-fix-double-click-guide
🐛 Fix double click not editing the guide
2026-06-18 12:06:13 +02:00
Andrey Antukh 540bc97787 🐛 Remove inconsistent library :is-indirect handling on frontend state
Related to #9506
2026-06-18 11:29:21 +02:00
kapil971390andkapilvus 11f3ef2549 🐛 Fix stale MCP token data after create/regenerate (#10280)
Fix the root cause in profile.cljs: remove the optimistic conj from
access-token-created and instead chain a fetch-access-tokens after the
create-access-token API call succeeds. This ensures all callers get a
fresh, server-consistent token list automatically.

Suggested-by: niwinz

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-18 11:26:50 +02:00
Alonso Torres b573a71017 🐛 Fix numeric values for tokens (#10270) 2026-06-18 11:05:14 +02:00
Alonso Torres a7e57c78cf 🐛 Add validation for current page on plugins API (#10271) 2026-06-18 11:04:35 +02:00
Andrey Antukhanddeepseek-v4-flash 18c8769f05 ♻️ Extract wait-for-persistence into shared helper (#10272)
Add wait-persisted and force-persist-and-wait to app.main.data.persistence,
removing 5 inline copies and 2 private helper functions across the codebase.

Replaced in:
- assets.cljs       -> dwp/force-persist-and-wait 400
- clipboard.cljs    -> dps/force-persist-and-wait 400
- versions.cljs     -> dwp/wait-persisted (3 call sites, dropped 2 priv fns)
- shape.cljs        -> dwp/wait-persisted 5000

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-18 10:43:30 +02:00
Andrey Antukh 203817fe6a Merge remote-tracking branch 'origin/staging' into develop 2026-06-18 09:44:10 +02:00
Andrey Antukhanddeepseek-v4-flash 71f5c11a11 Qualify MCP Redis channel names with tenant prefix
Read PENPOT_TENANT env var (defaulting to "default") and embed it in
Redis Pub/Sub channel names as penpot.mcp.<tenant>.task.{req,res}.<id>.

This prevents cross-tenant interference when multiple environments share
a Redis instance, matching the backend convention
(e.g. penpot.rlimit.<tenant>.window.<name> in app.rpc.rlimit).

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-18 09:22:29 +02:00
andrés gonzález fb8587ed3f 🐛 Fix register modal heading copy (Sign up for free) (#10263) 2026-06-18 09:12:25 +02:00
XavijuandXavier Julian 9a3023e5d0 🎉 Add flyout and semantic improvements to main toolbar (#9480)
Co-authored-by: Xavier Julian <xavier.julian@kaleidos.net>
2026-06-17 21:29:38 +02:00
Eva Marco 895c9cb8da 🐛 Fix tokens fonts combobox to show resolved value 2026-06-17 21:18:08 +02:00
Andrey Antukh a2e69db265 ⬆️ Update deps 2026-06-17 21:18:08 +02:00
Andrey Antukh 18a77953a7 Merge remote-tracking branch 'origin/staging' into develop 2026-06-17 19:13:32 +02:00
Andrey Antukh fe9598f96c Merge remote-tracking branch 'origin/main' into staging 2026-06-17 19:13:16 +02:00
Andrey Antukh 594bbf9dd6 📎 Update pr and commits worflow on serena 2026-06-17 19:12:26 +02:00
Elena Torró 80abc3fe3d 🐛 Fix shape fill flickering from color picker (#10273) 2026-06-17 18:41:41 +02:00
Alonso Torres 0afa108804 🐛 Fix stacked backdrop blurs 2026-06-17 17:44:40 +02:00
Andrey Antukh 2defd5c155 ⬆️ Upgrade docs/ dependencies and migrate to elevently v3 (#10242)
- Convert .eleventy.js to eleventy.config.mjs (ESM) since
  @11ty/eleventy-plugin-rss@3.0.0 is ESM-only
- Replace search-index.json.njk with search-index.json.11ty.js
  to avoid async templateContent access in Nunjucks filters
- Update feed.njk to use new RSS plugin v3 filter names:
  rssLastUpdatedDate -> getNewestCollectionItemDate | dateToRfc3339
  rssDate -> dateToRfc3339
- Add 11ty.js to templateFormats for search index generation
2026-06-17 17:37:20 +02:00
Alonso Torres c783260265 🐛 Fix problem with export and fonts (#10238) 2026-06-17 16:24:50 +02:00
David Barragán Merino 25484f53e7 🔧 Fix pnpm-workspace settings to allow the installation of wrangler (#10241) 2026-06-17 16:14:02 +02:00
Alejandro AlonsoandElena Torro 61cd9fe886 🐛 Fix performance regression
* 🔧 Preserve atlas on zoom interaction 

Co-authored-by: Elena Torro <elenatorro@gmail.com>
2026-06-17 14:52:12 +02:00
Eva Marco bdc9b092c5 Add proper props checking to several workspace sidebar components (#10159)
*  Add memo to sidebar components

*  Add memo to layout-container component

*  Add memo to layout-item component

*  Add memo to constraits component

*  Add memo to stroke-menu component

*  Add memo to shadows-menu component

*  Add memo to blur-menu component

*  Add memo to frame-grid-menu component

*  Add memo to grid-cell/options component

*  Add memo to svg-attrs component

*  Add check props to text-menu component

* 🐛 Fix CI
2026-06-17 14:39:03 +02:00
Alejandro Alonso 585d6944fc Merge pull request #10243 from penpot/elenatorro-fix-docatlas-cap
🐛 Fix DocAtlas cap
2026-06-17 10:42:48 +02:00
Elena Torro 82cd11a1ad 🐛 Fix DocAtlas cap 2026-06-17 09:52:57 +02:00
Eva Marco 8b20a3da15 🐛 Fix replace text by ref when dropdown is opened by click (#10174)
* 🐛 Fix replace text by ref when dropdown is opened by click

* 🎉 Add test
2026-06-17 08:39:40 +02:00
Andrey Antukh 0a54533240 Merge remote-tracking branch 'origin/staging' into develop 2026-06-17 00:11:23 +02:00
Andrey Antukh cc4cdff729 🔥 Remove duplicated github workflow 2026-06-17 00:05:07 +02:00
Andrey Antukh b645f51486 Backport .github directory from develop 2026-06-17 00:03:25 +02:00
Andrey Antukh 0338655cd0 📎 Add frontend pnpm-lock.yaml dedup 2026-06-17 00:02:07 +02:00
Andrey Antukh c9f9bd5029 📎 Use same playwright version on all frontend subpackages 2026-06-16 23:22:19 +02:00
Eva Marcoandalonso.torres 2a098e5b16 🎉 Add background blur (#10034)
* 🎉 Add background blur

* 🎉 Add test

* 🎉 Add background blur info to plugins API

* 🎉 Suport in wasm for both layer and background blur

* 🐛 Fix failing test

* ♻️ Fix comments

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-06-16 19:46:03 +02:00
Andrey AntukhandLuis de Dios b391a4c8d3 ♻️ Add mcp integration state management refactor (#10226)
* ♻️ Add mcp integration state management refactor

* 🐛 Fix access tokens do not appear

* ♻️ Refactor some names

* ♻️ Refactor token deletion

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-16 18:35:30 +02:00
andrés gonzález 565e173918 📚 Update troubleshooting WebGL doc (#10233) 2026-06-16 16:03:14 +02:00
Eva Marco 7863692c98 🐛 Fix fonts select position (#10192) 2026-06-16 15:28:27 +02:00
Eva Marco 6fed3e1212 🐛 Fix undo on numeric-input drag (#10193) 2026-06-16 15:28:11 +02:00
Andrey Antukh d5e6ea7572 🐛 Fix mcp menu incorrect state when key has not expiration (#10211) 2026-06-16 15:05:49 +02:00
Juanfran 09db565bc2 🐛 Skip org membership lookup for anonymous invite recipients
When an organization invitation token is verified by a logged-out recipient
(e.g. an unregistered invitee opening the emailed link), profile-id is nil.
The team-invitation branch still evaluated get-org-membership eagerly, calling
nitrate with that nil profile-id. That request fails and surfaces as a generic
error, masking the clean :invalid-token response and dropping the user on the
login screen instead of the dedicated "Invite invalid" page.

Only query membership when a logged-in profile is present, so a canceled or
otherwise invalid org invite reaches the :invalid-token path as intended.
2026-06-16 15:01:47 +02:00
Andrey Antukh c55d910e95 Merge remote-tracking branch 'origin/staging' into develop 2026-06-16 13:07:59 +02:00
Andrey Antukh 3dff9b8f28 Merge remote-tracking branch 'origin/main' into staging 2026-06-16 13:07:40 +02:00
Belén Albeza 8b92200488 🐛 Fix double click not editing the guide 2026-06-16 11:21:18 +02:00
Alejandro Alonso 392caa4ec1 Merge pull request #10150 from penpot/elenatorro-remove-to-blob
 Replace toBlob to capture snapshots without blocking the main thread
2026-06-16 10:00:46 +02:00
Alejandro Alonso cd239d24f8 🐛 Fix transparent tile holes in viewer WASM render
Use direct backbuffer tile compositing for render_sync_shape (viewer/thumbnails)
instead of the tile texture atlas cache, which left 512×512 transparent holes.
2026-06-16 09:52:19 +02:00
Alejandro Alonso 92c9517322 Merge pull request #10184 from penpot/azazeln28-fix-frame-stroke
🐛 Board stroke disappears when a layer is placed inside
2026-06-16 09:32:14 +02:00
Belén Albeza d1dd5d9016 🎉 Render guides in wasm (#10014)
*  Remove guides from svg overlay

* 🎉 Draw guides in wasm

* 🎉 Serialize guides to wasm

*  Store separate and sorted horizontal and vertical guides

* 🎉 Implement collision detection with guides

* 🎉 Right click on guides to change color or remove

*  Implement dragging guides

* 🎉 Edit wasm guides by double clicking them

* 🎉 Implement changing mouse cursor on hovering a guide

*  Show guide pill on hover

* 🎉 Implement removing guide on hovering + Del

* 🔧 Fix lint + fmt errors

* 🎉 Clip out outer board guide lines

* ♻️ Extract common code into guide-pill* component

* 🎉 Draw dotted lines on hovering board guides

* 🐛 Fix board rotation when it has guides

* 🎉 Make foreign guides not visible in focus mode
2026-06-16 09:19:58 +02:00
Alejandro Alonso b06942c668 Merge pull request #10191 from penpot/superalex-fix-viewer-webgl-issues
🐛 Fix some viewer webgl issues
2026-06-15 18:54:32 +02:00
Andrey Antukh 8b1845366a 🐛 Allow pasting comma-separated emails in multi-input (#10186)
The multi-input component did not handle paste events for
comma-separated values. When users pasted emails like
'qa@example.com, test@example.com', the entire string was
inserted as-is, triggering validation errors.

The on-key-down handler already split text on commas/spaces
when typing, but paste events bypassed this logic.

Added an on-paste handler that:
- Detects if pasted text contains commas or whitespace
- Splits the text by commas and/or whitespace
- Validates each part individually
- Adds valid items to the items list
- Prevents default paste behavior
- Resets input state after processing

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-15 17:50:59 +02:00
Alejandro Alonso 3802cdf9dd 🐛 Fix overlay canvas misalignment with shadow margin in WASM viewer 2026-06-15 17:16:19 +02:00
Aitor Moreno c54ab3bd34 🐛 Fix frame stroke 2026-06-15 16:09:23 +02:00
David Barragán Merino b03537fa68 ⬆️ Upgrade devenv base imagen to ubuntu 26.04 2026-06-15 14:00:29 +02:00
David Barragán Merino 3f3a52a098 ⬆️ Upgrade imagemagick version and base image to ubuntu 26.04 2026-06-15 14:00:29 +02:00
David Barragán Merino 575bed5c6e 🐛 Add missed info to manage help message 2026-06-15 14:00:29 +02:00
Andrey Antukh 03341ed857 ⬆️ Update npm deps and pnpm on all subpackages (#10183) 2026-06-15 13:57:29 +02:00
Andrey Antukh d44c6250ea Merge remote-tracking branch 'origin/staging' into develop 2026-06-15 13:04:19 +02:00
Marina López 98e04bc5f0 Improve nitrate manual renew banner 2026-06-15 12:30:59 +02:00
Andrey Antukh 61c52a665d ⬆️ Update dependencies (#10166)
* ⬆️ Update exporter dependencies

* ⬆️ Update frontend dependencies

* ⬆️ Update nodejs version devenv and docker images
2026-06-15 12:03:59 +02:00
Andrey Antukh 79227c4de8 Batch multiple thumbnail deletions into a single RPC call (#9943)
*  Batch multiple thumbnail deletions into a single RPC call

Replace the old per-object immediate thumbnail deletion with a
debounced batched approach. The frontend queues object-ids in state
and waits 200ms before sending a single RPC request with up to 200
object-ids. The backend deletes all matching thumbnails in one SQL
statement with a single RETURNING clause, then touches the affected
media objects.

This reduces RPC overhead when rapidly clearing thumbnails (e.g.
navigating pages) and makes deletions more efficient.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Fix missing issues

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-15 12:01:32 +02:00
Alejandro Alonso a01c4e1bad 🐛 Fix scale viewer WASM canvas by devicePixelRatio like workspace 2026-06-15 11:45:21 +02:00
Juan de la CruzandLuis de Dios ddeaf3ce2a Add MCP status button to workspace toolbar with single-tab connection control (#9930)
*  Add MCP connection badge to the workspace toolbar

*  Add MCP status button with single-tab connection control

* ♻️ Extract component for MCP indicator in the toolbar

* ♻️ Some improvements

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-15 11:29:25 +02:00
Alonso Torres 92cf0cda7b 🐛 Fix openPage plugin problem (#10085)
* 🐛 Fix openPage plugin problem

* 🐛 Make history safer for tests
2026-06-15 11:27:35 +02:00
David Barragán Merino 6a1c3348f3 🐳 Clean apt package cache in all Dockerfiles 2026-06-15 11:13:12 +02:00
David Barragán Merino fe86359dc8 🐳 Migrate storybook image to alpine 2026-06-15 11:13:12 +02:00
David Barragán Merino 572b094bab 🐳 Migrate frontend image to alpine 2026-06-15 11:13:12 +02:00
Alejandro Alonso 9d7a8eaeda 🐛 Fix rendering artifacts from previous frames 2026-06-15 11:00:56 +02:00
Luis de Dios 739a2d4958 💄 Update translations for company size options in in-app onboarding (#10164) 2026-06-15 10:57:08 +02:00
Eva Marco c66ee1803f 🎉 Toggle color library visibility from the colorpicker shortcut button (#10129)
* ♻️ Transform a show button to a toggle button on colorpicker

* 🎉 Add test

* 🎉 Add aria-pressed to toggle palette button
2026-06-12 13:30:57 +02:00
María Valderrama 68d4238277 🐛 Fix license not loading in theme change 2026-06-12 12:13:50 +02:00
Andrey Antukh a8d0c18c1b 🐛 Fix race condition between MCP initialization and plugin runtime (#10137)
* 🐛 Fix race condition between MCP init and plugin runtime

Add promise-based synchronization to ensure MCP initialization waits
for plugin runtime to be ready before calling global.ɵloadPlugin.

- Add runtime-ready-promise in app.plugins that resolves when
  init-plugins-runtime completes
- Add wait-for-runtime function for other modules to await readiness
- MCP init now waits for runtime via rx/from before starting plugin
- Add defensive guards in start-plugin!, load-plugin!, close-plugin!
  to check if plugin APIs exist before calling
- Rename init-plugins-runtime! to init-plugins-runtime

Fixes: global.ɵloadPlugin is not a function error when MCP plugin
starts before async plugin runtime initialization completes.

* 📎 Add 'create-pr' opencode skill
2026-06-12 11:40:02 +02:00
Andrey Antukh f5874e159e 🔧 Replace UAParser.js with @penpot/ua-parser (#10007) 2026-06-12 10:46:09 +02:00
Alejandro Alonso 87eb91f805 ♻️ Migrate viewer WASM viewport to modern syntax (#10106) 2026-06-12 10:29:58 +02:00
David Barragán Merino 7cb7f7adb2 🐳 Add command to upgrade packages during the creation 2026-06-12 09:34:26 +02:00
Alonso Torres b2439694af 🐛 Fix float precision in typography line-height/letter-spacing string conversion (#9973) 2026-06-12 09:10:36 +02:00
Alonso Torres d6c973e269 🐛 Improved error handling on a tool error (#10148) 2026-06-12 08:59:05 +02:00
Justin Lin 9c7af3aeb5 📚 Add WebSocket proxy configuration for MCP in Nginx example (#10152)
The current Nginx example doesn't include the required Websocket settings to correctly proxy /mcp/ws, and leads to WebSocket connection error when trying to connect in a design. Adding this lines fixed the issue.

Signed-off-by: Justin Lin <30039756+lancatlin@users.noreply.github.com>
2026-06-12 08:55:23 +02:00
Elena Torro 4a86431dfd Replace toBlob to capture snapshots without blocking the main thread 2026-06-11 17:56:46 +02:00
Andrey Antukh 2e1839f898 🐛 Fix NotReadableError in rasterizer during thumbnail generation
The rasterizer's create-image function was clearing image.src in its
Rx teardown cleanup. This caused the decoded pixel data to be discarded
before downstream operators (drawImage / createImageBitmap) could read
it, resulting in a browser NotReadableError.

Changes:
- Remove image.src = "" from cleanup; the image element will be
  garbage collected naturally. Event handler nulling is kept to break
  circular references.
- Add dimension validation in svg-get-adjusted-size to return nil for
  zero/NaN dimensions instead of producing invalid sizes.
- Add fallback in svg-set-intrinsic-size! to use [max max] when SVG
  dimensions can't be determined.

Error occurred in production (2.16.0-RC10) during thumbnail generation
in the workspace.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-11 17:04:24 +02:00
Andrey Antukh 5f21ebd08d 🐛 Filter ignorable React removeChild errors at error boundary and fix HTML anti-pattern (#10145)
* 🐛 Filter ignorable exceptions in error-boundary onError callback

The global uncaught-error-handler already skips NotFoundError/removeChild
and other harmless errors, but react-error-boundary's onError callback fires
independently of the window.onerror pipeline. This means the error boundary
was still logging these errors and setting last-exception, causing them to
continue appearing in error reports despite being non-actionable.

Add the is-ignorable-exception? check to the error-boundary* onError so
harmless errors are silently ignored, matching the behavior of the global
handler.

* 🐛 Fix dangerouslySetInnerHTML anti-pattern in context-notification

The previous code used dangerouslySetInnerHTML on the same element that
could also contain React children. This is a React anti-pattern that can
cause reconciliation mismatches and lead to removeChild DOMExceptions.

Refactor to use two separate element branches: one for raw HTML injection
and one for normal React children with links.
2026-06-11 16:59:00 +02:00
David Barragán Merino 045f177a1f 🐳 Pin docker images to 2.16 2026-06-11 16:48:38 +02:00
David Barragán Merino b2c7854f5e 🐳 Pin docker images to 2.16 2026-06-11 16:48:24 +02:00
David Barragán Merino ab614d2ae9 🐳 Pin docker images to 2.16 2026-06-11 16:47:35 +02:00
David Barragán Merino 11096f1829 🔧 Remove the confirmation step for publishing docker images 2026-06-11 16:26:36 +02:00
David Barragán Merino b1ec03a5d2 🔧 Remove the confirmation step for publishing docker images 2026-06-11 16:26:18 +02:00
Alonso Torres 6cc1c1596f 📚 Improve plugin api docs (#10142) 2026-06-11 15:51:03 +02:00
Andrey Antukh 715bd1c09e 📎 Update mcp server api_types.yml file 2026-06-11 14:51:02 +02:00
Andrey Antukh 6827c4554b 📎 Update version on mcp package.json 2026-06-11 14:49:48 +02:00
Andrey Antukh 8b16326334 📎 Update changelog 2026-06-11 14:49:09 +02:00
Luis de Dios 9670140448 🐛 Fix first element on shared libraries list does not have border (#10062) 2026-06-11 13:35:10 +02:00
Alonso Torres 5b6041624a 🐛 Fix export presets not preserved in view mode inspect (#9972)
* 🐛 Fix export presets not preserved in view mode inspect

* 🐛 Changes after review
2026-06-11 13:33:48 +02:00
Andrey Antukh 2d843da2cd 📎 Update mcp server api_types.yml 2026-06-11 13:31:34 +02:00
Andrey Antukh 70cbb65f9c Merge remote-tracking branch 'origin/staging' into develop 2026-06-11 13:27:43 +02:00
Andrey Antukh 86d508eaf1 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 13:24:57 +02:00
Luis de Dios 331e66c1c6 Update company size options in in-app onboarding (#10110) 2026-06-11 13:23:41 +02:00
Alejandro Alonso 38d12b3363 Add hard reload path for renderer menu A/B test (#10133) 2026-06-11 13:16:09 +02:00
Alonso Torres f5c258b868 🐛 Activate inactive profile when re-registering with disable-email-verification (#9999) 2026-06-11 12:52:07 +02:00
Madalena Melo 777f335a18 📎 Update THANKYOU.md (#10100)
Update THANKYOU.md to include Noob Researcher and Kirubakaran N

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-06-11 12:47:20 +02:00
Andrey Antukh 935fd85e9c 🐛 Fix exporter deps install script 2026-06-11 12:42:43 +02:00
Andrey Antukh 3a386c0ee6 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 11:56:05 +02:00
andrés gonzález a8c1a673a8 📚 Add MCP server block and setup videos to docs (#10121) 2026-06-11 11:46:40 +02:00
David Barragán Merino 726a0cf89f 🐳 Add command to upgrade packages during the creation 2026-06-11 11:13:20 +02:00
Andrey Antukh 5eb8c9ee70 ⬆️ Update frontend deps 2026-06-11 11:06:12 +02:00
Andrey Antukh 12e7ea038f Merge remote-tracking branch 'origin/staging' into develop 2026-06-11 10:58:19 +02:00
Elena Torró 6ebefa2c16 Add single-file export to PDF using the WebGL render (#9860) 2026-06-11 10:41:21 +02:00
Andrey Antukh d65e8317e3 📎 Update changelog 2026-06-11 10:41:05 +02:00
Andrey Antukh 99bf493030 📎 Update the update-changelog skill 2026-06-11 10:40:03 +02:00
Andrey Antukh 3377473f05 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 10:12:12 +02:00
Andrey Antukh ba9b03268a ⬆️ Update backend and common dependencies (#10108)
* 🐛 Fix incorrect valkey uri on backend tests

* ⬆️ Update backend dependencies

* ⬆️ Update pnpm dependencies

* 📎 Fix minor linter issues
2026-06-11 10:11:23 +02:00
Aitor Moreno 17b38e3e6b Merge pull request #10123 from penpot/superalex-fix-wasm-selected-text-hover-double-click
🐛 Keep hover on selected text layers in WASM renderer
2026-06-11 09:47:50 +02:00
María Valderrama 577ddfa03e 🐛 Fix number of days for deletion for nitrate 2026-06-11 09:41:45 +02:00
Belén Albeza 8eedd951a8 🐛 Fix mistmatch in mapping custom font variants (wasm) (#10122) 2026-06-11 09:36:29 +02:00
Alejandro Alonso 435cdcb1d4 🐛 Keep hover on selected text layers in WASM renderer 2026-06-11 09:23:03 +02:00
Andrey Antukh d1fb1d34a1 🐛 Fix email validation to reject consecutive dots in domain (#10096)
* 🐛 Tighten email validation regex to reject consecutive dots in domain

* 📎 Add minor adjustments to gh-issue-from-pr skill
2026-06-10 19:21:51 +02:00
Dexterity 920a05de74 ♻️ Migrate typography sidebar option components to modern syntax (#9443) 2026-06-10 18:16:13 +02:00
Alonso Torres ccd34a2705 🐛 Fix problems with async font loading (#10081) 2026-06-10 14:51:46 +02:00
DexterityandAndrey Antukh 3ffc76a552 ♻️ Migrate token theme-selector components to modern syntax (#9447)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:48:11 +02:00
DexterityandAndrey Antukh 0b60e4e16e ♻️ Migrate flex-controls padding/margin/gap to modern syntax (#9458)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:47:48 +02:00
DexterityandAndrey Antukh 5d2e0cb7cb ♻️ Migrate viewer/interactions viewport components to modern syntax (#9442)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:46:25 +02:00
Eva Marco 6937e70dab 🎉 Activate by default token typography row (#10090)
* 🎉 Activate by default token typography row

* ♻️ Update test for text multiselection
2026-06-10 13:30:04 +02:00
Andrey Antukh 21d2037111 🔧 Fix default issue templates 2026-06-10 12:36:21 +02:00
Andrey Antukh 5a3458bf74 🔧 Fix typo on github workflows config 2026-06-10 11:36:42 +02:00
Andrey Antukh d04be98644 🔧 Add minor configuration fix for tests-frontend workflow 2026-06-10 11:31:54 +02:00
Andrey Antukh 9f5a0f767e Merge remote-tracking branch 'origin/staging' into develop 2026-06-10 11:26:48 +02:00
Andrey Antukh fa0531cd28 Merge remote-tracking branch 'origin/main' into staging 2026-06-10 11:26:14 +02:00
Andrey Antukh 3419f7e60a ⬆️ Update root repo deps 2026-06-10 11:25:59 +02:00
martrend33 5dea5db685 📎 Add license metadata for MCP package (#10089)
Add the repository license SPDX identifier to the published @penpot/mcp package metadata.

Signed-off-by: martrend33 <145278472+martrend33@users.noreply.github.com>
2026-06-10 11:17:07 +02:00
Andrey Antukh 0ac092d177 🐛 Make set-file-shared idempotent to fix race condition with optimistic updates (#10093) 2026-06-10 10:58:50 +02:00
DexterityandAndrey Antukh a178de2c69 ♻️ Migrate v3 text-editor to modern component syntax (#9444)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:57:19 +02:00
DexterityandAndrey Antukh 890ce378a7 ♻️ Migrate template-item to modern component syntax (#9441)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:54:46 +02:00
sxxtonyandAndrey Antukh ebb5738c7b ♻️ Migrate dashboard assets to modern syntax (#9404)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:52:54 +02:00
sxxtonyandAndrey Antukh bfad78d39c ♻️ Migrate account settings to modern syntax (#9401)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:52:14 +02:00
DexterityandAndrey Antukh 32d974aa3e ♻️ Migrate onboarding team-choice to modern component syntax (#9463)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:51:35 +02:00
Alejandro Alonso becba0a82b 🐛 Fix frame inner strokes ignoring focus mode in wasm renderer 2026-06-10 09:36:28 +02:00
Andrey Antukh 7fd4e35203 ♻️ Refactor CI workflows 2026-06-09 18:57:44 +02:00
Madalena Melo 1a4cee7e5a 📎 Update SECURITY.md (#10082)
Update SECURITY.md file to request that vulnerabilities be reported through the GitHub Security Advisories feature in the Penpot repository

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-06-09 18:48:03 +02:00
Andrey Antukh 4b03cfd20c Merge remote-tracking branch 'origin/staging' into develop 2026-06-09 14:39:07 +02:00
Andrey Antukh f7c5ce7ac9 Merge remote-tracking branch 'origin/main' into staging 2026-06-09 14:38:48 +02:00
Andrey Antukh e0a44eede0 Backport serena memory and other minor config fixes from develop 2026-06-09 14:37:59 +02:00
Madalena Melo 4df399ab5a 📎 Update THANKYOU.md (#10020)
Update THANKYOU.md to include Alisher (@7megaumka7)

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-06-09 14:32:06 +02:00
Andrey Antukh 6671037ff7 📎 Update tooks/gh.py script 2026-06-09 14:01:52 +02:00
David Barragán Merino c37cff7687 📎 Set pnpm version on docs/package.json 2026-06-09 13:54:31 +02:00
David Barragán Merino 82acec1191 📎 Set pnpm version on docs/package.json 2026-06-09 13:50:00 +02:00
David Barragán Merino 11a8d08f95 📎 Set pnpm version on docs/package.json 2026-06-09 13:28:54 +02:00
Andrey Antukh 9ae84dbfe9 📎 Update changelog 2026-06-09 13:01:01 +02:00
Andrey Antukh 62c85467f9 🔧 Update the default github issues template 2026-06-09 13:01:01 +02:00
Andrey Antukh 06c83553fd 📚 Add creating issues workflow to serena memory 2026-06-09 13:01:01 +02:00
Codex 744c1b98c0 🐛 Anchor variant switch geometry to target
Preserve real size overrides during variant switches without copying stale absolute composite geometry from the source variant.

Signed-off-by: Codex <codex@openai.com>
2026-06-09 12:20:15 +02:00
Alonso Torres d249fd106a 🐛 Fix theme problem after update (#9955) 2026-06-09 11:17:06 +02:00
Aitor Moreno facea16444 Merge pull request #10038 from penpot/superalex-viewer-wasm
🎉 Basic viewer with wasm
2026-06-09 11:00:19 +02:00
alonso.torres 70e8dbb38a 🐛 Fix cropped outer stroke of rotated board in view mode 2026-06-09 09:40:26 +02:00
Alonso Torres 3444c0589f 🐛 Fix parallel environments css hot reload (#10064) 2026-06-08 17:56:21 +02:00
Andrey Antukh f450e09e08 🔧 Remove direct project reference on issue templates
We will use workflows for this purpose
2026-06-08 14:47:30 +02:00
Andrey Antukh 25ee47a2d4 🔧 Fix issue on but report github template 2026-06-08 14:43:20 +02:00
Andrey Antukh 27ba1ffbe0 📎 Update version on mcp/package.json 2026-06-08 14:38:47 +02:00
Andrey Antukh 7aa720f150 Merge remote-tracking branch 'origin/staging' into develop 2026-06-08 14:36:44 +02:00
Andrey Antukh c7fae1f353 Merge remote-tracking branch 'origin/main' into staging 2026-06-08 14:36:24 +02:00
Andrey Antukh 51a9eed02e Backport from develop AGENTS.md changes 2026-06-08 14:35:19 +02:00
Andrey Antukh 0e16db66b8 Backport from develop AGENTS.md changes 2026-06-08 14:34:31 +02:00
Andrey Antukh eff533374d 🐛 Ignore Safari browser extension errors in error handler
Add detection for Safari's webkit-masked-url:// extension URLs and filter
the "Attempting to change value of a readonly property" TypeError to prevent
Safari browser extension errors from being surfaced to users.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-08 14:32:01 +02:00
Andrey Antukh 82cfbedc26 Merge remote-tracking branch 'origin/main' into staging 2026-06-08 14:28:30 +02:00
Andrey Antukh c2f2e0e34b 📎 Add opencode issue-title skill 2026-06-08 14:27:07 +02:00
Andrey Antukh a326cc416e Backport github issue templates from develop 2026-06-08 14:26:45 +02:00
Andrey Antukh 8a2274cbc0 🔧 Update default github issue templates 2026-06-08 14:25:38 +02:00
Alonso Torres 6808390827 🐛 Fix problem with color picker error (#10056) 2026-06-08 13:25:45 +02:00
David Barragán Merino 67ee0b0625 🔧 Remove wokflow to build main-staging branch 2026-06-08 13:15:56 +02:00
Andrey Antukh 5426092d68 📚 Remove the requirement of changelog update 2026-06-08 12:02:28 +02:00
Andrey Antukh 4d0a3efc5c 🐛 Fix plugin API crash when setting text fills (#10051)
The `update-text-range` event's `watch` method was returning a bare
potok event object (`dwwt/resize-wasm-text-debounce id`) directly
inside `rx/concat`, instead of wrapping it in `rx/of`. This caused
RxJS to throw "You provided an invalid object where a stream was
expected" when a plugin set text fills via the Plugin API.

The fix wraps the event in `rx/of` so it becomes a valid Observable,
matching the pattern used elsewhere in the codebase (e.g.,
`clipboard.cljs` lines 1050/1082 and `texts.cljs` line 1232).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-08 11:33:36 +02:00
Pablo Alba 2a48747cf6 Review nitrate add team members permission 2026-06-08 10:56:18 +02:00
Andrey Antukh 4f852e33bf Backport mcp package changes from develop 2026-06-08 09:59:33 +02:00
Dr. Dominik Jain 03c02d5adf 🎉 Enable multi-instance horizontal scaling for MCP server (#10013)
* 📎 Ignore .iml files (IntelliJ module files)

* 🎉 Enable multi-instance horizontal scaling for MCP server

Allow the MCP server to run as multiple instances behind a plain
round-robin load balancer, removing the previous requirement that a
user's plugin WebSocket and MCP client connection terminate on the same
instance. Behaviour is unchanged when run as a single instance or
without Redis.

Cross-instance MCP sessions: when a request arrives with an
mcp-session-id that was initialised on another instance, the session is
adopted locally instead of rejected. The user token is read from the
query parameter (present on every request, as the configured endpoint
URL is never rewritten), so no shared session store is needed; the
transport is pre-initialised so the SDK's validateSession() accepts it.

Cross-instance task routing: when a Redis URI is configured in
multi-user mode, plugin task requests are routed via Redis pub/sub keyed
by user token. The instance holding a plugin's WebSocket subscribes to
that token's request channel; any instance handling a tool call
publishes the request and awaits the response on a per-request channel.
RedisBridge is a pure transport for the existing serialised
PluginTaskRequest/Response objects. PluginTask is split into an abstract
base plus a local (promise-backed) PluginTask and a RemotePluginTask
whose resolve/reject publish the outcome back over Redis, so the
existing local dispatch and response-correlation paths are reused
unchanged on the executing instance.

Refs #10000
2026-06-08 09:53:54 +02:00
Andrey Antukh c183380e0d Merge remote-tracking branch 'origin/staging' into develop 2026-06-08 09:51:03 +02:00
Andrey Antukh bae4d23c67 Merge remote-tracking branch 'origin/staging' 2026-06-08 09:40:28 +02:00
Andrey Antukh c5bd583b1f 📎 Update root deps 2026-06-08 09:39:59 +02:00
Alexis MorinandAndrey Antukh 3da7e7eb77 🐛 Fix French Canada locale not loading correct translations (#10027)
* 🐛 Fix French Canada locale not loading correct translations

Signed-off-by: Alexis Morin <alexis.morin@autodesk.com>

* Update CHANGES.md

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Alexis Morin <alexis.morin@autodesk.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-08 09:36:45 +02:00
Alejandro Alonso 9f5e89d5f8 🎉 Basic viewer with wasm 2026-06-05 16:41:30 +02:00
Alejandro Alonso f9f4d7e2cd 🐛 Fix offscreen canvas resizing 2026-06-05 14:55:17 +02:00
Andrey Antukh 15f469becb Merge remote-tracking branch 'origin/staging' into develop 2026-06-05 11:49:37 +02:00
Andrey Antukh 4755ebbedf Merge remote-tracking branch 'origin/main' into staging 2026-06-05 11:49:18 +02:00
Andrey Antukh 2ad63d8887 📎 Backport .opencode directory fron develop 2026-06-05 11:49:06 +02:00
Andrey Antukh adcc2ebd1a Merge remote-tracking branch 'origin/staging' into develop 2026-06-05 11:44:50 +02:00
Andrey Antukh 7736104daa Merge remote-tracking branch 'origin/main' into staging 2026-06-05 11:44:36 +02:00
Andrey Antukh f457c68355 📎 Backport devenv improvements 2026-06-05 11:44:20 +02:00
Andrey Antukh e2f96a6ba0 📎 Add minor changes to opencode setup 2026-06-05 11:30:58 +02:00
Elena Torró 47ce68eed0 🐛 Fix masked group applied blur and bounds (#10028) 2026-06-05 11:01:47 +02:00
Marina López fc038e72fc Allow send events from nitrate 2026-06-05 09:35:14 +02:00
Elena TorróandAlejandro Alonso 60d3c81450 Add wasm rulers (#9858)
*  Add wasm rulers

* 🔧 Fix dpr on page zoom

Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
Co-authored-by: Elena Torro <elenatorro@gmail.com>

* 🔧 Change page-switch behavior to refresh rulers and keep blurred snapshot

* 🐛 Restore WASM rulers after WebGL context recovery

Co-Authored-By: Elena Torro <elenatorro@gmail.com>
Co-Authored-By: Alejandro Alonso <alejandroalonsofernandez@gmail.com>

---------

Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
2026-06-05 07:51:35 +02:00
alonso.torres 8d3516d06d 🐛 Fix path export crop when stroke has arrow/marker caps 2026-06-04 23:48:55 +02:00
Andrey Antukh 9911ff7959 Merge remote-tracking branch 'origin/staging' into develop 2026-06-04 19:01:54 +02:00
Andrey Antukh 6d77ca3fc1 Merge remote-tracking branch 'origin/main' into staging 2026-06-04 19:01:25 +02:00
Juanfran cb2994fc3b Add lang to nitrate authenticate endpoint response
Expose the user's `:lang` profile field alongside `:theme` from the
internal nitrate `authenticate` RPC so the Nitrate admin console can
load translations matching the user's Penpot language preference.
2026-06-04 14:58:09 +02:00
Andrey Antukh 3a44e291f4 📎 Add minor improvements to manage.sh 2026-06-04 13:00:04 +02:00
Andrey Antukhandalonso.torres 945c44c505 🔧 Update docker terminal settings and refactor devenv management (#10018)
* 🔧 Update docker terminal settings

* 🔧 Get back the HTTP listener for devenv

* 🐛 Fix problem with https port

* 📎 Fixup

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-06-04 12:22:35 +02:00
Alonso Torres ccc734055f 🐛 Fix problem with stroke-cap migration (#10019) 2026-06-04 11:38:46 +02:00
Pablo Alba 785b07313b Add nitrate endpoint to send renewal email 2026-06-04 10:31:47 +02:00
David Barragán Merino 97c3a9facf 🐳 Add improvements related to Docker and Podman compatibility (#10012)
* 📎 Add tests for boolean parser coverage

* 🐳 Normalize boolean handling in nginx entrypoint

* 🐳 Quote boolean env vars in compose example (add Podman compatibility)

* 🔥 Remove deprecated and duplicated nginx.conf file for Storybook
2026-06-04 10:11:58 +02:00
Eva Marco c3f107e830 🎉 Add color list to colorpicker (#9953)
* 🎉 Add color list to colorpicker

* 🎉 Improve performance

* 🎉 Add accessibility roles

* 🎉 Add test

* 🎉 Add empty state
2026-06-04 08:47:00 +02:00
Elena Torró dfa88a28fd 🐛 Fix text editor swap when WebGL render is enabled/disabled 2026-06-04 08:39:33 +02:00
Andrey Antukh 7e66929010 🐛 Fix crash when typography token value is an array (#9992)
Add guard in parse-composite-typography-value to check if the
converted value is a map before attempting map operations. When
a typography token has an array value like ["Roboto"], return
an invalid-token-value-typography error instead of crashing with
IMap.-dissoc protocol error.

Add regression test to verify the fix.
2026-06-03 16:54:40 +02:00
alonso.torres 892869b039 🐛 Fix stroke caps misplaced when adding node in middle of path 2026-06-03 16:10:28 +02:00
alonso.torres 6a0f24e691 🐛 Fix view mode child click blocked by parent mouse-leave interaction overlay 2026-06-03 16:10:13 +02:00
alonso.torres e90b14eb37 🐛 Fix grid layout track menu button missing in WebKit/Safari 2026-06-03 16:09:52 +02:00
16dc83616a Add the ability to launch parallel devenv instances (#9906)
* 🐳 Split devenv compose for parallel workspaces

Move shared services into an infra compose file and keep the main devenv container plus Valkey in a separate compose file driven by defaults.env. Parameterize host-side ports, container names, source path, and runtime env while keeping container-internal ports fixed for same-origin proxying.

Make tmux startup idempotent, add attach-devenv for the live instance, move shared MinIO user setup to infra startup, and let exporter scripts load backend _env.local overrides.

Co-authored-by: Codex <codex@openai.com>

* 🐳 Run parallel devenv instances against shared infra

Add support for running N parallel devenv instances under separate compose
projects sharing Postgres, MinIO, mailer, and LDAP. Each instance has its
own main container, Valkey, source checkout, tmux session, and host port
range offset by 10000 (3449 -> 13449 -> 23449, etc.).

./manage.sh run-devenv-agentic --n-instances N reconciles the running set
to exactly {ws0..ws(N-1)}: missing instances are created (workspace sync
from the live repo via git ls-files + per-instance env-file generation
under docker/devenv/instances/ + detached tmux startup), surplus instances
are stopped highest-first via compose down (never -v), already-running
instances are left untouched. ws0 binds the live repo at PWD; ws1+ are
scratch clones under ~/.penpot/penpot_workspaces/.

Backend workers (enable-backend-worker) are gated on PENPOT_BACKEND_WORKER
in backend/scripts/_env; ws1+ overlays disable them so async-task
notifications stay bound to a single Valkey Pub/Sub instance.

Compose helpers wrap docker compose with env -i so per-instance overlay
--env-file actually overrides defaults.env -- without the strip, the shell
env from sourcing defaults.env at startup would shadow the overlay (Compose
gives shell precedence over --env-file).

Other:
- Drop network aliases (- main, - redis); use container_name for
  cross-container DNS so multiple instances on the shared network don't
  fight over the same DNS name.
- Pin volume names via name: (PENPOT_*_VOLUME) so volumes survive project
  renames; ws0 keeps the pre-existing physical names (penpotdev_*).
- Remove cross-project depends_on from main.yml (postgres/minio-setup now
  live in penpotdev-infra); manage.sh ensure-infra-up docker-waits on the
  minio-setup one-shot.
- Strict arg parsing in run-devenv / run-devenv-agentic; --n-instances 0
  rejected.
- Remove unused Host-matched server block from the Caddyfile.

Memory mem:devenv/core and developer docs updated.

Co-authored-by: Codex <codex@openai.com>

*  Document and stabilise the parallel-workspace CLI; wire AI agents

Improve parallel-workspaces developer CLI,
and add an opt-in layer that lets four AI
coding agents (Claude Code, opencode, VS Code Copilot, OpenAI Codex CLI)
drive a specific workspace through a single launcher command.

Parallel-workspace semantics
----------------------------

each run-devenv-agentic call brings up one wsN;
--ws N (integer; default 0) targets a specific workspace and auto-starts
ws0 first when N>=1 so the worker invariant holds. --sync is forbidden on
ws0 and re-seeds the workspace from the live repo for ws1+. Stop semantics
mirror the start invariant -- ws0 is the last to stop, shared infra stops
with it, --all walks every instance highest-first. The worker policy
section explains why workers run only on ws0 (Postgres FOR UPDATE
SKIP LOCKED is safe across many workers but the cron dedup primitive is
best-effort, and :telemetry / :audit-log-archive are not idempotent).
Per-instance Valkey Pub/Sub isolation, msgbus topology, and the
"async task notifications miss ws1+ tabs" caveat are stated explicitly.

The mem:prod-infra/core memory captures the same external-services and
task-queue / Pub-Sub topology in agent-readable form, and
mem:backend/core and mem:critical-info now cross-link it so backend work
surfaces the horizontal-scaling constraints from the start.

AI coding agent integration
---------------------------

New top-level .devenv/ directory holds committed templates
(templates/{claude-code,opencode,vscode}.json and templates/codex.toml,
each with \${PENPOT_MCP_PORT} and \${SERENA_MCP_PORT} placeholders) plus
committed shared entries (matching shared/* files for Playwright, the
only workspace-independent server we ship today).

./manage.sh start-coding-agent <claude|opencode|vscode|codex> [--ws N]
launches the chosen client against one workspace. It cd's into the
target's directory (the live repo for ws0; workspace-path "wsN" for ws1+)
and refuses to launch unless (a) the binary is on PATH, (b) the
workspace directory exists for ws1+, and (c) the instance is up
(devenv-main-running) -- the MCP servers only exist while the devenv is
running. The agentic-devenv guide is restructured around this Quick
start path, with a per-client table and a Manual configuration fallback
for clients we don't cover.

Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ Scope the shadow devtools to the dev build

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 15:48:25 +02:00
Andrey Antukh 88f50b6ddd Merge remote-tracking branch 'origin/staging' into develop 2026-06-03 14:36:30 +02:00
Andrey Antukh 2808268e52 📎 Update changelog 2026-06-03 14:36:05 +02:00
Andrey Antukh 7ddc93a4df Merge remote-tracking branch 'origin/staging' into develop 2026-06-03 14:19:47 +02:00
bb89ca526b Avoid deduplicating temporary export files (#9959)
* 🐛 Avoid deduplicating temporary export files

* 📎 Update changelog

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: yongjin <yongjin@yongjinui-Macmini.local>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-03 14:08:10 +02:00
Elena Torró 0fe4337359 🐛 Fix webgl thumbnail label (#10009) 2026-06-03 14:06:05 +02:00
Aitor Moreno ff3587ca2d Merge pull request #9997 from penpot/elenatorro-fix-background-clear
🐛 Fix clear canvas
2026-06-03 12:02:48 +02:00
DexterityandAndrey Antukh ea0e248d4b ♻️ Migrate release-notes to modern component syntax (#9436)
* ♻️ Migrate release-notes to modern component syntax

* 📎 Minor changes

Remove props metadata from release-notes component.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-02 18:12:34 +02:00
DexterityandAndrey Antukh 8b9a7b257f ♻️ Migrate rulers component to modern syntax (#9437)
* ♻️ Migrate rulers component to modern syntax

* 📎 Remove the usage of mf/memo on rules

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-02 18:12:03 +02:00
DexterityandAndrey Antukh cb5f59533d ♻️ Migrate color-item asset component to modern syntax (#9440)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-02 18:11:33 +02:00
Andrey Antukh feca7cef41 Merge remote-tracking branch 'origin/staging' into develop 2026-06-02 17:50:45 +02:00
Alonso Torres ba9d225c2b 🐛 Fix stroke-cap-start/end stored at wrong level in SVG imports (#9982) 2026-06-02 17:42:35 +02:00
Andrey Antukh 3cedf11e1c 🔧 Update tests github workflow config 2026-06-02 17:24:35 +02:00
Andrey Antukh 6bf7c33c43 🐛 Fix del-page change constructed with nil id (#9990)
Guard against nil id and missing page in delete-page to prevent
broken changes from being sent to the server. This can happen due
to a race condition where the page is no longer present in the
pages-index. Also add assertion in changes-builder/del-page as
defense-in-depth.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-02 17:23:13 +02:00
Andrey Antukh a57833f3cd 🐛 Fix get-comment-threads called with empty params due to race condition (#9988)
Prevent navigate-to-comment-id from making an RPC call with nil
file-id when current-file-id has been cleared by finalize-workspace
during rapid workspace navigation.  The deferred stream observer
(rx/observe-on :async) could fire after the workspace state was
already cleaned up, causing {:file-id nil} to become {} after
query-string nil-filtering in map->query-string.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-02 17:22:39 +02:00
Elena Torro d9fea603f8 🐛 Fix clear canvas 2026-06-02 17:14:25 +02:00
ruizterce e6f5b270de 💄 Fix typos in configuration.md (#9975)
Corrected typos in the configuration documentation.

Signed-off-by: ruizterce <127963868+ruizterce@users.noreply.github.com>
2026-06-02 16:32:34 +02:00
Belén Albeza e2545915b8 🔧 Fix log level of migration exceptions (#9986) 2026-06-02 16:17:22 +02:00
Belén Albeza d5fe5f82f3 🐛 Fix wasm info label positioning (#9981) 2026-06-02 15:18:37 +02:00
Andrey Antukh 3744186510 🔧 Update default nginx limit configuration 2026-06-02 14:05:21 +02:00
Belén Albeza 7fdd2ceb5c 🐛 Fix crash when dismissing the restore version modal (#9969) 2026-06-02 11:33:06 +02:00
David Barragán Merino 9df1e99c08 🔧 Remove the confirmation step for publishing docker images 2026-06-02 11:02:35 +02:00
Andrey Antukh 1f2f1bdaf4 📚 Add minor improvements to AGENTS.md and serena memories (#9919)
* 📚 Add minor improvements to AGENTS.md and serena memories

*  Add minor format and linter restructuration on memories
2026-06-02 10:39:51 +02:00
Andrey Antukh 7517ba1559 Merge remote-tracking branch 'origin/staging' into develop 2026-06-02 10:38:54 +02:00
Andrey Antukh 17fb1c49f8 Redunce the render throttling to 50ms of the layers-tree* component 2026-06-02 10:30:08 +02:00
Marina López fc3a95765d Add expired subscription banner 2026-06-02 10:26:56 +02:00
Andrey Antukh fe9e47f947 Merge remote-tracking branch 'origin/main' into staging 2026-06-02 10:15:24 +02:00
Andrey Antukh cd18a2bcb2 📎 Update version on mcp/package.json 2026-06-02 10:13:01 +02:00
Andrey Antukh d49fa51fef Update changelog 2026-06-02 10:10:20 +02:00
Andrey Antukh 0d2e0f8367 📎 Update the update-changelog skill and gh.py tool 2026-06-02 10:09:53 +02:00
Juanfran e12e5f8373 🐛 Fix overflow on delete account modal with many owned orgs 2026-06-02 09:47:02 +02:00
Aitor Moreno d0f6d5b3a1 ♻️ Refactor render pipeline (#9891)
* ♻️ Refactor viewbox

* 🎉 Add draw_atlas alternative to draw tiles

* 🐛 Fix minor glitches

* ♻️ Change how process_animation_frame works

* ♻️ Refactor document atlas

* ♻️ Refactor max texture size

* ♻️ Refactor entrypoints and dead_code
2026-06-02 09:38:52 +02:00
María Valderrama 7bf519a127 Inherit subscriptions perks to Nitrate 2026-06-02 09:33:02 +02:00
Andrés Moya 06c9a18ab0 🔧 Revert migration for tokens with clashing names (#9950)
* Revert "🐛 Detect duplicated token names in the whole library (#9034)"

This reverts commit 61cd757355.

* 🔧 Preserve some enhancements and fixes that are still valid

* 🔧 Fix broken integration tests
2026-06-02 09:09:58 +02:00
Eva Marco 53a4d2a18a 🐛 Fix CI (#9952) 2026-06-01 17:47:17 +02:00
Andrey Antukh 5d80f7f5b2 🌐 Validate and Rehash translation files 2026-06-01 14:58:07 +02:00
Anonymous d91f5be12f 🌐 Add translations for: Spanish
Currently translated at 95.2% (2226 of 2338 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-06-01 14:56:07 +02:00
Nicola Bortoletto 78609f776d 🌐 Add translations for: Italian
Currently translated at 89.7% (2099 of 2338 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-01 14:56:07 +02:00
Andrey Antukh 0044e76cb4 🐛 Revert throttle timeout increase on layers pannel 2026-06-01 14:40:55 +02:00
Nicola Bortoletto badf38922c 🌐 Add translations for: Italian
Currently translated at 90.1% (2100 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-01 13:52:27 +02:00
VKing9 d9257d8187 🌐 Add translations for: Hindi
Currently translated at 84.5% (1969 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-06-01 13:52:27 +02:00
K.B.Dharun Krishna 2163d40c8c 🌐 Add translations for: Tamil
Currently translated at 1.9% (46 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ta/
2026-06-01 13:52:26 +02:00
Henrik Allberg 4c18837c12 🌐 Add translations for: Swedish
Currently translated at 83.9% (1956 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-01 13:52:26 +02:00
Hugo Vermaak 92cfe174e8 🌐 Add translations for: Afrikaans
Currently translated at 3.2% (75 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/af/
2026-06-01 13:52:26 +02:00
Late Night Defender 68aa5c0ce7 🌐 Add translations for: Thai
Currently translated at 7.8% (184 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/th/
2026-06-01 13:52:26 +02:00
Revenant 0ea1b6d95a 🌐 Add translations for: Malay
Currently translated at 27.7% (646 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2026-06-01 13:52:26 +02:00
Eranot ab7ce12785 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 59.0% (1375 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-06-01 13:52:26 +02:00
Renan Mayrinck 948936116a 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 59.0% (1375 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-06-01 13:52:26 +02:00
deveronica 095ab6d822 🌐 Add translations for: Korean
Currently translated at 85.3% (1988 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-06-01 13:52:26 +02:00
Dário e64a83e995 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 66.1% (1542 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-01 13:52:26 +02:00
TheScientistPT b3ffb63434 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 66.1% (1542 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-01 13:52:26 +02:00
Tummas Jóhan Sigvardsen b8c7954f98 🌐 Add translations for: Faroese
Currently translated at 7.0% (164 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2026-06-01 13:52:26 +02:00
AlexTECPlayz 57477f203e 🌐 Add translations for: Romanian
Currently translated at 82.0% (1911 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-06-01 13:52:26 +02:00
George Lemon 4e6eb83829 🌐 Add translations for: Romanian
Currently translated at 82.0% (1911 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-06-01 13:52:26 +02:00
Црнобог 79880593f5 🌐 Add translations for: Serbian
Currently translated at 57.5% (1341 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2026-06-01 13:52:26 +02:00
Nicola Bortoletto d85576d6ee 🌐 Add translations for: Italian
Currently translated at 86.5% (2016 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-01 13:52:26 +02:00
Valentina Chapellu 50d2af9930 🌐 Add translations for: Italian
Currently translated at 86.5% (2016 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-01 13:52:26 +02:00
Mikel Larreategi a0c1e519ba 🌐 Add translations for: Basque
Currently translated at 49.4% (1153 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2026-06-01 13:52:26 +02:00
Louis Chance db98140d3a 🌐 Add translations for: French
Currently translated at 85.7% (1999 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-01 13:52:26 +02:00
Ingrid Pigueron 879d9df47e 🌐 Add translations for: French
Currently translated at 85.7% (1999 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-01 13:52:26 +02:00
Pablo Alba 11ee30d05a 🌐 Add translations for: French
Currently translated at 85.7% (1999 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-01 13:52:25 +02:00
Alexis Morin ef07cb8f7c 🌐 Add translations for: French (Canada)
Currently translated at 85.4% (1991 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-01 13:52:25 +02:00
Simon Bechmann 9c3c1fafec 🌐 Add translations for: Danish
Currently translated at 4.5% (105 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/da/
2026-06-01 13:52:25 +02:00
Vin 0c99a64cf0 🌐 Add translations for: Russian
Currently translated at 71.4% (1664 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-01 13:52:25 +02:00
Egor Filatov 50d8c581f2 🌐 Add translations for: Russian
Currently translated at 71.4% (1664 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-01 13:52:25 +02:00
The_BadUser 197808c2af 🌐 Add translations for: Russian
Currently translated at 71.4% (1664 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-01 13:52:25 +02:00
Stephan Paternotte a9712ab77e 🌐 Add translations for: Dutch
Currently translated at 86.7% (2021 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-01 13:52:25 +02:00
Sebastiaan Pasma fe0bc1f0b8 🌐 Add translations for: Dutch
Currently translated at 86.7% (2021 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-01 13:52:25 +02:00
Radek Sawicki b566e6df01 🌐 Add translations for: Polish
Currently translated at 48.4% (1128 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-06-01 13:52:25 +02:00
Oğuz Ersen c74750664d 🌐 Add translations for: Turkish
Currently translated at 86.6% (2020 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-01 13:52:25 +02:00
Anonymous cbbefa3410 🌐 Add translations for: Turkish
Currently translated at 86.6% (2020 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-01 13:52:25 +02:00
Vincas Dundzys 0aa7c06309 🌐 Add translations for: Lithuanian
Currently translated at 5.0% (118 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lt/
2026-06-01 13:52:25 +02:00
Josep Ponsà f5ac88d43e 🌐 Add translations for: Catalan
Currently translated at 45.7% (1067 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2026-06-01 13:52:25 +02:00
Aryiu f6b883b44b 🌐 Add translations for: Catalan
Currently translated at 45.7% (1067 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2026-06-01 13:52:25 +02:00
Zvonimir Juranko 25e4597d1a 🌐 Add translations for: Croatian
Currently translated at 67.3% (1570 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-06-01 13:52:25 +02:00
Ahmad HosseinBor 62e840d6c0 🌐 Add translations for: Persian
Currently translated at 32.7% (763 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2026-06-01 13:52:25 +02:00
Semon Xue e64840e788 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 76.0% (1773 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-01 13:52:25 +02:00
Maemolee 8185fe51ea 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 76.0% (1773 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-01 13:52:25 +02:00
Tatsuto Yamamoto d69ba665ac 🌐 Add translations for: Japanese
Currently translated at 10.1% (236 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ja/
2026-06-01 13:52:25 +02:00
william chen 13afcb372d 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 67.3% (1569 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-06-01 13:52:25 +02:00
Andy Li 89af02da96 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 67.3% (1569 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-06-01 13:52:25 +02:00
Yaron Shahrabani 3ca98e34df 🌐 Add translations for: Hebrew
Currently translated at 86.5% (2017 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-06-01 13:52:25 +02:00
ascarida c69a834412 🌐 Add translations for: Galician
Currently translated at 15.8% (370 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2026-06-01 13:52:25 +02:00
Joseph V M 75ca626f55 🌐 Add translations for: Malayalam
Currently translated at 2.2% (52 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2026-06-01 13:52:25 +02:00
Ņikita K. 01ea3be262 🌐 Add translations for: Latvian
Currently translated at 79.2% (1846 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-06-01 13:52:25 +02:00
Edgars Andersons d6a0fac9ab 🌐 Add translations for: Latvian
Currently translated at 79.2% (1846 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-06-01 13:52:25 +02:00
Alejandro Alonso 1e66f8d637 🌐 Add translations for: Yoruba
Currently translated at 49.1% (1146 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2026-06-01 13:52:25 +02:00
Alejandro Alonso e472304d64 🌐 Add translations for: Igbo
Currently translated at 21.0% (490 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2026-06-01 13:52:25 +02:00
Amerey.eu 19faebf292 🌐 Add translations for: Czech
Currently translated at 67.1% (1565 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-06-01 13:52:25 +02:00
matl-17 af36428a29 🌐 Add translations for: Czech
Currently translated at 67.1% (1565 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-06-01 13:52:25 +02:00
Anonymous 901ffe0c09 🌐 Add translations for: Greek
Currently translated at 21.8% (509 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/el/
2026-06-01 13:52:25 +02:00
Denys Kisil 1d4e4aa7df 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 85.9% (2002 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-06-01 13:52:25 +02:00
Linerly 6690803559 🌐 Add translations for: Indonesian
Currently translated at 71.5% (1668 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-06-01 13:52:25 +02:00
liimee 2b3a256461 🌐 Add translations for: Indonesian
Currently translated at 71.5% (1668 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-06-01 13:52:25 +02:00
Stas Haas 490a7bc046 🌐 Add translations for: German
Currently translated at 83.4% (1945 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-06-01 13:52:25 +02:00
Marius 6974dfdd4d 🌐 Add translations for: German
Currently translated at 83.4% (1945 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-06-01 13:52:25 +02:00
Pablo Alba 6e985a460f 🌐 Add translations for: German
Currently translated at 83.4% (1945 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-06-01 13:52:25 +02:00
Alejandro Alonso 47d6601e13 🌐 Add translations for: Hausa
Currently translated at 52.0% (1212 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2026-06-01 13:52:24 +02:00
Andrey Antukh dadab03891 🌐 Add translations for: Spanish
Currently translated at 95.3% (2221 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-06-01 13:52:24 +02:00
Andrés Moya adc0c967f3 🌐 Add translations for: Spanish
Currently translated at 95.3% (2221 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-06-01 13:52:24 +02:00
Anderson Paulo ed6e4db749 🌐 Add translations for: Portuguese
Currently translated at 3.2% (75 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt/
2026-06-01 13:52:24 +02:00
Yessenia Villarte Vaca 8a9e2722ab 🌐 Add translations for: Spanish (Latin America)
Currently translated at 4.8% (114 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2026-06-01 13:52:24 +02:00
jonnysemon 582dd3beef 🌐 Add translations for: Arabic
Currently translated at 48.0% (1120 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-06-01 13:52:24 +02:00
Amine Gdoura 5a7a8aa83d 🌐 Add translations for: Arabic
Currently translated at 48.0% (1120 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-06-01 13:52:24 +02:00
Anonymous a77147a22b 🌐 Add translations for: Finnish
Currently translated at 2.4% (58 of 2330 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fi/
2026-06-01 13:52:24 +02:00
Hosted Weblate c753506039 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-06-01 13:52:24 +02:00
Andrey Antukh 4a8fb5af53 Merge remote-tracking branch 'origin/staging' into develop 2026-06-01 13:15:57 +02:00
Andrey Antukh c5de4c27b0 Merge remote-tracking branch 'origin/main' into staging 2026-06-01 12:57:39 +02:00
Alejandro Alonso 88f2366c6f 🎉 Enable render switch and wasm info by default and simplify feature helpers to use pre-computed features set (#9942)
The setup-wasm-features function is the single source of truth for
    resolving the renderer choice (URL param > profile preference > team
    flags), storing the result in state[:features]. Several helpers were
    re-deriving the same priority chain independently, duplicating logic:

    - wasm-enabled?, wasm-url-override, wasm-url-override-ref
    - enabled-by-flags?, enabled-without-migration?

    This change removes all duplicated helpers and simplifies the
    remaining functions to rely exclusively on the pre-computed
    :features set:

    - active-feature? — now just checks (contains? (:features state)
      feature) without special-casing render-wasm/v1
    - use-feature — uses the reactive features-ref for all features
    - initialize/recompute-features effects — use the local features
      binding directly

    Since :features is rebuilt by setup-wasm-features on every
    initialization and recompute, this preserves correctness while
    eliminating ~50 lines of duplicated code.
2026-06-01 12:52:34 +02:00
Andrey Antukh 9e12e413ca 🐛 Fix typo in icon name elipse to ellipse (#9948)
Rename the icon file and fix the icon ID from "elipse" (single "l")
to "ellipse" (double "l") across the codebase.

The root cause was a mismatch: the icon file/ID was named "elipse"
but get-shape-icon returns "ellipse" for circle shapes. Since the
icon* component validates icon-id against the auto-generated
icon-list set, the string "ellipse" failed validation.

Changes:
- Rename frontend/resources/images/icons/elipse.svg to ellipse.svg
- Fix icon def in icon.cljs: ^:icon-id elipse -> ^:icon-id ellipse
- Fix deprecated icon in icons.cljs: ^:icon elipse -> ^:icon ellipse
- Fix usage in top_toolbar.cljs: deprecated-icon/elipse -> ellipse
- Fix usage in history.cljs: deprecated-icon/elipse -> ellipse

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 12:49:24 +02:00
Aitor Moreno 2410bcb0df Merge pull request #9941 from penpot/alotor-layout-fixes
🐛 Fix layout render-wasm issues
2026-06-01 12:38:01 +02:00
Dexterity a9e88b8fa8 🐛 Translate layout panel header and add-layout options (#9424)
* 🐛 Translate layout panel header and add-layout options

* ♻️ Wrap shared layout dropdown binding in mf/html
2026-06-01 12:13:56 +02:00
DexterityandAndrey Antukh 67f6786809 ♻️ Migrate plugin-entry to modern component syntax (#9462)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 11:56:23 +02:00
BitTobyandAndrey Antukh 4b2ddfd7b2 ♻️ Migrate inspect annotation to modern component syntax (#9402)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 11:56:10 +02:00
alonso.torres dddb4cf0b6 🐛 Fix layout render-wasm issues 2026-06-01 10:27:59 +02:00
Luis de Dios d3148e1a10 🐛 Fix add modal confirmation when clicking restore from saved version preview (#9804) 2026-06-01 10:08:40 +02:00
Andrey Antukh a9c0b5394c Refresh dependencies on plugins directory (#9935) 2026-06-01 10:07:06 +02:00
156c888e2d 🐛 Add STS dependency for IRSA/web identity token S3 auth (#9928)
The S3 storage backend uses DefaultCredentialsProvider which includes
  WebIdentityTokenFileCredentialsProvider in its chain. However, that
  provider requires software.amazon.awssdk/sts on the classpath to call
  AssumeRoleWithWebIdentity. Without it, the provider silently fails and
  credentials cannot be resolved when using IRSA on EKS.

  Closes #9927

  Signed-off-by: Joshua C <joshua.cullum@gmail.com>

Co-authored-by: Joshua C <joshua.c@data-edge.co.uk>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 10:06:18 +02:00
DexterityandAndrey Antukh 61d44a374a ♻️ Migrate session-widget to modern component syntax (#9460)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 10:04:54 +02:00
Juan de la Cruz af81818b97 Add new release 2.16 slides (#9940)
*  Add new slides for the 2.16 release

* 🎉 Add new slide gif images
2026-06-01 09:59:10 +02:00
John Eismeier c156559f2c 📚 Fix several typos on code comments and messages (#9946)
Signed-off-by: John E <jeis4wpi@outlook.com>
2026-06-01 09:43:07 +02:00
DexterityandAndrey Antukh d7c155ac4f 🐛 Route render fallback errors through the project logger (#9421)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-01 09:40:12 +02:00
Andrey Antukh 3a4e3aaeac 🐛 Fix consistency issues on mcp readme 2026-06-01 09:36:44 +02:00
Andrey Antukh d66e8702e7 📎 Update root repo deps 2026-06-01 09:27:58 +02:00
Luis de Dios 0b56fd2f77 🐛 Fix locked flex and grid elements cannot be selected in viewer role (#9865)
* 🐛 Fix locked flex and grid elements cannot be selected in viewer role

*  Add playwright test
2026-06-01 08:30:27 +02:00
Belén Albeza a5c8bcaf9e 🐛 Fix text editor crash when switching from svg to wasm renderer (#9926)
* 🐛 Fix crash when switching renderers with text editor open

* ♻️ Use new initialized? helper in wasm api
2026-05-29 14:00:49 +02:00
Alejandro Alonso b5108ca1ad 🎉 Update wasm label (#9938) 2026-05-29 13:40:35 +02:00
Andrés Moya 7e6884e330 🐛 Fix error when copy & paste a swapped copy (#9934) 2026-05-29 13:36:17 +02:00
Eva Marco 6e8d2b3708 🎉 Add clear error messages (#9886) 2026-05-29 13:27:43 +02:00
Francis Santiago 3d7dbbe6fc 🐛 Fix duplicated GitHub issue templates (#9937)
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-29 13:00:54 +02:00
Eva Marco 237fa568e8 🐛 Fix comments 2026-05-29 12:28:10 +02:00
Eva Marco ba39600192 🐛 Fix show error on name-input 2026-05-29 12:28:10 +02:00
Andrés Moya 05cceab768 🔧 Add small adjustments and spanish translation 2026-05-29 12:28:10 +02:00
rene0422 0efebc5e0e 🐛 Keep colliding tokens visible as broken pills 2026-05-29 12:28:10 +02:00
Andrés Moya 429103d076 🐛 Fix errors when token name conflicts with group name 2026-05-29 12:28:10 +02:00
JeffandAndrey Antukh 300be392f6 🐛 Fix onboarding template spinner stuck after failed download (#9504)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 12:13:37 +02:00
JeffandAndrey Antukh bec21e69e6 🐛 Preserve explicit hide-in-viewer when adding prototype interactions (#9695)
`cls/show-in-viewer` unconditionally dissoc'ed `:hide-in-viewer` on the
interaction destination, so every `add-interaction`, `add-new-interaction`,
and `update-interaction` call silently re-enabled the destination's
view-mode visibility — even when the user had just deliberately hidden
that frame. Reporter (#9049) hid a board, dragged a prototype arrow at
it, and watched the board reappear in View Mode.

Make `show-in-viewer` a no-op when the destination already has
`:hide-in-viewer true`. The auto-unhide still fires on destinations with
no explicit hide flag (the original ergonomic — new prototype targets
default to visible), but explicit user intent is now preserved across
interaction-add / interaction-update.

Behaviour change: dropping the auto-unhide on explicitly-hidden
destinations matches the reporter's expectation ("nothing would show up
in View Mode unless explicitly marked as such") and the surrounding
`:hide-in-viewer`-aware UI in `measures.cljs`, which already lets users
toggle the same property directly.

Closes #9049.

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 11:31:59 +02:00
Eva Marco 8dbbd49c0e 🐛 Fix foreground color on numeric input (#9920) 2026-05-29 11:30:19 +02:00
Yamila Moreno ddba2ffa75 📎 Update Kaleidos Copyright (#9929) 2026-05-29 11:24:58 +02:00
Pablo Alba c51a137ca9 Add nitrate permission team members design review changes 2026-05-29 11:23:53 +02:00
ChanandAndrey Antukh ac3950e36c 🐛 Fix CORS middleware reflecting arbitrary origins (#9675)
*  Align profile and dashboard files with penpot develop

* 🐛 Fix CORS origin allowlist for issue #9659

---------

Signed-off-by: Chan <101856681+enjoyandlove@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 11:23:16 +02:00
NativeTeachingAidsBandAdmin b08ceca81d 🐛 Remove dead css/ui.css <link> from frontend index template (#9840)
Fixes #9135.

The <link href="css/ui.css">  tag in
frontend/resources/templates/index.mustache references a CSS file that
the build pipeline never produces:

- compileStyles() in frontend/scripts/_helpers.js only writes main.css
  (always) and debug.css (dev-only) — there is no write to ui.css
- compileStorybookStyles() writes ds.css (design system), not ui.css
- No ui.scss source exists anywhere in frontend/resources/styles/

The reference was added in 45d04942c (" Add example ui
storybook") but no corresponding build step was added to emit the file.

Result: every page load issues a request for /css/ui.css that nginx
returns as 404. In self-hosted Penpot deployments behind a reverse
proxy, the SPA's CSS init promise rejects on the 404, the React root
never mounts, and the user sees a black screen.

This patch removes the dead reference. If a future change actually
emits ui.css (or another distinct UI bundle), the <link> can be
re-added at that time.

Co-authored-by: Admin <admin@Admins-MacBook-Pro.local>
2026-05-29 11:19:52 +02:00
Bolaji Ayodeji 3b6cefbb85 Update Verified DPG badge in README (#9815)
Signed-off-by: Bolaji Ayodeji <sirbeejay1@gmail.com>
2026-05-29 11:16:37 +02:00
DexterityandAndrey Antukh c4a5f0098e ♻️ Migrate color-bullet and color-name to modern component syntax (#9433)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 09:56:12 +02:00
DexterityandAndrey Antukh 53b1837b11 ♻️ Migrate button-link to modern component syntax (#9428)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 09:46:28 +02:00
DexterityandAndrey Antukh 01ac1529e1 ♻️ Migrate perf/profiler to modern component syntax (#9429)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 09:41:21 +02:00
FlorisandAndrey Antukh ede1cd86f4 📚 Update available plugin link to actual Penpot Hub URL (#9481)
Signed-off-by: Floris <floris@fmjansen.nl>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 09:27:48 +02:00
Milos MilicandAndrey Antukh 9c6e3f5ec3 🐛 Fix Storybook docs and canvas pages missing scrollbar (#6049) (#9319)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-28 16:25:06 +02:00
Pablo Alba aa996c5118 🐛 Fix nested components becomes flat after reset (#9841) 2026-05-28 15:57:35 +02:00
Andrey Antukh 3cce47094e ♻️ Refactor font upload to process variants sequentially (#9921)
* ♻️ Refactor font upload to process variants sequentially

Change the batch upload handler so fonts are uploaded one at a time
instead of all concurrently, preventing excessive simultaneous
upload requests.

Previously `on-upload-all` used `run!` which fired all font variant
uploads simultaneously. Now it uses `rx/from` combined with
`rx/mapcat` to process each font sequentially.

As part of this change, extract the upload logic into a standalone
`handle-font-upload` helper for reuse between single and batch
upload paths, and remove the separately memoized `on-upload*` hook.

Also fix error logging to use `js/console.error` instead of
`js/console.log` for consistency with project conventions.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add code comment

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-28 15:51:46 +02:00
DexterityandAndrey Antukh 78597374ab ♻️ Migrate history-entry to modern component syntax (#9461)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-28 15:37:20 +02:00
Milos MilicandAndrey Antukh 09c274bd92 🐛 Match version preview banner text to History sidebar labels (#9697)
The version preview banner in `enter-preview` derives its title from
`(:label snapshot)` directly. For system-created autosaves that
field is the internal snapshot label (e.g. `internal/snapshot/20`),
so the banner shows the raw internal string while the History sidebar
already renders the same autosave through `workspace.versions.autosaved.version`
plus a localized date. The mismatch makes it hard to be sure which
sidebar entry you're previewing, especially when several autosaves
sit close together (#9503).

Switch the label resolution to mirror the sidebar's `snapshot-entry*`:
- `:created-by "system"` snapshots format the label as
  `(tr "workspace.versions.autosaved.version" (ct/format-inst ...
   :localized-date))` — the exact same translation key + date format
  the sidebar's autosave group already uses
- `:created-by "user"` (pinned) versions keep their custom `:label`
  with the existing `unnamed` fallback

No behavior change for pinned/user-named versions or for the
restore/exit dialog buttons.

Closes #9503.

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-28 15:26:15 +02:00
Eva Marco 7843bb1208 ♻️ Fix regression and add test 2026-05-28 15:06:29 +02:00
Aitor Moreno 92a3030f05 Merge pull request #9904 from penpot/superalex-fix-tile-spiral-missing-column
🐛 Fix inclusive TileRect width/height for tile spiral scheduling
2026-05-28 13:34:18 +02:00
alonso.torres 111d7daa94 🐛 Fix problem with absolute elements in flex layout 2026-05-28 13:09:06 +02:00
Alejandro Alonso 6663105f4b 🐛 Avoid restoring empty saved-font-size to prevent invalid text shapes on save (#9917) 2026-05-28 12:39:43 +02:00
Aitor Moreno bda977202a 🐛 Fix shift enter not working on text editor v2 2026-05-28 11:52:46 +02:00
Andrés MoyaandMilosM348 1f35f57258 🐛 Fix DTCG token import discriminator and group-level $type inheritance (#9912)
* 🐛 Fix DTCG token import discriminator and group-level $type inheritance

Closes #8342.

The DTCG Community Group Final Report (W3C, 2025-10-28) specifies:

  "The presence of a $value property definitively identifies an object
   as a token."

  "A token's type can be specified by the optional $type property [...]
   Furthermore, the $type property on a group applies to all tokens
   nested within that group."

Two bugs in `common/src/app/common/types/tokens_lib.cljc` violate the
spec and silently break import of any third-party DTCG file that uses
group-level type inheritance:

1. `flatten-nested-tokens-json` used `(not (contains? v "$type"))` as
   the group-vs-token discriminator. A group node carrying only a
   `$type` (to set a default for child tokens) was misidentified as a
   token, then immediately discarded because it had no `$value`.

2. `schema:dtcg-node` declared both `$type` and `$value` as required,
   so even after the discriminator was fixed any leaf token that
   relied on group-level type inheritance failed `dtcg-node?`
   validation and never reached the parser.

The combined effect: importing a spec-compliant DTCG file that
expressed types at the group level produced a TokensLib with no
tokens at all, because every leaf was discarded as "unknown type".

Penpot-exported files were unaffected because Penpot always emits
both `$type` and `$value` on every token and never attaches `$type`
to a group, so the existing tests covered only the inline-type
shape.

- `schema:dtcg-node`: mark `$type` optional.
- `flatten-nested-tokens-json`: use `$value` as the discriminator
  (anything without `$value` is a group), accept an optional
  `inherited-type` accumulator that carries the nearest enclosing
  group `$type` down through recursion, and resolve a token's type
  from its own `$type` first, falling back to the inherited type.
  A token's own `$type` always wins over the inherited one (per
  spec).

Added `parse-dtcg-group-type-inheritance` covering both cases:
- group `$type` is inherited by tokens that don't declare their own
  (`colors.red`, `colors.blue`, `space.small`)
- token `$type` overrides the inherited group `$type`
  (`colors.danger`, `space.large`)

Existing DTCG round-trip tests continue to pass because they all
declare `$type` at the token level, which the new code still honours.

CHANGES.md entry added under the 2.17.0 Bugs-fixed section.

* 📚 Do not update CHANGES.md

We are changing the procedures to not update the changelog on each PR. Instead, we use github tracking to check what issues come in a release, and update the changelog automatically in a batch.

Signed-off-by: Andrés Moya <hirunatan@hammo.org>

---------

Signed-off-by: Andrés Moya <hirunatan@hammo.org>
Co-authored-by: MilosM348 <milos.milic001@outlook.com>
2026-05-28 11:01:11 +02:00
Juanfran 4c8b33691a Use shared org-avatar component in delete account modal
Render owned organizations in the delete-account modal with the same
org-avatar* component used across the dashboard, so logo and avatar
background are shown consistently and initials are extracted via
d/get-initials instead of a raw first-character substring.

Extends the get-owned-organizations-summary endpoint and the underlying
nitrate API schema to carry :avatar-bg-url and :logo-id, deriving
:custom-photo from logo-id with the public uri, matching the pattern
already used by set-team-org-api.
2026-05-28 11:01:08 +02:00
María Valderrama dd7d5bb113 🐛 Fix nitrate's plan button size 2026-05-28 10:23:06 +02:00
Alejandro Alonso 03e10fa871 🐛 Fix inclusive TileRect width/height for tile spiral scheduling 2026-05-28 10:22:22 +02:00
Juanfran 5c5ee73f2d 🐛 Fix createToken calls missing textFieldType argument 2026-05-28 09:36:33 +02:00
Alejandro Alonso 921c5a4294 🐛 Fix drag shapes out of frames (#9905) 2026-05-28 09:13:04 +02:00
Belén Albeza a278820230 Adjust viewport interest area (#9897) 2026-05-27 19:21:42 +02:00
Andrey Antukh 8430358621 Merge remote-tracking branch 'origin/staging' into develop 2026-05-27 17:46:44 +02:00
Andrey Antukh bee1a89698 Add minor usability improvements to update-changelog skill 2026-05-27 17:44:50 +02:00
Andrey Antukh 50ec6ad777 📚 Add missing entries on changelog 2026-05-27 16:11:14 +02:00
Andrey Antukh 3ba70337ea 📎 Update changelog 2026-05-27 15:37:46 +02:00
María Valderrama 1a0e497c84 Remove nitrate's yearly plan 2026-05-27 15:07:22 +02:00
Belén Albeza 0dd40776f8 🐛 Fix default path stroke thickness 2026-05-27 14:47:11 +02:00
Alonso Torres 0fe59cac94 🐛 Fix problem with fill/stroke proxy properties (#9647) 2026-05-27 14:05:28 +02:00
giraficandAndrey Antukh 763ec4c4fe 📎 Update changelog for #8632 (#9701)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-27 13:50:33 +02:00
Eva Marco 7d76a1caa3 🐛 Fix settings dropdown (#9883) 2026-05-27 13:44:06 +02:00
Eva Marco 16d0ee99c1 ♻️ Update copy on context menu (#9888) 2026-05-27 13:43:31 +02:00
Andrey Antukh 1a1c7355e2 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2026-05-27 13:37:17 +02:00
Andrey Antukh 3858993a57 Merge remote-tracking branch 'origin/staging' into develop 2026-05-27 13:37:02 +02:00
María Valderrama 15d6df48f5 🐛 Fix default team showing up in count 2026-05-27 13:36:35 +02:00
Eva Marco 5ffec3e5e9 🐛 Fix shadow token creation 2026-05-27 13:06:47 +02:00
Andrey Antukh 95e7edc9d2 📚 Update changelog 2026-05-27 12:51:26 +02:00
Andrey Antukh 243798f458 Add improvements to 'update-changelog' skill 2026-05-27 12:51:26 +02:00
David Barragán Merino a0f869b917 🐳 Remove the configuration of the mcp from Nginx if it is not enabled 2026-05-27 12:36:26 +02:00
Andrey Antukhandalonso.torres 40ce360c99 Improve performance and fix orphan detection in validate-file (#9789)
*  Improve performance and fix orphan detection in validate-file

- Add `*ref-shape-cache*` dynamic var to memoize `find-ref-shape`
  lookups per page, avoiding repeated O(depth) ancestor walks.
- Add `*children-sets*` pre-computed maps for O(1) parent-child
  containment checks, replacing linear `some` scans.
- Short-circuit `inside-component-main?` when the shape context
  already implies a main component.
- Use single-pass reduce with early exit for duplicate detection
  (children, swap slots) instead of count/distinct or frequencies.
- Guard `check-missing-slot` to skip expensive `find-near-match`
  when the shape already has a swap slot.
- Refactor variant-set validation to use `run!` with direct `get`.
- Refactor `check-ref-cycles` to use a single `reduce-kv` pass.
- Fix `get-orphan-shapes`: the original `map` pipeline produced
  nils so orphan shapes were never validated; rewrite with
  `reduce-kv` for correct results.
- Add `validate-file-affected!` for change-scoped validation,
  replacing full file validation in `process-changes-and-validate`
  to only validate pages and components touched by the changes.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

*  Improved validation

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-05-27 12:36:21 +02:00
Alejandro Alonso 30bba7cd38 🎉 Increase viewport interest area threshold (#9885) 2026-05-27 11:47:40 +02:00
Andrey Antukh 57397bc32a Merge remote-tracking branch 'origin/main' into staging 2026-05-27 11:35:47 +02:00
Andrey Antukh 676e7f8646 🔧 Update devenv postgresql config 2026-05-27 11:35:21 +02:00
Andrey Antukh f6c76711f4 Merge remote-tracking branch 'origin/main' into staging 2026-05-27 11:34:59 +02:00
Pablo Alba 3cecc29276 🐛 Fix update library dialog when a component position changes
Do not show the library sync popup when the only differences are global x/y changes on library components. We now generate the actual sync changes and only notify if there are real redo-changes to apply.
Run cll/generate-sync-file-changes for candidate libraries and filter out those with empty :redo-changes. The expensive check is deferred via rx/timer 0 so it runs asynchronously and does not block the UI.
Why: Position-only changes are normalized during sync (via reposition-shape) and never propagate to copies; showing the popup in that case was a false positive.
Performance: The check is deferred to the next tick to avoid UI stutter on large files with many libraries.
2026-05-27 11:22:57 +02:00
Andrey Antukh e43703c368 📎 Update root dependencies 2026-05-27 10:57:06 +02:00
DexterityandAndrey Antukh 56d8dc678c 🐛 Populate is-indirect flag on file libraries from relation graph (#9289)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-27 09:23:48 +02:00
Alejandro Alonso 1025d2e756 🐛 Fix WASM context-restore background reapplication (#9880) 2026-05-27 09:16:02 +02:00
Pablo AlbaandAndrey Antukh c9d43006c8 🐛 Fix restore saved version keeps view-only (#9514)
* 🐛 Fix restore saved verrsion keeps view-only

* 📎 Remove outdated note from CHANGES.md

Remove note about restoring saved version from Preview mode.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-27 09:09:32 +02:00
Andrey Antukh 110db4380e 🐛 Fix invitation token propagation in login flow
Pass invitation-token through login-from-token event so it reaches
the logged-in state. Fix component render syntax (:& -> :>) for the
verify-token route. Remove redundant navigation that re-visited
verify-token after login. Fix missing dependency in effect hook
to re-run when token changes.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-26 19:03:47 +02:00
Andrés Moya 7a8fa7a9cb 🐛 Token remap preserves child component sync after renaming a token group (#9566) (#9878) 2026-05-26 17:12:03 +02:00
Eva Marco 310f224ab6 🐛 Fix keep copy untranslated to preserve token name validation (#9877) 2026-05-26 16:26:13 +02:00
Eva Marco 0c568b0b2d 🐛 Fix highlight on token pill (#9829) 2026-05-26 16:25:18 +02:00
Eva Marco 0eb8cabd39 🐛 Fix text color on tooltip (#9851) 2026-05-26 16:13:58 +02:00
RenzoandAndrés Moya 02ab41f420 🐛 Token remap preserves child component sync after renaming a token group (#9566)
* 🐛 Token remap preserves child component sync after renaming a token group

* 📚 Do not update CHANGES.md

We are changing the procedures to not update the changelog on each PR. Instead, we use github tracking to check what issues come in a release, and update the changelog automatically in a batch.

Signed-off-by: Andrés Moya <andres.moya@kaleidos.net>

---------

Signed-off-by: Andrés Moya <andres.moya@kaleidos.net>
Co-authored-by: Andrés Moya <andres.moya@kaleidos.net>
2026-05-26 15:46:53 +02:00
Alonso Torres b609a964be 🐛 Fix boolean issues on the wasm render
* 🐛 Fix sharp angles in text-to-path due to wrong quad/conic degree elevation

* 🐛 Preserve even-odd fill type through Skia path conversions

* 🐛 Fix wrong quadratic-to-cubic degree elevation in push_bezier

* 🐛 Skip zero-length degenerate close segments in path_to_beziers

* 🐛 Replace BTreeMap for Vec for bool calculation

* 🐛 Fix even_odd missing when creating Path
2026-05-26 15:11:52 +02:00
David Barragán Merino f1c78945c4 🐳 Start penpot-frontend always after penpot-mcp 2026-05-26 13:55:27 +02:00
María Valderrama 04d4abc766 📎 Code review 2026-05-26 13:34:19 +02:00
María Valderrama 8542d44aaa 🐛 Fix nitrate remove-team permission flow 2026-05-26 13:34:19 +02:00
Pablo Alba a637dda554 Check nitrate permission only org members for move teams 2026-05-26 13:25:20 +02:00
Elena Torró 75c61f9211 🐛 Fix stroke rendering on drag (#9871) 2026-05-26 13:14:38 +02:00
Juanfran 5c93ad0ab3 🐛 Fix delete account modal copy for users with organizations 2026-05-26 12:43:50 +02:00
Belén Albeza 34f30e38aa 🐛 Fix migrations throwing exception on corrupted file (#9868) 2026-05-26 12:07:22 +02:00
Aitor Moreno eb095169b8 Merge pull request #9814 from penpot/hiru-fix-detach-token-typ
🐛 Fix changed font size when editing a text with no changes
2026-05-26 11:42:30 +02:00
Eva Marco 10074bc700 🎉 Add combobox test (#9864) 2026-05-26 10:52:09 +02:00
Alonso Torres 5a3a855b24 🐛 Fix problem with position-data not present
* 🐛 Fix problem with position-data not present

* 🐛 Async set-objects wait before calculate-position-data
2026-05-26 09:50:23 +02:00
alonso.torres eafd261ca6 🐛 Add a e2e test for undo after renaming 2026-05-26 09:46:18 +02:00
alonso.torres 63886d097b 🐛 Fix problem with undo rename tokens 2026-05-26 09:46:18 +02:00
Eva Marco 9439d63682 🐛 Fix rename on non empty page (#9850) 2026-05-26 08:58:09 +02:00
Marina López 40b1757c6e 🐛 Fix separate penpot from organizations (#9853) 2026-05-25 15:49:34 +02:00
Marina López b9e13c12f2 🐛 Fix subscription plan icon alignment (#9857) 2026-05-25 15:49:16 +02:00
Juanfran 0b84ada977 🐛 Fix unavailable logo state to match the design 2026-05-25 15:20:11 +02:00
María Valderrama 81f1668e3d 🐛 Fix nitrate invitation empty state layout 2026-05-25 15:15:04 +02:00
María Valderrama 87384aaccd 🐛 Fix nitrate delete and leave org flow 2026-05-25 14:39:03 +02:00
Juanfran 6b3d4e38b0 🎉 Enable Nitrate renewal with a manual activation code
Add a manual activation code flow to renew Nitrate when automatic activation is not available.
2026-05-25 14:37:14 +02:00
Marina López 57d47f8e5e 🐛 Fix navigate to admin console after subscription (#9848) 2026-05-25 13:06:52 +02:00
Eva Marco f3f697b4a2 🐛 Fix colorpicker inputs (#9793) 2026-05-25 10:45:49 +02:00
Eva Marco 841ad69d26 🐛 Fix typography asset name color and ellipsis (#9784) 2026-05-25 07:48:49 +02:00
Eva Marco 017f1d9994 🐛 Fix filter tokens to be case-sensitive (#9800)
* 🐛 Fix filter tokens to be case-sensitive

* ♻️ Add test
2026-05-25 07:47:58 +02:00
Pablo Alba dac98c0625 Add nitrate add team members permission 2026-05-23 17:18:27 +02:00
Elena Torro 2fdd3aab98 🐛 Fix nested inherited transformations 2026-05-22 14:11:50 +02:00
Andrés Moyaandmoorsecopers99 3e733bb762 🐛 Skip group nodes when processing StyleDictionary tokens (#9025) (#9825)
StyleDictionary returns all nodes from the token tree, including
intermediate group nodes that have no corresponding origin token.
Previously process-sd-tokens tried to parse these as real tokens,
which produced garbage resolved values (e.g. {"$meta$": null, …})
and caused the entire token set validation to fail, blocking all
token creation and editing.

Skip sd-tokens whose origin lookup returns nil so that only actual
tokens are processed.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Signed-off-by: Andrés Moya <andres.moya@kaleidos.net>
Co-authored-by: moorsecopers99 <46223049+moorsecopers99@users.noreply.github.com>
2026-05-22 12:46:09 +02:00
Xaviju ee1b61914e 🐛 Scroll to newly created tokens on the token tree (#9803) 2026-05-22 11:32:51 +02:00
andrés gonzález 649efd124e 📚 Update User Guide with 2.6 features (#9768) 2026-05-22 10:49:39 +02:00
Marina López 1d8da08144 🐛 Fix typo in organization name invitation email (#9817) 2026-05-22 09:59:24 +02:00
María Valderrama 3527ffdc4d 🐛 Fix navigation to admin console 2026-05-22 09:49:26 +02:00
Marina López 1688741c21 Separate penpot from rest of organizations (#9753) 2026-05-22 08:50:07 +02:00
Belén Albeza 3fd114550f 🐛 Fix library update tab UX (#9812)
* 🐛 Fix update library modal UI not working properly when updating a lib

*  Add playwright test for bug 14214
2026-05-21 19:50:12 +02:00
David Barragán Merino d574ec4ed2 🔧 Change the path to the cache directories in the custom runner 2026-05-21 19:04:41 +02:00
David Barragán Merino 0e399b7ad8 🔧 Change the path to the cache directories in the custom runner 2026-05-21 19:03:58 +02:00
David Barragán Merino d5bbfc43d3 🔧 Change the path to the cache directories in the custom runner 2026-05-21 19:02:42 +02:00
Eva Marco 8119ed132e 🐛 Fix sorting of standalone and grouped tokens (#9736) 2026-05-21 17:15:07 +02:00
Luis de Dios bfa338bdee 🐛 Fix resize line between sitemaps and layers 2026-05-21 16:54:31 +02:00
Andrés Moya 65c6b7be7c 🐛 Fix changed font size when editing a text with no changes 2026-05-21 16:46:52 +02:00
Alonso Torres a7b17f54f1 🐛 Fix problem of path position on variant change (#9801) 2026-05-21 16:39:26 +02:00
Belén Albeza 4a73a97a32 🐛 Fix shapes not being rendered after render switch 2026-05-21 16:31:56 +02:00
Juanfran e6848170c8 🎉 Show dedicated screen when Nitrate is unavailable 2026-05-21 14:47:32 +02:00
Michael Panchenko cec90416c2 Improve common JS test runner
Add focused common JavaScript test execution with log-level control and a quiet test wrapper matching the frontend workflow.

Update developer docs and testing memories to reflect the common/frontend test split, and document why runner helper extraction is deferred.

Signed-off-by: Codex <codex@openai.com>
2026-05-21 14:20:10 +02:00
Michael Panchenko e252bcf901 Add --log-level flag to frontend test runner
Lets a caller pin `app.common.logging`'s level for the duration of a
test run via `--log-level <trace|debug|info|warn|error>`. The flag is
off by default, so when absent the runner doesn't touch logger state
and stdout looks exactly as before.

When passed, the runner calls `(l/setup! {:app level})` right before
dispatching to the test block, so production code exercised by tests
emits only at the requested level or higher.

  pnpm run test:quiet -- --focus frontend-tests.logic.groups-test \
                         --log-level warn

Composes with `--focus`; the two flags are independent.

Caveats worth knowing: top-level log calls fired at namespace load
time run before the runner parses CLI options and therefore slip past
this flag; direct `println` / `js/console.log` calls bypass the
logging system entirely and are unaffected.
2026-05-21 14:20:10 +02:00
Michael Panchenko c29f32c7ae Add quiet variant of frontend test command
Introduces `pnpm run test:quiet` for non-interactive runs (CI, scripted
invocations, agent loops). It runs the same pipeline as `pnpm run test`
— `build:wasm`, then `build:test`, then `node target/tests/test.js` —
but buffers each build step's stdout and stderr and only replays them
when that step exits non-zero. Test-runner output streams through
unchanged, so failures and the summary are never hidden. Short progress
hints (`Building wasm...`, `Building test bundle...`, `Running tests...`)
are written to stderr, leaving stdout to carry only the test results
for clean capture and parsing.

Forwards arguments verbatim, so `pnpm run test:quiet -- --focus ...`
composes with the existing `--focus` flag. The default `pnpm run test`
script and its output are unchanged.

Also documents the new command in the developer guide and updates the
frontend testing memory to recommend it for agent runs.
2026-05-21 14:20:10 +02:00
Michael Panchenko 17041b53a7 Allow running a single frontend test via --focus
Previously `pnpm run test` always ran the full frontend-tests suite,
which made tight iteration on a single namespace or var painful. The
runner now accepts `--focus <ns>` or `--focus <ns>/<var>` and executes
only the matching tests, preserving each namespace's `:once` and `:each`
fixtures so behavior matches a full-suite run.

  pnpm run test -- --focus frontend-tests.logic.groups-test
  pnpm run test -- --focus frontend-tests.logic.groups-test/some-test

Also updates the developer guide and the testing memory so the flag is
discoverable from both docs and agent context.
2026-05-21 14:20:10 +02:00
63e7df5fda Add structured memories for agents
Memories use a system of progressive disclosure:
Starting from a root memory, memories reference other memories using explicit
references.

The new system of hierarchical memories replaces AGENTS.md files.

GitHub #9215

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
Co-authored-by: Codex <codex@openai.com>
2026-05-21 14:20:10 +02:00
Dominik Jain c7a4532838 Add Serena update mechanism for agentic devenv
Add SERENA_UPDATE_VERSION env var (in devenv docker-compose.yml)
to dynamically update Serena on agentic devenv without requiring
an image rebuild.
Apply for update to v1.5.0 (also changing initial installation
in Dockerfile to this version).
2026-05-21 14:20:10 +02:00
2c453e4a00 🎉 Add dash and gap inputs for dashed strokes (#9765)
*  Add dash and gap customization for dashed strokes

Signed-off-by: eureka0928 <meobius123@gmail.com>

* ♻️ Change old numeric-inputs for new components

---------

Signed-off-by: eureka0928 <meobius123@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: eureka0928 <meobius123@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>
2026-05-21 13:52:01 +02:00
05fa8af479 🎉 Add search bar to prototype interaction destination dropdown (#9769)
*  Add search bar to prototype interaction destination dropdown

On pages with many boards the destination dropdown becomes hard to
navigate. Add an optional `searchable?` flag to the shared select
component that renders a case-insensitive filter input at the top of
the dropdown, and opt it in for the interaction destination select.

- Filtering reuses the already-computed option list (no extra queries).
- Arrow-key navigation tracks the filtered list.
- Search clears automatically when the dropdown closes, so reopening
  starts with the full list.
- New `labels.no-matches` i18n key renders when nothing matches.

Closes #8618

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>

*  Use trigger input as search field in destination dropdown

Per review on #9006, the searchable select now uses the visible trigger
input as the filter field itself rather than an extra sticky input
inside the dropdown. The trigger behaves like a filterable select:
typing filters the options without permitting free-text values.

Signed-off-by: moorsecopers99 <vadanamihai409@gmail.com>

* ♻️ Update css on old select component

---------

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Signed-off-by: moorsecopers99 <vadanamihai409@gmail.com>
Co-authored-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: moorsecopers99 <vadanamihai409@gmail.com>
Co-authored-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>
2026-05-21 13:21:16 +02:00
andrés gonzálezandAndrey Antukh 6d6f624d09 Track WebGL rendering toggle events (#9683) (#9772)
*  Track WebGL rendering toggle events (#9683)

* 📎 Normalize workspace menu events origin

* 📎 Normalize event origin for profile settings page

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-21 13:17:14 +02:00
e2ed6a488d Polish workspace find and replace UX (#9687)
*  Polish workspace find and replace UX

Co-authored-by: Cursor <cursoragent@cursor.com>

*  Add toggle mode button

This button toggles between search and search and replace modes

* ♻️ Refactor and CSS cleanup

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-05-21 12:10:36 +02:00
Alejandro Alonso f3053fc844 🐛 Fix 2.15 release notes crash on invalid slide index (#9805) 2026-05-21 12:09:43 +02:00
María Valderrama 5c503591b4 🐛 Fix nitrate translation 2026-05-21 12:05:09 +02:00
DexterityandBelén Albeza 5156866f20 ♻️ Make ShapeImageIds byte conversion fallible (#9283)
Co-authored-by: Belén Albeza <belen.albeza@kaleidos.net>
2026-05-21 12:03:05 +02:00
Alonso Torres 3cfd1e1a48 🐛 Fix problem with request file access 2026-05-21 11:34:45 +02:00
Belén Albeza 3512a57df7 🐛 Fix referential integrity data in old files (#9771) 2026-05-21 11:18:13 +02:00
Alonso Torres c0e7bfae00 🐛 Fix problem with shift and alt in numeric inputs 2026-05-21 11:02:47 +02:00
Elena Torró df0a58af93 🐛 Fix team members request loop on dashboard 2026-05-21 09:26:53 +02:00
Belén Albeza d7a50735ba 🐛 Fix page and delete page icons in the sidebar
* 🐛 Fix page delete icon not disappearing on selected page

* 🐛 Fix page icon being clipped on its right side
2026-05-21 09:14:45 +02:00
Francis Santiago 0d8785a0bd Merge pull request #9738 from penpot/fc-8897-disable-ipv6-listen
🐳 Fix frontend startup on hosts without IPv6 support
2026-05-20 21:07:07 +02:00
Francis Santiago 2ad85db016 🐳 Fix frontend startup on hosts without IPv6 support
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-20 20:53:30 +02:00
Eva Marco 857aa4175c 🐛 Fix Numeric inputs rejects values with leading whitespaces 2026-05-20 17:38:16 +02:00
Belén Albeza d96442483e 🐛 Fix blurry boards (svg renderer) 2026-05-20 17:37:07 +02:00
María Valderrama a157ecdc5b Add nitrate advanced permissions for invite to teams
*  Add nitrate advanced permissions for invite to teams

* 📎 Code review
2026-05-20 16:02:37 +02:00
Eva Marco 371bd58878 👷 Fix develop CI (#9779) 2026-05-20 15:28:07 +02:00
Eva Marco f1c0ea2a19 🐛 Fix typing full token name on numeric input (#9725) 2026-05-20 13:23:21 +02:00
Andrés Moya 3e2b00b97f 🐛 Reload libraries when the tokens change (#9715) 2026-05-20 13:12:52 +02:00
Francis Santiago 106b10e971 📚 Clarify self-hosted OIDC configuration for containerized deployments (#9758)
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-20 13:05:40 +02:00
Andrey Antukh 565ed042bc 🐛 Fix regression on shape rendering (#9762)
caused by previous merge
2026-05-20 11:58:34 +02:00
Alonso Torres 29d449b42f 🐛 Deactivate text update (#9757) 2026-05-20 11:08:41 +02:00
Pablo Alba ead9bd9ccc 🐛 Make nitrate calls skip ssrf-check 2026-05-20 10:13:23 +02:00
Elena Torro fb6c522cc0 🐛 Fix inner strokes clipped on boards created from rect 2026-05-20 00:15:53 +02:00
Dr. Dominik Jain 14b53ecfec Bound MCP memory consumption by limiting parallel exports & response size (#9748)
*  Bound the size of plugin task responses

When using the integrated remote MCP server, bound response size.
All responses are passed to LLMs, which themselves impose bounds.
This is a measure to bound memory usage in the centrally provided
MCP server.

GitHub #9493

*  Bound parallelism in ExportShapeTool

Use an integer semaphore to bound parallel requests to this
memory-intensive tool, thus bounding memory usage.

GitHub #9493

*  Add (manual) integration test script for ExportShapeTool parallelism

Add dependency tsx to facilitate executions.

GitHub #9493

*  Make number of parallel export requests configurable in ExportShapeTool

Use env var PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS to configure
the maximum number of requests in multi-user mode (default 0, no limit).
2026-05-19 19:37:29 +02:00
Eva Marco 27df2aebfc 🐛 Fix token pill not updating when token name is equal token group (#9742) 2026-05-19 18:58:18 +02:00
Eva Marco b72389b5e3 🐛 Fix unset color on delete invitations modal (#9747) 2026-05-19 18:56:39 +02:00
Andrey Antukh 5f30704b28 📚 Update changelog 2026-05-19 17:55:41 +02:00
Andrey Antukh 0b0bd72dce 📎 Backport opencode skills from staging 2026-05-19 17:47:00 +02:00
Andrey Antukh d0cc859bc2 Migrate svg-attrs, optimize set-shape-svg-attrs, filter invalid URLs (#9118)
*  Add svg-attrs casing fix migration

*  Optimize set-shape-svg-attrs by removing redundant operations

- Remove backward compatibility for kebab-case SVG attribute keys
  (fill-rule, stroke-linecap, stroke-linejoin) since svg-attrs are
  already normalized to camelCase by the attrs->props migration.
- Remove unnecessary select-keys filtering and intermediate map
  construction (dissoc :style + merge style).
- Directly extract values from style and attrs using or, avoiding
  any intermediate map allocation.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Filter non-http(s) URLs in upload-images to prevent invalid calls

Skip upload for image items that are not data URIs and do not have
an http:// or https:// URL, avoiding unnecessary RPC calls with
invalid URLs to create-file-media-object-from-url.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:41:41 +02:00
DexterityandAndrey Antukh 6be4f157d6 Avoid holding pool connection during font variant creation (#9287)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:38:55 +02:00
36c58287ae ♻️ Migrate debug playground to modern component syntax (#9367)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:38:30 +02:00
DexterityandAndrey Antukh ade587968f Cache OIDC provider records to skip per-login discovery (#9295)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:38:08 +02:00
DexterityandAndrey Antukh bcc0b0d313 Validate shape on add-object to catch malformed inputs early (#9291)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:37:48 +02:00
83cc71e585 ♻️ Migrate viewport snap, pixel-overlay and outline components to modern syntax (#9394)
Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 17:37:31 +02:00
Alejandro Alonso 197c7c0f9a Merge remote-tracking branch 'origin/staging' into develop 2026-05-19 17:00:21 +02:00
Alejandro Alonso 20c6da2138 Merge pull request #9745 from penpot/superalex-fix-numeric-input-unmount
🐛 Fix commit pending numeric input on unmount without blur side effects
2026-05-19 16:59:55 +02:00
Alejandro Alonso 1d2c158ebe 🐛 Fix commit pending numeric input on unmount without blur side effects 2026-05-19 16:59:39 +02:00
Alejandro Alonso 783cfd3e55 Merge pull request #9724 from penpot/alotor-fix-grid-position
🐛 Fix problem with grid child positions
2026-05-19 16:57:39 +02:00
Alejandro Alonso 0de351fcf6 Merge pull request #9734 from penpot/elenatorro-14211-fix-translation-drag-out-of-board
🐛 Clean modifiers when needed
2026-05-19 16:54:53 +02:00
Elena Torro 29ad9aa057 🐛 Fix redirect after leaving team 2026-05-19 15:44:27 +02:00
Andrey Antukh 405a73e8ba Add climit impl and config for file snapshot methods (#9722)
*  Add dedicated concurrency limit for restore-file-snapshot

This adds a dedicated climit configuration for the restore-file-snapshot
RPC method with :permits 1 per profile (plus queue of 2 and 60s timeout)
and a global limit of 3. Previously the method only used the generic
root/by-profile and root/global limits, allowing up to 7 concurrent
restore operations per profile which caused database row lock contention
on FOR UPDATE and connection pool exhaustion.

*  Skip locking on restore! to avoid blocking other operations

Changes the row lock acquisition in restore! from a blocking FOR UPDATE
to FOR UPDATE SKIP LOCKED. If the file row is already locked by another
concurrent operation (e.g., another restore or an update-file), the query
returns no rows and the caller fails fast with a clear conflict error
instead of blocking indefinitely holding a database connection.

*  Add queue and timeout limits to root/by-profile concurrency limit

Previously root/by-profile had no queue limit (unbounded Integer/MAX_VALUE)
and no timeout, allowing requests to pile up indefinitely behind a profile
whose permits were exhausted by long-running operations. This could lead
to memory pressure and cascading failures. Now limited to 30 queued
requests with a 30-second timeout so excess requests fail fast.

*  Move backup snapshot creation outside restore transaction

The backup snapshot (fsnap/create!) is now created in its own short-lived
connection before the actual restore transaction begins. This ensures the
backup is persisted independently of the restore outcome and reduces the
restore transaction window.

The restore itself runs inside a db/tx-run! block with an optimistic
locking check: it reads the file with FOR UPDATE and compares its revn
against the value captured at backup time. If the file was edited
concurrently, the restore aborts with a conflict error to prevent data
loss.

Co-dependent with the SKIP LOCKED change in restore! — the FOR UPDATE
acquired here is in the same transaction as restore!, so the SKIP LOCKED
inside restore! correctly sees the row as unlocked (same transaction).

* ♻️ Remove unused private function get-minimal-file

The local get-minimal-file function in file_snapshots.clj is no longer
used since restore! switched to direct exec-one! with FOR UPDATE SKIP
LOCKED. The sql:get-minimal-file SQL constant is still used directly.

*  Add minor improvements on db connection management

* ♻️ Refactor create-file-snapshot to use explicit transaction management

Remove automatic transaction wrapping (`::db/transaction true`) and
pass `cfg` through the call chain instead of destructured `conn`.
Wrap `fsnap/create!` in an explicit `db/tx-run!` for clearer
transaction boundaries.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

*  Add dedicated concurrency limit for create-file-snapshot

This adds a dedicated climit configuration for the create-file-snapshot
RPC method with :permits 1 per profile (plus queue of 2 and 60s timeout)
and a global limit of 3. Previously the method only used the generic
root/by-profile and root/global limits, allowing up to 10 concurrent
snapshot creation operations per profile which could cause database
contention and connection pool exhaustion.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 14:30:44 +02:00
Andrey Antukh fd5ae84a9f 🚑 Fix syntax issue introduced in previous merges 2026-05-19 13:41:01 +02:00
DexterityandAndrey Antukh 408a9b033a 🐛 Fix conditional use-ctx hook violation in shape-wrapper (#9281)
* 🐛 Fix conditional use-ctx hook violation in shape-wrapper

*  Avoid subscribing non-root shapes to active-frames context

* 🐛 Wrap render-shape-content hiccup with mf/html

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 13:32:40 +02:00
Alonso Torres ee6489b202 🐛 Fix problem with login shoing wrong credentials 2026-05-19 13:19:06 +02:00
Alonso Torres aa1fb718e0 🐛 Fix invalid token on anonymous session 2026-05-19 13:13:11 +02:00
54a866d0b5 ♻️ Migrate workspace path-wrapper to modern component syntax (#9393)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-19 13:09:24 +02:00
Elena Torro c53856b5a9 🐛 Clean modifiers when needed 2026-05-19 12:45:14 +02:00
alonso.torres 8098250b23 🐛 Fix problem with grid child positions 2026-05-19 12:43:56 +02:00
Elena Torró d9ee28229c 🐛 Toggle token path on token rename 2026-05-19 11:35:30 +02:00
Eva Marco ed746bb694 🐛 Fix no gap on token list 2026-05-19 11:01:39 +02:00
Alonso Torres a9d0feb8fd 🐛 Fix problem with caret color value (#9717) 2026-05-19 09:56:16 +02:00
Eva Marco e854309049 🎉 Add typography token row to multiselected texts (#9128)
* 🐛 Fix text multiselection messages

* ♻️ Add tooltip to typography tooltip

* ♻️ Improve copy and add detach buttons
2026-05-19 09:47:04 +02:00
Elena Torró 8dd4b486e7 Improve drag performance avoiding unnecessary modifiers 2026-05-19 09:44:58 +02:00
Eva Marco 44f4c43f15 🐛 Fix apply tokens on token creation (#9713) 2026-05-19 09:40:10 +02:00
Andrey Antukh 46c35b01a8 📎 Update changelog 2026-05-19 09:02:34 +02:00
Andrey Antukh d9bcc1431c 📎 Update the 'update-changelog' opencode skill 2026-05-19 09:02:28 +02:00
Andrey Antukh 595ec599c6 Merge remote-tracking branch 'origin/staging' into develop 2026-05-18 20:00:47 +02:00
Andrey Antukh 5b7c732449 Merge remote-tracking branch 'origin/main' into staging 2026-05-18 19:59:46 +02:00
Andrey Antukh 87b969bd05 📎 Update changelog 2026-05-18 19:59:12 +02:00
Andrey Antukh 1161a163a7 ⬆️ Update root repo opencode dependency 2026-05-18 19:59:12 +02:00
Andrey Antukh 4ad137aef3 📎 Update gh-issue-from-pr opencode skill 2026-05-18 19:59:12 +02:00
Andrey Antukh 1b6b367951 Add diagnostic keys to SSRF validation exceptions
Add :uri and :scheme/:host keys to exceptions raised by
`validate-uri` for better error diagnostics. Also fix a bug
where (str url) was used instead of (str uri) in the
host-missing exception path.

Update the existing blocked-target test to verify the new :uri
key, and add three new tests covering scheme rejection, missing
host, and DNS failure error paths. All 27 tests pass with 60
assertions and 0 failures.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-18 15:57:55 +00:00
Belén Albeza 5c423c3678 🐛 Fix measurement guides not showing up in wasm when user has viewer role 2026-05-18 17:17:18 +02:00
Eva MarcoandAndrey Antukh 53530e958a 🐛 Fix incorrect warning when token applied (#9708)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-18 16:26:28 +02:00
Andrey Antukh 122a47359d Merge remote-tracking branch 'origin/staging' into develop 2026-05-18 16:22:16 +02:00
Andrey Antukh 4d9c6eba38 📎 Add missing bugfix entries to changelog 2026-05-18 16:20:27 +02:00
Juanfran 8e86416b0b Cascade owned organization deletion on account removal 2026-05-18 16:05:08 +02:00
Andrey Antukh 6f41a2b729 Merge remote-tracking branch 'origin/staging' into develop 2026-05-18 15:24:02 +02:00
Andrey Antukh 208182cab1 Merge remote-tracking branch 'origin/main' into staging 2026-05-18 15:23:46 +02:00
Andrey Antukh f5acea7cd7 📎 Update opencode 'update-changelog' skill 2026-05-18 15:22:32 +02:00
Andrey Antukh 7e522ae777 📎 Fix inconsistencies on CHANGES.md 2026-05-18 15:11:11 +02:00
Pablo Alba ddfe2f7406 Remove nitrate teams with expired license from the teams list 2026-05-18 14:37:38 +02:00
Marina López d26412740a ♻️ Rename control center to admin console (#9705) 2026-05-18 14:33:24 +02:00
Andrés MoyaandAndrey Antukh 82169bc0a3 🐛 Fix loss of swap slot in some cases of variant switch (#9147)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-18 14:25:32 +02:00
Andrey Antukh 725a0c966c 📎 Fix incorrect entries on changelog 2026-05-18 14:23:18 +02:00
María Valderrama 637ff3005a Add nitrate advanced permissions for move teams 2026-05-18 13:40:30 +02:00
Andrés Moya ab284febf7 🐛 Fix token application to grid padding (#9630) 2026-05-18 13:32:28 +02:00
Andrey Antukh 9de25c5404 🐛 Fix incorrect content-type on doc endpoint response (#9681)
The /api/main/doc endpoint was returning HTML content with a
text/plain content-type header instead of text/html. This caused
browsers to render the response as plain text.

Added content-type: text/html; charset=utf-8 header to the
response in the doc handler and added a regression test to
verify the fix.

Closes #9680

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-18 12:54:16 +02:00
Alonso Torres 9928249d4f ⬆️ Downgrade archive dependency (#9704) 2026-05-18 12:47:41 +02:00
Alejandro Alonso 0956becd12 🎉 Reduce heap allocations 2026-05-18 12:35:16 +02:00
Andrés Moya 25ee8dee78 🐛 Fix editing a text element detaches applied tokens (#9525) 2026-05-18 12:28:48 +02:00
Alejandro Alonso 1ac503f6bc Merge pull request #9510 from penpot/alotor-fix-viewer-texts
🐛 Fix problem with viewer texts
2026-05-18 11:24:02 +02:00
alonso.torres b2bfd627ae 🐛 Fix problem with viewer texts 2026-05-18 11:00:45 +02:00
andrés gonzálezandCursor 24fe5559c5 📚 Update 2.16 changelog (#9689)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 10:31:24 +02:00
Eva Marco 26e583c2a6 🎉 Add tooltip information on typography dropdown (#9375) 2026-05-18 09:42:28 +02:00
Marina López 83183e15c6 Adapt subscription page to selfhost (#9466) 2026-05-18 07:39:18 +02:00
Belén Albeza 0300058605 🐛 Fix delete page icon being clipped (#9685) 2026-05-15 13:41:38 +02:00
Andrey Antukh ff23f786b4 🐛 Fix broken authentication on /assets handlers
- Add ::setup/props and ::db/pool to :app.http.assets/routes config
  so session renewal works correctly for asset requests.
- Add actoken/authz middleware to the assets middleware chain so
  access tokens are properly recognized.
- Add authenticated? helper that checks both ::session/profile-id
  and ::actoken/profile-id, fixing 401 errors when accessing
  protected assets with a valid access token.
- Add comprehensive test suite for assets auth scenarios.

Closes #9677

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-15 12:05:02 +02:00
Belén Albeza fc36fb0959 🐛 Fix text editor being hidden to Playwright when empty text (#9682) 2026-05-15 12:04:51 +02:00
Andrey Antukh d620c86053 Merge remote-tracking branch 'origin/staging' into develop 2026-05-15 11:58:06 +02:00
Andrey Antukh 6ac8012258 Merge remote-tracking branch 'origin/main' into staging 2026-05-15 11:57:16 +02:00
Andrey Antukh 6cc36e4fcc 📎 Backport more changes for opencode 2026-05-15 11:56:30 +02:00
Andrey Antukh fe76567180 📎 Backport opencode skills from staging 2026-05-15 11:51:49 +02:00
Andrey Antukh 3db0e5ee0d 📎 Update changelog 2026-05-15 11:31:58 +02:00
Andrey Antukh 1f8ab6fed2 📎 Update the 'update-changelog' skill
And add specific tool for extracting info from github
2026-05-15 11:31:58 +02:00
Andrey Antukh 0b65431137 📎 Add taiga skill and script for opencode
Allows easy extraction of information from taiga urls
2026-05-15 11:10:02 +02:00
Elena Torró 053d4a23f5 🐛 Fix shape deletion after tiles refactor (#9678) 2026-05-15 11:06:17 +02:00
andrés gonzálezandAndrey Antukh 27ac0b7469 🐛 Unify layout creation telemetry for plugins and MCP (#9654)
* 🐛 Unify layout creation telemetry for plugins and MCP

* 📚 Update changelog for version 2.15.4

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-15 10:53:43 +02:00
Rene Arredondo de1c942292 🐛 Use copia not copiar for Spanish duplicate-suffix (#9671) 2026-05-15 10:24:42 +02:00
Andrey Antukh 1dea84b7b1 📚 Update mcp readme 2026-05-15 10:19:29 +02:00
Dominik Jain 7c42a1f9ac Catch serialisation issues in penpot.ui.sendMessage
This prevents timeouts in the MCP server in cases where there is an
issue with the serialisation.

GitHub #9634
2026-05-14 22:19:25 +02:00
Dominik Jain 94a5c6c4fd Add optional parameter throwOnError to penpot.ui.sendMessage
This provides more flexibility to callers, who may need to react
to a failure appropriately.
2026-05-14 22:19:25 +02:00
Dominik Jain 2a326ba23e 🎉 Add ReadTaigaIssueTool to Penpot MCP server
The tool is enabled in the agentic devenv to enable agents to
read Penpot issues on Taiga.

GitHub #9303
2026-05-14 22:18:31 +02:00
María Valderrama e3df1d6f1f Restrict team delete to owners, prep org-owner flow 2026-05-14 19:30:03 +02:00
Aitor Moreno 58c42df37e 🐛 Fix atlas texture leak 2026-05-14 17:17:06 +02:00
alonso.torres 46c642cf6d 🐛 Fix broken test 2026-05-14 17:14:31 +02:00
310bf6fd6a 💄 Change auth illustration (#9552)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:25:53 +02:00
andrés gonzálezandAndrey Antukh 7e7bf7c458 Update Open Graph and link preview metadata (#9557)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:23:57 +02:00
andrés gonzález c62ce866a8 📚 Change sentence at MCP docs (#9568) 2026-05-14 16:23:13 +02:00
andrés gonzález 846958d79e 📚 Change slogan at Help Center footer (#9554) 2026-05-14 16:22:19 +02:00
Alonso Torres dc878572da 🐛 Fix problem with set activation after renaming (#9545) 2026-05-14 16:04:07 +02:00
Pablo AlbaandAndrey Antukh 5dafd44966 🐛 Fix library update button freezes and does not apply updates (#9513)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:03:47 +02:00
Xaviju fb2734cd02 🐛 Save numeric input value on unmount (#9548) 2026-05-14 16:02:34 +02:00
Andrey Antukh 9021544c05 Merge remote-tracking branch 'origin/main' into staging 2026-05-14 15:24:29 +02:00
Andrey Antukh 05d40e3370 📚 Update changelog 2026-05-14 15:19:24 +02:00
Andrey Antukh 237f61fda0 📎 Add update changelog opencode skill 2026-05-14 15:19:03 +02:00
Andrey Antukh eb1707788b 📎 Add gh-issue-from-pr SKILL for opencode 2026-05-14 15:01:26 +02:00
Alonso Torres 8afe8a5dfa 🐛 Fix plugins schema validation error (#9632) 2026-05-14 15:00:41 +02:00
Andrey Antukh fd19bf121f 📎 Update changelog 2026-05-14 14:23:59 +02:00
Alejandro Alonso 134391bc3a Merge pull request #9633 from penpot/elenatorro-fix-auto-width
🐛 Fix regression on auto-width
2026-05-14 14:01:19 +02:00
Andrey Antukh 67d9567971 🐛 Prevent CSS injection vulnerability in font family names
Add a shared `schema:font-family` whitelist validator in
app.common.types.font that only allows letters, digits, spaces,
hyphens, underscores, and dots in font family names. Apply the schema
to create-font-variant and update-font RPC endpoints on the
backend, and add client-side validation in the dashboard fonts UI.
Include unit tests for the schema and integration tests for the RPC
handlers.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 13:46:02 +02:00
Elena Torro b13aedb231 🐛 Fix regression on auto-width 2026-05-14 13:43:00 +02:00
Alejandro Alonso 7429b97f86 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 13:27:38 +02:00
Alejandro Alonso 009e505ba1 Merge pull request #9570 from penpot/superalex-interactive-drag-crop-atlas-snapshopt
🎉 Rebuild drag-crop cache from tile textures with hybrid atlas fill
2026-05-14 13:14:36 +02:00
Alejandro Alonso 575f4b9df0 🎉 Optimize drag-crop cache rebuild path 2026-05-14 13:00:25 +02:00
Alejandro Alonso d4be6686c7 🎉 Rebuild drag-crop cache from tile textures with hybrid atlas fill 2026-05-14 13:00:25 +02:00
Aitor Moreno 64f73ef23b ♻️ Remove Mutex from mem buffer (#9479) 2026-05-14 12:57:10 +02:00
Belén Albeza f62ee7d1ae 🐛 Fix asset icon (#9612) 2026-05-14 12:56:54 +02:00
BitCompassandAndrey Antukh fbb1f9e634 🐛 Fix plugin API error message for nested malli validation paths (#9486)
When a plugin call fails malli validation, the frontend renders one
"plugins.validation.message" line per error via
`app.plugins.utils/error-messages`, which reduces the explain via
`csm/interpret-schema-problem` and then destructures each entry as
`[field {:keys [message]}]` for translation.

That works only when the underlying malli error path has a single
element. `interpret-schema-problem` calls `(assoc-in acc field ...)`
where `field` can be a multi-element vector (e.g. `[:sets 0 :name]`).
For single-element paths the resulting map is flat
(`{:group {:message "..."}}`); for multi-element paths it is nested
(`{:sets {0 {:name {:message "..."}}}}`). The destructure assumes the
flat shape, so for a nested error the consumer reads:

    field   -> :sets
    message -> nil (the nested entry has no :message at the top level)

and the produced i18n line resolves to `Field sets is invalid: ` --
or, when several errors are merged together at the same outer key,
to the user-facing `Field message is invalid` that the bug report
calls out, because `:message` then becomes the field name of the
deepest nested entry.

The original consumer carried a `#_(mapcat (comp seq val))` FIXME
that hinted at the missing flattening but did not implement one,
because the data shape produced by `interpret-schema-problem` is
not uniform.

Fix
---

Add a private `flatten-error-map` helper inside `app.plugins.utils`
that walks the error map produced by `interpret-schema-problem` and
yields `[path message]` pairs where `path` is the dot-joined field
path. Keywords use `(name k)`, strings pass through, anything else
(such as numeric indices from vector positions in the malli path)
is coerced via `str`. The recursion descends until it hits a leaf
that carries `:message`, which matches what
`interpret-schema-problem` produces in every branch.

The producer side (`csm/interpret-schema-problem` in
`common/src/app/common/schema/messages.cljc`) is left alone: it
already has another consumer (`collect-schema-errors` + the
form-validators pipeline) that depends on the keyed-by-field-path
shape, so normalising it at the source would require auditing every
validator. Flattening at the plugin consumer is the narrowest fix.

The FIXME comment is removed because the new helper supersedes it.

Tests
-----

`frontend-tests.plugins.utils-test` (new file, registered in
`runner.cljs`) covers:

- flat single-segment paths (`{:group {:message "..."}}`)
- nested multi-segment paths
  (`{:sets {0 {:name {:message "..."}}}}`) -- the case from #9417
- mixed single- and multi-segment paths at the same explain
- mixed key types (keyword / string / numeric index)
- empty explain (no validation errors)

Closes #9417

Signed-off-by: bitcompass <devwiz.sh@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 12:43:57 +02:00
Andrey Antukh 74ca40abd4 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 12:43:13 +02:00
Belén Albeza 78e3077a37 🔧 Use polyfilled helpers instead of raf (#9628) 2026-05-14 12:42:58 +02:00
Dexterity 8242015395 🐛 Log template download failures via console.error (#9363) 2026-05-14 12:40:30 +02:00
DexterityandAndrey Antukh ee714adf5c 🐛 Remove stray println from onboarding team_choice success handler (#9366)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 12:28:13 +02:00
Marina López 08b30f76f3 ♻️ Refactor nitrate copies (#9619) 2026-05-14 12:19:55 +02:00
Andrey Antukh 67e9c44b98 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 12:03:29 +02:00
Alonso Torres f389fcf468 🐛 Fix problem with copy-as-image action (#9586) 2026-05-14 12:01:30 +02:00
Andrey Antukh 8b06096019 🐛 Fix playwright version inconsistencies 2026-05-14 11:40:33 +02:00
Andrey Antukh 29f940fb7a 🐛 Sanitize comment content on rendering (#9605)
Add escape-html function that escapes HTML special characters and apply
it in the comment editor at four dom/set-html! call sites where
user-provided text is inserted as innerHTML, preventing stored XSS.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 11:20:11 +02:00
Andrey Antukh 52588412c7 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 11:12:01 +02:00
Alonso Torres c752e194d6 🐛 Fix problem with preview version in svg render (#9626) 2026-05-14 11:11:01 +02:00
Andrey Antukh eec05271e0 Merge remote-tracking branch 'refs/remotes/origin/staging' into staging 2026-05-14 11:07:59 +02:00
Andrey Antukh d78074307f Merge remote-tracking branch 'origin/main' into staging 2026-05-14 11:07:42 +02:00
Alejandro Alonso 4bd0049ddf Merge pull request #9622 from penpot/superalex-fix-grid-not-visible-2
🐛 Fix grid not visible
2026-05-14 10:56:18 +02:00
Andrey AntukhandYamila Moreno 55dd6d2b00 🔧 Add cache to github tests CI worflow. (#9621)
*  Remove usage of RELEASE placeholder on deps.edn

* 🔧 Add Maven cache to CI

---------

Co-authored-by: Yamila Moreno <yamila.moreno@kaleidos.net>
2026-05-14 10:54:22 +02:00
Andrey AntukhandYamila Moreno e9bec0a13b 🔧 Add cache to github tests CI worflow. (#9621)
*  Remove usage of RELEASE placeholder on deps.edn

* 🔧 Add Maven cache to CI

---------

Co-authored-by: Yamila Moreno <yamila.moreno@kaleidos.net>
2026-05-14 10:53:05 +02:00
Alejandro Alonso 8d51a88326 🐛 Fix grid not visible 2026-05-14 10:48:31 +02:00
Alejandro Alonso a961008188 Merge pull request #9550 from penpot/ladybenko-14033-fix-pasted-text-styles
🐛 Fix text styles sometimes being overidden when chaging shape text attrs
2026-05-14 10:09:06 +02:00
Belén Albeza abc290582d 🐛 Fix text styles sometimes being overidden when chaging shape text attrs 2026-05-14 09:42:55 +02:00
Alejandro Alonso 49d76d0f1c Merge pull request #9618 from penpot/luis-fix-linting-issue
🐛 Fix linting error
2026-05-14 08:59:54 +02:00
Luis de Dios 4c9e1d4015 🐛 Fix linting error 2026-05-14 08:53:23 +02:00
Alejandro Alonso 14fca46886 Merge pull request #9610 from penpot/ladybenko-14185-fix-exit-focus
🐛 Fix rendering glitch when exiting focus mode
2026-05-14 07:58:28 +02:00
Belén Albeza ae282eb6e2 🐛 Fix rendering glitch when exiting focus mode 2026-05-14 07:45:51 +02:00
Alejandro Alonso 009d805394 Merge pull request #9614 from penpot/elenatorro-fix-text-focus-on-empty-text-shape
🐛 Fix text focus on empty text
2026-05-14 07:26:30 +02:00
Elena Torro 374c64da74 🐛 Fix text focus on empty text 2026-05-14 07:12:30 +02:00
Alejandro Alonso 2685389aad Merge pull request #9562 from penpot/elenatorro-fix-layout-freeze
🐛 Reflow only when needed
2026-05-13 16:26:05 +02:00
Elena Torro 38c62a465f 🐛 Reflow only when needed 2026-05-13 16:22:21 +02:00
Alejandro Alonso 757aae1df3 Merge pull request #9572 from penpot/azazeln28-refactor-target-and-backbuffer-rendering
♻️ Refactor how target and backbuffer works
2026-05-13 16:11:17 +02:00
Aitor Moreno a5da9449b5 ♻️ Refactor how target and backbuffer works 2026-05-13 16:05:19 +02:00
Milos Milic 884b125cf5 🐛 Fix two plugin error i18n keys broken by leading whitespace in en.po (#9501) 2026-05-13 15:59:04 +02:00
Alonso Torres c56f5cc01b 🐛 Fix problem with colorpicker in multiselect color with texts (#9549) 2026-05-13 15:57:30 +02:00
Alonso Torres c65b24495b 🐛 Fix several issues with color picker (#9558) 2026-05-13 15:54:05 +02:00
Madalena Melo f8744c8285 🔥 Remove issue template for new render bug reports (#9569)
Remove the template for new render bug reports; it was used for the open beta test but is no longer valuable
2026-05-13 15:52:29 +02:00
Luis de Dios b125c2cabb 🐛 Fix remove resize cursor CSS for inputs (#9590)
* 🐛 Remove cursor CSS for all inputs

- Restore the default cursor for the dashboard inputs.
- Make the numeric-input component from DS to work as expected.

* 🐛 Fix remove drag-to-change behaviour from old numeric-input
2026-05-13 15:51:24 +02:00
Pablo Alba bf880467b4 🐛 Fix dependency libraries visible in UI after unlinking main library (#9511) 2026-05-13 15:35:42 +02:00
Andrey Antukh da85e02a6f ⬆️ Update dependencies (#9597)
* ⬆️ Update dependencies

* 📎 Fix playwright dep
2026-05-13 14:14:10 +02:00
Alejandro Alonso 7617e42547 Merge pull request #9576 from penpot/superalex-fix-atlas-issues
🐛 Fix atlas corruption when dragging large shapes after zoom change
2026-05-13 13:08:20 +02:00
Alejandro Alonso 1a3b057814 🐛 Fix atlas corruption when dragging large shapes after zoom change 2026-05-13 13:00:56 +02:00
Alejandro Alonso 4f5d269313 Merge pull request #9599 from penpot/superalex-fix-drag-crop-cache-viewport-clamp
🐛 Fix clamp backbuffer crop origin for partially off-screen shapes
2026-05-13 13:00:03 +02:00
Alejandro Alonso 9c61aa4f17 🐛 Fix clamp backbuffer crop origin for partially off-screen shapes 2026-05-13 12:48:05 +02:00
Yamila Moreno 4df707c0ff 🐳 Add enable-mcp to docker-compose as default behaviour 2026-05-13 11:53:10 +02:00
Pablo Alba fffafdab93 🐛 Fix library updates reappear after file is reloaded (#9563)
* 🐛 Fix library updates reappear after file is reloaded

Summary
Migrate synced_at timestamps to a standalone file_library_sync table to ensure sync state is tracked for both direct and transitive libraries.

Problem
Transitive libraries (libraries imported by other libraries) are not stored as direct rows in file_library_rel. Because the system previously coupled synced_at directly to the file_library_rel schema, transitive libraries lacked a persistent location for their sync timestamps. This caused sync states to be lost or incorrectly reported for nested dependencies.

Changes
Schema Migration: Created file_library_sync and migrated existing synced_at values from file_library_rel.

Decoupling: Removed tight Foreign Key coupling to allow sync rows to exist independently of specific relationship records.

Persistent Writes: Added upsert-file-library-sync! helper. Updated all import, duplication, and RPC write paths (v1/v2/v3 importers, link-file-library) to ensure every write persists a sync row.

Unified Reads: Updated both direct and recursive/transitive library queries to fetch synced_at from the new table.

Testing: Added regression tests to verify that sync rows are correctly created/updated even when a transitive relation is absent in file_library_rel.

Impact
This fix ensures that the system accurately records and retrieves sync states for the entire library dependency tree, resolving the bug where nested libraries appeared out of sync.

*  MR review
2026-05-13 11:29:05 +02:00
Yamila Moreno d4dade2c3e 🐳 Pin minor version in docker-compose.yaml 2026-05-13 07:48:06 +02:00
Andrey Antukh 382efe3449 📚 Update changelog 2026-05-12 23:33:39 +02:00
Yamila Moreno 4289cad9ab 🐳 Improve nginx configuration for MCP server (#9565) 2026-05-12 23:28:20 +02:00
Andrey Antukh db7fcfcb1a 🐛 Fix metrics for rpc methods 2026-05-12 19:06:25 +02:00
Andrey Antukh e5c99231da 📚 Update changelog 2026-05-12 18:43:08 +02:00
Yamila Moreno 02c3d2c27c 🐳 Add mcp server to release workflow 2026-05-12 18:38:17 +02:00
Yamila Moreno eb22c59e5a 🐳 Add penpot-mcp service to official docker-compose.yml 2026-05-12 18:31:13 +02:00
Andrey Antukh 947f6d392d 🎉 Add chunked upload support for font variants (#9551)
*  Add additional logging and validation for image upload

* 🎉 Add chunked upload support for font variants

Extend the font variant upload flow across frontend, backend, and common
to support the standardized chunked upload protocol.

**Backend:**
- Add \`:font-max-file-size\` config default (30 MiB) and schema entry
- Add \`validate-font-size!\` in \`media.clj\` (mirrors
  \`validate-media-size!\`, raises \`:font-max-file-size-reached\`)
- Extend \`schema:create-font-variant\` to accept either \`:data\`
  (legacy bytes or chunk-vector) or \`:uploads\` (new chunked session
  map), with a validator requiring exactly one
- Add \`prepare-font-data-from-uploads\`: assembles each chunked
  session via \`cmedia/assemble-chunks\`, validates type+size
- Add \`prepare-font-data-from-legacy\`: normalises legacy byte/chunk
  entries, writing to a tempfile (joining via SequenceInputStream),
  validates type+size
- Add structured logging ("init"/"end") with \`:size\`, \`:mtypes\`,
  and \`:elapsed\` in \`create-font-variant\`

**Frontend:**
- \`upload-blob-chunked\` accepts a per-caller \`:chunk-size\` option
- Add \`font-upload-chunk-size\` (10 MiB) and \`upload-font-variant\`
  fn that uploads each mtype as a separate chunked session
- \`on-upload*\` in dashboard fonts now calls \`upload-font-variant\`
  instead of issuing \`create-font-variant\` RPC directly
- \`process-upload\` stores raw ArrayBuffer instead of chunking
  client-side

**Common:**
- Replace \`"font/opentype"\` with \`"font/woff2"\` in \`font-types\`

**Tests:**
- 25 tests / 224 assertions covering all three upload paths (direct
  bytes, legacy chunk-vector, new chunked sessions), size validation,
  and media type validation

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add a script for check the commit format locally

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-12 18:30:19 +02:00
Alejandro Alonso 63ff5c87c2 Merge pull request #9523 from penpot/superalex-test-firefox
🐛 Fix WASM viewport interaction lifecycle for pan/zoom
2026-05-12 15:34:25 +02:00
Andrey Antukh 1e746add31 Merge pull request #9299 from oraios/mcp-self-improvement
 Add agentic DevEnv (with extended MCP server for self-improvement)
2026-05-12 15:05:42 +02:00
Alejandro Alonso 9ebf7875ea 🐛 Fix WASM viewport interaction lifecycle for pan/zoom 2026-05-12 14:13:18 +02:00
Andrey Antukh ade0d2d0a8 📎 Update changelog with PR info 2026-05-12 13:01:42 +02:00
Michael Panchenko 7a2ca6c08f 🎉 Add two new MCP tools for Clojure development
* CljsCompilerOutputTool: Checks compiler output and reports errors
* CljCheckParentheses: Precisely locates incorrect/unbalanced parentheses

GitHub #9214
2026-05-12 12:49:58 +02:00
Dominik Jain b952783621 📚 Add documentation page on the agentic DevEnv #9216 2026-05-12 12:49:58 +02:00
Michael Panchenko c2a1d5c6f7 Add run-devenv-agentic command, starting the Serena MCP server in the container
Serena provides useful tools for the agentic workflow for penpot.
The following additional extensions are added:

1. uv and Serena installation, including a suitable serena_config.yml, are added to the devenv docker image
2. Serena configuration options are set via env vars and flags in manage.sh
3. run-devenv can now take -e flags which it forwards to docker exec

GitHub #9315
2026-05-12 12:49:47 +02:00
Dominik Jain 85cf3fcc3c 📚 Improve/restructure critical-info memory, adding navigation memory 2026-05-12 12:36:55 +02:00
Dominik JainandClaude 65fce36898 🎉 Add ImportPenpotFileTool for importing .penpot files via URL
Adds a new MCP tool (devenv-only) that imports .penpot files into the
running Penpot instance. The tool downloads the file from a given URL,
stages it in the frontend's static directory, and triggers the import
via the ClojureScript REPL using the frontend's web worker infrastructure.
The temporary file is cleaned up after the import completes or fails.

Registered alongside CljsReplTool, sharing the same NreplClient instance.

Github #9217

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 12:36:55 +02:00
Dominik JainandClaude 1630561382 📚 Add memory on handling Penpot frontend crashes #9300
Documents how to detect, diagnose, and recover from frontend crashes
(the Internal Error page) when working through the Penpot Plugin API:

- Detect via (some? (:exception @app.main.store/state)) in the cljs REPL
- Read cause from the same map (:type, :code, :hint, :details, :uri, ...)
- Reload by listing/selecting the workspace tab in playwright and
  re-navigating to its URL, then re-checking the exception is gone

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 12:36:55 +02:00
Dominik Jain fe23c731d4 📚 Add memory on PR creation 2026-05-12 12:36:55 +02:00
Dominik Jain a25f43ff42 📚 Reorganise memories, introducing an entrypoint memory
Add `critical-info` memory as an entrypoint (bootstrap memory) for the LLM, which
points to critical tools and memories, allowing the LLM to dynamically build up
relevant context
2026-05-12 12:36:55 +02:00
Dominik Jain af1c72df01 📚 Add memory on commit creation 2026-05-12 12:36:55 +02:00
Dominik Jain eee8ee3103 📎 Exclude versioned .md files from .gitignore pattern
Exclude files like CONTRIBUTING.md or README.md from being ignored by /*.md pattern,
as this can influence agent behaviour (configurations that disallow ignored files
from being edited)
2026-05-12 12:36:55 +02:00
Dominik Jain f1affdbadc Revamp cljs expression evaluation to full-blown REPL 2026-05-12 12:36:55 +02:00
Dominik Jain 66d518f15d 🎉 Add MCP tool for ClojureScript expression evaluation
New tool to evaluate ClojureScript expressions by connecting to the
nREPL service already provided in devenv.

Add dependency 'nrepl-client' and a corresponding client class
as well as types to support this.

Add a new environment variable for 'devenv mode', which enables
the new tool (PENPOT_MCP_DEVENV).
2026-05-12 12:36:44 +02:00
Dominik Jain e1493de777 📎 Ignore .idea 2026-05-12 12:35:38 +02:00
Dominik Jain 6de41f072c Add initial Serena project 2026-05-12 12:35:38 +02:00
Dominik Jain b7b31f6ee3 Start MCP server in devenv if PENPOT_FLAGS contains 'enable-mcp' 2026-05-12 12:35:38 +02:00
Alejandro Alonso 97c8fcd4ad Merge pull request #9509 from penpot/elenatorro-14038-fix-component-thumbnail-update
🐛 Fix component thumbnail sync
2026-05-12 12:29:28 +02:00
Elena Torro 76e3df5836 🐛 Use extrect to capture component's thumbnail 2026-05-12 12:21:06 +02:00
Elena Torro 847d55bfb4 🐛 Update component thumbnails to match current shape 2026-05-12 12:21:06 +02:00
Alejandro Alonso 086ed83847 Merge pull request #9389 from penpot/azazeln28-refactor-state
♻️ Refactor State into DesignState
2026-05-12 12:15:05 +02:00
Aitor Moreno a126379cc7 ♻️ Refactor State into DesignState 2026-05-12 12:14:38 +02:00
Pablo AlbaandAndrey Antukh 269edcd0ee 🐛 Fix restore saved version keeps view-only (#9514)
* 🐛 Fix restore saved verrsion keeps view-only

* 📎 Remove outdated note from CHANGES.md

Remove note about restoring saved version from Preview mode.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-12 10:32:12 +02:00
Luis de Dios f62a89dcc2 🐛 Fix font selection position is hiding fonts (#9499) 2026-05-12 10:25:16 +02:00
Alejandro Alonso f044104db1 Merge pull request #9524 from penpot/elenatorro-fix-blur-on-page-transition
🐛 Fix blur on page transition
2026-05-12 10:10:49 +02:00
Yamila Moreno 328efd4e16 📚 Add notice regarding architectural constraints with MCP Server (#9423) 2026-05-12 10:06:13 +02:00
Andrey Antukh 11a72abdcd 📎 Update version on mcp server 2026-05-12 10:04:12 +02:00
Elena Torro 463017b5c9 🐛 Fix blur on page transition 2026-05-12 09:47:00 +02:00
Andrey Antukh bd3ca6f8e5 📚 Update changelog 2026-05-12 09:33:33 +02:00
Jack StormentandAlejandro Alonso c394a281c8 🐛 Revert blend-mode hover preview when dismissing dropdown (#9237)
* 🐛 Revert blend-mode hover preview when dismissing dropdown

When the blend-mode dropdown was dismissed by clicking outside instead
of selecting an option, the canvas kept rendering the last hovered
blend mode even though the inspector and data state had reverted. The
visible state of the shape no longer matched its stored state. Reset
the canvas render back to the shape's saved blend mode on dropdown
close so the preview never outlives the dropdown.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* Fix blend-mode dropdown and various bugs

Fix blend-mode dropdown behavior and revert WASM render on pointer leave. Also, address multiple bugs related to user interactions and data handling.

Signed-off-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
Co-authored-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>
2026-05-12 08:17:07 +02:00
Alejandro Alonso 7650fa1716 Merge pull request #9500 from penpot/alotor-fix-text-edition
🐛 Fix problem when editing text not being redrawn
2026-05-12 06:50:02 +02:00
alonso.torres effb8bcf10 🐛 Fix problem when editing text not being redrawn 2026-05-12 06:45:28 +02:00
Andrey Antukh 962bb1fa9b Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 17:53:03 +02:00
Andrey Antukh fd82744c62 Merge branch 'main' into staging 2026-05-11 17:52:52 +02:00
Andrey Antukh 843a4a5b58 🐛 Fix mattermost and database logger related to the audit event change 2026-05-11 17:08:38 +02:00
Andrey Antukh d670ba4bff 🐛 Fix mattermost and database logger related to the audit event change 2026-05-11 17:07:59 +02:00
Andrey Antukh 27e6c1e420 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 16:24:55 +02:00
Andrey Antukh c76e536cd8 Merge remote-tracking branch 'origin/main' into staging 2026-05-11 16:24:27 +02:00
Andrey Antukh 102c97040a 🐛 Fix unexpected exception on handling webhook events 2026-05-11 16:23:14 +02:00
Andrey Antukh 1de2718d43 Merge remote-tracking branch 'origin/main' into staging 2026-05-11 15:24:15 +02:00
Andrey Antukh 8f4f948104 🐛 Skip the ssrf check on internal audit-log archive task 2026-05-11 15:22:59 +02:00
Michael Panchenko 7fb19fc1a2 🐛 Fix float comparison #14070 (#9434)
* 📎 Add test that surfaces the bug described in #14070

The bug: alt-drag-duplicating a variant master into the variant container
auto-creates a new variant component cloned from the source
via duplicate-component, which preserves :touched on every cloned shape.
The resulting copy's children inherit those :geometry-group touched flags
through add-touched-from-ref-chain on switch.

variants-switch -> component-swap (keep-touched? true) -> generate-keep-touched
then runs update-attrs-on-switch for each touched child. The new shape's
:y is correctly skipped by the per-attr "different masters" guard, but
:selrect/:points fall through to a width/height-based safety check that
uses exact equality. In practice, the alt-drag modifier path leaves a
sub-pixel drift in :width on the copy, so equal-geometry? returns false,
the safety check is bypassed, and the :else branch copies the source
variant's :selrect verbatim onto the freshly instantiated target shape.
The shape ends up with :y from the target master but :selrect.y from the
source — the renderer reads :selrect, so the child appears at the source
position inside a parent that has resized to the target's dimensions:
the visible "button cut off" symptom.

The new test sets up a variant container whose children are themselves
component copies (matching production: variant masters' children carry
:touched), introduces the same kind of width drift on the copy that the
alt-drag path produces, and runs the swap directly via the existing
test harness. It asserts (a) :y matches the target, (b) :selrect.y
matches the target, and (c) :y and :selrect.y agree. With the current
code (a) passes and (b)/(c) fail — capturing both the wrong value and
the internal inconsistency that causes the visible regression.

* 🐛 Fix #14070 by no longer comparing floats for exact equality in equal-geometry?
2026-05-11 15:17:48 +02:00
Leona LeeandAndrey Antukh 02bbbae0b0 🐛 Fix typography removal from plugin API (#9279)
* 🐛 Fix typography removal from plugin API

* 🔥 Remove unrelated delete-typography test

---------

Signed-off-by: Leona Lee <63717587+leonaIee@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:58:42 +02:00
dhgoalandAndrey Antukh 3094d512f4 🐛 Expose Source Sans Pro 600 weight in builtin fonts list (#9247)
The font-family list at frontend/src/app/main/fonts.cljs registers
Source Sans Pro variants for weights 200, 300, 400, 700 and 900, but
omits the semibold (600) entries even though the font assets are
already bundled (frontend/resources/fonts/sourcesanspro-semibold.*)
and the CSS @font-face declarations that load them are present
(frontend/resources/styles/common/dependencies/fonts.scss:55-56).

Result: weight 600 cannot be selected from the font picker even
though the bytes are downloadable; users see a 400 -> 700 jump.

Add the two missing variant entries (600 and 600 italic) using the
same :suffix style as the other numeric-id entries (200, 300), since
the file-name component "semibold" doesn't match the weight number.

Issue mentions weights 500 and 800 as also missing, but no
sourcesanspro-medium or sourcesanspro-extrabold assets exist in the
repo, so this PR scopes to weight 600 only — the recoverable subset.

Closes #7378.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:55:25 +02:00
FairyPiggyDevandAndrey Antukh 09bd7f96f6 Add author, relative timestamp and short identifier to history entries (#9132)
The workspace Actions history panel previously showed the operation
icon and a one-line message for each undo entry with no indication
of when the action happened, any stable way to refer to it, or who
made the change. The reporter of issue #7660 (and @Takhoffman's
follow-up comment) asked for a git-like display: `<hash> · <time> by
<name>`.

This change stamps each undo entry with its creation timestamp and
author at the moment it lands on the undo stack and surfaces three
extra pieces of information in the history sidebar:

- A short 7-character identifier derived from the entry's existing
  `:undo-group` UUID. Hovering shows the full UUID.
- A relative timestamp (e.g. `just now`, `5 minutes ago`, `2 hours
  ago`, `3 days ago`) rendered via `app.common.time/timeago` so it
  matches the formatting already used for comments and the dashboard.
- The display name of the profile that created the entry, rendered
  as `by <Name>` in the same metadata row.

The undo stack is client-side per profile, so every entry is always
the current user; the author is stored on the entry anyway so the UI
does not need to reach into profile state while rendering and so the
data stays correct if the stack shape ever changes.

Changes at a glance:

- `data/workspace/undo.cljs`: extend `schema:undo-entry` with an
  optional `:timestamp` and `:by`; new `profile-display-name` helper
  that falls back from full name to email to nil; `stamp-entry` now
  takes state and fills in both fields on entries that do not
  already carry them. Pre-stamped entries (e.g. coming out of an
  accumulated transaction) keep their original values.
- `ui/workspace/sidebar/history.cljs`: propagate `:timestamp`,
  `:undo-group`, and `:by` through `parse-entries`; add `short-id`
  helper; render the metadata row in `history-entry` using
  `app.common.time/timeago` against `:timestamp`; skip the author
  span entirely when `:by` is nil.
- `ui/workspace/sidebar/history.scss`: styling for the new metadata
  row (monospace hash, muted separator, truncated time/author).
- `translations/en.po`: 1 new string for `by %s`.

Existing undo entries created before this change have neither
timestamp nor author; the UI is defensive about both, so old entries
simply render with whatever data they have (and often the plain
title on its own).

Github #7660

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:48:32 +02:00
f7fbd3007e 🐛 Prevent viewers from overwriting file thumbnails (#9285)
* 🐛 Prevent viewers from overwriting file thumbnails

* 🐛 Fix message

---------

Co-authored-by: jony376 <jony376@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:38:40 +02:00
Andrey Antukh 06986e25a3 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 14:06:31 +02:00
Andrey Antukh 6ef231bf38 Merge remote-tracking branch 'origin/main' into staging 2026-05-11 14:04:27 +02:00
Dr. Dominik Jain 1f9f4126b7 Improve MCP server logging, adding Loki support (#9425)
*  Improve MCP server logging

Log only fingerprints of user tokens

*  Add Loki transport support to MCP server logger

Loki logging is enabled iff PENPOT_LOGGERS_LOKI_URI is non-empty.

File logging is now enabled iff PENPOT_MCP_LOG_DIR is set to a non-empty value
(previously defaulted to the "logs" directory when unset).

GitHub #9415
2026-05-11 14:01:05 +02:00
Dr. Dominik Jain 313777d1c3 Improve MCP server logging, adding Loki support (#9425)
*  Improve MCP server logging

Log only fingerprints of user tokens

*  Add Loki transport support to MCP server logger

Loki logging is enabled iff PENPOT_LOGGERS_LOKI_URI is non-empty.

File logging is now enabled iff PENPOT_MCP_LOG_DIR is set to a non-empty value
(previously defaulted to the "logs" directory when unset).

GitHub #9415
2026-05-11 14:00:23 +02:00
bitloiandAndrey Antukh 58ca0a16ba 🐛 Fix MCP SSE sessions leaking on zombie connections (#9432) (#9464)
SSE sessions were never included in the periodic inactivity timeout
checker, so a stale connection whose TCP close event never fired would
retain its SSEServerTransport and McpServer indefinitely.

Changes:
- Add lastActiveTime: number to the sseTransports entry type
- Initialise lastActiveTime at SSE session creation (GET /sse)
- Refresh lastActiveTime on every incoming message (POST /messages)
- Extend startSessionTimeoutChecker() to sweep and forcibly close SSE
  sessions idle for more than SESSION_TIMEOUT_MINUTES, mirroring the
  existing Streamable HTTP logic
- Update the checker log to count both transport maps

The existing res.on('close') cleanup path is preserved unchanged:
it remains the primary cleanup for normal disconnections; the timer
is a safety net for zombie sessions only.

Closes #9432

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 13:55:11 +02:00
Milos MilicandAndrey Antukh b54fa2f11c 🐛 Reject clipboard helpers gracefully on insecure origins (#9188)
* 🐛 Reject clipboard helpers gracefully on insecure origins

Closes #6514. Resolves the user-visible crash originally reported
in #4478.

`app.util.clipboard/to-clipboard` and `to-clipboard-promise` called
`(unchecked-get js/navigator "clipboard")` and then immediately
invoked `.writeText` / `.write` on the result, with no guard for the
case where `navigator.clipboard` is `undefined`. The W3C Clipboard
API spec requires a "secure context" (HTTPS or localhost), so a
Penpot instance served over plain HTTP - which the SSDP/LAN
self-hosted setup in #4478 was - throws

  TypeError: Cannot read properties of undefined (reading 'writeText')

synchronously the moment the user clicks any copy button. The error
escapes the consuming function before any error-handling rx/of arm
runs, so the whole UI ends up on the error screen instead of just
the affected control showing a "could not copy" message.

A third helper (`to-clipboard-multi`) already guards `clipboard` and
`clipboard.write`, but if both are missing it silently returns nil
which is also surprising for callers expecting a Promise.

## Fix

Add a small `get-clipboard` accessor and an `unavailable-error`
factory that returns `Promise.reject(Error(...))` with a clear
message ("Clipboard API is unavailable. This usually happens when
the page is served over plain HTTP; serve Penpot over HTTPS to
enable copy-to-clipboard."). Wire all three helpers through the
same defensive contract:

- `to-clipboard` - return the rejected Promise when
  `navigator.clipboard.writeText` is missing.
- `to-clipboard-promise` - return the rejected Promise when
  `navigator.clipboard.write` is missing.
- `to-clipboard-multi` - convert the existing `if` into a `cond`
  with three branches: prefer `clipboard.write` for true multi-MIME
  output, fall through to `writeText` with the text/plain payload
  when only the legacy text path is available, and finally reject
  with the unavailable error when neither path exists. Previously
  the no-API case fell off the `when-let` and silently returned
  nil.

The contract is now consistent: every helper either resolves or
rejects a Promise, never throws synchronously, and never returns
nil. Callers (which are already structured around rx streams that
call `rx/from` on the helper's return value) can chain `.catch` /
`rx/catch` to surface a status-bar message instead of crashing.

The two stale `;; FIXME` comments on `to-clipboard` (rename to
`write-text`) and `to-clipboard-promise` (this API is very confuse)
are removed - the rename remains an open follow-up across 13+ call
sites and is intentionally out of scope, but the API is no longer
"confuse" once the contract is documented and uniform.

CHANGES.md entry added under the 2.17.0 Bugs-fixed section
describing the user-visible behaviour change.

* 🐛 Reject paste-from-navigator gracefully on insecure origins

Symmetric companion to the to-clipboard / to-clipboard-promise /
to-clipboard-multi guards added earlier in this PR. The paste path
went through fromNavigator (frontend/src/app/util/clipboard.js) which
called `navigator.clipboard.read()` with no nil-check; on insecure
origins (plain HTTP / non-localhost) this raised an opaque
`TypeError: Cannot read properties of undefined (reading 'read')` and
the workspace surfaced a generic 'Something wrong has happened' toast
instead of the descriptive 'serve Penpot over HTTPS …' message users
get for the copy direction.

Mirror the get-clipboard pattern from clipboard.cljs:
- Read `navigator.clipboard` once into a local.
- If it's missing the `.read` method, throw a descriptive Error that
  matches the wording the copy direction already uses (only the verb
  swaps: 'paste-from-clipboard' instead of 'copy-to-clipboard').
- Otherwise, dispatch through the local handle.

The existing app.util.clipboard/from-navigator (clipboard.cljs:32)
already wraps impl/fromNavigator in rx/from, so a rejected Promise
from the async function propagates as an rx error event. Existing
callers that subscribe with .catch / on-error see the structured
Error and surface the toast, identical to how to-clipboard's
unavailable-error already flows.

Repro (matches niwinz's reproduction in the PR comment):

  Object.defineProperty(navigator, 'clipboard', { value: undefined });
  // … then attempt a paste action in the workspace …

Before: TypeError in console + 'Something wrong has happened' toast.
After: descriptive Error caught by the rx subscription and rendered
through the existing unavailable-Clipboard-API surface.

Refs #6514, #4478

* 🐛 Show user-facing toast when clipboard API is unavailable

Niwinz's review on penpot#9188 caught that the rejected Promise from
to-clipboard / to-clipboard-promise / to-clipboard-multi /
fromNavigator now surfaces the correct error to the console, but the
workspace UI still falls through to the generic "Something wrong has
happened" toast because the on-clipboard-permission-error and the
paste error-handler in paste-from-clipboard only branched on
clipboard-permission-error?.

Apply the patch he suggested in the review:

- Add clipboard-unavailable-error? predicate that matches the
  Promise.reject(Error("Clipboard API is unavailable. ...")) thrown
  by the get-clipboard / unavailable-error helpers added earlier in
  this PR. Uses str/starts-with? on the message prefix so the
  predicate stays stable even if the trailing "serve Penpot over
  HTTPS ..." advice text is reworded later.
- Convert on-clipboard-permission-error from `if` to `cond` and add
  a third arm that fires errors.clipboard-api-unavailable as a
  warning toast.
- Add the same arm in the second cond block inside
  paste-from-clipboard, before the :not-implemented and :else arms.
- Add the matching errors.clipboard-api-unavailable entry to
  frontend/translations/en.po with the wording niwinz suggested:
  "Clipboard API is unavailable. Serve Penpot over HTTPS to enable
  clipboard access".

Refs penpot#9188 review.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 13:52:36 +02:00
Andrey Antukh feb49bc07a 🐛 Add missing migrations for audit-log tables 2026-05-11 13:28:53 +02:00
Belén Albeza 0639ca53de Implement WebGL context restoring (#9317)
*  Implement asset re-uploading to wasm

*  Show toast instead of error screen when webgl context is lost

* 🎉 Recover context after webgl context restored event

* 🎉 Set Read-only mode when the context has been lost

*  Disable scroll & zoom when context loss

*  Fix stale reload payload

*  Use existing debounce util to take screenshots

*  Implement design / ux specs

*  Fix playwright test by looking for toast, not error page
2026-05-11 13:15:45 +02:00
Andrey Antukh 7d4be33d4f 🎉 Add telemetry anonymous event collection (#9483)
* 🎉 Add telemetry anonymous event collection

Rewrite the audit logging subsystem to support three operating modes and
add anonymous telemetry event collection:

Modes:
- A (audit-log only): events persisted with full context
- B (audit-log + telemetry): same as A, plus events are collected for
  telemetry shipping
- C (telemetry-only): events stored anonymously with PII stripped,
  telemetry flag active, audit-log flag inactive

Audit system refactoring (app.loggers.audit):
- Replace qualified map keys (::audit/name etc.) with plain keywords
- Rename submit! -> submit, insert! -> insert, prepare-event ->
  prepare-rpc-event
- Add submit* as a lower-level public API
- Add process-event dispatch function that handles all three modes and
  webhooks in a single tx-run!
- Add :id to event schema (auto-generated if omitted)
- Add filter-telemetry-props: anonymises event props per event type.
  Keeps UUID/boolean/number values; for login/identify events preserves
  lang, auth-backend, email-domain; for navigate events preserves route,
  file-id, team-id, page-id; instance-start trigger passes through.
- Add filter-telemetry-context: retains only safe context keys.
  Backend: version, initiator, client-version, client-user-agent.
  Frontend: browser, os, locale, screen metrics, event-origin.
- Timestamps truncated to day precision via ct/truncate for telemetry
  storage
- PII stripped: props emptied, ip-addr zeroed, session-linking and
  access-token fields removed from context

Config (app.config):
- Derive :enable-telemetry flag from telemetry-enabled config option

Email utilities (app.email):
- Add email/clean and email/get-domain helper functions for domain
  extraction from email addresses

Setup (app.setup):
- Emit instance-start trigger event at system startup
- Simplify handle-instance-id (remove read-only check)

RPC layer (app.rpc):
- wrap-audit now activates when :telemetry flag is set
- Add :request-id to RPC params context for event correlation

RPC commands (management, teams_invitations, verify_token, OIDC auth,
webhooks): migrate all audit call sites to use the new plain-key API

SREPL (app.srepl.main):
- Migrate all audit/insert! calls to audit/insert with plain keys

Telemetry task (app.tasks.telemetry):
- Restructure legacy report into make-legacy-request; distinguish
  payload type as :telemetry-legacy-report
- Add collect-and-send-audit-events: loop fetching up to 10,000 rows
  per iteration, encodes and sends each page, deletes on success,
  stops immediately on failure for retry
- Add send-event-batch: POSTs fressian+zstd batch (base64 via
  blob/encode-str) to the telemetry endpoint with instance-id per event
- Add gc-telemetry-events: enforces 100,000-row safety cap by dropping
  oldest rows first
- Add delete-sent-events: deletes successfully shipped rows by id

Blob utilities (app.util.blob):
- Add encode-str/decode-str: combine fressian+zstd encoding with URL-
  safe base64 for JSON-safe string transport

Database:
- Add migration 0145: index on audit_log (source, created_at ASC) for
  efficient telemetry batch collection queries

Frontend:
- Always initialize event system regardless of :audit-log flag
- Defer auth events (signin identify) to after profile is set
- Refactor event subsystem for telemetry support

Tests (21 test vars, 94 assertions in tasks-telemetry-test):
- Cover all code paths: disabled/enabled telemetry, no-events no-op,
  happy-path batch send and delete, failure retention, payload anonymity,
  context stripping, timestamp day precision, batch encoding round-trip,
  multi-page iteration, GC cap enforcement, partial failure handling
- blob encode-str/decode-str round-trip tests (14 test vars)
- RPC audit integration tests (5 test vars)

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add pr feedback changes

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 12:42:01 +02:00
Andrey Antukh cd882f9ebd 🔧 Add minor changes to devenv config 2026-05-11 12:39:03 +02:00
Andrey Antukh cd4a4da0f2 🎉 Add telemetry anonymous event collection (#9483)
* 🎉 Add telemetry anonymous event collection

Rewrite the audit logging subsystem to support three operating modes and
add anonymous telemetry event collection:

Modes:
- A (audit-log only): events persisted with full context
- B (audit-log + telemetry): same as A, plus events are collected for
  telemetry shipping
- C (telemetry-only): events stored anonymously with PII stripped,
  telemetry flag active, audit-log flag inactive

Audit system refactoring (app.loggers.audit):
- Replace qualified map keys (::audit/name etc.) with plain keywords
- Rename submit! -> submit, insert! -> insert, prepare-event ->
  prepare-rpc-event
- Add submit* as a lower-level public API
- Add process-event dispatch function that handles all three modes and
  webhooks in a single tx-run!
- Add :id to event schema (auto-generated if omitted)
- Add filter-telemetry-props: anonymises event props per event type.
  Keeps UUID/boolean/number values; for login/identify events preserves
  lang, auth-backend, email-domain; for navigate events preserves route,
  file-id, team-id, page-id; instance-start trigger passes through.
- Add filter-telemetry-context: retains only safe context keys.
  Backend: version, initiator, client-version, client-user-agent.
  Frontend: browser, os, locale, screen metrics, event-origin.
- Timestamps truncated to day precision via ct/truncate for telemetry
  storage
- PII stripped: props emptied, ip-addr zeroed, session-linking and
  access-token fields removed from context

Config (app.config):
- Derive :enable-telemetry flag from telemetry-enabled config option

Email utilities (app.email):
- Add email/clean and email/get-domain helper functions for domain
  extraction from email addresses

Setup (app.setup):
- Emit instance-start trigger event at system startup
- Simplify handle-instance-id (remove read-only check)

RPC layer (app.rpc):
- wrap-audit now activates when :telemetry flag is set
- Add :request-id to RPC params context for event correlation

RPC commands (management, teams_invitations, verify_token, OIDC auth,
webhooks): migrate all audit call sites to use the new plain-key API

SREPL (app.srepl.main):
- Migrate all audit/insert! calls to audit/insert with plain keys

Telemetry task (app.tasks.telemetry):
- Restructure legacy report into make-legacy-request; distinguish
  payload type as :telemetry-legacy-report
- Add collect-and-send-audit-events: loop fetching up to 10,000 rows
  per iteration, encodes and sends each page, deletes on success,
  stops immediately on failure for retry
- Add send-event-batch: POSTs fressian+zstd batch (base64 via
  blob/encode-str) to the telemetry endpoint with instance-id per event
- Add gc-telemetry-events: enforces 100,000-row safety cap by dropping
  oldest rows first
- Add delete-sent-events: deletes successfully shipped rows by id

Blob utilities (app.util.blob):
- Add encode-str/decode-str: combine fressian+zstd encoding with URL-
  safe base64 for JSON-safe string transport

Database:
- Add migration 0145: index on audit_log (source, created_at ASC) for
  efficient telemetry batch collection queries

Frontend:
- Always initialize event system regardless of :audit-log flag
- Defer auth events (signin identify) to after profile is set
- Refactor event subsystem for telemetry support

Tests (21 test vars, 94 assertions in tasks-telemetry-test):
- Cover all code paths: disabled/enabled telemetry, no-events no-op,
  happy-path batch send and delete, failure retention, payload anonymity,
  context stripping, timestamp day precision, batch encoding round-trip,
  multi-page iteration, GC cap enforcement, partial failure handling
- blob encode-str/decode-str round-trip tests (14 test vars)
- RPC audit integration tests (5 test vars)

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add pr feedback changes

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 12:19:59 +02:00
Andrey Antukh b312e6b059 📚 Update changelog 2026-05-11 11:26:54 +02:00
Andrey Antukh 15379f37f5 🐛 Fix maximum call stack size exceeded in SSE read-stream (#9484)
The recursive `read-items` function in `app.util.sse/read-stream`
caused a synchronous stack overflow when reading buffered stream
data. Each `rx/mapcat` call chained another recursive invocation
on the same call stack without yielding to the event loop.

Replace the recursive pattern with an `rx/create`-based async pump
that uses Promise `.then()` chaining, keeping the call stack depth
constant regardless of stream size.

Also add progress reporting with names and IDs during binfile
export and import, and bump `eventsource-parser` dependency.

Closes #9470

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 11:06:15 +02:00
Andrey Antukh ec0d692856 🐛 Fix maximum call stack size exceeded in SSE read-stream (#9484)
The recursive `read-items` function in `app.util.sse/read-stream`
caused a synchronous stack overflow when reading buffered stream
data. Each `rx/mapcat` call chained another recursive invocation
on the same call stack without yielding to the event loop.

Replace the recursive pattern with an `rx/create`-based async pump
that uses Promise `.then()` chaining, keeping the call stack depth
constant regardless of stream size.

Also add progress reporting with names and IDs during binfile
export and import, and bump `eventsource-parser` dependency.

Closes #9470

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 10:59:35 +02:00
Andrey Antukh 08bd53b6a1 📎 Update changelog 2026-05-11 09:49:28 +02:00
Andrey Antukh a228b257e9 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 09:37:32 +02:00
Andrey Antukh f2c631b8b7 Merge remote-tracking branch 'origin/main-staging' into staging 2026-05-11 09:30:10 +02:00
Andrey Antukh 1a212a2769 Merge remote-tracking branch 'origin/main-staging' 2026-05-11 08:46:25 +02:00
Alonso Torres 9f05ba2fdf Add plugins and mcp event data (#9228)
*  Add plugins and mcp event data

* ♻️ Changed data-event ::ev/event to ev/event
2026-05-11 08:36:53 +02:00
Andrey Antukh 6eba2e6c42 📚 Update changelog 2026-05-10 20:23:10 +02:00
Jack Storment 9dc607902b 🐛 Only fall back to anonymous on :not-found in get-profile (#9254)
* 🐛 Only fall back to anonymous on :not-found in get-profile

::get-profile caught Throwable and silently returned the anonymous
user payload for every error - contradicting the in-code comment that
states in all other cases we need to reraise the exception. Under
transient DB conditions (pool checkout timeout, replica lag, statement
timeout, network blip) this masked real DB outages as ordinary
anonymous responses, returning HTTP 200 instead of 5xx and leaving
logged-in users on the login screen with a valid session cookie.

Narrow the catch so only :type :not-found falls through; everything
else propagates and reaches the standard error pipeline.

Closes #9235

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Only fall back to anonymous on :not-found in get-profile

::get-profile caught Throwable and silently returned the anonymous
user payload for every error - contradicting the in-code comment that
states in all other cases we need to reraise the exception. Under
transient DB conditions (pool checkout timeout, replica lag, statement
timeout, network blip) this masked real DB outages as ordinary
anonymous responses, returning HTTP 200 instead of 5xx and leaving
logged-in users on the login screen with a valid session cookie.

Narrow the catch so only :type :not-found falls through; everything
else propagates and reaches the standard error pipeline.

Closes #9253

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
2026-05-10 19:40:29 +02:00
TinyClawandiot2edge 7df53a46f2 🔥 Remove stray debug log in exporter upload-resource (#9272)
Signed-off-by: iot2edge <tylerprice830@gmail.com>
Co-authored-by: iot2edge <tylerprice830@gmail.com>
2026-05-10 19:36:55 +02:00
Dexterity e30e5906c8 ♻️ Remove unreachable try/catch in hex->hsl (#9245) 2026-05-10 19:28:12 +02:00
andrés gonzález 9c771ae6b9 🐛 Fix MCP integrations copy button to match displayed URL (#9239) 2026-05-10 19:23:03 +02:00
Andrey Antukh 49759021bf Merge remote-tracking branch 'origin/staging' into develop 2026-05-10 14:27:53 +02:00
tmimmanuelandtmimmanuel f06a2ae4e3 ♻️ Migrate inspect fill/stroke deprecated blocks to modern syntax (#9392)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
2026-05-10 14:16:43 +02:00
ef4f57c4a1 ♻️ Migrate components/link to modern component syntax (#9383)
* ♻️ Migrate components/link to modern component syntax

Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>

* 📎 Fix cljfmt indent after link* rename

Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>

---------

Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-10 14:12:30 +02:00
Andrey Antukh 79937027eb Merge remote-tracking branch 'origin/main' into staging 2026-05-10 10:50:29 +02:00
Andrey Antukh 9b336e9a3d Add nrepl-eval script and skill 2026-05-10 10:49:53 +02:00
Andrey Antukh 60c718eba1 Merge remote-tracking branch 'origin/staging' into develop 2026-05-10 09:20:27 +02:00
Andrey Antukh 55406be084 Merge remote-tracking branch 'origin/main' into staging 2026-05-10 09:20:08 +02:00
Andrey Antukh f414392f13 📎 Update changelog 2026-05-10 09:19:56 +02:00
Andrey Antukh cf3455a487 📎 Add missing entry on CHANGES.md 2026-05-10 09:18:52 +02:00
Andrey Antukh 10a23a6869 Merge remote-tracking branch 'origin/main' into staging 2026-05-10 09:16:41 +02:00
Dexterity a53237ce9f 🐛 Route Google fonts fetch warning through project logger (#9422) 2026-05-08 17:41:09 +02:00
JeffandAndrey Antukh f5b38a5025 🐛 Skip add-recent-color when colorpicker has no completed color (#9251)
Closing the fill dialog while an image-fill upload is still in flight
(or while a gradient is mid-edit) leaves the colorpicker's
current-color with only :opacity — no :image, :gradient, or :color.
update-colorpicker-color's WatchEvent then constructed
`(add-recent-color partial)`, which runs the value through
`clr/check-color` and threw "expected valid color". The user saw an
Internal Assertion Error toast and lost the in-flight upload.

The existing `ignore-color?` guard reads `:type` from the *output* of
`get-color-from-colorpicker-state` — but that helper strips :type from
its result, so the guard never actually fires. Add a schema-based gate
(same validator add-recent-color itself uses) right before `rx/of`, so
a partial selection is silently dropped instead of crashing the
workspace. Behaviour for fully-valid colors is unchanged.

Tests cover three cases: (1) a partial image-tab state with only
:opacity returns nil from watch (was: throws); (2) the same partial
shape on the color tab also returns nil — pinning down that the prior
:type guard wouldn't have caught it; (3) a fully-populated plain color
still produces a watch observable so the guard isn't over-eager.

Closes #8443

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 17:37:12 +02:00
RenzoandAndrey Antukh ea24445c2c 🐛 Toggle display-guides via physical key code so the shortcut works on non-US layouts (#9209)
* 🐛 Toggle display-guides via physical key code so the shortcut works on non-US layouts

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

* 🐛 Add tests for display-guides shortcut on non-US layout

---------

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Renzo <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 17:36:52 +02:00
BitTobyandAndrey Antukh 6aeccb1208 🎉 Add selection size badge below bounding box (#9210)
* 🎉 Add selection size badge below bounding box

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Address review comments

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Move selection size badge text styles to SCSS class

---------

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>
Signed-off-by: BitToby <218712309+bittoby@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 14:20:24 +02:00
web-dev0521 bb93928099 🐛 Fix lost-update race on team.features during concurrent file cr… (#9198)
* 🐛 Fix lost-update race on team.features during concurrent file creation

* 📚 Add CHANGES.md entry for team.features race condition fix (#9197)
2026-05-08 14:12:20 +02:00
Francis Santiago e9588f3939 🐳 Reuse shared Nginx security headers (#9473)
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-08 14:11:09 +02:00
Yamila Moreno be92e37af3 🔧 Add notification when a new devenv is published 2026-05-08 14:09:21 +02:00
María Valderrama 5a3d5f86af 🐛 Fix nitrate lookups to use nested organization
* 🐛 Fix nitrate lookups to use nested organization

* 📎 Code review
2026-05-08 13:33:31 +02:00
Pablo Alba 639a457c69 💄 Change error message on nitrate subscriptions 2026-05-08 12:27:58 +02:00
Marina López 175fb67afc 💄 Change margin for current plan 2026-05-08 11:59:09 +02:00
Pablo Alba f3c2c0bee2 Change team organization structure on state 2026-05-08 11:18:26 +02:00
Aitor Moreno 4e98dfb99f ♻️ Refactor GpuState and RenderState
* ♻️ Refactor GpuState

* ♻️ Refactor RenderState

* 🔧 Tweak some _build_env options
2026-05-08 11:10:14 +02:00
Eva Marco cccd7bc6de 🐛 Fix pixel grid color row (#9360) 2026-05-08 11:06:56 +02:00
Belén Albeza a52c4e099a 🐛 Fix round/square linecaps not being applied correctly in open paths 2026-05-08 10:31:18 +02:00
Andrey Antukh 18e289b15a ♻️ Migrate link-button component to rumext modern syntax (#9264)
Rename component from link-button to link-button* and remove the legacy
::mf/wrap-props false metadata. Update all callsites to use the modern
[:> lb/link-button* ...] syntax instead of [:& lb/link-button ...].

Part of the #9260 issue.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 09:53:12 +02:00
Andrey Antukh a50785f105 📎 Update changelog 2026-05-08 09:29:28 +02:00
Andrey Antukh 279231240d 🐛 Harden outbound HTTP requests against SSRF and restrict assets handlers (#9390)
* ⬆️ Update root deps

* 🐛 Harden outbound HTTP requests against SSRF and restrict unauthenticated asset access

- Add app.util.ssrf URL/host validator that resolves hostnames and blocks
  loopback, link-local, site-local, cloud metadata, and operator-supplied CIDRs
- Add app.media.sanitize image EOF truncator that strips trailing data after
  PNG IEND, JPEG EOI, GIF trailer, and WebP RIFF markers
- Disable HTTP client auto-redirect; add req-with-redirects! helper that
  revalidates every redirect hop against the SSRF blocklist
- Wire SSRF validation and EOF sanitization into media/download-image
- Validate webhook URLs and OIDC profile picture URLs against SSRF
- Restrict /assets/by-id to require authentication for non-public buckets
  (profile) while keeping public access for file-media-object,
  file-object-thumbnail, team-font-variant, and file-data-fragment
- Add config knobs: ssrf-protection-enabled, ssrf-allowed-hosts,
  ssrf-extra-blocked-cidrs

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 09:18:22 +02:00
Andrés Moya 61cd757355 🐛 Detect duplicated token names in the whole library (#9034)
* 🐛 Detect duplicated token names in the whole library

* 🔧 Review comments

* 🐛 Prevent and repair token themes with inexistent sets

* 🔧 Convert tokens lib migration into file migration
2026-05-08 08:26:15 +02:00
Andrey Antukh 3496435e69 📚 Update changelog 2026-05-08 00:32:21 +02:00
Andrey Antukh d103feebfa 📚 Update changelog 2026-05-07 23:57:49 +02:00
Dr. Dominik Jain 362440fead 🚑 Use base64 envelope for Uint8Array task results to avoid JSON expansion (#9431)
Resolves #9420 (critical memory usage issue in PROD deployment)

When the plugin's ExecuteCodeTaskHandler returns a Uint8Array (e.g. from penpotUtils.exportImage),
JSON.stringify previously serialized it as an object with numeric string keys,
causing ~10x payload expansion and large peak heap usage on the server side.

The plugin now wraps a top-level Uint8Array result in a tagged envelope
{ __type: "base64", data: <base64> }, and ImageContent.byteData decodes this envelope
on the server. The legacy numeric-keyed-object path is retained as a fallback for
compatibility with older plugin builds.
2026-05-07 23:51:50 +02:00
Dr. Dominik Jain c3743930c2 🚑 Use base64 envelope for Uint8Array task results to avoid JSON expansion (#9431)
Resolves #9420 (critical memory usage issue in PROD deployment)

When the plugin's ExecuteCodeTaskHandler returns a Uint8Array (e.g. from penpotUtils.exportImage),
JSON.stringify previously serialized it as an object with numeric string keys,
causing ~10x payload expansion and large peak heap usage on the server side.

The plugin now wraps a top-level Uint8Array result in a tagged envelope
{ __type: "base64", data: <base64> }, and ImageContent.byteData decodes this envelope
on the server. The legacy numeric-keyed-object path is retained as a fallback for
compatibility with older plugin builds.
2026-05-07 23:50:20 +02:00
María Valderrama 7c5fa038c1 Add Nitrate advanced permissions delete (#9416)
*  Add Nitrate advanced permissions delete

* 📎 Code review
2026-05-07 21:14:30 +02:00
Dr. Dominik Jain 6a44b19311 🐛 Fix keep-alive interval leak in PluginBridge (#9435)
The ping interval was stored in a single variable shared across all
WebSocket connections, so each new connection overwrote the previous
handle and leaked the prior interval.

Move the interval onto ClientConnection as a per-connection field,
and centralize teardown in a new removeConnection(ws) method used
by the close, error and duplicate token rejection paths.

Resolves #9430
2026-05-07 20:37:22 +02:00
Aitor Moreno 0817f13340 ♻️ Change how rendering spiral is generated 2026-05-07 17:25:50 +02:00
Alejandro Alonso fc7748fc84 🐛 Fix(render-wasm): stabilize interactive drag backbuffer crops 2026-05-07 17:12:00 +02:00
Aitor Moreno bc0f081371 ♻️ Refactor text editor state (#9379) 2026-05-07 16:16:44 +02:00
Francis Santiago d84685c0cb Merge pull request #9426 from penpot/nginx-security-headers
🐳 Nginx security headers
2026-05-07 16:06:59 +02:00
Eva Marco c5f2ffab69 🐛 Fix internal error when applying not valid value to margin input (#9311) 2026-05-07 15:24:25 +02:00
FairyPiggyDev fa06efa84d ♻️ Migrate fo-text and html-text renderers to modern component syntax (#9385)
Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Twin namespaces with parallel structure: each defines six components
that drive a recursive text rendering pass over the editor's content
tree (root -> paragraph-set -> paragraph -> node -> text). Both files
were uniformly legacy: every component carried `::mf/wrap-props
false` and read its props with `(obj/get props "key")`. None had
`::mf/register`, `unchecked-get` or `obj/merge!`, so they qualify as
clean Case-A migrations.

frontend/src/app/main/ui/shapes/text/fo_text.cljs (6 components)
----------------------------------------------------------------

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (forward-props case,
                                                 see below)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The four leaf components switch from `[props]` + per-key
`(obj/get props "key")` to standard destructuring. `text-shape`
already used destructuring under `::mf/props :obj`; that legacy
metadata is dropped because the modern `*` form handles props
automatically. Its single `::mf/forward-ref true` is kept per the
prompt's "preserve forward-ref" rule.

`render-node` is the recursive driver. It needs to forward all of
its incoming props to the matched paragraph-* / text component and
then to a child `render-node*` after overriding `:node`, `:index`
and `:key`. The migrated form uses `::mf/props :obj` together with
`{:keys [node] :as props}` to keep the JS-object props symbol
available, and `(mf/spread-props props {…})` replaces the previous
`obj/clone` + `obj/set!` chain.

`app.util.object` is no longer required by this namespace and the
`(:require ... [app.util.object :as obj] ...)` line is removed.

frontend/src/app/main/ui/shapes/text/html_text.cljs (6 components)
-----------------------------------------------------------------

Identical six-component shape as `fo_text.cljs`, plus a `code?`
flag threaded through every component to switch the rendering path
between regular shapes and code-style shapes.

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (same forward-props
                                                 treatment as above,
                                                 plus `is-code` in
                                                 the spread)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The `code?` boolean prop is renamed to `is-code` per the migration
prompt's "?-suffixed boolean -> `is-` prefix" rule. The rename is
applied at every read site (5 components) and at the `text-shape*`
internal call to `render-node*`, so the prop is consistent inside
the namespace.

`app.util.object` is no longer required by this namespace either
and the corresponding `:require` line is dropped.

External call sites (3 files, 4 sites)
--------------------------------------

- `frontend/src/app/main/ui/shapes/text.cljs` - the legacy
  text-shape wrapper (intentionally kept legacy in this PR because
  it dispatches to `svg/text-shape`, which is still being touched by
  the in-flight PR #9016) now calls `[:> fo/text-shape* props]`.
  The `props` symbol is the wrapper's incoming JS-object; modern
  destructured components accept JS-object props at the call site
  via `[:>` so this works unchanged.

- `frontend/src/app/util/code_gen/markup_html.cljs` -
  `(mf/element text/text-shape #js {:shape shape :code? true})`
  becomes
  `(mf/element text/text-shape* #js {:shape shape :is-code true})`
  (component renamed and the `code?` JS key updated to match the
  renamed prop).

- `frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs`
  - `[:& html/text-shape {…}]` -> `[:> html/text-shape* {…}]`.

Behavior preserved verbatim
---------------------------

Same render output, same forward-ref forwarding semantics, same
recursive children-by-index keying, same default `:dir "auto"` on
`render-paragraph*`. The visible-prop changes are only the `code?`
-> `is-code` rename, all driven from this namespace and its single
caller in `markup_html.cljs`.

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
2026-05-07 15:03:51 +02:00
Xaviju ddad228849 📚 Update CONTRIBUTING (#9418) 2026-05-07 14:13:02 +02:00
Madalena Melo 3136b39404 Update issue templates to include the issue type (#9345)
*  Update issue templates to include the issue type

Added the type "bug" to the "New render bug report" and the "Bug report" templates and the type "feature" to the "Feature request template".

This will allow us to use the issue Type instead of labels to identify what kind of issue is being created.

*  Update bug_report.md to request screen recordings

Update the Screenshots section to also request screen recordings

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>

---------

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-05-07 14:13:02 +02:00
RenzoandAndrey Antukh dd1ceae667 🐛 Fix plugin API fills/strokes arrays read-only (#9161)
* 🐛 Fix plugin API fills/strokes arrays read-only

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

* 🐛 Support mutable plugin fill and stroke gradients

---------

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 14:13:02 +02:00
Juanfran f79cfafae5 Show nitrate checkout error on subscription page
When the Stripe checkout fails to start, the subscription page now
  shows an inline error in the Business Nitrate card under the CTA
  instead of a toast. When the post-payment activation fails, the toast
  message is updated to point users to support@penpot.app.

  The nitrate-form modal also passed a URI object to
  build-nitrate-callback-urls while the underlying append-query-param
  relied on lambdaisland's u/parse, which only accepts strings. Switched
  to the local u/uri helper so both strings and URI records work, so
  failures opened from the modal land on the subscription page.
2026-05-07 14:13:02 +02:00
Xaviju 10a0e9e78c ♻️ Revert ESC keypress closes plugins (#9267) 2026-05-07 14:13:02 +02:00
Marina López bc13dfcf9e Refactor subscriptions page 2026-05-07 14:13:02 +02:00
wdeveloper16andwdeveloper16 6e186143d5 ♻️ Migrate viewport debug and workspace shape debug components to modern syntax (#9395)
Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
2026-05-07 14:13:02 +02:00
Dexterity a08f052da0 🐛 Remove stray println debug logs from dashboard team invitations (#9365) 2026-05-07 14:13:02 +02:00
4f1512186f ♻️ Migrate components/code-block to modern component syntax (#9384)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 14:13:02 +02:00
deb3085de5 ♻️ Migrate frame-preview to modern component syntax (#9382)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 14:13:02 +02:00
2ceddc3932 ♻️ Migrate debug icons-preview to modern component syntax (#9381)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 14:13:02 +02:00
Alejandro Alonso 173ef0dbb0 🐛 Avoid opaque fill check in drag crop cache hot path 2026-05-07 14:13:02 +02:00
Elena Torro d457eb5e5c Translation-aware modifier propagation and lazy parent walks 2026-05-07 14:13:02 +02:00
Elena Torro 5c4d16fc2b Coalesce live drag preview state and reduce sidebar churn 2026-05-07 14:13:02 +02:00
BitCompassandAndrey Antukh 55d085117b ♻️ Rename measurement and svg-defs components to defc* form (#9306)
Adopts the rumext * suffix convention for the following components,
invoking them with the [:> JS-style syntax to match the rest of the
codebase (see e.g. rea*, single-selection* in viewport/selection):

- measurements: size-display, distance-display-pill, selection-rect,
  distance-display, selection-guides, measurement
- shapes/svg-defs: svg-node, svg-defs (also drop the now-redundant
  {::mf/wrap-props false} annotations)

Updates all call sites in inspect/selection_feedback, shapes/shape,
workspace/viewport, and workspace/viewport_wasm. Pure rename — no
behavioral change.

Signed-off-by: bitcompass <devwiz.sh@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 14:13:02 +02:00
Dexterity 7e6e7baa71 🔥 Remove stray prn debug log in stroke-row* render (#9318) 2026-05-07 14:13:02 +02:00
Dexterity 2fc4f35cde 💄 Fix typos in comments and docstrings (#9362) 2026-05-07 14:13:02 +02:00
Dexterity 5fd758597e 🐛 Fix MCP "active in another tab" notification not clearing (#9321) 2026-05-07 14:13:02 +02:00
Dexterity cc29334684 🐛 Fix swapped analytics event names on MCP tab-switch dialog (#9322) 2026-05-07 14:13:02 +02:00
Milos Milic e61d512889 🐛 Fix missing labels.open i18n key surfacing raw key as aria-label (#9320) 2026-05-07 14:13:02 +02:00
Xaviju defeeab054 📚 Update CONTRIBUTING (#9418) 2026-05-07 14:01:43 +02:00
Aitor Moreno 9fccee8689 ♻️ Refactor how viewport interest area works 2026-05-07 13:46:51 +02:00
Francis Santiago 4f172afce5 🐳 Reuse Nginx security headers config
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-07 13:42:02 +02:00
Madalena Melo df9cef1bb8 Update issue templates to include the issue type (#9345)
*  Update issue templates to include the issue type

Added the type "bug" to the "New render bug report" and the "Bug report" templates and the type "feature" to the "Feature request template".

This will allow us to use the issue Type instead of labels to identify what kind of issue is being created.

*  Update bug_report.md to request screen recordings

Update the Screenshots section to also request screen recordings

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>

---------

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-05-07 13:29:25 +02:00
RenzoandAndrey Antukh 691679d90b 🐛 Fix plugin API fills/strokes arrays read-only (#9161)
* 🐛 Fix plugin API fills/strokes arrays read-only

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

* 🐛 Support mutable plugin fill and stroke gradients

---------

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 13:10:48 +02:00
Andrey Antukh 798ee46b4a 🐛 Bind MCP ReplServer to localhost to prevent unauthenticated RCE
The ReplServer Express app was calling `app.listen(port)` with no host
argument, causing Node/Express to default to binding on all interfaces
(0.0.0.0). Combined with the unauthenticated /execute endpoint, any
network peer could POST arbitrary JS and get it run inside the MCP
process.

Fix: add a `host` parameter (default "localhost") to the ReplServer
constructor and pass it to `app.listen`. The call site in
PenpotMcpServer now forwards `this.host` (sourced from
PENPOT_MCP_SERVER_HOST env var, default "localhost"), so environment-
variable overrides continue to work.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 12:59:31 +02:00
Juanfran bd91036b95 Show nitrate checkout error on subscription page
When the Stripe checkout fails to start, the subscription page now
  shows an inline error in the Business Nitrate card under the CTA
  instead of a toast. When the post-payment activation fails, the toast
  message is updated to point users to support@penpot.app.

  The nitrate-form modal also passed a URI object to
  build-nitrate-callback-urls while the underlying append-query-param
  relied on lambdaisland's u/parse, which only accepts strings. Switched
  to the local u/uri helper so both strings and URI records work, so
  failures opened from the modal land on the subscription page.
2026-05-07 12:48:43 +02:00
Xaviju 7b1f0eaaf0 ♻️ Revert ESC keypress closes plugins (#9267) 2026-05-07 12:34:37 +02:00
Marina López b2e3dbe558 Refactor subscriptions page 2026-05-07 12:06:46 +02:00
Alejandro Alonso 03487f90e5 🐛 Fix double-clicking a text element selected via Ctrl+click in nested layouts jumps to parent instead of entering edit mode 2026-05-07 09:57:15 +02:00
wdeveloper16andwdeveloper16 70e1a16bb8 ♻️ Migrate viewport debug and workspace shape debug components to modern syntax (#9395)
Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
2026-05-07 08:44:09 +02:00
Dexterity 61b791368a 🐛 Remove stray println debug logs from dashboard team invitations (#9365) 2026-05-07 01:43:15 +02:00
f173fafb62 ♻️ Migrate components/code-block to modern component syntax (#9384)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 01:41:10 +02:00
eca487afc5 ♻️ Migrate frame-preview to modern component syntax (#9382)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 01:40:38 +02:00
bffec015d7 ♻️ Migrate debug icons-preview to modern component syntax (#9381)
Signed-off-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: tmimmanuel <155203395+tmimmanuel@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 01:40:20 +02:00
Andrey Antukh 697a825d76 📚 Update opencode planner agent 2026-05-07 01:04:38 +02:00
Francis Santiago 50df7cb5c4 🐳 Harden Nginx security headers
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-06 20:06:35 +02:00
Alejandro Alonso 0a0db15548 Merge remote-tracking branch 'origin/staging' into develop 2026-05-06 19:28:09 +02:00
Alejandro Alonso db1e2a9cfc Merge pull request #9391 from penpot/superalex-fix-drag-crop-cache-perf
🐛 Avoid opaque fill check in drag crop cache hot path
2026-05-06 19:27:58 +02:00
Alejandro Alonso 33396df2e2 🐛 Avoid opaque fill check in drag crop cache hot path 2026-05-06 19:15:00 +02:00
BitCompassandAndrey Antukh 3433b41aa8 ♻️ Rename measurement and svg-defs components to defc* form (#9306)
Adopts the rumext * suffix convention for the following components,
invoking them with the [:> JS-style syntax to match the rest of the
codebase (see e.g. rea*, single-selection* in viewport/selection):

- measurements: size-display, distance-display-pill, selection-rect,
  distance-display, selection-guides, measurement
- shapes/svg-defs: svg-node, svg-defs (also drop the now-redundant
  {::mf/wrap-props false} annotations)

Updates all call sites in inspect/selection_feedback, shapes/shape,
workspace/viewport, and workspace/viewport_wasm. Pure rename — no
behavioral change.

Signed-off-by: bitcompass <devwiz.sh@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 19:11:45 +02:00
Dexterity 3885c9ee74 🔥 Remove stray prn debug log in stroke-row* render (#9318) 2026-05-06 17:43:58 +02:00
Dexterity 3226660812 💄 Fix typos in comments and docstrings (#9362) 2026-05-06 17:43:09 +02:00
Dexterity db77780227 🐛 Fix MCP "active in another tab" notification not clearing (#9321) 2026-05-06 17:42:34 +02:00
Dexterity a5b7bd90c7 🐛 Fix MCP "active in another tab" notification not clearing (#9321) 2026-05-06 17:42:06 +02:00
Dexterity ae7c7a7972 🐛 Fix swapped analytics event names on MCP tab-switch dialog (#9322) 2026-05-06 17:41:16 +02:00
Dexterity f4317d00e5 🐛 Fix swapped analytics event names on MCP tab-switch dialog (#9322) 2026-05-06 17:40:39 +02:00
Milos Milic aa8f2ab80d 🐛 Fix missing labels.open i18n key surfacing raw key as aria-label (#9320) 2026-05-06 17:39:26 +02:00
Alejandro Alonso c36887e0bf Merge pull request #9357 from penpot/elenatorro-14100-cljs-optimizations
 Event optimizations on drag
2026-05-06 16:23:09 +02:00
Elena Torro 97511ba6e5 Translation-aware modifier propagation and lazy parent walks 2026-05-06 15:10:26 +02:00
Elena Torro 9230091492 Coalesce live drag preview state and reduce sidebar churn 2026-05-06 15:10:26 +02:00
Andrey Antukh e07ad9cb53 Merge remote-tracking branch 'origin/staging' into develop 2026-05-06 15:07:45 +02:00
Andrey Antukh 14a0660352 Merge remote-tracking branch 'origin/main' into staging 2026-05-06 15:06:59 +02:00
FairyPiggyDevandAndrey Antukh 4892799cf6 ♻️ Migrate fontfaces and viewer thumbnails components to modern syntax (#9293)
Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Two unrelated namespaces, both clean Case-A migrations grouped in a
single PR for review efficiency.

frontend/src/app/main/ui/shapes/text/fontfaces.cljs
---------------------------------------------------

Three components, all previously using `::mf/wrap-props false` with a
custom memoizer (`#(mf/memo' % (mf/check-props ["fonts"]))`) and
reading `fonts` via `(obj/get props "fonts")`. The custom memoizer
existed because the legacy components received raw JS-object props,
where the default `mf/memo` Clojure-equality comparison would always
fail.

- `fontfaces-style-html`   → `fontfaces-style-html*`
- `fontfaces-style-render` → `fontfaces-style-render*`
- `fontfaces-style`        → `fontfaces-style*`

Migration:

- Standard destructuring `[{:keys [fonts]}]` replaces the
  `[props]` + `(obj/get props "fonts")` pattern.
- `::mf/wrap-props false` removed.
- Custom memoizer collapses to `::mf/wrap [mf/memo]`. With modern
  destructuring the props are Clojure data, so default `=`-based memo
  is structurally correct (and a stronger guarantee than the previous
  shallow JS-prop check).
- `app.util.object` require dropped from the namespace — it was only
  used for the `obj/get props "fonts"` reads that are now gone.
- Internal call site of `fontfaces-style-render*` (within
  `fontfaces-style*`) keeps its `[:>` form, just with the new name.

External call sites updated:

- `frontend/src/app/main/render.cljs` — three sites
  (`[:& ff/fontfaces-style {:fonts fonts}]` × 3) →
  `[:> ff/fontfaces-style* {:fonts fonts}]`.
- `frontend/src/app/main/ui/workspace/shapes.cljs` — one site,
  call signature unchanged. (Note: this caller passes `:shapes`
  rather than `:fonts`; the legacy component already ignored
  `:shapes` because it only read `(obj/get props "fonts")`, so the
  modern destructuring `{:keys [fonts]}` preserves the same
  behavior. Pre-existing bug, intentionally left out of scope.)

frontend/src/app/main/ui/viewer/thumbnails.cljs
-----------------------------------------------

Four components, all using standard destructuring already (no
`::mf/wrap-props false`, no `unchecked-get`). Migration is the
straight `*` rename plus `?`-prop renames per the prompt's mapping:

- `thumbnails-content`  → `thumbnails-content*`   (`expanded?` →
  `is-expanded`)
- `thumbnails-summary`  → `thumbnails-summary*`   (no `?`-props)
- `thumbnail-item`      → `thumbnail-item*`       (`selected?` →
  `is-selected`; `::mf/wrap [mf/memo #(mf/deferred …)]` preserved)
- `thumbnails-panel`    → `thumbnails-panel*`     (`show?` →
  `show` — no `is-` prefix needed, reads naturally as a verb)

Internal callsites of all three sub-components in `thumbnails-panel*`
updated to `[:> …*` with renamed kwargs (`:expanded?` →
`:is-expanded`, `:selected?` → `:is-selected`).

The `expanded?` symbol still appears in `thumbnails-panel*`'s body —
it's a local `let`-binding deref'd from the `expanded-state` atom,
not a component param, so the `?` suffix is preserved per the
prompt's "local bindings stay" rule.

External call sites updated:

- `frontend/src/app/main/ui/viewer.cljs` — one site, plus the
  `:refer [thumbnails-panel]` → `:refer [thumbnails-panel*]` require
  update.

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:55:55 +02:00
Andrey Antukh 54928e9ffb Merge branch 'backport-2.14' 2026-05-06 14:38:17 +02:00
Andrey Antukh df01f76056 🐛 Fix incorrect invitation token handling on register process (#9380)
* 🐛 Fix incorrect invitation token handling on register process

- Reject prepare-register-profile when an active profile already
  exists for the requested email.
- Stop embedding an existing profile's :profile-id into the
  prepared-register JWE. Profile resolution in register-profile is
  now done exclusively by email lookup, never by a JWE claim.
- Add created? guard to the invitation-success branch in
  register-profile, so existing profiles (active or not) cannot
  reach session creation via anonymous registration.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Restructure invitation handling inside register-profile

Move the invitation-success branch into the created? sub-cond so it
sits alongside the other post-creation branches, making the control
flow consistent.

- Active new profile + matching invitation: mint session and return
  :invitation-token (frontend redirects to :auth-verify-token).
- Not-yet-active new profile + matching invitation: embed the
  invitation token inside the verify-email JWE and send the
  verification email. When the user clicks the link, they get
  logged in and the frontend completes the team-invitation flow.
- Extend send-email-verification! with an optional invitation-token
  parameter propagated into the verify-email JWE claims.
- Update the frontend verify-email handler to navigate to
  :auth-verify-token when the response carries :invitation-token.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Handle email-already-exists error on registration form

Add a specific handler for the [:validation :email-already-exists] error
code in the registration form's on-error callback. The backend raises
this error when an active profile already exists for the requested email,
but the frontend was falling through to the generic error message.

Now it shows the existing "Email already used" i18n message instead of
the generic "Something wrong has happened" toast.

* 🐛 Reset submitted state on registration form error

The on-error handler in the registration form was not resetting the
submitted? state, causing the submit button to remain disabled after
any error. The completion callback in rx/subs! only fires on success,
not on error.

Add (reset! submitted? false) at the beginning of the on-error handler
so the form becomes submittable again after any error, allowing the user
to fix their input and retry.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:32:31 +02:00
Aitor Moreno 4cd44efa93 Merge pull request #9371 from penpot/superalex-fix-drag-and-drop-cache-eligibility-rules
🐛 Fix drag and drop cache eligibility rules
2026-05-06 14:28:22 +02:00
Andrey Antukh 1e1ca82ba5 📚 Add missing changelog entry and document changelog locations
Add changelog entry for the fix-incorrect-invitation-token-handling
change (PR #9380) under `## 2.15.0 (Unreleased)` > `:bug: Bugs fixed`.
Add a `## Changelogs` section to AGENTS.md documenting both changelog
locations (main project: `CHANGES.md`, plugins: `plugins/CHANGELOG.md`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 12:24:55 +00:00
Andrey Antukh 9e681260cc 🐛 Fix incorrect invitation token handling on register process (#9380)
* 🐛 Fix incorrect invitation token handling on register process

- Reject prepare-register-profile when an active profile already
  exists for the requested email.
- Stop embedding an existing profile's :profile-id into the
  prepared-register JWE. Profile resolution in register-profile is
  now done exclusively by email lookup, never by a JWE claim.
- Add created? guard to the invitation-success branch in
  register-profile, so existing profiles (active or not) cannot
  reach session creation via anonymous registration.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Restructure invitation handling inside register-profile

Move the invitation-success branch into the created? sub-cond so it
sits alongside the other post-creation branches, making the control
flow consistent.

- Active new profile + matching invitation: mint session and return
  :invitation-token (frontend redirects to :auth-verify-token).
- Not-yet-active new profile + matching invitation: embed the
  invitation token inside the verify-email JWE and send the
  verification email. When the user clicks the link, they get
  logged in and the frontend completes the team-invitation flow.
- Extend send-email-verification! with an optional invitation-token
  parameter propagated into the verify-email JWE claims.
- Update the frontend verify-email handler to navigate to
  :auth-verify-token when the response carries :invitation-token.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Handle email-already-exists error on registration form

Add a specific handler for the [:validation :email-already-exists] error
code in the registration form's on-error callback. The backend raises
this error when an active profile already exists for the requested email,
but the frontend was falling through to the generic error message.

Now it shows the existing "Email already used" i18n message instead of
the generic "Something wrong has happened" toast.

* 🐛 Reset submitted state on registration form error

The on-error handler in the registration form was not resetting the
submitted? state, causing the submit button to remain disabled after
any error. The completion callback in rx/subs! only fires on success,
not on error.

Add (reset! submitted? false) at the beginning of the on-error handler
so the form becomes submittable again after any error, allowing the user
to fix their input and retry.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:20:55 +02:00
Pablo Alba e8ac5f26db 💄 Fix nitrate change org combo style 2026-05-06 13:37:37 +02:00
Alejandro Alonso 708c4065b3 🐛 Fix drag and drop cache eligibility rules 2026-05-06 13:20:57 +02:00
Alejandro Alonso 9dd7835815 Merge remote-tracking branch 'origin/staging' into develop 2026-05-06 12:43:13 +02:00
María Valderrama 7efeed1348 🐛 Fix nitrate activation modal not opening 2026-05-06 12:41:19 +02:00
XavijuandEva Marco 0ea3ea332f 🎉 Display autocomplete combobox on token creation (#9109)
Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-05-06 12:40:23 +02:00
Alonso Torres 2fbff4f88e Buffer update shapes changes on token application 2026-05-06 12:16:35 +02:00
Alejandro Alonso 528d006b8d Events enhancements
*  Compute moved-subtree data once in find-valid-parent-and-frame-ids
*  Smooth WASM drag preview stream
*  Limit WASM outline modifier application
2026-05-06 12:16:09 +02:00
María Valderrama e65ce8bdeb 🐛 Fix date issue in nitrate activation success modal 2026-05-06 11:58:00 +02:00
Dominik JainandClaude ed935e533f Expose variants retrieval via isVariant() type guard on LibraryComponent
Change isVariant() return type from boolean to 'this is LibraryVariantComponent',
enabling TypeScript users to directly access variants, variantProps, and
variantError after a type-narrowing check. Update MCP instructions with
improved variant navigation guidance.

Closes #9185

Co-authored-by: Claude (Anthropic) <noreply@anthropic.com>
2026-05-06 11:28:15 +02:00
Andrey Antukh 34cc0e9d56 🔥 Materialize the canary feature flag across the codebase
Remove the :canary flag from the flags definition and make all
features gated behind it always available:

- Enable "download font" option in dashboard fonts context menu
- Enable Tab/Shift+Tab keyboard navigation for renaming shapes
  in layer items
- Enable "duplicate color" option in asset panel when applicable
- Enable "duplicate typography" option in asset panel when applicable
- Enable "copy as image" context menu option for frame shapes

Also remove unused [app.config :as cf] requires from files that
no longer reference it after the materialization.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 11:19:04 +02:00
Marina López 6ad83d24c9 Add nitrate manual cancel subscription 2026-05-06 11:04:35 +02:00
Eva Marco 5f40673fde 🐛 Remove drag to change when there is a token applied on numeric-inputs (#9314) 2026-05-06 10:31:39 +02:00
María Valderrama 4ddabaebff Add Nitrate's advanced permissions
*  Add Nitrate's advanced permissions

* 📎 Code review
2026-05-06 10:13:17 +02:00
Andrey Antukh 1744d17385 Merge remote-tracking branch 'origin/develop' into staging 2026-05-06 09:34:46 +02:00
Hosted Weblate 94f8370d98 🌐 Merge branch 'origin/develop' into Weblate. 2026-05-06 08:58:11 +02:00
Eva Marco ce24fed32b 🐛 Fix incorrect text-edition warning when applying tokens (#9355) 2026-05-06 08:47:30 +02:00
Alejandro Alonso dc5f02a11c 📎 Fix linting issues 2026-05-05 18:50:25 +02:00
Alejandro Alonso 67bb109331 📎 Fix linting issues 2026-05-05 18:32:25 +02:00
Alejandro Alonso 00c27287bd Merge remote-tracking branch 'origin/staging' into develop 2026-05-05 18:27:09 +02:00
Alejandro Alonso b34054940f Merge pull request #9361 from penpot/elenatorro-14100-wasm-fast-paths
 Improve drag rendering on text and paths
2026-05-05 18:21:36 +02:00
Aitor Moreno 61f5df8461 Merge pull request #9354 from penpot/superalex-fix-allow-selection-while-bounding-box-cloaking-is-active
🐛 Fix allow selection while bounding-box cloaking is active
2026-05-05 18:20:45 +02:00
Elena Torro e950ec56eb Reduce per-render text layout work 2026-05-05 17:47:50 +02:00
Pablo Alba 2fbab08bde 🐛 Fix nitrate penpot-version schema 2026-05-05 17:47:47 +02:00
Elena Torro 4a0cd0b7ce Translation-only fast paths for Shape and Path transforms 2026-05-05 17:35:42 +02:00
Alejandro Alonso 2e8d188d87 🐛 Fix allow selection while bounding-box cloaking is active 2026-05-05 16:55:28 +02:00
Alejandro Alonso ce1045c265 Merge pull request #9335 from penpot/alotor-fix-flex-auto-height
🐛 Fix problem with flex layout and text auto-height
2026-05-05 15:50:22 +02:00
Marina López 41996ed9a5 Add nitrate subscription text 2026-05-05 13:52:01 +02:00
Pablo Alba 3431aee177 🐛 Fix move org dialog must be select 2026-05-05 12:34:40 +02:00
alonso.torres 843b2aebd4 🐛 Fix problem with flex layout and text auto-height 2026-05-05 10:40:25 +02:00
Alejandro Alonso 7d923f8e1d Merge remote-tracking branch 'origin/staging' into develop 2026-05-05 06:52:38 +02:00
Alejandro Alonso c794e0ed73 🎉 Events enhancements (#9310)
* 🎉 Coalesce viewport pointermove into one PointerEvent

* 🎉 Skip worker hover selection query during transform

* 🎉 Coalesce snap X/Y into one worker query-snap-xy
2026-05-04 17:57:00 +02:00
Dominik Jain 07ad152ae5 🐛 Fix .component() returning outermost component for nested instances
The shape API method .component() used locate-component which walks
to the outermost instance root via get-instance-root. For nested
component instances (e.g. a button inside a card), this incorrectly
returned the outer component (the card) instead of the nearest one
(the button).

Added locate-head-component in utils.cljs which uses get-head-shape
to find the nearest component head, and updated the :component
property in shape.cljs to use it.

Fixes #9183
2026-05-04 16:13:25 +02:00
Clayton 4ce56e96fe 🐛 Fix MCP media uploads and SVG data URI image parsing (#9201)
* 🐛 Fix MCP media uploads and SVG data URI image parsing

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix lint

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix test

Signed-off-by: Clayton <claytonlin1110@gmail.com>

---------

Signed-off-by: Clayton <claytonlin1110@gmail.com>
2026-05-04 13:33:58 +02:00
Eva MarcoandXavier Julian a2bcbe81dd 🎉 Add token numeric inputs for inputs on right sidebar (#9143)
Co-authored-by: Xavier Julian <xavier.julian@kaleidos.net>
2026-05-04 13:02:19 +02:00
Alejandro Alonso 164f0cba7a Merge remote-tracking branch 'origin/staging' into develop 2026-05-04 11:56:07 +02:00
María Valderrama 152967bea6 🐛 Fix sidebar overflow 2026-05-04 11:02:54 +02:00
Alonso Torres e948020886 🐛 Fix problem with rounding in flex elements 2026-05-04 10:35:16 +02:00
andrés gonzález 66337f2ab9 📚 Add WebGL Troubleshooting Guide 2026-05-04 09:34:19 +02:00
María Valderrama f24ad6bee4 Show current plan in Nitrate
*  Show current plan in Nitrate

* 📎 Code Review
2026-05-04 09:29:14 +02:00
alonso.torres f6bd991968 🐛 Improved e2e tests stability 2026-05-04 09:04:30 +02:00
Elena Torró 7c0465de6b Merge pull request #9268 from penpot/superalex-drag-backbuffer-crop-cache-3
🎉 Cache selection crops from Backbuffer during drag
2026-04-30 16:46:39 +02:00
Jack StormentandAndrey Antukh 8f03b5ed9c 🔥 Remove stray debug log in frame-preview load-ref callback (#9258)
`(.log js/console "load-ref" iframe-dom)` was left in the iframe
ref callback of `frame-preview`. Mirrors the defect PR #9243
removed from `color-row*` — fires on every ref invocation and
pollutes the browser console.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 16:21:40 +02:00
JeffandAndrey Antukh d09985edee 🐛 Preserve Inkscape labels when pasting SVGs (#9252)
Steps to reproduce: paste an SVG authored in Inkscape (or any editor
that follows the inkscape:label convention) into a penpot file. The
group/element names visible in the source editor are dropped — penpot
shows generic auto-ids like 'g1234' or 'path5678' instead.

Root cause: parse-svg-element in common/src/app/common/files/shapes_
builder.cljc derived the shape name from (or (:id attrs) (tag->name
tag)). Inkscape stores user-given element labels in the inkscape:label
and sodipodi:label namespaced attributes while id holds an auto-
generated technical id, so the operator's chosen name was always
overridden by the technical id when present.

tubax/xml->clj (the SVG parser the import pipeline already uses for
upload, paste, and library import) keeps namespaced attributes as
:prefix:name keywords — the same shape this file already reads
:xlink:href from on line 134, and that app.common.svg uses for the
xlink: namespace at lines 300-307.

Fix: extract the name-resolution logic into a public resolve-element-
name helper that prefers :inkscape:label, then :sodipodi:label, then
:id, then (tag->name tag). Existing SVGs that don't carry either label
namespace fall through the same chain as before, so the behaviour for
non-Inkscape-authored SVGs is unchanged.

This restores the behaviour dfelinto's penpot-icon-generator-plugin
relies on (linked from the issue body) — that plugin reads element
names from the imported SVG to map Blender icons to penpot components.

Tests: 6 deftest blocks in common/test/common_tests/files/shapes_
builder_test.cljc covering the priority order (inkscape > sodipodi >
id > tag), each fallback in isolation, and the empty-attrs case.
Registered in common-tests.runner.

Closes #7869

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 16:17:34 +02:00
Andrey Antukh 13414e7bed 📚 Update changelog 2026-04-30 16:15:04 +02:00
Alejandro Alonso 17e0b545d2 🎉 Cache selection crops from Backbuffer during drag 2026-04-30 16:13:57 +02:00
Alejandro Alonso ddb6eca5ea Merge pull request #9269 from penpot/superalex-optimize-wasm-release-profile
🎉 Optimize wasm release profile (thin LTO, size optimizations)
2026-04-30 16:13:46 +02:00
Andrey Antukh b42e81e1a4 📎 Update changelog (candidate for freeze) 2026-04-30 15:57:41 +02:00
FairyPiggyDevandAndrey Antukh 9c2a80bfa1 🐛 Fix crash pasting component with variants from shared library (#9136)
Copying a component with variants from a shared library file ("Lib")
and pasting it into a file that uses that library ("Using Lib") would
crash the destination file with the referential-integrity validator
error:

    {:code :component-main-external
     :hint "Main instance should refer to a component in the same file"}

Root cause
----------

Paste goes through `generate-duplicate-shape-change` in
`common/src/app/common/logic/libraries.cljc`. When the shape is a
main instance of a known component and the copy set includes its
variant container, dispatch lands in `duplicate-variant`, then
`generate-duplicate-component`, and finally `duplicate-component`,
which clones the main-instance shape tree. Its `update-new-shape`
helper already re-links the new outer main's `:component-id` to the
freshly created local component (`new-component-id`), but it never
touches `:component-file`. The cloned shape therefore inherits
`:component-file` from the source library while the new component is
registered in the destination's local library
(`:apply-changes-local-library? true`), leaving the main-instance
dangling.

Fix
---

Extend `update-new-shape` with a second clause, sibling to the
existing `:component-id` rewrite: when a destination file id is
provided and differs from the new main's current `:component-file`,
re-root the shape. The same `(= (:component-id new-shape) (:id
component))` guard already used for the id rewrite ensures only the
outer main-instance is touched; nested shapes are unaffected.

The destination file id is threaded from the paste entry point
through the two orchestration functions that already knew the
source/destination distinction:

- `generate-duplicate-shape-change` — supplies the destination
  `file-id` it already has in scope when dispatching to
  `generate-duplicate-component-change`.
- `generate-duplicate-component-change` — accepts `:new-component-file`
  as a kwarg; renames its internal `file-id` binding to
  `source-file-id` for clarity (it was always the component's
  originating library file); forwards `new-component-file` to
  `duplicate-variant`.
- `duplicate-variant` — takes and forwards the `new-component-file`
  positional arg.
- `generate-duplicate-component` — accepts `:new-component-file` kwarg
  and passes it to `duplicate-component`.
- `duplicate-component` — applies the rewrite inside
  `update-new-shape`. The `new-component-file` parameter is placed
  right after `new-component-id` since component-id and component-file
  are typically managed together.

Same-file duplication is not affected: without `:new-component-file`
the new clause is skipped, and when source and destination match the
`(not= new-component-file (:component-file new-shape))` guard fails.

Tests
-----

Added in `common/test/common_tests/logic/comp_creation_test.cljc`:

- `test-duplicate-component-rewrites-component-file-to-destination`
  asserts that passing `:new-component-file` to
  `generate-duplicate-component` produces a main-instance with
  `:component-file` equal to the destination id.
- `test-duplicate-component-keeps-component-file-without-dest`
  baseline: without `:new-component-file`, `:component-file` is left
  untouched, matching pre-existing same-file behavior.

Github #8144

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 15:54:24 +02:00
FairyPiggyDev 76c1b9afab ♻️ Migrate navigation-bullets to modern component syntax (#9265)
Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Component
---------

`app.main.ui.releases.common/navigation-bullets` is a small (7-line)
self-contained presentational component used by every release-notes
modal to render the slide-progress dots. It already used standard
keyword destructuring (`[{:keys [slide navigate total]}]`), had no
`?`-suffixed props, no `unchecked-get`, no `obj/merge!`, no
`::mf/wrap-props false`, and (importantly) no `::mf/register`, so it
satisfies the migration pre-flight checks unchanged.

Changes
-------

- `releases/common.cljs` — definition renamed to `navigation-bullets*`.
  Body, props and metadata are otherwise unchanged.

- `releases/v1_4.cljs` … `v2_15.cljs` (29 files) — every existing call
  site `[:& c/navigation-bullets {…}]` becomes
  `[:> c/navigation-bullets* {…}]`. The `:slide`, `:navigate`, `:total`
  props are passed exactly as before. The `:as c` alias of the require
  is unchanged, so no require edits are needed.

No props were renamed (none ended in `?`); no helpers had to be
swapped for `mf/spread-props` / `mf/props` (callers pass plain literal
maps); no metadata had to be removed (none of the legacy options were
in use).

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
2026-04-30 15:50:38 +02:00
4902037c7d Add HEX, HSB, and HSL support in the third color tab (#9134)
*  Add HEX, HSB, and HSL support in the third color tab

Relabel the existing HSVA tab to HSBA (the math was already HSB) and add
an inline HSB ↔ HSL model toggle inside the tab, matching Figma's color
panel. Sliders, gradients, and labels update dynamically per mode; HSL
values roundtrip through RGB/HSV so no color-storage changes are needed.
Model choice persists across sessions.

* 💄 Fix lint errors

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 🐛 Fix Plugin API token application for JS array of strings (#9166)

* 🐛 Fix Plugin API token application for JS array of strings

Plugin code calling `shape.applyToken(token, ["fill"])` or
`token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS
array of strings. The plugin proxies expected a Clojure set of
keywords, and two coupled defects made the calls silently no-op (or,
with `throwValidationErrors` enabled, throw "check error"):

1. `token-attr-plugin->token-attr` only consulted its alias map when
   the input was already a keyword. String inputs like "fill" fell
   through to the identity branch, so the downstream
   `cto/token-attr?` predicate (which checks against a set of
   keywords) returned false for every string. Coerce strings to
   keywords first.

2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas
   used plain `[:set ...]`, which has no `:decode/json` transformer
   for JS array → Clojure set coercion. Switch to the registered
   `[::sm/set ...]` (in `app.common.schema`) which provides the
   array → set decoder. After the switch, the standard JSON pipeline
   converts `["fill"]` to `#{"fill"}`, then the inner
   `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a
   keyword and validates it.

Also extends the docstring on `token-attr-plugin->token-attr` to make
the string-friendly contract explicit, and registers a new
`tokens-test` ns under `frontend/test/frontend_tests/plugins/` with
six `deftest` blocks covering:

- known keywords passing through unchanged
- keyword aliases (`:r1` → `:border-radius-top-left`, etc.)
- string inputs coerced to keywords (regression for #9162)
- `token-attr?` accepting both keyword and string inputs
- `token-attr?` rejecting unknown attrs and nil

Closes #9162

* 🐛 Fix wrong direction in plugin-name alias tests

The added tests in tokens_test.cljs and the new docstring in tokens.cljs
described the alias resolution in the wrong direction. The map is
{:r1 :border-radius-top-left, …} then map-invert'd, so
token-attr-plugin->token-attr maps verbose plugin-side names
(:border-radius-top-left) to canonical internal short names (:r1),
not the other way around. Inputs already in canonical form (:r1, :fill,
"fill", …) pass through unchanged. Flipped the alias-resolution test
expectations and the keyword/string-input cases, refreshed the docstring
and the regression-coverage comment to match.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>

* 💄 Fix sucess typo in subscription dialog i18n keys (#9204)

Rename subscription.settings.sucess.dialog.{title,footer} to
subscription.settings.success.dialog.{title,footer} in en.po and
update the three callsites in subscription.cljs.

Closes #9203

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Fix HSVA → HSBA test rename and Prettier formatting

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 🐛 Fix CI failures and address review feedback for HSB color tab

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 💄 Resolve Conflicts

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

---------

Signed-off-by: juan-flores077 <toptalent399@gmail.com>
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: boskodev790 <boskomaljkovic790@outlook.com>
Co-authored-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
2026-04-30 15:41:04 +02:00
Andrés Moya 9f94566005 💄 Rename i18n keys for tokens errors (#9207) 2026-04-30 15:30:20 +02:00
Statxc 547750e8bf 🐛 Preserve OpenType variant name for custom fonts (#9193) 2026-04-30 15:29:04 +02:00
Alejandro Alonso 97688cb790 🎉 Optimize wasm release profile (thin LTO, size optimizations) 2026-04-30 14:17:25 +02:00
Elena Torro 27d854ed5b Skip component-sync on pure-translation drag commits 2026-04-30 13:45:45 +02:00
alonso.torres c14dbba7fd 🐛 Fix z-index for profile menu 2026-04-30 12:59:27 +02:00
Andrey Antukh 22a325cc72 📎 Fix linter issue 2026-04-30 11:42:21 +02:00
Xaviju aa87ae194c 🔥 Remove unused var (#9262) 2026-04-30 11:29:15 +02:00
Alonso Torres c9b81284d2 🐛 Fix z-index for profile menu (#9257) 2026-04-30 11:28:27 +02:00
Alonso Torres de9170d96b 🐛 Fix z-index for profile menu (#9257) 2026-04-30 11:27:50 +02:00
Andrey Antukh 1de8a074ef 🐛 Fix incorrect gh client install on devenv 2026-04-30 11:23:44 +02:00
Aitor Moreno acb3997ed7 🐛 Fix text editor v2 min size 2026-04-30 09:21:53 +02:00
FairyPiggyDevandAndrey Antukh ed021711b6 ♻️ Extract make-delete-asset-group-fn helper for assets panel (#9211)
Reviewer follow-up on PR #9151. The "Delete group" handler was
duplicated across the three assets-panel sections (colors,
typographies, components), each carrying the same skeleton — filter
by group path, build an undo-id, run the deletes inside one
transaction, and show the same confirm modal — with only the path
predicate and the per-asset delete event differing.

Add `app.main.ui.workspace.sidebar.assets.common/make-delete-asset-group-fn`
that takes the differing parts as options:

- `:assets`             collection to filter.
- `:on-clear-selection` invoked before the deletes.
- `:delete-events`      `(fn [matching-assets] => seq-of-events)`.
- `:path-filter`        predicate (defaults to `str/starts-with?`),
                        overridden to `cpn/inside-path?` for
                        components so nested group paths match the
                        same way the existing ungroup/combine helpers
                        do.

The factory returns `(fn [path] …)` so each call site stays a
straight `mf/use-fn`. The variant-container dedup in components
(one `delete-shapes` per container, not one per sibling variant)
moves into that section's `:delete-events` fn and is unchanged in
behavior.

Cleanup
-------

The `:as i18n` alias is no longer needed in any of the three section
files (its only use was `i18n/c` for the modal count, which the
helper now handles); reduced to `:refer [tr]`.

Github #9141

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 08:48:04 +02:00
TinyClawandiot2edge 400414776b 🐛 Fix :heigth typo in clipboard frame-same-size? (#9250)
The height comparison in frame-same-size? used the misspelled keyword
:heigth on both sides. (:heigth selrect) returns nil for any selrect,
so (= nil nil) is always true and the function degenerated to a width-only
comparison.

Result: the 'paste next to selected frame' branch in clipboard.cljs fired
whenever pasted-content width matched a target frame's width, even if the
heights differed.

Introduced in #9033 ( Add paste to replace (Cmd+Shift+V)).

Signed-off-by: iot2edge <tylerprice830@gmail.com>
Co-authored-by: iot2edge <tylerprice830@gmail.com>
2026-04-30 08:37:00 +02:00
Dexterity 25c5bb2019 Restore deleted team files in bulk instead of per file (#9248) 2026-04-30 08:35:02 +02:00
Andrey Antukh fc414b23d2 📚 Update changelog with entries for 2.17.0
Add 3 new features/enhancements (file import errors, read-only version
preview, clipboard permissions) and 4 bug fixes (restore typo, layer
sibling selection, tooltip duplication, library notification link) to
the 2.17.0 changelog section.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:59:33 +00:00
Andrey Antukh 346614edc3 🐛 Fix SCSS mixin names in v2_15 release modal styles
Rename 5 deprecated mixin calls from camelCase to kebab-case to
match the actual mixin names defined in common-refactor.scss:

- deprecated.flexCenter -> deprecated.flex-center
- deprecated.headlineSmallTypography -> deprecated.headline-small-typography
- deprecated.headlineLargeTypography -> deprecated.headline-large-typography
- deprecated.bodyLargeTypography -> deprecated.body-large-typography
- deprecated.bodyMediumTypography -> deprecated.body-medium-typography

This fixes the "Undefined mixin" SCSS compilation error.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:36:20 +00:00
Andrey Antukh 404ebcc63e 💄 Fix SCSS linter errors in v2_15 release modal styles
Replace class selectors with placeholder selectors for @extend
(.modal-overlay-base -> %modal-overlay-base,
 .button-primary -> %button-primary).

Add blank lines after @include mixin calls in 6 rulesets
(.version-tag, .modal-title, .feature-title, .feature-content,
 .feature-list, .next-btn) to satisfy the
declaration-empty-line-before stylelint rule.

Fixes 8 SCSS linter errors in total.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:31:59 +00:00
Andrey Antukh a004219405 Merge remote-tracking branch 'origin/staging' into develop 2026-04-29 19:28:02 +02:00
Andrey Antukh 8b29ca61c6 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-29 19:23:38 +02:00
Ingrid Pigueron b5cd4d96ee 🌐 Add translations for: French
Currently translated at 98.8% (2050 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-04-29 19:10:01 +02:00
Andrey Antukh e81dad21ea 🎉 Add backport-commit skill for manual diff-based commit porting
Introduce a new OpenCode workflow skill that guides users through
backporting commits by applying diffs instead of using cherry-pick.
This is useful when cherry-pick is undesirable (e.g. divergent
histories, binary conflicts, or partial porting).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 19:09:25 +02:00
Andrey Antukh d06b45ec90 🐛 Fix Plugin API token application for JS array of strings
Two coupled defects made shape.applyToken(), token.applyToShapes() and

token.applyToSelected() silently no-op when invoked from JavaScript with

an array of strings (e.g. token.applyToShapes([rect], ["fill"])):

1. token-attr-plugin->token-attr only consulted its alias map when the

   input was already a keyword; string inputs fell through unchanged,

   causing downstream token-attr? to return false.

2. The inner schemas used plain [:set ...] which lacks the :decode/json

   transformer for JS array -> Clojure set coercion. Switching to

   Penpot's custom [::sm/set ...] lets the standard JSON decoder

   pipeline handle the conversion automatically.

This is a backport of commit 1eac3e2be5

which fixes GitHub #9162.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 19:09:25 +02:00
Dexterity 1213640693 🐛 Fix typo in restore-deleted-team-files reduce accumulator (#9241) 2026-04-29 18:48:07 +02:00
Dexterity f530a0ba26 🔥 Remove stray debug log in color-row component (#9243) 2026-04-29 18:47:27 +02:00
Andrey Antukh 1e09e00634 Encourage use of layouts and proper naming in MCP
Improve MCP instructions on design creation:
 * Agents should make use of layouts when appropriate
 * Agents should name all elements appropriately

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 16:12:50 +00:00
Jack StormentandAndrey Antukh 710fd30f78 🐛 Preserve renamed layer name when re-entering edit mode (#9231)
* 🐛 Preserve renamed layer name when re-entering edit mode

When a layer was renamed and the user clicked its name again to edit
it, the input opened with the type-based default name instead of the user's saved name. Pressing Enter then
silently overwrote the saved name with the default. Read the current
shape :name when seeding the rename input so the user's previous
rename is preserved.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Remove redundant DOM-refresh effect from layer rename input

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 18:07:48 +02:00
RenzoandAndrey Antukh 8821ada1bb 🐛 Suppress browser context menu on empty workspace sidebar space (#9196)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Renzo <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:58:38 +02:00
Jack StormentandAndrey Antukh 22b85f1a92 Show specific error messages for invitation token failures (#9223)
*  Show specific error messages for invitation token failures

Surface distinct error messages for the three invitation-token failure
modes that the backend already distinguishes: email mismatch, expired
token, and invalid/corrupted token. Replaces the single generic
could not accept invitation message with actionable text so the
user knows what went wrong and how to recover.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Update CHANGE.md

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Address review feedback on invitation-error messages

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:57:59 +02:00
Andrey Antukh 4829b843b2 🐛 Fix dashboard modal clipping behind sidebar (#9233)
Backport from develop commit 510a015424.

- Fix release notes modal appearing behind the dashboard sidebar (by @RenzoMXD)
- Change sidebar z-index from dropdown to panels layer

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 15:52:48 +00:00
Renzo 510a015424 🐛 Fix dashboard modal clipping behind sidebar (#9233)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-04-29 17:44:49 +02:00
alonso.torres 5e3e66a99b 📚 Updated docs for MCP development environment 2026-04-29 16:39:19 +02:00
Andrey Antukh 05b4760583 📚 Set clearer expectations for PR reviews and prior discussion
Explicitly state that the team is small and reviews may take a
few days as they are handled in dedicated time blocks.

Remove mention of GitHub Discussions since it is not used.

Reword the discussion requirement to manage expectations: do not
expect a PR to be accepted without prior discussion.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 16:27:44 +02:00
StatxcandAndrey Antukh fd170b23f6 🐛 Fix Heroicons arrow paths broken after SVG import (#5283) (#9156)
Signed-off-by: statxc <181730535+statxc@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 15:45:22 +02:00
DreamandAndrey Antukh d668744a1f Add search to board size presets dropdown (#9117)
Closes #4658

Signed-off-by: eureka0928 <meobius123@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 15:44:53 +02:00
Elena Torró 1c129ded1f Merge pull request #9200 from penpot/azazeln28-build-time-performance-optimizations
🔧 Change build and cargo settings
2026-04-29 15:12:23 +02:00
Aitor Moreno 73944e46b7 🔧 Change build and cargo settings 2026-04-29 14:35:00 +02:00
María Valderrama e22a03e7e8 Subscribe to nitrate with an activation code
*  Subscribe to nitrate with an activation code

* 📎 Code review
2026-04-29 12:42:25 +02:00
Jack Storment 3f40be6b4d 💄 Fix sucess typo in subscription dialog i18n keys (#9204)
Rename subscription.settings.sucess.dialog.{title,footer} to
subscription.settings.success.dialog.{title,footer} in en.po and
update the three callsites in subscription.cljs.

Closes #9203

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
2026-04-29 11:07:30 +02:00
boskodev790andAndrey Antukh 1eac3e2be5 🐛 Fix Plugin API token application for JS array of strings (#9166)
* 🐛 Fix Plugin API token application for JS array of strings

Plugin code calling `shape.applyToken(token, ["fill"])` or
`token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS
array of strings. The plugin proxies expected a Clojure set of
keywords, and two coupled defects made the calls silently no-op (or,
with `throwValidationErrors` enabled, throw "check error"):

1. `token-attr-plugin->token-attr` only consulted its alias map when
   the input was already a keyword. String inputs like "fill" fell
   through to the identity branch, so the downstream
   `cto/token-attr?` predicate (which checks against a set of
   keywords) returned false for every string. Coerce strings to
   keywords first.

2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas
   used plain `[:set ...]`, which has no `:decode/json` transformer
   for JS array → Clojure set coercion. Switch to the registered
   `[::sm/set ...]` (in `app.common.schema`) which provides the
   array → set decoder. After the switch, the standard JSON pipeline
   converts `["fill"]` to `#{"fill"}`, then the inner
   `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a
   keyword and validates it.

Also extends the docstring on `token-attr-plugin->token-attr` to make
the string-friendly contract explicit, and registers a new
`tokens-test` ns under `frontend/test/frontend_tests/plugins/` with
six `deftest` blocks covering:

- known keywords passing through unchanged
- keyword aliases (`:r1` → `:border-radius-top-left`, etc.)
- string inputs coerced to keywords (regression for #9162)
- `token-attr?` accepting both keyword and string inputs
- `token-attr?` rejecting unknown attrs and nil

Closes #9162

* 🐛 Fix wrong direction in plugin-name alias tests

The added tests in tokens_test.cljs and the new docstring in tokens.cljs
described the alias resolution in the wrong direction. The map is
{:r1 :border-radius-top-left, …} then map-invert'd, so
token-attr-plugin->token-attr maps verbose plugin-side names
(:border-radius-top-left) to canonical internal short names (:r1),
not the other way around. Inputs already in canonical form (:r1, :fill,
"fill", …) pass through unchanged. Flipped the alias-resolution test
expectations and the keyword/string-input cases, refreshed the docstring
and the regression-coverage comment to match.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 11:02:01 +02:00
Aitor Moreno f59301a3d6 🐛 Fix text editor v2 with 0 width 2026-04-29 10:53:07 +02:00
Andrey Antukh 9751ac2b41 📎 Update changelog 2026-04-29 09:43:13 +02:00
Andrey Antukh ea971a0109 🐛 Fix redundant longhand margin-block properties in options.scss
Combine margin-block-start and margin-block-end into the margin-block
shorthand to satisfy the declaration-block-no-redundant-longhand-properties
stylelint rule.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 07:39:05 +00:00
Yamila Moreno d627d1cfac Improve team name validation (#9176) 2026-04-29 08:59:09 +02:00
Pablo Alba b8f1b6e0c3 Add nitrate api notify-user-orgs-deletion 2026-04-28 19:47:28 +02:00
bobsonzu0a5d198e17c343cb f060b8d3fa 🌐 Add translations for: Russian
Currently translated at 81.8% (1697 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-04-28 15:09:52 +00:00
Ingrid Pigueron a3ddf54043 🌐 Add translations for: French
Currently translated at 96.9% (2010 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-04-28 15:09:50 +00:00
FairyPiggyDev 61ce4b9e0d Add "Delete group" to assets panel context menu (#9151)
When working with large asset groups, users asked for a one-click way
to remove every asset under a group path. Multi-select across hundreds
of items is impractical, and ungrouping first and then deleting leaves
the orphaned items in the flat list.

This change adds a "Delete group" option to the assets-panel
context-menu for three asset types that already carry group structure:

- Components (including variants — sibling variants sharing a variant
  container are deduplicated, and the container is deleted once via
  the same dispatch the per-item delete uses in file_library.cljs).
- Colors.
- Typographies.

A confirmation modal is shown before deletion, with the count of
assets to be removed, so the action is never silent. All deletes run
inside a single undo transaction, so one Cmd+Z restores the whole
group.

Changes
-------

- `assets/groups.cljs` — `asset-group-title*` accepts an optional
  `on-delete-group` prop and conditionally adds the menu entry
  between "Ungroup" and "Combine as variants". When the callback is
  not supplied the option is hidden, so asset sections that do not
  implement it stay unaffected.
- `assets/components.cljs` — threads `on-delete-group` through the
  recursive `components-group*` and defines the section-level
  handler, dispatching to `dwsh/delete-shapes` for variant containers
  and `dwl/delete-component` for plain components.
- `assets/colors.cljs` — same threading + a simple `dwl/delete-color`
  dispatch per color in the group.
- `assets/typographies.cljs` — same threading + a
  `dwl/delete-typography` dispatch per typography in the group.
- `translations/en.po` — three new strings: the menu label
  (`workspace.assets.delete-group`) and the modal title/message
  (`modals.delete-asset-group.title`/`.message`, plural-aware).

Github #9141

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
2026-04-28 15:59:05 +02:00
Elena Torró 2aff116906 Merge pull request #9190 from penpot/elenatorro-improve-performance-on-dragging
 Improve drag performance
2026-04-28 13:01:13 +02:00
Andrey Antukh 94827f1848 📎 Update versionon mcp/package.json 2026-04-28 12:38:44 +02:00
Andrey Antukh 42c9c4a929 Merge remote-tracking branch 'origin/main-staging' 2026-04-28 12:37:02 +02:00
Alejandro Alonso e4af37a7ff 🎉 Use backbuffer + direct-to-target tiles during drag 2026-04-28 12:19:00 +02:00
Elena Torro 483ce8b1c9 Improve drag performance 2026-04-28 12:18:24 +02:00
Elena Torro 0f65774ba9 🐛 Fix render wasm generated component thumnails for instant changes 2026-04-28 11:10:35 +02:00
Aitor Moreno 31b09be405 🎉 Add overtype mode to text editor 2026-04-28 11:10:22 +02:00
Andrey Antukh ccd1da40ca 📎 Update mcp package.json version 2026-04-28 10:26:23 +02:00
Eva Marco c269df1441 🐛 Fix empty warning on login (#9056) 2026-04-28 10:25:33 +02:00
Juan de la Cruz 40ee1960a1 📚 Add 2.15.0 onboarding slides (#9172)
* 🎉 Add new slides content

* 🎉 Add new slides imgs

* 🐛 Fix a typo
2026-04-28 10:21:56 +02:00
Andrey Antukh b0ce644752 Merge remote-tracking branch 'origin/staging' into develop 2026-04-28 10:12:40 +02:00
Andrey Antukh 19e81560be Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-28 10:09:18 +02:00
Andrey Antukh c0989d4261 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-28 10:08:51 +02:00
alonso.torres ad1111a613 🐛 Fix problem with align center + grow auto-width 2026-04-28 09:44:57 +02:00
andrés gonzález aabdb69218 📚 Update MCP docs for public release (#9184) 2026-04-28 09:37:40 +02:00
Luis de Dios a35b61ee0c 🐛 Fix put onboarding modals of top of libraries & templates panel (#9178) 2026-04-28 09:26:15 +02:00
Andrey Antukh d9f099841a 🔥 Remove redundant mf/props metadata from modern components (#9192)
The ::mf/props and ::mf/wrap-props metadata keys are no-ops on modern
components (those defined with mf/defc and the * suffix) since the *
suffix already triggers the props behavior these keys attempt to
configure. This cleanup removes the redundant metadata from modern
components across all UI directories.

Changes:
- comments/: comments
- dashboard/: comments, deleted, files, fonts, grid, import, libraries,
  pin_button, projects, search, sidebar, subscription, team, templates
- exports/: files
- modal/: modal
- settings/: subscription
- static/: static
- viewer/: comments, interactions, viewer
- workspace/: context_menu, libraries, sidebar/assets,
  viewport/gradients, tokens/settings/menu
2026-04-28 09:23:56 +02:00
Andrey Antukh 4e1968bbab 📎 Add updated version of github cli to devenv 2026-04-28 00:01:46 +02:00
Jack Storment aa5bfe6dda Add customizable pixel grid color (#9155)
Let users pick the pixel grid color from a standard color picker.
The grid color was previously hardcoded, making it invisible on
mid-tone canvases. Choice is stored on the file so it persists
across sessions. Defaults preserve the current appearance when
unset.

Closes #7750

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
2026-04-27 21:37:27 +02:00
Milos Milic bd1e0fb23f Add Alt+click to expand a layer subtree in the Layers sidebar (#9179)
Closes #7736.

The Layers sidebar offered no way to expand every nested level of a
single subtree at once. Unfolding a layer that wraps a deep tree
required clicking each disclosure indicator one level at a time -
O(siblings * depth) clicks. The asymmetry was particularly visible
next to the existing Shift+click gesture, which collapses every
layer in the panel in a single action via `dwc/collapse-all`, with
no expand counterpart for either a single subtree or the whole
tree.

Add a new `dwc/expand-subtree` event in
`app.main.data.workspace.collapse` that uses
`cfh/get-children-ids-with-self` to gather the shape's id together
with every descendant id, then merges `{descendant-id true}` entries
into `[:workspace-local :expanded]` so the entire subtree opens in
one update. Existing expansion state on unrelated branches is left
untouched (`merge`, not `assoc`), matching the per-key shape used by
`toggle-collapse` and `expand-collapse`.

Wire the gesture into `layer_item.cljs` `toggle-collapse` callback as
a third branch:

  - Shift+click while expanded - collapse every layer (existing).
  - Alt+click while collapsed   - expand the entire subtree (new).
  - Otherwise                   - toggle this single level (existing).

Alt is chosen instead of Shift to avoid the ambiguity the issue
author flagged: "for a layer of middle depth it is unclear whether
[Shift+click] should fold all (up to the topmost parent) or expand
all (only the current subtree)". Alt is a common platform
convention for "do this recursively" (Finder, file managers,
several IDEs), so the asymmetric mapping matches user expectations.
The callback's `mf/deps` vector is extended with `id` and `objects`
so the closure refreshes when the shape tree changes.

CHANGES.md entry added under the 2.17.0 New features section.
2026-04-27 21:36:15 +02:00
RenzoandAndrey Antukh 8a8ebb7943 Preserve vector content when pasting from external tools (#9182)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 21:35:52 +02:00
Andrey Antukh 84b3d467cf Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-27 20:36:45 +02:00
Andrey Antukh 592cc47336 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 20:36:21 +02:00
Andrey Antukh a58dbec8f2 ⬆️ Update root repo deps 2026-04-27 20:35:46 +02:00
Andrey Antukh df4ffb9147 📚 Update prompt-assistant agent file 2026-04-27 20:35:28 +02:00
Andrey Antukh ac5736957e 📚 Update commiter opencode agent 2026-04-27 20:34:45 +02:00
Andrey Antukh eba4f15bba Backport transit and plugins hardening compatibility issue
From staging
2026-04-27 19:04:43 +02:00
boskodev790andAndrey Antukh ea265da1f3 🐛 Fix plugin library.connectLibrary breaking Promise contract on permission failure (#9158)
`library.connectLibrary()` declared its permission check **outside** the
`js/Promise.` wrapper, so when a plugin without `library:write` permission
called `await library.connectLibrary(id)` the method did not return a
`Promise` at all:

- With the default `throwValidationErrors` flag off → `u/not-valid`
  logs to console and returns `nil`. `await nil` resolves to `nil`, so
  the plugin sees a "successful" result and crashes later when it tries
  to use methods on what it thinks is a `LibraryProxy`.
- With `throwValidationErrors` on → `u/not-valid` throws synchronously,
  so the caller gets a thrown exception instead of a rejected promise —
  inconsistent with every other `library:*` / `content:*` method which
  always returns a Promise that rejects via `reject-not-valid`.

Additionally, the in-Promise `(not (string? library-id))` branch used
`(reject nil)` — the plugin got a rejected Promise but with no error
message.

Move the permission check inside the Promise constructor and replace
both validation errors with `u/reject-not-valid`, matching the pattern
used by the sibling methods `restore`, `remove`, `pin`, `saveVersion`,
`findVersions` in `frontend/src/app/plugins/file.cljs` and every other
promise-returning plugin method. No new imports.

Also add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 17:59:09 +02:00
Andrey Antukh f4cf667d2f 📚 Update changelog 2026-04-27 17:57:00 +02:00
Pablo Alba f8e40a1ca5 🐛 Fix can't add team to nitrate organization 2026-04-27 17:50:42 +02:00
Belén Albeza e99ed5e9f9 🐛 Fix modifiers streams 2026-04-27 17:45:18 +02:00
Belén Albeza 0bee3993ab 🐛 Avoid race condition in initialize-workspace 2026-04-27 17:45:18 +02:00
Belén Albeza 8f905be511 Ignore user renderer pref when render-switch flag is disabled 2026-04-27 17:45:18 +02:00
Belén Albeza 8afadb5199 Show toast when switching renderer 2026-04-27 17:45:18 +02:00
Belén Albeza 6ba68c1ac0 Hot swap of viewport when renderer changes 2026-04-27 17:45:18 +02:00
Belén Albeza ffdbe242a7 🎉 Disable wasm if renderer profile prop is set to svg 2026-04-27 17:45:18 +02:00
Belén Albeza 46b81f4302 🎉 Store chosen renderer in user profile 2026-04-27 17:45:18 +02:00
Belén Albeza 12549df65c 🎉 Add UI for webgl rendering setting (under config flag) 2026-04-27 17:45:18 +02:00
Andrey Antukh c41537eb55 Merge remote-tracking branch 'origin/staging' into develop 2026-04-27 17:31:15 +02:00
Andrey Antukh 82f1606377 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-27 17:31:00 +02:00
Andrey Antukh 839754715a 📚 Update changelog 2026-04-27 17:30:02 +02:00
Andrey Antukh db8aa9bccc Merge remote-tracking branch 'origin/staging' into develop 2026-04-27 17:27:45 +02:00
Andrey Antukh ef2fe78aac Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-27 17:27:28 +02:00
Andrey Antukh a3b9d7bed7 📎 Fix fmt issue 2026-04-27 17:26:59 +02:00
Andrey Antukh 57f1b80013 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 17:26:30 +02:00
Andrey Antukh cbd5f7795b Add minor compatibility adjustments for audit archive task (#8491) 2026-04-27 16:15:35 +02:00
Elenzakaleidos 99f006d728 💄 Update README.md (#9171)
We modified the Images and the text of the Read me page

Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
2026-04-27 15:44:01 +02:00
Luis de Dios edccda2038 🐛 Fix remove prints 2026-04-27 15:26:55 +02:00
Marina López 4867358428 Add modal to subscribe to nitrate from unlimited 2026-04-27 14:11:47 +02:00
Pablo Alba c6bea65a48 Add organization logo to nitrate invitations emails 2026-04-27 11:14:47 +02:00
boskodev790andAndrey Antukh e5314f4a13 🐛 Fix restore-version-from-plugin promise hanging on restore failure (#9111)
Closes #9092.

`restore-version-from-plugin` accepted `_reject` as a dead parameter and
its stream had no `rx/catch`, so errors raised during the restore flow
(failed `rp/cmd! :restore-file-snapshot`, persistence timeouts, or
exceptions inside the watch body) silently swallowed instead of
rejecting the plugin-facing promise at `file.cljs:81`. Plugin code
that did `await version.restore()` would hang indefinitely on any
failure.

Wire `reject` through and wrap the emission with the same `rx/catch`
pattern already used by `create-version-from-plugins` in this file.

- Rename `_reject` to `reject` in the function signature
- Wrap the `rx/concat` body with `rx/catch` that calls `(reject error)`
  and returns `rx/empty` on error, mirroring `create-version-from-plugins`
- Add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 10:17:00 +02:00
Pablo Alba 9c6cc5ec32 💄 Fix nitrate org arrow style 2026-04-27 10:16:40 +02:00
Andrey Antukh feec89679a Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 09:58:13 +02:00
boskodev790andAndrey Antukh 77c507000b 🐛 Fix LDAP schema typo bind-passwor -> bind-password (#9165)
The malli schema for the LDAP provider params (`schema:params` in
`backend/src/app/auth/ldap.clj`) declared the bind-password slot as
`:bind-passwor` (missing trailing `d`). The runtime code in the same
file uses `:bind-password` everywhere — `prepare-params` reads
`(:bind-password cfg)` on line 21 and `try-connectivity` reads
`(:bind-password cfg)` on line 89. Effects of the typo:

1. The schema slot for `:bind-password` is missing, so a wrong type
   (e.g. a number or vector instead of a string) for the actual key
   slips through `check-params` unvalidated. Malli `[:map ...]` is
   open by default, so the genuine `:bind-password` key is silently
   accepted as an unknown extra key.

2. Anyone reading the schema (operator, future contributor, or
   tooling generating docs) sees a non-existent `:bind-passwor`
   parameter and could legitimately set that key — schema would
   accept it, runtime would never read it, LDAP bind would silently
   fail with a confusing "no password" error.

Cross-checked against the pre-malli `clojure.spec` shape removed in
commit 88fb5e7ab (2024-10-29, "♻️ Update integrant to latest
version", which carried the spec→malli migration). The deleted spec
defined `(s/def ::bind-password ::us/string)` correctly — the typo
was introduced when re-typing the keys into the new malli vector-of-
tuples form.

Add a CHANGES.md entry under the 2.17.0 Unreleased 🐛 Bugs fixed
section.

One-character fix.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 09:41:21 +02:00
Luis de Dios a5a8ab5de6 🐛 Fix MCP status is displayed as disabled when setting MCP key without expiration date
Fixes #14058 and #14061 in Taiga
2026-04-27 09:37:11 +02:00
boskodev790 5ee65c5efb 🐛 Fix :hide typo dropping LDAP not-initialized error hint (#9159)
login-with-ldap raised a :restriction exception with the message
"ldap auth provider is not initialized" stored under :hide instead
of :hint. ex/raise (common/src/app/common/exceptions.cljc:33-34)
uses :hint as the ExceptionInfo message and the downstream error
formatters only read :hint (line 250, 312) — :hide is unread
anywhere in the codebase (0 other occurrences vs 447 for :hint).

Effect: when LDAP is misconfigured, operators saw the generic
"restriction" error message instead of the diagnostic string. The
typo has been present since the LDAP command was first introduced
by commit 14d1cb90bd (2022-06-30, "Refactor auth code") and was
carried forward through 6cdf696fc (2023-01-05, "Fix issues on ldap
provider and rpc method") without ever surfacing as a code-review
comment.

One-character fix: :hide -> :hint. Add a CHANGES.md entry under
the 2.17.0 Unreleased 🐛 Bugs fixed section.
2026-04-27 09:30:07 +02:00
Alejandro Alonso 7504c3b53e Merge pull request #9167 from penpot/superalex-fix-text-ellipsis
🐛 Fix text ellipsis merging error
2026-04-27 09:09:14 +02:00
Alejandro Alonso c4e508a606 🐛 Fix text ellipsis merging error 2026-04-27 08:49:22 +02:00
Andrey Antukh 37cba3355d 🔧 Update opencode tooling, agents, and devenv
Update agent configurations: change commiter mode to all, rename
engineer agent to "Penpot Engineer", and remove obsolete testing agent.

Add new read-only planner agent for architecture analysis and planning.

Add four new skills: bat-cat (syntax-highlighted cat clone), fd-find
(fast file finder), jq-json-processor (JSON processor), and ripgrep
(fast text search).

Add fd-find and bat packages to devenv Dockerfile.

Update .gitignore to exclude opencode package-lock and plans directory.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 00:25:23 +02:00
Egor Filatov d4955c7b78 🌐 Add translations for: Russian
Currently translated at 80.6% (1673 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-04-25 16:09:52 +02:00
bobsonzu0a5d198e17c343cb 63829d5fb7 🌐 Add translations for: Russian
Currently translated at 80.6% (1673 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-04-25 16:09:50 +02:00
Andrey Antukh 6d9019c383 📚 Improve pull request documentation in CONTRIBUTING.md
Expand the Pull Requests section with detailed guidance on PR title
format, description expectations, branch naming conventions, the review
process, and a list of PRs that will not be accepted. Also clarify the
'Discuss Before Building' rule to link to GitHub Issues and Discussions
and reference Taiga stories. Update the Table of Contents with nested
links for all new subsections.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-25 13:22:17 +00:00
Pablo Alba 700f3e9c10 MR changes 2026-04-24 17:19:41 +02:00
Pablo Alba debfe5490f 🐛 Fix switching a team nitrate organization lose the background 2026-04-24 17:19:41 +02:00
Yamila Moreno 7031052c4e 🐛 Prevent invitations to blacklisted domains 2026-04-24 16:48:59 +02:00
Andrey Antukh 01d68ec09b Merge remote-tracking branch 'origin/staging' into develop 2026-04-24 14:16:03 +02:00
Andrey Antukh 35f8e1b084 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-24 14:09:21 +02:00
Andrey Antukh 0b6416e53b Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 14:09:03 +02:00
Andrey Antukh d380efdb0c ⬆️ Update devenv dependencies (#9142)
* ⬆️ Update devenv dependencies

*  Fix formatting issues

* 📎 Fix linter issues
2026-04-24 14:07:51 +02:00
moorsecopers99andAndrey Antukh 7e499c5e5f 🐛 Fix Settings/Notifications submit button always active with no changes (#9091)
The "Update Settings" button in Your Account > Settings and Notifications
was always enabled, even when the form had no changes, and clicking it
emitted a success notification despite no data being modified.

Disable the submit button when the current form data equals its initial
state, so it activates only when there are actual changes to persist.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 13:24:55 +02:00
Juan Flores 38d67c8e96 🐛 Fix Help & Learning submenu vertical alignment in account menu (#9138)
The submenu opened by hovering Help & Learning in the user account
menu rendered with a vertical offset, making it appear visually
disconnected from its parent row and aligned instead with the
Community

Signed-off-by: Juan Flores <112629487+juan-flores077@users.noreply.github.com>
2026-04-24 13:17:57 +02:00
Eva Marco 6c4ab8940d 🐛 Fix colorpicker eyedropper on gradients tab (#9125)
* 🐛 Fix colorpicker eyedropper on gradients tab

* 🐛 Fix gradient test deleting opacity input
2026-04-24 12:48:58 +02:00
boskodev790andAndrey Antukh 9ebd17f31f 🐛 Fix PENPOT_OIDC_USER_INFO_SOURCE flag being silently ignored (#9114)
Closes #9108.

The `case` expression in `get-info` (`backend/src/app/auth/oidc.clj`)
dispatched on `:token` and `:userinfo` keywords, but the provider map's
`:user-info-source` value is a string — both from config (the malli
schema in `app.config` pins it to one of `"token"`, `"userinfo"`,
`"auto"`) and from the hard-coded Google / GitHub provider maps (which
already write `"userinfo"`). Strings never equal keywords in Clojure
`case`, so every call fell through to the auto-fallback that prefers
ID-token claims and only hits the UserInfo endpoint when claims are
empty. The net effect: setting `PENPOT_OIDC_USER_INFO_SOURCE=userinfo`
did nothing, and OIDC flows whose IdP requires the UserInfo endpoint
(so claims come back empty/partial) failed with "incomplete user info".

- Extract a pure helper `select-user-info-source` that maps the raw
  config string to a dispatch keyword (`:token`, `:userinfo`, `:auto`),
  falling back to `:auto` for unknown / missing / accidentally-keyword
  values
- Rewrite `get-info`'s `case` to dispatch on the helper's output so
  the arms unambiguously match the normalised keyword
- Add vitest-style deftests in `auth_oidc_test.clj` pinning the three
  valid strings, the nil / "auto" / unknown fallback, and the reverse
  regression (a keyword input must not slip through as if it were the
  matching string)
- Add a CHANGES.md entry under the 2.17.0 Unreleased `:bug: Bugs fixed`
  section linking back to #9108

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 12:14:46 +02:00
Alejandro Alonso 89a1ee7813 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-24 12:06:27 +02:00
Andrey Antukh 29ba336928 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 11:58:50 +02:00
Andrey Antukh cfb076dd61 📚 Update AGENTS.md with common github operations 2026-04-24 11:45:36 +02:00
Eva Marco 4a7140d82d 🐛 Fix theme modal height (#9105)
* 🐛 Fix CI

* 🐛 Fix theme modal height
2026-04-24 11:38:34 +02:00
Pablo Alba 4061673528 Add nitrate api endpoints to get and cancel org invitations (#9124)
*  Add nitrate api endpoints to get and cancel org invitations

*  MR changes
2026-04-24 11:35:53 +02:00
Alejandro Alonso e05ea1392a Merge pull request #9140 from penpot/superalex-fix-merge-develop
🐛 Fix text.cljs error from staging merge
2026-04-24 10:57:59 +02:00
Alejandro Alonso 58fae0a04d 🐛 Fix text.cljs error from staging merge 2026-04-24 10:10:00 +02:00
Alejandro Alonso 078663b0fa 🔧 Fix rust linter errors 2026-04-24 09:52:51 +02:00
Eva MarcoandAndrey Antukh 5a7ba7ee7e 🐛 Fix multiple selection on shapes with token applied to stroke-color (#9110)
*  Remove the need to navigate to page for deletion operation

* 🐛 Fix multiple selection with applied-tokens on stroke-color

* 🐛 Fix button position on page header

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 09:47:44 +02:00
Alejandro Alonso 7532bf411c Merge remote-tracking branch 'origin/develop' into develop 2026-04-24 09:32:35 +02:00
Alejandro Alonso 984d292ab2 Merge remote-tracking branch 'origin/staging' into develop 2026-04-24 09:29:24 +02:00
Full Stack Developer 25e6b939ba Show detailed messages on file import errors (#9004)
*  Show detailed messages on file import errors

Signed-off-by: jsdevninja <topit89807@gmail.com>

*  Fix test

*  Fix build error

---------

Signed-off-by: jsdevninja <topit89807@gmail.com>
2026-04-24 09:13:46 +02:00
FairyPiggyDevandAndrey Antukh 361c1c574b 🐛 Fix plugin parse-point returning plain map instead of Point record (#9129)
The plugin parser's parse-point returned a plain `{:x … :y …}` map,
but shape interaction schemas (for example schema:open-overlay-interaction)
require the attribute to be a `::gpt/point` record. `(instance? Point {:x 0 :y 0})`
is false, so validation silently rejected plugin `addInteraction` calls
that passed `manualPositionLocation`; only a console warning was produced.

Change parse-point to return a `gpt/point` record via `gpt/point`.
All three call sites (parser.cljs:open-overlay, plugins/page.cljs,
plugins/comments.cljs) continue to work because Point records support
the same `:x`/`:y` access plain maps do.

Add a unit test that covers nil input and verifies the returned value
satisfies `gpt/point?`.

Github #8409

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 09:12:13 +02:00
Juan Flores 841b2e156e 🐛 Fix typography style creation with tokenized line-height (#9121)
When a text element has a line-height coming from a design token, the value
may be a number (e.g. 1.5) and fails frontend data validation expecting a
string. Normalize line-height before creating the typography style so the
operation succeeds without throwing an assertion error.

Signed-off-by: juan-flores077 <toptalent399@gmail.com>
2026-04-24 09:11:31 +02:00
boskodev790andAndrey Antukh 6c7843f4b6 🐛 Fix obfuscate-email crashing on malformed email or dotless domain (#9120)
The viewer-side `obfuscate-email` helper used by `anonymize-member` when
building share-link bundles called `clojure.string/split` on the raw
email input and then on the extracted domain. Two failure modes:

1. When the stored email had no `@` (legacy data, LDAP-sourced UIDs, direct
   DB inserts, or fixtures that bypassed `::sm/email`), destructuring
   left `domain` bound to `nil` and the follow-up `(str/split nil "." 2)`
   raised `NullPointerException`. Because `obfuscate-email` runs inside
   `get-view-only-bundle`, the exception aborted the whole RPC response
   for share-link viewers, not just the field.

2. When the stored email used a single-label domain (`alice@localhost`),
   `(str/split "localhost" "." 2)` returned `["localhost"]`; destructuring
   bound `rest` to `nil` and the final `(str name "@****." rest)` produced
   a dangling-dot output `"****@****."` (nil coerces to empty in `str`).

Guard both split calls with `(or x "")` so the chain is nil-safe, and
emit the trailing `.<tld>` segment only when `rest` is present. Add three
`deftest` groups covering the happy path, dotless domains, and malformed
inputs (nil / empty / no-`@`), plus a CHANGES.md entry under the 2.17.0
Unreleased bugs-fixed section.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 09:09:49 +02:00
Renzo 8aacda2249 Add Shift+Numpad0/1/2 zoom shortcut aliases (#2457) (#9063)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-04-24 09:08:31 +02:00
50bee5e176 Add clipboard:read/write permissions to plugin system (#6980) (#9053)
*  Add clipboard:read/write permissions to plugin system (#6980)

* 🔧 Fix prettier formatting in clipboard permission files

---------

Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 09:07:58 +02:00
Andrey Antukh 20c6a28b52 📎 Add commit agent for opencode 2026-04-24 08:54:01 +02:00
Andrey Antukh 7135782e7d Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-24 08:19:47 +02:00
Andrey Antukh fd38f5b431 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 08:18:55 +02:00
Andrey Antukh 2d5e50f352 ⬆️ Update root repo deps 2026-04-24 08:17:32 +02:00
e280168de9 Add read-only preview mode for saved versions (#7622) (#8976)
*  Add read-only preview mode for saved versions (#7622)

* 🔧 Address review feedback on version preview (#7622)

* 🐛 Fix version preview for WASM renderer (#7622)

* 🐛 Fix stylelint color-named and color-function-notation in preview banner (#7622)

* 🐛 Fix invalid-arity call to initialize-workspace in exit-preview (#7622)

* 🐛 Fix unclosed defn paren in exit-preview (#7622)

* ♻️ Refactor version preview/restore flow

Separate enter-preview and enter-restore flows with dedicated dialogs
instead of a persistent banner. Removes preview-banner component in favor
of inline actions dialog. Uses backup/restore pattern for exit-preview
instead of full workspace reinitialization. Adds analytics events for
preview/restore actions.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

*  Extract on-name-input-focus as namespace-level private function

The callback had no dependencies on component-local state or props,
making it a pure function that can be hoisted to a defn-. This avoids
recreating the same callback identity on every render of version-entry*.

*  Extract extract-id-from-event helper to deduplicate snapshot callbacks

Three callbacks in snapshot-entry* shared the same DOM extraction logic
(get current target, read data-id, parse UUID). Extracted into a private
defn- to remove the duplication and simplify each callback.

*  Extract pure state-update callbacks from versions-toolbox* to namespace level

Eight callbacks that only emit fixed Potok events with no meaningful
deps were hoisted out of the component as defn- functions:

- on-create-version
- on-edit-version
- on-cancel-version-edition
- on-rename-version
- on-delete-version
- on-pin-version
- on-lock-version
- on-unlock-version

These no longer need mf/use-fn wrappers since namespace-level functions
have stable identity across renders, avoiding unnecessary callback
recreation on each render cycle.

*  Rename filter parameter to filter-value in on-change-filter to avoid core shadowing

The parameter name 'filter' shadowed clojure.core/filter within the
function scope. Renamed to 'filter-value' for clarity and to prevent
potential bugs if core/filter were needed in future changes.

* 🔧 Fix linter warnings and errors across version-related namespaces

frontend/src/app/main/ui/workspace.cljs:
- Remove unused requires: app.common.data, app.main.data.notifications,
  app.main.data.workspace.versions

frontend/src/app/main/data/workspace/versions.cljs:
- Remove unused require: app.common.uuid
- Fix duplicate reify type: enter-restore used ::restore-version
  (same as the private restore-version fn), renamed to ::enter-restore
- Remove unused bindings: state in enter-restore, team-id in
  exit-preview and restore-version-from-plugin

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Signed-off-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 08:13:16 +02:00
Renzo 7c1a29ccf7 🐛 Remove corepack dependency from MCP server for Node.js 25+ (#9119)
* 🐛 Remove corepack dependency from MCP server for Node.js 25+

* 🐛 Update
2026-04-23 22:08:11 +02:00
Luis de Dios cd417443f6 🐛 Fix layer hierarchy to match old and new SCSS (#9126) 2026-04-23 18:00:40 +02:00
Eva MarcoandAndrey Antukh 0c60db56a2 🐛 Fix multiselection error with typography texts (#9071)
* 🐛 Ensure typography-ref attrs are always present and fix nil encoding

Add :typography-ref-file and :typography-ref-id (both defaulting to nil)
to default-text-attrs so these keys are always present in text node maps,
whether or not a typography is attached.

Skip nil values in attrs-to-styles (Draft.js style encoder) and in
attrs->styles (v2 CSS custom-property mapper) so nil typography-ref
entries are never serialised to CSS.

Replace when with if/acc in get-styles-from-style-declaration to prevent
the accumulator from being clobbered to nil when a mixed-value entry is
skipped during style decoding.

* 🎉 Add test

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-23 16:08:56 +02:00
Marina López a3c330d6e7 Add downgrade nitrate to unlimited modal 2026-04-23 12:54:42 +02:00
Stas Haas 7428cfa684 🌐 Add translations for: German
Currently translated at 95.8% (1988 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-04-23 10:09:50 +00:00
Elena Torro 96722fde4b 🐛 Support EvenOdd SVG attribute across all path operations 2026-04-23 12:02:40 +02:00
Elena Torro 4a549d0907 Drain GPU queue during pan/zoom to avoid render_from_cache hitch 2026-04-23 11:19:51 +02:00
Eva Marco d6b341c053 🐛 Fix color token (#9095) 2026-04-23 10:51:30 +02:00
Eva Marco 5c9696e20c 🐛 Fix color dropdown option update (#9100) 2026-04-23 10:51:20 +02:00
Eva Marco 28b33b9acc 🐛 Fix props on text components (#9099) 2026-04-23 10:49:48 +02:00
Andrey Antukh c6b6b9ce00 📎 Update changelog 2026-04-23 09:59:11 +02:00
Yamila Moreno 5f7de04efe 🚑 Fix email blacklisting (#9122) 2026-04-23 09:42:40 +02:00
Elena Torró d43d1f431f Merge pull request #9112 from penpot/superalex-improve-atlas-growth
🎉 Improve atlas growth
2026-04-23 09:22:39 +02:00
Yamila Moreno dc8073f924 🐳 Add PENPOT_PUBLIC_URI to penpot-frontend 2026-04-23 09:06:10 +02:00
Renzo 5bbb2c5cff 🐛 Fix Copy as SVG for multi-shape selection (#838) (#9066)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-04-22 19:46:38 +02:00
Alejandro Alonso 9e990a975a 🎉 Improve atlas growth 2026-04-22 17:21:11 +02:00
Andrey Antukh ba42cc04b7 ♻️ Derive v-sizing from values instead of passing as prop
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 14:17:15 +00:00
Luis de Dios b60695f54a 🐛 Fix indicate that the mcp is disabled if the mcp key has expired
If the mcp key has expired, the switch that indicates the status in the dashboard will appear as disabled, and will show a modal for regenerate the key. It will also appear as disabled in the workspace, not allowing the plugin to connect
2026-04-22 16:00:52 +02:00
Yamila Moreno 3c542a1abc 🐛 Fix email validation (#9037) 2026-04-22 15:59:28 +02:00
DexterityandAndrey Antukh 3fd976c551 🐛 Fix UI bugs in account settings forms (#8997)
Closes #8977
Closes #8979

Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 15:21:02 +02:00
Edwin Rivera 7dbd602d1e 🐛 Fix text export with custom fonts across SVG, PNG and JPG (#9094)
* 🐛 Fix text export with custom fonts across SVG, PNG and JPG

Text layers using custom or non-standard fonts were rendered incorrectly
on export regardless of the target format. The exporter was not resolving
the font face correctly before rasterization/serialization, causing the
output to fall back to a default glyph set and producing broken or
misaligned text. This fix ensures font data is resolved and embedded
consistently in the export pipeline for all output formats.

Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>

* 📚 Add entry to CHANGES.md under 2.17.0

Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>

---------

Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
2026-04-22 15:19:58 +02:00
Juanfran 7d4092eeba 🐛 Fix column name mismatch when accepting org invitation 2026-04-22 14:24:03 +02:00
Elena Torro f673b32567 🐛 Fix image loading callback 2026-04-22 14:00:49 +02:00
Full Stack Developer d384f47253 🐛 Fix internal error on layer prev/next sibling selection (#9003)
Signed-off-by: jsdevninja <topit89807@gmail.com>
2026-04-22 13:59:42 +02:00
Andrey Antukh 8ad30e14b6 Merge remote-tracking branch 'origin/staging' into develop 2026-04-22 13:34:00 +02:00
Andrey Antukh b0b2c0d264 📎 Update version on mcp/ module 2026-04-22 13:18:24 +02:00
Andrey Antukh f00ea8789f 📎 Update version on mcp module 2026-04-22 13:16:34 +02:00
Andrey Antukh 112e81c397 📎 Fix the version reference
Caused by the recent version changes
2026-04-22 13:14:04 +02:00
moorsecopers99andAndrey Antukh b6487015b8 Add loader feedback while importing and exporting files (#9024)
*  Add loader feedback while importing and exporting files

Show a loader icon with a status label ("Importing files…" /
"Exporting files…") in the import and export dialog footers while the
operation is running, so users get clear in-progress feedback and
cannot retrigger the action by mistake.

Closes #9020

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>

*  Address import/export loader feedback PR review

- Show the loader beside file names in the import dialog while files
  are being imported (previously queued entries kept showing the
  Penpot logo until each one moved into :import-progress).
- Drop the loader from the "Importing files…" / "Exporting files…"
  footer status, leaving just the text styled with the modal title
  color, per the design proposal.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>

*  Match design proposal for import/export progress feedback

- Move the in-progress label from the modal footer into the modal
  body, under the file rows, styled italic with the modal title
  color.
- Rename the labels to match the design wording: "Uploading file…"
  for import and "Downloading file…" for export.
- Restore the disabled "Accept" button in the import footer during
  the import-progress phase, mirroring the disabled "Close" button
  used by export.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>

* 🐛 Rename deprecated bodySmallTypography mixin to body-small-typography

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>

---------

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 13:12:48 +02:00
Andrey Antukh 88008ce16c 📎 Update mcp types yaml file 2026-04-22 13:11:10 +02:00
Andrey Antukh 75d99a0725 🔧 Add missing public uri handling on nginx entrypoint 2026-04-22 13:11:10 +02:00
Andrey Antukh 09637f9794 Allow render entrypoint load alternative config
The render entrypoint is used by exporter
2026-04-22 13:11:10 +02:00
Andrey Antukh 3225319e0c 🐛 Fix frontend tests 2026-04-22 12:54:07 +02:00
Edwin Rivera 2579527e64 🎉 Add get-file-stats RPC command (#9074)
* 🎉 Add get-file-stats RPC command

Introduce a new lightweight RPC query that returns aggregate statistics
for a single file: page count, shape counts by type, component/color/
typography counts, and inbound and outbound library reference counts.
Mirrors the existing get-file-summary permission and decoding pattern.

Useful for plugin authors enforcing per-file budgets, the
@penpot/library npm SDK, and future admin dashboards. Purely additive
— no migrations, no UI, no breaking changes.

Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>

* 🐛 Bind *load-fn* around file data walk in get-file-stats

The binding previously wrapped only  — a plain key
lookup that does not realize any pointers — so by the time
 walked  and accessed  on
each page,  was unbound and every PointerMap
dereference threw , failing the three new  tests.

Move  inside the  form so the walk runs
with  available, matching the existing pattern used in
.

Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>

---------

Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
2026-04-22 12:49:39 +02:00
Elena Torró 3561b2d1eb Merge pull request #9078 from penpot/alotor-fix-thumbnail-errors
🐛 Fix errors in thumbnails
2026-04-22 12:00:56 +02:00
Elena Torró ba1842792f Merge pull request #9076 from penpot/alotor-fix-flex-align-self
🐛 Fix default alignself behavior
2026-04-22 11:55:00 +02:00
Eva Marco 09fca1c820 🐛 Fix tooltip appearing two times when nested elements (#9031) 2026-04-22 11:33:43 +02:00
Aitor Moreno e3981a0cf3 Merge pull request #9077 from penpot/ladybenko-13971-fix-trailing-whitespace
🐛 Fix trailing whitespace behavior in v2 editor
2026-04-22 11:30:01 +02:00
Eva Marco c02f0a2bc9 🐛 Fix grow options (#9097)
* 🐛 Fix grow options

* 🐛 Fix props when hidden is nil
2026-04-22 11:09:44 +02:00
Yamila Moreno 6de5370a0b 🐛 Fix nginx configuration for mcp 2026-04-22 10:29:52 +02:00
Andrey Antukh 448b5d4786 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-22 09:58:20 +02:00
Andrey Antukh 47b3667248 🐛 Fix exporter renderer URI path construction
Apply consistent path construction across bitmap, PDF, and SVG
renderers in the exporter. Use path join utilities instead of
hardcoding the render.html path, ensuring the path is properly
appended to the public URI base path.

- bitmap.cljs: Use u/ensure-path-slash and u/join for path
- pdf.cljs: Use u/join and ensure-path-slash on base-uri
- svg.cljs: Use u/ensure-path-slash and u/join for path
2026-04-22 09:52:44 +02:00
Andrey Antukh 98e8160875 ♻️ Remove worker URI from global templates and compute from public URI
- Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates
- Remove worker_main entry from the build manifest
- Construct worker URI in config.cljs by joining public-uri with worker path
- Fix global variable casing for plugins-list-uri and templates-uri
- Fix alignment in worker.cljs let bindings
2026-04-22 09:52:44 +02:00
Eva Marco d549be3376 ♻️ Remove duplicated code (#9096) 2026-04-22 09:39:38 +02:00
María Valderrama b67394199b Add the ability to upload organization profile image
*  Upload org logo

* 📎 Code review

* 📎 Code review 2

* 📎 Code review 3
2026-04-22 09:37:09 +02:00
Yamila Moreno 6ea7a64e01 Add nginx configuration for mcp server 2026-04-22 09:33:58 +02:00
Pablo Alba 534701f04f 🐛 Fix org options space should be hidden when there are no options 2026-04-22 09:33:42 +02:00
Pablo Alba ad974f4047 💄 Unify naming on nitrate-api 2026-04-22 09:31:09 +02:00
Alejandro Alonso 8bf8601d29 Merge pull request #9093 from penpot/superalex-avoid-unnecesary-rerenders-on-frame-hover
🐛 Avoid unnecessary repainting of frames when mouse hover
2026-04-22 09:28:49 +02:00
Andrey Antukh 81faa5a728 Replace duplicate on-blur lambda with stable on-text-blur ref
The inline (fn [] (ts/schedule ...)) passed as :on-blur to text-options
was an exact copy of the on-text-blur callback already defined via
mf/use-fn earlier in the same let block. Pass on-text-blur directly
to avoid allocating a new function object on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh 7751d9a69b Remove spurious deps from toggle callbacks in text-menu*
toggle-main-menu and toggle-more-options close over a state atom and
need no external deps. The declared deps (main-menu-open? /
more-options-open?) were unused inside the function bodies, causing
each callback to be reallocated on every toggle — self-reinforcing
churn. Drop the mf/deps calls to make both callbacks stable.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh 74d1288003 Hoist token-typography-row? flag check to namespace level
(contains? cf/flags :token-typography-row) is a pure constant:
cf/flags is immutable after startup. Define it once as a private
namespace-level var token-typography-row? instead of re-evaluating
the check on every render in text-decoration-options* and text-menu*.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh d28c0ea066 Memoize label computation in text-menu* component
Wrap the (case type (tr ...) ...) expression in mf/with-memo [type]
so the translation is resolved only when the type prop changes
instead of on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh a94a7221fb Memoize options in text-decoration-options* component
Wrap the two radio-button option maps in mf/with-memo [token-applied]
so the vector and its (tr ...) calls are evaluated only when the
token-applied prop changes, not on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh b8aa243c2b Memoize static options in grow-options* component
Wrap the three radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh dfd992aa49 Memoize static options in text-direction-options* component
Wrap the two radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh 466f27eb7c Memoize static options in text-align-options* component
Wrap the four radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh 6c19c7c0c4 Memoize static options in vertical-align* component
Wrap the radio-button options vector in `mf/with-memo []` so the
vector allocation and `(tr ...)` calls happen once per component
mount instead of on every render.

Also document the translation memoization rule in frontend/AGENTS.md:
`(tr ...)` must never be called at namespace level (locale is
runtime-only), and static option lists should always be wrapped in
`mf/with-memo []`.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 08:55:29 +02:00
Andrey Antukh 97d234a566 Add 2h min-age threshold to storage/gc_touched task
Skip storage objects touched less than 2 hours ago, matching the pattern
used by upload-session-gc. Update all affected tests to advance the clock
past the threshold using ct/*clock* bindings.
2026-04-22 08:48:04 +02:00
Marina López 11c970a945 Add nitrate trial text 2026-04-22 08:17:28 +02:00
Alejandro Alonso ca97a28408 🐛 Avoid unnecesary repainting of frames when mouse hover 2026-04-22 07:37:38 +02:00
Andrey Antukh 6723e3bbea 🐛 Fix issues after staging merge 2026-04-21 22:52:19 +02:00
Andrey Antukh c259fbdb5b Merge remote-tracking branch 'origin/staging' into develop 2026-04-21 22:43:46 +02:00
Andrey Antukh f331325941 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-21 21:42:03 +02:00
Andrey Antukh f716995ffd 📚 Update changelog 2026-04-21 21:08:57 +02:00
Andrey Antukh e5f9c1e863 🎉 Add chunked upload API for large media and binary files
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.

Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
  created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
    * `create-upload-session`  – allocates a session row; enforces
      `upload-sessions-per-profile` and `upload-chunks-per-session`
      quota limits (configurable in config.clj, defaults 5 / 20).
    * `upload-chunk`           – stores each slice as a storage object;
      validates chunk index bounds and profile ownership.
    * `assemble-file-media-object` – reassembles chunks via the shared
      `assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
  `assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
  when provided, materialises the temp file from chunks instead of
  expecting an inline multipart body, removing the 200 MiB body limit
  on .penpot imports.  Schema updated with an `:and` validator requiring
  either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
  (never-completed) sessions older than 1 hour; scheduled daily at
  midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.

Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
  helper drives steps 1–2 (create session + upload all chunks with a
  concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
  via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
  (`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
  use the existing direct `upload-file-media-object` path.
  `handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
  inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
  `form-data?` removed from `import-binfile` (JSON params only).

Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
  isolation, invalid media type, missing chunks, session-not-found,
  chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
  correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
  blobs, chunked path for large blobs, and chunk-count correctness for
  `process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
  `make-json-response`, `make-transit-response`, `url->cmd`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 18:51:10 +00:00
Andrey Antukh a395768987 🐛 Fix incorrect handlig of version restore operation (#9041)
- Add session ID tracking to RPC layer (backend and frontend)
- Send session ID header with RPC requests for request correlation
- Rename file-restore to file-restored for consistency
- Extract initialize-file function from initialize-workspace flow
- Improve file restoration initialization with wait-for-persistence
- Extract initialize-version event handler for version restoration
- Fix viewport key generation with file version numbers for proper re-renders
- Update layout item schema and constraints to use internal sizing state
- Add v-sizing state retrieval in layout-size-constraints component
- Refactor file-change notifications stream handling with rx/map
- Fix team-id lookup in restore-version-from-plugins

Improves request traceability across frontend/backend sessions and streamlines
the workspace initialization flow for file restoration scenarios.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 20:50:41 +02:00
Andrey Antukh 8f2c467b82 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-21 20:44:31 +02:00
Andrey Antukh c1688edf66 ♻️ Convert frame-grid and grid-options to modern * suffix
Rename grid-options -> grid-options*, frame-grid -> frame-grid*.
Update internal call and call site in shapes/frame.cljs to [:> ...] syntax.
2026-04-21 20:31:30 +02:00
Andrey Antukh 08247aec3f ♻️ Convert svg-attrs-menu and attribute-value to modern * suffix
Rename attribute-value -> attribute-value*, svg-attrs-menu -> svg-attrs-menu*.
Update all call sites (circle, path, svg_raw, rect, group) to use [:> ...] syntax.
2026-04-21 20:31:30 +02:00
Andrey Antukh 95ca68e2b8 ♻️ Convert history-entry-details to modern rumext * format
Rename to history-entry-details* and update internal call site.
2026-04-21 20:31:30 +02:00
Andrey Antukh e9e6796f05 ♻️ Convert text-menu and sub-components to modern rumext * format
Rename text-align-options, text-direction-options, vertical-align,
grow-options, text-decoration-options and text-menu to their * variants.
Update all call sites in shapes/text.cljs, shapes/group.cljs and
shapes/multiple.cljs.
2026-04-21 20:31:30 +02:00
Andrey Antukh 0e5d3e2619 ♻️ Convert blur-menu, constraints-menu and stroke-menu to modern rumext * format
Rename components to blur-menu*, constraints-menu* and stroke-menu*.

Update :refer imports and all [:& ...] call sites to [:> ...*] for
blur-menu*, constraints-menu* and stroke-menu* across all nine
shapes-specific options panels.
2026-04-21 20:31:30 +02:00
Andrey Antukh f0d6e8cb2f ♻️ Convert text-edition-outline to modern rumext * format
Rename to text-edition-outline*, update call sites in viewport.cljs and
viewport_wasm.cljs to [:> text-edition-outline* ...].
2026-04-21 20:31:30 +02:00
Andrey Antukh 304a324529 ♻️ Convert image-upload to modern rumext * format
Rename to image-upload*, update internal call site in top-toolbar* to
[:> image-upload*].
2026-04-21 20:31:30 +02:00
Andrey Antukh 14ff56bc89 ♻️ Convert text-palette-ctx-menu to modern rumext * format
Rename to text-palette-ctx-menu*, rename show-menu? prop to show-menu,
update call site in palette.cljs to [:> text-palette-ctx-menu* ...].
2026-04-21 20:31:30 +02:00
Andrey Antukh d8340d765a Merge remote-tracking branch 'origin/staging' into develop 2026-04-21 20:28:38 +02:00
Andrey Antukh 2bbd63287f Merge remote-tracking branch 'origin/main' into staging 2026-04-21 19:22:50 +02:00
Andrey Antukh 6eccffb8bb 🐛 Fix incorrect handlig of version restore operation (#9041)
- Add session ID tracking to RPC layer (backend and frontend)
- Send session ID header with RPC requests for request correlation
- Rename file-restore to file-restored for consistency
- Extract initialize-file function from initialize-workspace flow
- Improve file restoration initialization with wait-for-persistence
- Extract initialize-version event handler for version restoration
- Fix viewport key generation with file version numbers for proper re-renders
- Update layout item schema and constraints to use internal sizing state
- Add v-sizing state retrieval in layout-size-constraints component
- Refactor file-change notifications stream handling with rx/map
- Fix team-id lookup in restore-version-from-plugins

Improves request traceability across frontend/backend sessions and streamlines
the workspace initialization flow for file restoration scenarios.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 19:19:51 +02:00
moorsecopers99andAndrey Antukh 95b2d7b083 🐛 Add ability to delete uploaded profile avatar (#9068)
Fixes #9067. Adds a delete button that appears on hover over an
uploaded profile photo; clicking it opens a confirm modal and, on
accept, clears the stored photo so the generated fallback avatar is
shown again. A new :delete-profile-photo RPC schedules the old
storage object for garbage collection and sets photo-id to null.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 19:19:30 +02:00
DexterityandAndrey Antukh bb91c06390 🐛 Show check icon after copying team invitation link (#8996)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 17:32:07 +02:00
DexterityandAndrey Antukh e1d3106f61 Add color customization for ruler guides (#8986)
*  Add customizable colors for ruler guides

*  Update CHANGES.md

* 💄 Move guide color menu styles to SCSS

* 💄 Fix trailing whitespace in guides.cljs

---------

Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 17:31:39 +02:00
Andrey Antukh aed2f8a8f8 🐛 Fix removeChild errors from unmount race conditions (#8927)
Guard imperative DOM operations (removeChild, RAF callbacks) against
race conditions where React has already unmounted the target nodes.

- assets/common.cljs: add dom/child? guard before removeChild in RAF
- dynamic_modifiers.cljs: capture RAF IDs and cancel them on cleanup;
  add null guards for DOM nodes that may no longer exist
- hooks.cljs: guard portal container removal with dom/child? check
- errors.cljs: extract is-ignorable-exception? to a top-level defn
  and add NotFoundError/removeChild to ignorable exceptions, since
  these are caused by browser extensions modifying React-managed DOM
- Add unit tests for is-ignorable-exception? predicate

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 17:31:05 +02:00
Dr. Dominik Jain 77560b9305 Encourage use of layouts and proper naming #9081 (#9084)
Improve MCP instructions on design creation:
 * Agents should make use of layouts when appropriate
 * Agents should name all elements appropriately
2026-04-21 17:23:58 +02:00
Pablo Alba cd320c0cd6 On profile deletion, remove the user from nitrate too 2026-04-21 15:44:37 +02:00
Belén Albeza f9f3955503 🐛 Fix trailing whitespace behavior in v2 editor 2026-04-21 15:43:10 +02:00
Yamila Moreno f19c968bc6 🔧 Add main-staging workflow 2026-04-21 15:40:51 +02:00
Yamila Moreno d5cf7dcf9d 🔧 Add main-staging workflow 2026-04-21 15:40:37 +02:00
Yamila Moreno bd82829cb7 🔧 Add main-staging workflow 2026-04-21 15:40:16 +02:00
Yamila Moreno 66e34950b2 🔧 Add main-staging workflow 2026-04-21 15:39:35 +02:00
alonso.torres 2901d00862 🐛 Fix errors in thumbnails 2026-04-21 15:35:06 +02:00
Eva Marco f18670ed00 🐛 Fix errors from numeric input design review (#8993)
* 🐛 Fix blur input after enter value

* 🐛 Catch error on invalid maths

* 🐛 Fix race condition

* 🎉 Add tests that cover issues

* 🐛 Fix padding applying only to one side

* 🐛 Fix show broken pill when reference is on not active set
2026-04-21 14:39:06 +02:00
alonso.torres 719f4a5035 🐛 Fix default alignself behavior 2026-04-21 14:33:47 +02:00
Elena Torró c636517499 Merge pull request #9072 from penpot/alotor-fix-swap-component
🐛 Fix problem on component swap
2026-04-21 13:53:21 +02:00
Elena Torró 04f29a0d72 Merge pull request #9059 from penpot/azazeln28-fix-caret-dimensions
🐛 Fix caret dimensions
2026-04-21 13:41:07 +02:00
Xaviju 78c48f1953 🐛 Fix broken update library notification link UI (#9070)
* 🐛 Fix broken update library notification link UI

* ♻️ Format and lint
2026-04-21 13:33:38 +02:00
alonso.torres f89f4e0047 🐛 Fix problem on component swap 2026-04-21 13:11:03 +02:00
alonso.torres 3da74ed864 🐛 Fix problem with component thumbnails in swap component panel 2026-04-21 13:11:03 +02:00
Aitor Moreno 612855452a 🎉 Add render perf options 2026-04-21 11:43:22 +02:00
Elena Torro 62ec66b974 Add lazy async rendering for component thumbnails 2026-04-21 11:40:53 +02:00
alonso.torres 88ec9e4ff1 🐛 Fix problem with store font after cleanup 2026-04-21 11:03:04 +02:00
Xaviju cd9151bf9f 🐛 Fix duplicate modal title (#9064) 2026-04-21 09:54:30 +02:00
Aitor Moreno 7efc4d6d53 🐛 Fix caret dimensions 2026-04-21 09:44:44 +02:00
Elena Torró 0b49c1f3e9 Merge pull request #9069 from penpot/superalex-improve-modifiers-flickering
🐛 Avoid sequential tile draws and flicker during shape transforms
2026-04-21 09:20:29 +02:00
Alejandro Alonso 0d17debde7 Merge remote-tracking branch 'origin/staging' into develop 2026-04-21 08:24:29 +02:00
Alejandro Alonso 98c8bb1746 🐛 Avoid sequential tile draws and flicker during shape transforms 2026-04-21 07:45:27 +02:00
Xaviju e9105f3670 ♻️ Fix linter errors under legacy resources scss (#9035) 2026-04-20 23:58:53 +02:00
Andrey Antukh eeeb698d91 ⬆️ Bump opencode-ai dev dependency 1.4.3 -> 1.14.19
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-20 20:13:15 +02:00
Andrey Antukh 3a39676969 Backport MCP from staging (part 1) 2026-04-20 19:37:02 +02:00
alonso.torres c42eb6ff86 🐛 Fix problem with selection performance 2026-04-20 17:30:07 +02:00
Elena Torro b5701923ba 🐛 Fix dragging shape 2026-04-20 16:49:18 +02:00
Elena Torró 9ba8d4667c Merge pull request #9044 from penpot/superalex-atlas-fixes
🐛 Atlas fixes
2026-04-20 16:23:06 +02:00
Alejandro Alonso 1d454f3790 🐛 Fix stale tile cache when flex reflow changes modifier set between frames 2026-04-20 15:50:22 +02:00
Juan de la CruzandAndrey Antukh 876b8d645d 🎉 Add new page separators feature (#8561)
* 🎉 Add new page separators feature

* 📎 Add PR feedback changes

* 🐛 Fix page sitemap icons

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-20 15:33:42 +02:00
Eva Marco adea81ceee ♻️ Update icon on typography section and scss files (#9021)
* ♻️ Update icon on new buttons

* ♻️ Update scss on typography section
2026-04-20 15:32:55 +02:00
Alejandro Alonso bc9496deaa 🎉 Replace run_script tiles-complete dispatch with wapi extern binding 2026-04-20 15:29:45 +02:00
Alejandro Alonso 88dbfe7602 🐛 Fix restore renderer state after thumbnail render_shape_pixels 2026-04-20 15:27:46 +02:00
Alejandro Alonso 9cf787d154 🐛 Update atlas when removing shape 2026-04-20 15:27:46 +02:00
Elena Torró 3f0d103cb3 Merge pull request #9036 from penpot/alotor-fix-thumbnail-images
🐛 Fix problem with dashboard thumbnails images
2026-04-20 13:03:53 +02:00
Eva Marco 003b54421d 🐛 Fix empty warning on login (#9056) 2026-04-20 12:23:47 +02:00
Pablo Alba 73b55ee47e Add nitrate api method get-remove-from-org-summary 2026-04-20 11:18:07 +02:00
Pablo Alba ae66317d6c Add nitrate api to remove user from org 2026-04-20 11:18:07 +02:00
Pablo Alba b2c9e08d42 🐛 Fix bad check on leave nitrate org 2026-04-20 11:18:07 +02:00
Dream 42ebee88d6 Add paste to replace (Cmd+Shift+V) (#9033)
Paste clipboard contents in place of the currently selected shape,
inheriting its position, parent, and z-index. The replaced shape
is deleted in the same transaction for a single undo step.

Signed-off-by: eureka0928 <meobius123@gmail.com>
2026-04-20 11:03:50 +02:00
DreamandAndrey Antukh f0c68fb826 Add search bar to color palette (#8994)
*  Add search bar to color palette

Fixes #7653

Signed-off-by: eureka0928 <meobius123@gmail.com>

* ♻️ Use search icon toggle for color palette search

Address UX feedback: replace always-visible search input with a
search icon that toggles the input on click. Hide the search
functionality when no colors exist. Move CHANGES.md entry to
2.16.0 section.

Signed-off-by: eureka0928 <meobius123@gmail.com>

---------

Signed-off-by: eureka0928 <meobius123@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-20 11:02:23 +02:00
Stas Haas e14de6ea30 🌐 Add translations for: German
Currently translated at 95.2% (1976 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-04-18 13:10:42 +00:00
wdeveloper16andwdeveloper16 d772632b08 Allow customising the OIDC login button label (#9026)
*  Allow customising the OIDC login button label (#7027)

* 📚 Add CHANGES entry and docs for PENPOT_OIDC_NAME (#7027)

---------

Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
2026-04-17 16:56:29 +02:00
Alejandro Alonso ec773703cc Merge pull request #9042 from penpot/alotor-fix-thumbnail-generation
🐛 Fix thumbnail generation
2026-04-17 13:06:43 +02:00
alonso.torres 97496d8ad7 🐛 Fix thumbnail generation 2026-04-17 12:30:13 +02:00
Pablo Alba c5a2b592a2 Move team to another nitrate organization 2026-04-17 11:38:52 +02:00
Pablo Alba a206d57443 Add team to a nitrate organization 2026-04-17 11:38:52 +02:00
Alejandro Alonso f1f612f265 Merge pull request #9038 from penpot/andy-antialias-threshold
 Update antialias threshold
2026-04-17 11:24:22 +02:00
Andres Gonzalez ea53d24dde Update antialias threshold 2026-04-17 10:55:21 +02:00
alonso.torres bfa1ae051f 🐛 Fix problem with dashboard thumbnails images 2026-04-17 10:43:41 +02:00
Elena Torró b74d920d03 Merge pull request #8980 from penpot/superalex-atlas-2
🎉 Tiles atlas support
2026-04-17 09:38:27 +02:00
Alejandro Alonso fb1f55c13e Merge pull request #9039 from penpot/alotor-fix-position-data-ff
🐛 Fix problem with position data in Firefox
2026-04-17 09:36:48 +02:00
Alejandro Alonso 8775e234f3 🎉 Waiting for tiles complete in a non blocking way 2026-04-17 09:17:26 +02:00
Alejandro Alonso c08c3bd160 🎉 Tiles atlas support 2026-04-17 09:05:52 +02:00
Ingrid Pigueron e54e02b736 🌐 Add translations for: French
Currently translated at 96.8% (2009 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-04-16 18:09:53 +00:00
Andrey Antukh 6fa440cf92 🎉 Add chunked upload API for large media and binary files
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.

Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
  created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
    * `create-upload-session`  – allocates a session row; enforces
      `upload-sessions-per-profile` and `upload-chunks-per-session`
      quota limits (configurable in config.clj, defaults 5 / 20).
    * `upload-chunk`           – stores each slice as a storage object;
      validates chunk index bounds and profile ownership.
    * `assemble-file-media-object` – reassembles chunks via the shared
      `assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
  `assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
  when provided, materialises the temp file from chunks instead of
  expecting an inline multipart body, removing the 200 MiB body limit
  on .penpot imports.  Schema updated with an `:and` validator requiring
  either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
  (never-completed) sessions older than 1 hour; scheduled daily at
  midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.

Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
  helper drives steps 1–2 (create session + upload all chunks with a
  concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
  via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
  (`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
  use the existing direct `upload-file-media-object` path.
  `handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
  inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
  `form-data?` removed from `import-binfile` (JSON params only).

Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
  isolation, invalid media type, missing chunks, session-not-found,
  chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
  correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
  blobs, chunked path for large blobs, and chunk-count correctness for
  `process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
  `make-json-response`, `make-transit-response`, `url->cmd`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-16 19:43:57 +02:00
Andrey Antukh 974beca12d Add 2h min-age threshold to storage/gc_touched task
Skip storage objects touched less than 2 hours ago, matching the pattern
used by upload-session-gc. Update all affected tests to advance the clock
past the threshold using ct/*clock* bindings.
2026-04-16 19:43:57 +02:00
Yamila Moreno b38912f3cb 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:22:22 +02:00
Yamila Moreno 697de53c16 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:21:35 +02:00
Yamila Moreno 32d9688c3c 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:20:44 +02:00
alonso.torres 47abe09cfe 🐛 Fix problem with position data in Firefox 2026-04-16 18:08:34 +02:00
Elena Torró b02e05e23d Merge pull request #8919 from penpot/alotor-fix-change-font-grow-text
🐛 Fix problem when changing font and grow text
2026-04-16 16:38:32 +02:00
Eva Marco 7f409eadd4 ♻️ Update copy to be more specific (#9028) 2026-04-16 13:49:48 +02:00
Pablo Alba 39f4c13493 Add nitrate remove team from org 2026-04-16 11:46:05 +02:00
Pablo Alba 65a0fcb15b 🐛 Fix on nitrate leave org default org team must be deleted if empty 2026-04-16 11:45:37 +02:00
Pablo Alba ac472c615a 🐛 Fix nitrate invitations org ux review 2026-04-16 11:18:11 +02:00
aliworksx08andAndrey Antukh 81061013b1 Add openid-attr support and dot notation for OIDC attribute (#8946)
*  Add openid-attr support and dot notation for OIDC attribute paths

* ♻️ Simplify OIDC: add dot-notation for attr paths and retain sub claim

* ♻️ Fix OIDC: fix

* 🐛 Fix OIDC nested attr lookup for dot notation

* ♻️ Remove unused OIDC openid-attr support

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-16 11:12:37 +02:00
Andrey Antukh b5922d32ca Merge remote-tracking branch 'origin/main' into staging 2026-04-16 10:59:36 +02:00
Andrey Antukh b2f173675e 📎 Fix changelog 2026-04-16 10:56:44 +02:00
Andrey Antukh 69e505a6a2 📎 Update changelog 2026-04-16 10:21:15 +02:00
Andrey Antukh 390796f36e 📎 Update changelog 2026-04-16 10:20:05 +02:00
Dream 78381873eb Edit ruler guide position by double-clicking the guide pill (#8987)
Drag-and-drop is the only way to move a ruler guide today, which makes
hitting an exact pixel painful. Double-clicking the guide pill now
swaps the position label for a numeric input — Enter commits, Escape
cancels — so users can type a precise value relative to the guide's
frame (or canvas).

Closes #2311

Signed-off-by: eureka0928 <meobius123@gmail.com>
2026-04-16 10:03:28 +02:00
Andrey Antukh de27ea904d Add minor adjustments to the auth events (#9027) 2026-04-16 09:59:45 +02:00
Andrey Antukh 146219a439 Add tests for app.common.geom namespaces 2026-04-15 23:37:53 +02:00
Andrey Antukh fa89790fd6 🐛 Fix grid layout case dispatch, divide-by-zero, and add set-auto-multi-span tests
Three critical fixes for app.common.geom.shapes.grid-layout.layout-data:

1. case dispatch on runtime booleans in get-cell-data (case→cond fix)
   In get-cell-data, column-gap and row-gap were computed with (case ...)
   using boolean locals auto-width? and auto-height? as dispatch values.
   In Clojure/ClojureScript, case compares against compile-time constants,
   so those branches never matched at runtime. Replaced both case forms
   with cond, using explicit equality tests for keyword branches.

2. divide-by-zero guards in fr/auto/span calc (JVM ArithmeticException fix)
   Guard against JVM ArithmeticException when all grid tracks are fixed
   (no flex or auto tracks):
   - (get allocated %1) → (get allocated %1 0) in set-auto-multi-span
   - (get allocate-fr-tracks %1) → (get allocate-fr-tracks %1 0) in set-flex-multi-span
   - (/ fr-column/row-space column/row-frs) guarded with (zero?) check
   - (/ auto-column/row-space column/row-autos) guarded with (zero?) check
   In JS, integer division by zero produces Infinity (caught by mth/finite),
   but on the JVM it throws before mth/finite can intercept.

3. Exhaustive tests for set-auto-multi-span behavior
   Cover all code paths and edge cases:
   - span=1 cells filtered out (unchanged track-list)
   - empty shape-cells no-op
   - even split across multiple auto tracks
   - gap deduction per extra span step
   - fixed track reducing budget; only auto tracks grow
   - smaller children not shrinking existing track sizes (max semantics)
   - flex tracks causing cell exclusion (handled by set-flex-multi-span)
   - non-spanned tracks preserved via (get allocated %1 0) default
   - :row type symmetry with :column type
   - row-gap correctly deducted in :row mode
   - documents that (sort-by span -) yields ascending order (smaller spans
     first), correcting the misleading code comment

All tests pass on both JS (Node.js) and JVM environments.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh 71904c9ab6 🐛 Fix CLJS bounds-map deduplication and update intersect test
In the CLJS branch of resolve-modif-tree-ids, get-parent-seq returns
shape maps, but the js/Set was populated with UUIDs. As a result,
.has and .add were passing full shape maps instead of their :id
values, so parent deduplication never worked in ClojureScript.
Fixed both .has and .add calls to extract (:id %) from the shape map.

Also update the collinear-overlap test in geom-shapes-intersect-test
to expect true now that the ::coplanar keyword fix (commit 847bf51)
makes on-segment? collinear checks actually reachable.
2026-04-15 23:37:53 +02:00
Andrey Antukh d13e464ed1 🐛 Fix three flex layout bugs in drop-area, positions and layout-data
drop_area.cljc: v-end? was guarded by row? instead of col?, making
vertical-end alignment check fire under horizontal layout conditions.
Aligned with v-center? which correctly uses col?.

positions.cljc: In get-base-line, the col? around? branch passed 2 as
a third argument to max instead of as a divisor in (/ free-width
num-lines 2). This made the offset clamp to at least 2 pixels rather
than computing half the per-line free space. Fixed parenthesization.

layout_data.cljc: The second cond branch (and col? space-evenly?
auto-height?) was permanently unreachable because the preceding branch
(and col? space-evenly?) is a strict superset. Removed the dead branch.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh 7e9fac4f35 🐛 Fix constraint-modifier :default arity mismatch
All concrete constraint-modifier methods accept 6 arguments
(type, axis, child-before, parent-before, child-after, parent-after)
but the :default fallback only declared 5 parameters. Any unknown
constraint type would therefore receive 6 args and throw an arity
error at runtime. Added the missing sixth underscore parameter.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh 80124657b8 🐛 Fix double rotation negation in adjust-shape-flips
When both dot-x and dot-y were negative (both axes flipped),
(update :rotation -) was applied twice which cancelled itself out,
leaving rotation unchanged. The intended behaviour is to negate
rotation once per flip, but flipping both axes simultaneously is
equivalent to a 180° rotation and should not alter the stored angle.

Replaced the two separate conditional rotation updates with a single
one gated on (not= (neg? dot-x) (neg? dot-y)) so the rotation is
negated only when exactly one axis is flipped.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh cf47d5e53e 🐛 Fix coplanar keyword mismatch in intersect-segments?
orientation returns the auto-qualified keyword ::coplanar
(app.common.geom.shapes.intersect/coplanar) but intersect-segments?
was comparing against the plain unqualified :coplanar keyword, which
never matches. This caused all collinear/on-segment edge cases to be
silently skipped, potentially missing valid segment intersections.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh adfe4c3945 🐛 Fix update-rect :size and unqualified abs in corners->rect
update-rect with :size type was only updating :x2 and :y2 but not
:x1 and :y1, leaving the Rect record in an inconsistent state (x1/y1
would not match x/y). Aligned its behaviour with update-rect! which
correctly updates all four corner fields.

corners->rect was calling unqualified abs which is not imported in
app.common.geom.rect namespace. Replaced with mth/abs which is
the proper namespaced version already available in the ns require.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh 179bb51c76 🐛 Fix gpt/multiply docstring and gpt/abs Point record downcast
gpt/multiply had a copy-paste docstring from gpt/subtract claiming it
performs subtraction; corrected to accurately describe multiplication.

gpt/abs was using clojure.core/update on a Point record, which returns
a plain IPersistentMap instead of a Point instance, causing point?
checks on the result to return false. Replaced with a direct pos->Point
constructor call using mth/abs on each coordinate.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh 3d4c914daa 🐛 Fix trailing comma in matrix->str and remove duplicate dispatch
matrix->str was producing malformed strings like '1,0,0,1,0,0,'
instead of '1,0,0,1,0,0', breaking string serialization of matrix
values used in transit and print-dup handlers.

Also remove the first pp/simple-dispatch registration for Matrix at
line 362 which was dead code shadowed by the identical registration
further down in the file.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh f5271dabee 🐛 Fix error handling issues (#8962)
* 🚑 Fix RangeError from re-entrant error handling in errors.cljs

Two complementary changes to prevent 'RangeError: Maximum call stack
size exceeded' when an error fires while the potok store error pipeline
is still on the call stack:

1. Re-entrancy guard on on-error: a volatile flag (handling-error?)
   is set true for the duration of each on-error invocation. Any
   nested call (e.g. from a notification emit that itself throws) is
   suppressed with a console.error instead of recursing indefinitely.

2. Async notification in flash: the st/emit!(ntf/show ...) call is
   now wrapped in ts/schedule (setTimeout 0) so the notification event
   is pushed to the store on the next event-loop tick, outside the
   error-handler call stack. This matches the pattern already used by
   the :worker-error, :svg-parser and :comment-error handlers.

* 🐛 Add unit tests for app.main.errors

Test coverage for the error-handling module:

- stale-asset-error?: 6 cases covering keyword-constant and
  protocol-dispatch mismatch signatures, plus negative cases
- exception->error-data: plain JS Error, ex-info with/without :hint
- on-error dispatch: map errors routed via ptk/handle-error, JS
  exceptions wrapped into error-data before dispatch
- Re-entrancy guard: verifies that a second on-error call issued
  from within a handle-error method is suppressed (exactly one
  handler invocation)

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:04 +02:00
Andrey Antukh a7e362dbfe 📎 Add commented helpers on backend _env for testing nexus 2026-04-15 18:03:11 +02:00
Andrey Antukh f8f7a0828e Add missing indexes on audit_log table 2026-04-15 18:02:13 +02:00
Elena Torró e186a27174 Merge pull request #9019 from penpot/azazeln28-fix-pointer-selection
🐛 Fix text editor v3 pointer selection
2026-04-15 17:15:20 +02:00
Aitor Moreno 1477758656 🐛 Fix pointer selection 2026-04-15 16:44:24 +02:00
Elena Torro 41bc8c9b9d 🐛 Fix masked shapes causing render cuts at tile boundaries 2026-04-15 16:36:16 +02:00
Juanfran 3829443046 🐛 Skip onboarding modal for nitrate entry users 2026-04-15 16:32:58 +02:00
Elena Torró b442ca2209 Merge pull request #8988 from penpot/alotor-improve-token-set-change
🐛 Improve change token set performance
2026-04-15 16:23:55 +02:00
alonso.torres 4d2d559383 🐛 Fix problem with finish render callback 2026-04-15 16:09:38 +02:00
alonso.torres e3bafab529 🐛 Fix problem with resizes in plugins 2026-04-15 16:09:38 +02:00
alonso.torres 3f5226485b 🐛 Fix problem when changing font and grow text 2026-04-15 16:09:38 +02:00
Ingrid Pigueron e131fba675 🌐 Add translations for: French
Currently translated at 95.9% (1989 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-04-15 13:09:52 +00:00
Stas Haas 44536e2eaa 🌐 Add translations for: German
Currently translated at 95.2% (1975 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-04-15 13:09:50 +00:00
Aitor Moreno 424b689dca 🐛 Fix mixed fills issues 2026-04-15 14:32:32 +02:00
Aitor Moreno 77b4d07d1f 🐛 Fix v3 text styles not being applied when inc/dec value 2026-04-15 14:32:32 +02:00
Aitor Moreno 6fd264051a 🐛 Fix v2/v3 wrong styling 2026-04-15 14:32:32 +02:00
Marina López c10f945473 Add nitrate subscription expected cancel date 2026-04-15 13:42:04 +02:00
Juanfran f5591ed22e 🐛 Forward email when adding user to Nitrate organization 2026-04-15 13:28:39 +02:00
Andrey Antukh 8f30a95ca0 🐛 Guard against nil variant-data in typography-item
When d/seek finds no matching font variant (e.g. the variant-id stored
on the typography no longer exists in the font's variants list),
variant-data is nil and (:name nil) produces nil, resulting in a
malformed label like "14px | ". Fall back to "--" in that case.
2026-04-15 12:27:18 +02:00
Andrey Antukh e8547ab6dd 🐛 Pass on-finish-drag to harmony-selector in colorpicker
Without this, dragging the value/opacity sliders in the harmony tab
never called on-finish-drag, so the undo transaction started by
on-start-drag was never committed.
2026-04-15 12:27:18 +02:00
Andrey Antukh 628ce604c5 ♻️ Convert colorpicker and its sub-components to modern rumext * format
slider-selector (slider_selector.cljs):
- Rename to slider-selector*
- Rename prop vertical? to is-vertical
- Remove prop reverse? entirely: it was never passed by any callsite,
  so the related reversal logic in calculate-pos and handler positioning
  is also removed as dead code

value-saturation-selector (ramp.cljs):
- Rename to value-saturation-selector*
- Update internal call site to [:> value-saturation-selector* ...]
- Update slider-selector call sites to [:> slider-selector* ...]

harmony-selector (harmony.cljs):
- Rename to harmony-selector*
- Update slider-selector call sites to [:> slider-selector* ...] with
  renamed is-vertical prop
- Remove stale duplicate :vertical true prop
- Fix spurious extra wrapping vector around the opacity slider in the
  when branch

hsva-selector (hsva.cljs):
- Rename to hsva-selector*
- Update all four slider-selector call sites to [:> slider-selector* ...]
- Remove no-op :reverse? false prop from the value slider

color-inputs (color_inputs.cljs):
- Rename to color-inputs*

colorpicker.cljs:
- Update :refer imports for color-inputs*, harmony-selector*,
  hsva-selector* and libraries*
- Update all corresponding call sites from [:& ...] to [:> ...]
2026-04-15 12:27:18 +02:00
Andrey Antukh 90d052464f ♻️ Convert text-palette components to modern * format
Convert typography-item, palette and text-palette to typography-item*,
palette* and text-palette* using {:keys [...]} destructuring. Rename
prop name-only? to is-name-only in typography-item*. Update internal
call sites to [:> ...] and update the :refer import in palette.cljs.
2026-04-15 12:27:18 +02:00
Andrey Antukh fbee875d75 ♻️ Convert active-sessions to modern * component format
Convert active-sessions to active-sessions* (zero-prop component).
Update call site in right_header.cljs to use [:> ...] and update the
:refer import accordingly.
2026-04-15 12:27:18 +02:00
Andrey Antukh bf7c12ae75 ♻️ Convert coordinates to modern * component format
Convert coordinates to coordinates* using {:keys [...]} destructuring
and rename prop colorpalette? to is-colorpalette. Update call site in
workspace.cljs to use [:> ...] with new prop name.
2026-04-15 12:27:18 +02:00
Andrey Antukh 175f122a0f ♻️ Convert viewport-scrollbars to modern * component format
Convert viewport-scrollbars to viewport-scrollbars* using {:keys [...]}
destructuring and update call sites in viewport.cljs and viewport_wasm.cljs
to use [:> ...].
2026-04-15 12:27:18 +02:00
Andrey Antukh b2f4e90a79 ♻️ Convert shape-distance-segment to modern * component format
Convert shape-distance-segment to shape-distance-segment* using {:keys [...]}
destructuring and update its internal call site in shape-distance to use [:> ...].
2026-04-15 12:27:18 +02:00
Andrey Antukh b4ec0a6d55 🐛 Add missing zoom and page-id dep on snap-feedback use-effect 2026-04-15 12:27:18 +02:00
Elena Torró 9cd1542dd9 Merge pull request #9000 from penpot/niwinz-main-bugfixes
🐛 Add several fixes for app.common.types namespace
2026-04-15 11:51:14 +02:00
Andrey Antukh 2e97f01838 🐛 Fix safe-subvec 2-arity rejecting start=0
The guard used (> start 0) instead of (>= start 0), so
(safe-subvec v 0) returned nil instead of the full vector.
2026-04-15 11:42:49 +02:00
Andrey Antukh 176edadb6f 🐛 Fix nan? returning false for ##NaN on JVM
Clojure's = uses .equals on doubles, and Double.equals(Double.NaN)
returns true, so (not= v v) was always false for NaN. Use
Double/isNaN with a number? guard instead.
2026-04-15 11:42:49 +02:00
Andrey Antukh b26ef158ef 📚 Fix typos in vec2, zip-all, and map-perm docstrings 2026-04-15 11:42:49 +02:00
Andrey Antukh 95d4d42c91 🐛 Add missing string? guard to num-string? on JVM
The CLJS branch of num-string? checked (string? v) first, but the
JVM branch did not. Passing non-string values (nil, keywords, etc.)
would rely on exception handling inside parse-double for control
flow. Add the string? check for consistency and to avoid using
exceptions for normal control flow.
2026-04-15 11:42:49 +02:00
Andrey Antukh bba3610b7b ♻️ Rename shadowed 'fn' parameter to 'pred' in removev
The removev function used 'fn' as its predicate parameter name,
which shadows clojure.core/fn. Rename to 'pred' for clarity and
to follow the naming convention used elsewhere in the namespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh 83da487b24 🐛 Fix append-class producing leading space for empty class
When called with an empty string as the base class, append-class
was producing " bar" (with a leading space) because (some? "")
returns true. Use (seq class) instead to treat both nil and empty
string as absent, avoiding invalid CSS class strings with leading
whitespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh da8e44147c Remove redundant str call in format-number
format-precision already returns a string, so wrapping its result
in an additional (str ...) call was unnecessary.
2026-04-15 11:42:49 +02:00
Andrey Antukh 69e25a4998 📚 Fix typo in namespace docstring ('if' -> 'of') 2026-04-15 11:42:49 +02:00
Andrey Antukh eca9b63d68 Remove redundant map lookups in map-diff
The :else branch of diff-attr was calling (get m1 key) and
(get m2 key) again, but v1 and v2 were already bound to those
exact values. Reuse the existing bindings to avoid the extra
lookups.
2026-04-15 11:42:49 +02:00
Andrey Antukh 29ea1cc495 📚 Fix misleading without-obj docstring
The docstring claimed the function removes nil values in addition to
the specified object, but the implementation only removes elements
equal to the given object. Fix the docstring in both data.cljc and
the local copy in files/changes.cljc.
2026-04-15 11:42:49 +02:00
Andrey Antukh d73ab3ec92 🐛 Fix safe-subvec 3-arity evaluating (count v) before nil check
The 3-arity of safe-subvec called (count v) in a let binding before
checking (some? v). While (count nil) returns 0 in Clojure and does
not crash, the nil guard was dead code. Restructure to check (some? v)
first with an outer when, then compute size inside the guarded block.
2026-04-15 11:42:49 +02:00
Andrey Antukh 1cc860807e Use seq/next idiom in enumerate instead of empty?/rest
Replace (empty? items) + (rest items) with (seq items) + (next items)
in enumerate. The seq/next pattern is idiomatic Clojure and avoids
the overhead of empty? which internally calls seq and then negates.
2026-04-15 11:42:49 +02:00
Andrey Antukh 92dd5d9954 🐛 Fix index-of-pred early termination on nil elements
The index-of-pred function used (nil? c) to detect end-of-collection,
which caused premature termination when the collection contained nil
values. Rewrite using (seq coll) / (next s) pattern to correctly
distinguish between nil elements and end-of-sequence.
2026-04-15 11:42:49 +02:00
Andrey Antukh 057c6ddc0d 🐛 Fix deep-mapm double-applying mfn on leaf entries
The deep-mapm function was applying the mapping function twice on
leaf entries (non-map, non-vector values): once when destructuring
the entry, and again on the already-transformed result in the else
branch. Now mfn is applied exactly once per entry.
2026-04-15 11:42:49 +02:00
Andrey Antukh a2e6abcb72 🐛 Fix spurious argument to dissoc in patch-object
The patch-object function was calling (dissoc object key value) when
handling nil values. Since dissoc treats each argument after the map
as a key to remove, this was also removing nil as a key from the map.
The correct call is (dissoc object key).
2026-04-15 11:42:49 +02:00
Xaviju 431056404c 🎉 Save tokens tree state in local storage (#8922) 2026-04-15 11:24:01 +02:00
Clayton 5dec75fe62 📚 Clarify manifest version 2 for relative plugin asset paths (#8992)
Signed-off-by: Clayton <claytonlin1110@gmail.com>
2026-04-15 10:38:53 +02:00
alonso.torres 988c277e37 🐛 Post-review enhancements 2026-04-15 09:53:36 +02:00
alonso.torres 1d8299a919 🐛 Fix problem with component thumbnails 2026-04-15 09:53:36 +02:00
Eva Marco b0caa15516 🎉 Add test to bug (#8928) 2026-04-15 09:16:16 +02:00
Marina López c63b9583a2 Add callback to nitrate billing url 2026-04-15 08:53:28 +02:00
Juanfran de577a803c 🎉 Add get-org-member-team-counts endpoint to Nitrate API 2026-04-15 08:50:13 +02:00
Andrés MoyaandAndrey Antukh a3ea9fbecb 🔧 Add more validations for components, to avoid some crashes (#7820)
* 🔧 Validate only after propagation in tests

* 💄 Enhance some component sync traces

* 🔧 Add fake uuid generator for debugging

* 🐛 Remove old feature of advancing references when reset changes

Since long time ago, we only allow to reset changes in the top copy
shape. In this case the near and the remote shapes are the same, so
the advance-ref has no effect.

* 🐛 Fix some bugs and add validations, repair and migrations

Also added several utilities to debug and to create scripts that
processes files

* 🐛 Fix misplaced parenthesis passing propagate-fn to wrong function

The :propagate-fn keyword argument was incorrectly placed inside the
ths/get-shape call instead of being passed to tho/reset-overrides.
This caused reset-overrides to never propagate component changes,
making the test not validate what it intended.

* 🐛 Accept and forward :include-deleted? in find-near-match

Callers were passing :include-deleted? true but the parameter was not
in the destructuring, so it was silently ignored and the function
always hardcoded true. This made the API misleading and would cause
incorrect behavior if called with :include-deleted? false.

* 💄 Use set/union alias instead of fully-qualified clojure.set/union

The namespace already requires [clojure.set :as set], so use the alias
for consistency.

* 🐛 Add tests for reset-overrides with and without propagate-fn

Add two focused tests to comp_reset_test to cover the propagate-fn
path in reset-overrides:

- test-reset-with-propagation-updates-copies: verifies that resetting
  an override on a nested copy inside a main and supplying propagate-fn
  causes the canonical color to appear in all downstream copies.

- test-reset-without-propagation-does-not-update-copies: regression
  guard for the misplaced-parenthesis bug; confirms that omitting
  propagate-fn leaves copies with the overridden value because the
  component sync never runs.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 08:42:42 +02:00
Xaviju 909427d442 ♻️ Improve import/export warning semantics (#8991) 2026-04-14 22:41:47 +02:00
Dream dfec9004bf Add delete and duplicate buttons to typography dialog (#8983)
*  Add delete and duplicate buttons to typography dialog

Add delete and duplicate action buttons to the expanded typography
editing panel, allowing users to quickly manage typographies without
needing to close the panel and use the context menu.

Fixes #5270

* ♻️ Use DS icon-button for typography dialog actions

Address review feedback: replace raw `:button`/`:div` elements and
deprecated-icon usage with the design system `icon-button*` and
non-deprecated icons (`i/add`, `i/delete`, `i/tick`).

* ♻️ Only show typography delete/duplicate buttons in assets sidebar

`typography-entry` is reused from the right sidebar text options
panel, where the delete and duplicate actions don't make sense.
Add an `is-asset?` opt-in prop and gate the `on-delete`/`on-duplicate`
handlers behind it, so the buttons only appear when the entry is
rendered from the assets sidebar.

* ♻️ Move typography delete/duplicate handlers next to their use site

Refine the previous opt-in: instead of plumbing on-delete/on-duplicate
function props through typography-entry, build them directly inside
typography-advanced-options where they're actually rendered. The
component now takes :file-id and :is-asset? and gates the action
buttons on a single `show-actions?` flag.

---------

Signed-off-by: eureka0928 <meobius123@gmail.com>
2026-04-14 21:32:56 +02:00
Andrey Antukh 6d1d044588 ♻️ Move app.common.types.color tests to their own namespace
Tests that exercise app.common.types.color were living inside
common-tests.colors-test alongside the app.common.colors tests. Move
them to common-tests.types.color-test so the test namespace mirrors
the source namespace structure, consistent with the rest of the
types/ test suite.

The [app.common.types.color :as colors] require is removed from
colors_test.cljc; the new file is registered in runner.cljc.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 1e0f10814e 🔥 Remove duplicate gradient helpers from app.common.colors
The five functions interpolate-color, offset-spread, uniform-spread?,
uniform-spread, and interpolate-gradient duplicated the canonical
implementations in app.common.types.color. The copies in colors.cljc
also contained two bugs: a division-by-zero in offset-spread when
num=1, and a crash on nil idx in interpolate-gradient.

All production callers already use app.common.types.color. The
duplicate tests that exercised the old copies are removed; their
coverage is absorbed into expanded tests under the types-* suite,
including a new nil-idx guard test and a single-stop no-crash test.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh db7c646568 Add missing tests for session bug fixes and uniform-spread?
Add indexed-access-with-default in fill_test.cljc to cover the two-arity
(nth fills i default) form on both valid and out-of-range indices, directly
exercising the CLJS Fills -nth path fixed in 593cf125.

Add segment-content->selrect-multi-line in path_data_test.cljc to cover
content->selrect on a subpath with multiple consecutive line-to commands
where move-p diverges from from-p, confirming the bounding box matches
both the expected coordinates and the reference implementation; this
guards the calculate-extremities fix in bb5a04c7.

Add types-uniform-spread? in colors_test.cljc to cover
app.common.types.color/uniform-spread?, which had no dedicated tests.
Exercises the uniform case (via uniform-spread), the two-stop edge case,
wrong-offset detection, and wrong-color detection.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh caac452cd4 🐛 Fix wrong extremity point in calculate-extremities for line-to
In the :line-to branch of calculate-extremities, move-p (the subpath
start point) was being added to the extremities set instead of from-p
(the actual previous point). For all line segments beyond the first one
in a subpath this produced an incorrect bounding-box start point.

The :curve-to branch correctly used from-p; align :line-to to match.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 30931839b5 🐛 Fix reversed d/in-range? args in CLJS Fills -nth with default
In the ClojureScript Fills deftype, the two-arity -nth implementation
called (d/in-range? i size) but the signature is (d/in-range? size i).
This meant -nth always fell through to the default value for any valid
index when called with an explicit default, since i < size is the
condition but the args were swapped.

The no-default -nth sibling on line 378 and both CLJ nth impls on
lines 286 and 291 had the correct argument order.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 6da39bc9c7 🐛 Fix ObjectsMap CLJS negative cache keyed on 'key' fn instead of 'k'
In the CLJS -lookup implementation, when a key is absent from data the
negative cache entry was stored under 'key' (the built-in map-entry
key function) rather than the 'k' parameter.  As a result every
subsequent lookup of any missing key bypassed the cache and repeated
the full lookup path, making the negative-cache optimization entirely
ineffective.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 2b67e114b6 🐛 Fix inside-layout? passing id instead of shape to frame-shape?
`(cfh/frame-shape? current-id)` passes a UUID to the single-arity
overload of `frame-shape?`, which expects a shape map; it always
returns false. Fix by passing `current` (the resolved shape) instead.
Update the test to assert the correct behaviour.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 8b08c8ecc9 🐛 Fix wrong mapcat call in collect-main-shapes
`(mapcat collect-main-shapes children objects)` passes `objects` as a
second parallel collection instead of threading it as the second
argument to `collect-main-shapes` for each child. Fix by using an
anonymous fn: `(mapcat #(collect-main-shapes % objects) children)`.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 8253738f01 🐛 Fix reversed get args in convert-dtcg-shadow-composite
\`(get "type" shadow)\` always returns nil because the map and key
arguments were swapped. The correct call is \`(get shadow "type")\`,
which allows the legacy innerShadow detection to work correctly.
Update the test expectation accordingly.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:03 +02:00
Andrey Antukh c30c85ff07 🐛 Remove duplicate font-weight-keys in typography-keys union
font-weight-keys was listed twice in the set/union call for
typography-keys, a copy-paste error. The duplicate entry has no
functional effect (sets deduplicate), but it is misleading and
suggests a missing key such as font-style-keys in its place.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh ff41d08e3c 🐛 Fix stale accumulator in get-children-in-instance recursion
get-children-rec passed the original children vector to each recursive
call instead of the updated one that already includes the current
shape. This caused descendant results to be accumulated from the wrong
starting point, losing intermediate shapes. Pass children' (which
includes the current shape) into every recursive call.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh 08ca561667 🐛 Add better nil handling in interpolate-gradient when offset exceeds stops
When no gradient stop satisfies (<= offset (:offset %)),
d/index-of-pred returns nil. The previous code called (dec nil) in
the start binding before the nil check, throwing a
NullPointerException/ClassCastException. Guard the start binding with
a cond that handles nil before attempting dec.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh 7b0ea5968d 🚑 Fix typo :podition in swap-shapes grid cell
The key :podition was used instead of :position when updating the
id-from cell in swap-shapes, silently discarding the position value
and leaving the cell's :position as nil after every swap.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
rockchris099andAndrey Antukh 8cc05d9579 Show alpha percentage in asset library color names (#8975)
When several library colors share the same RGB value but differ only
in opacity, append the alpha percentage (e.g. "#ff0000 50%") next to
the displayed default name and in the color bullet tooltip so users
can tell them apart at a glance.

Closes #6328

Signed-off-by: rockchris99 <chrisleo0721@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:02:42 +02:00
Andrey Antukh f07b954b7e Add efficiency improvements to workspace components (refactor part 1) (#8887)
* ♻️ Convert snap-points components to modern rumext format

Migrate snap-point, snap-line, snap-feedback, and snap-points from
legacy mf/defc format to modern * suffix format. This enables
optimized props handling by the rumext macro, eliminating implicit
JS object wrapping overhead on each render. All internal and
external call sites updated to use [:> component* props] syntax.

* ♻️ Convert frame-title to modern rumext format

Migrate frame-title from legacy mf/defc format to modern * suffix
format. The component was using legacy implicit props wrapping without
::mf/wrap-props false or ::mf/props :obj, causing unnecessary JS
object conversion overhead on each render. The parent frame-titles*
call site updated to use [:> frame-title* props] syntax.

* ♻️ Convert interactions components to modern rumext format

Migrate interaction-marker, interaction-path, interaction-handle,
overlay-marker, and interactions from legacy mf/defc format to modern
* suffix format. These five components had zero props optimization
applied, causing implicit JS object wrapping on every render. All
internal and external call sites updated to use [:> component* props]
syntax.

* ♻️ Convert rulers components to modern rumext format

Migrate rulers-text, viewport-frame, and selection-area from legacy
mf/defc format to modern * suffix format. These three components in
the always-visible rulers layer had zero props optimization applied.
Internal call sites in the parent rulers component updated to use
[:> component* props] syntax.

* ♻️ Convert frame-grid components to modern rumext format

Migrate square-grid, layout-grid, grid-display-frame, and frame-grid
from legacy mf/defc format to modern * suffix format. These four
components render grid patterns per-frame with zero props optimization.
All internal and external call sites updated to use [:> component* props]
syntax.

* ♻️ Convert gradient handler components to modern rumext format

Migrate shadow, gradient-color-handler, and gradient-handler-transformed
from legacy mf/defc format to modern * suffix format. These components
are rendered during gradient editing with zero props optimization applied.
Internal call sites in gradient-handler-transformed and
gradient-handlers-impl updated to use [:> component* props] syntax.

* ♻️ Rename ?-ending props in modernized workspace viewport components

Apply prop naming rules to all * components migrated in the previous batch:
- remove-snap? -> remove-snap in snap-feedback* (and get-snap helper)
- selected? -> is-selected in interaction-path*
- hover-disabled? -> is-hover-disabled in overlay-marker* and interactions*
- show-rulers? -> show-rulers in viewport-frame*

Update all internal and external call sites consistently.

* 🐛 Fix get-snap call in snap-feedback* using JS props object

Modern rumext *-suffix components receive props as JS objects, not
Clojure maps. snap-feedback* was pushing the raw props object into the
rx/subject and get-snap was destructuring it as a Clojure map, causing
all keys to resolve to nil.

Fix by:
- Changing get-snap to take positional arguments (coord, shapes,
  page-id, remove-snap, zoom) instead of a map-destructured opts arg
- Building an explicit Clojure map from the bound locals before pushing
  to the subject
- Destructuring that map inside the rx/switch-map callback and calling
  get-snap with positional args

Also mark get-snap and add-point-to-snaps as private (defn-), consistent
with the other helpers in the namespace — none are referenced externally.
2026-04-14 19:48:59 +02:00
Andrey Antukh 6c90ba1582 🐛 Fix move-files allowing same project as target when multiple files selected
The 'Move to' menu in the dashboard file context menu only filtered
out the first selected file's project from the available target list.
When multiple files from different projects were selected, the other
files' projects still appeared as valid targets, causing a 400
'cant-move-to-same-project' backend error.

Now all selected files' project IDs are collected and excluded from
the available target projects.
2026-04-14 15:19:19 +02:00
Andrey Antukh 18f0ad246f 🐛 Fix parse-long crash when index query param is duplicated in URL
lambdaisland/uri's query-string->map uses :multikeys :duplicates by
default: a key that appears once yields a plain string, but the same
key repeated yields a vector. cljs.core/parse-long only accepts
strings and therefore threw "Expected string, got: object" whenever
a URL contained a duplicate 'index' parameter.

Add rt/get-query-param to app.main.router. The helper returns the
scalar value of a query param key, taking the last element when the
value is a sequential (i.e. the key was repeated). Use it at every
call site that feeds a query-param value into parse-long, in both
app.main.ui (page*) and app.main.data.viewer.
2026-04-14 15:16:04 +02:00
alonso.torres dc5f222230 🐛 Improve change token set performance 2026-04-14 14:51:12 +02:00
rockchris099andAndrey Antukh 207cb87d5e Reorder prototype overlay options (position before relative to) (#8972)
Closes #2910

Signed-off-by: rockchris99 <chrisleo0721@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 13:56:26 +02:00
Andrey Antukh 650f725f11 📎 Update changelog 2026-04-14 13:43:41 +02:00
ClaytonandAndrey Antukh 39b0e011fc Differentiate incoming and outgoing interaction link colors (#8923)
*  Color incoming and outgoing interaction links differently

* 🐛 Fix lint

---------

Signed-off-by: Clayton <claytonlin1110@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 13:30:22 +02:00
DexterityandAndrey Antukh 7c3a1a905e Fix locked elements not selectable in viewer + add guide locking (#8949)
* 🐛 Allow viewers to select locked elements in canvas

*  Add ability to lock guides to prevent accidental movement

---------

Signed-off-by: Dexterity104 <hatanokanjiro@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 13:21:51 +02:00
Eva Marco 3469e867ff 🐛 Fix gap input throwing an error (#8984) 2026-04-14 13:02:03 +02:00
JamesandAndrey Antukh b211594ce8 🐛 Fix hyphens stripped from export filenames (#8944)
Replace str/slug with a targeted regex that only removes
filesystem-unsafe characters when generating export filenames.
The slug function strips all non-word characters including hyphens,
causing names like "my-board" to become "myboard" on export.

Fixes #8901

Signed-off-by: jamesrayammons <jamesrayammons@outlook.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 12:36:26 +02:00
Andrey Antukh 62f3454607 🔧 Backport ci configuration changes from develop 2026-04-14 12:34:04 +02:00
Andrey Antukh 3264bc746f 🔧 Backport ci configuration changes from develop 2026-04-14 12:33:10 +02:00
DreamandAndrey Antukh 68595e90eb Persist asset search query when switching sidebar tabs (#8985)
When users switch between the Layers and Assets sidebar tabs, the
`assets-toolbox*` component unmounts and its local `use-state` is
discarded, so the search query and section filter are lost.

Lift the search term and section filter into a per-file, in-memory
session atom that survives tab switches but doesn't leak across files
or persist across reloads. Ordering and list-style continue to use
localStorage as before.

Closes #2913

Signed-off-by: eureka0928 <meobius123@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 12:31:25 +02:00
Andrey Antukh 6788df02ca 🔧 Add minor adjustments on ci workflow related to e2e tests 2026-04-14 11:49:16 +02:00
Dream 8b14de2610 Sort asset subfolders alphabetically (#8952)
Subgroups in asset libraries were rendered in hash-map order because
update-in descends into plain maps instead of sorted ones. Add a
recursive post-process that rebuilds every level as a sorted-map so
subfolders are alphabetical at every nesting depth.

Closes #2572

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-14 10:50:46 +02:00
Andrey Antukh a81cded0aa Make the common fressian module more testable (#8859)
*  Add exhaustive unit tests for app.common.fressian encode/decode

Add a JVM-only test suite (41 tests, 172 assertions) for the fressian
serialisation layer, covering:

- All custom handlers: char, clj/keyword, clj/symbol, clj/vector,
  clj/set, clj/map, clj/seq, clj/ratio, clj/bigint, java/instant,
  OffsetDateTime, linked/map (order preserved), linked/set (order preserved)
- Built-in types: nil, boolean, int, long, double (NaN, ±Inf, boundaries),
  String, byte[], UUID
- Edge cases: empty collections, nil values, ArrayMap/HashMap size boundary,
  mixed key types
- Penpot-domain structures: shape maps with UUID keys, nested objects maps
- Correctness: encode→decode→encode idempotency, independent encode calls

* ♻️ Extract fressian handler helpers to private top-level functions

Extract adapt-write-handler, adapt-read-handler, and merge-handlers
out of the letfn in add-handlers! into reusable private functions.
Also creates xf:adapt-write-handler and xf:adapt-read-handler
transducers and adds overwrite-read-handlers and overwrite-write-handlers
for advanced handler override use cases.
2026-04-14 10:48:58 +02:00
Andrey Antukh c39609b991 ♻️ Use shared singleton containers for React portals (#8957)
Refactor use-portal-container to allocate one persistent <div> per
logical category (:modal, :popup, :tooltip, :default) instead of
creating a new div for every component instance. This keeps the DOM
clean with at most four fixed portal containers and eliminates the
arbitrary growth of empty <div> elements on document.body while
preserving the removeChild race condition fix.
2026-04-14 10:48:30 +02:00
StatxcandAndrey Antukh d90e7f8164 Add Find & Replace for text content and layer names (#8899)
*  Add Find & Replace for text content and layer names

* 💄 Fix cross-browser styling for Find & Replace radio buttons and action buttons

* 💄 Fix stylelint empty line before declaration in layers.scss

*  Improve match-filters and match-ids efficiency

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 10:41:31 +02:00
Dream 19b9c696fc 🐛 Reset account submenu state when profile menu closes (#8953)
Closes #8947

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-14 10:33:32 +02:00
Dream 4703fe6e3b Add visibility toggle for strokes (#8913)
*  Add visibility toggle for strokes

* ♻️ Use single emit! call for stroke visibility toggle

* 💄 Disable stroke controls when hidden, matching shadow/blur pattern

When a stroke is hidden, the alignment/style selects, cap selects, and
cap switch button are now disabled. A .hidden CSS class dims the
options area with reduced opacity. This matches the existing behavior
in shadow_row and blur menu where controls are disabled when the
effect is hidden.

* 💄 Move stroke hide button before remove button

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-14 10:08:13 +02:00
Andrey Antukh b3645658fb Merge pull request #8963 from penpot/raguirref-fix/builder-bool-media-validation
🐛 Fix builder bool styles and media validation
2026-04-13 18:35:12 +02:00
Andrey Antukh 9106a994f1 Merge remote-tracking branch 'origin/staging' into develop 2026-04-13 18:31:50 +02:00
Andrey Antukh bc47b992eb Merge remote-tracking branch 'origin/main' into staging 2026-04-13 18:31:32 +02:00
Eva Marco a3f7a1def6 🐛 Fix bugs with multiselection (#8932)
* 🐛 Fix app crash when selecting shapes with one hidden

* 🐛 Fix opacity input when mixed values
2026-04-13 18:29:15 +02:00
Alejandro Alonso 2ccaa3f0c5 Merge remote-tracking branch 'origin/staging' into develop 2026-04-13 16:51:51 +02:00
Alejandro Alonso 367262f5a0 Merge pull request #8959 from penpot/elenatorro-fix-render-wasm-loading
🔧 Improve loading times
2026-04-13 16:50:30 +02:00
Alejandro Alonso dfc5a256b4 Merge remote-tracking branch 'origin/staging' into develop 2026-04-13 16:47:18 +02:00
Elena Torro 6b3d5d930f 🔧 Improve zoom and pan performance 2026-04-13 16:35:36 +02:00
Marina López a52831aa8c Show professional card when has nitrate subscription 2026-04-13 16:07:51 +02:00
rockchris099 bbd200f869 🐛 Fix dashboard Recent/Deleted titles overlapped by scrolling content (#8945)
Add z-index to the sticky .nav element in the dashboard so that
section titles (Recent, Deleted) stay above scrolling content
instead of being obscured by project cards and file thumbnails.

Fixes #8577
Signed-off-by: rockchris99 <chrisleo0721@gmail.com>
2026-04-13 15:55:13 +02:00
Juanfran 87179e806f Add subscribe-nitrate route with post-registration nitrate modal (#8941) 2026-04-13 15:49:22 +02:00
Andrey Antukh e46b34efc7 📎 Fix formatting issues 2026-04-13 15:41:38 +02:00
raguirrefandCopilot 94c6045dd9 🔥 Remove accidental dev_server.pid
Remove unrelated local pid file that was accidentally included in previous commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: raguirref <ricardoaguirredelafuente@gmail.com>
2026-04-13 15:40:40 +02:00
raguirrefandCopilot f656266e5c Fix builder bool and media handling
Fixes three concrete builder issues in common/files/builder:\n- Use bool type from shape when selecting style source for difference bools\n- Persist :strokes correctly (fix typo :stroks)\n- Validate add-file-media params after assigning default id\n\nAlso adds regression tests in common-tests.files-builder-test and registers them in runner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: raguirref <ricardoaguirredelafuente@gmail.com>
2026-04-13 15:40:40 +02:00
Elena Torró a6c3767e2b Merge pull request #8956 from penpot/alotor-poc-nofast-mode
🐛 Fix problem with fast mode
2026-04-13 15:38:04 +02:00
alonso.torres 2d07b9e77c 🐛 Fix problem with fast mode 2026-04-13 15:18:12 +02:00
Andrey Antukh 0fc2050526 ⬆️ Update deps on root package.json 2026-04-13 15:00:47 +02:00
Elena Torro 47eadab82e 🔧 Include DropShadows surface to reset 2026-04-13 14:42:03 +02:00
Elena Torro d85d63ef3c 🔧 Improve page loading 2026-04-13 14:42:03 +02:00
Aitor Moreno 83e9f85ccf Merge pull request #8943 from penpot/ladybenko-13949-fix-resize-call
🐛 Fix initializing guards in viewport loading
2026-04-13 13:37:25 +02:00
Pablo Alba d91ce0f9d1 🐛 Fix nitrate go to control center 2026-04-13 12:30:49 +02:00
Andrey Antukh 28f65fec91 📚 Update changelog 2026-04-13 12:15:17 +02:00
Aitor Moreno 9c44f5bf65 🐛 Fix text editor v1 focus not being handled correctly (#8942) 2026-04-13 12:08:06 +02:00
Eva Marco 443fb60743 🐛 Fix highlight on frames after rename (#8938) 2026-04-13 12:04:04 +02:00
Luis de DiosandAndrey Antukh cbe9d31599 🐛 Fix dashboard navigation tabs overlap with content when scrolling (#8937)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-13 12:01:10 +02:00
Luis de DiosandAndrey Antukh 599a66979a 🐛 Fix dashboard navigation tabs overlap with content when scrolling (#8937)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-13 11:59:19 +02:00
Pablo Alba 5c761125f3 Add invite-to-org to Nitrate API 2026-04-13 11:49:01 +02:00
Andrey Antukh d6045c80a1 💄 Fix docstrings and clarify filter expression in path namespaces
- Fix 'conten' typo to 'content' in path.cljc docstring
- Fix 'curvle' typo to 'curve' in shape_to_path.cljc docstring
- Replace confusing XOR-style filter with readable
  (contains? #{:line-to :curve-to} ...) in bool.cljc
- Align handler-indices and opposite-index docstrings with
  matching API in path.cljc
2026-04-13 11:48:30 +02:00
Andrey Antukh 8d1906f56e 🐛 Fix ^:cosnt typo to ^:const on bool-group-style-properties
The metadata key was misspelled as :cosnt instead of :const,
preventing the compiler from recognizing the Var as a compile-time
constant.
2026-04-13 11:48:30 +02:00
Andrey Antukh 2eaf117b56 🐛 Fix swapped arguments in CLJS PathData -nth with default
The CLJS implementation of PathData's -nth protocol method had
swapped arguments in the 3-arity version (with default value).
The call (d/in-range? i size) should be (d/in-range? size i)
to match the CLJ implementation. With swapped args, valid indices
always returned the default value, and invalid indices attempted
out-of-bounds buffer reads.
2026-04-13 11:48:30 +02:00
Andrey Antukh e511576f66 🐛 Normalize PathData coordinates to safe integer bounds on read
Add normalize-coord helper function that clamps coordinate values to
max-safe-int and min-safe-int bounds when reading segments from PathData
binary buffer. Applies normalization to read-segment, impl-walk,
impl-reduce, and impl-lookup functions to ensure coordinates remain
within safe bounds.

Add corresponding test to verify out-of-bounds coordinates are properly
clamped when reading PathData.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-13 11:48:30 +02:00
Marina López 707cc53ca4 Revert Add can use trial prop in nitrate profile (#8954) 2026-04-13 11:41:32 +02:00
Andrey Antukh a403175d5c 🐛 Fix TypeError in sd-token-uuid when resolving tokens interactively (#8929)
The backtrace-tokens-tree function used a namespaced keyword :temp/id
which clj->js converted to the JS property "temp/id". The sd-token-uuid
function then tried to access .id on the sd-token top-level object,
which was undefined, causing "Cannot read properties of undefined
(reading uuid)".

Fix by using the existing token :id instead of generating a temporary
one, and read it from sd-token.original (matching sd-token-name pattern).
2026-04-13 11:34:15 +02:00
Aitor Moreno bb85b312d6 🐛 Fix text editor v1 focus not being handled correctly (#8942) 2026-04-13 10:00:56 +02:00
Dream 78a16d99a9 Add clear artboard guides option to context menu (#8936)
*  Add clear artboard guides option to context menu

Adds a "Clear artboard guides" option to the right-click context menu
when one or more frames with guides are selected. Closes #6987

* ♻️ Address review feedback from niwinz

- Replace deprecated dm/assert! with assert
- Replace (map :id) with d/xf:map-id

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-13 09:58:23 +02:00
Dream 8dccb2a427 Make links in comments clickable (#8894)
*  Make links in comments clickable

Detect URLs in comment text and render them as clickable links that
open in a new tab. Extends the existing mention parsing to also split
text elements by URL patterns, handling trailing punctuation and
mixed mention+URL content.

Closes #1602

* 📚 Add changelog entry for clickable links in comments

* 🐛 Fix URL elements dropped in comment input initialization

* 🐛 Keep empty text elements in parse-urls to preserve cursor anchors

The remove filter in parse-urls was stripping empty text elements
produced by str/split at URL boundaries. These elements are needed
as cursor anchor spans in the contenteditable input, without them
ESC keydown and visual layout broke.

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-13 09:55:53 +02:00
Eva Marco 6d1a2d449a 🐛 Fix highlight on frames after rename (#8938) 2026-04-13 09:09:03 +02:00
Yamila Moreno e7e5a19db7 🔧 Prevent draft pr from executing the CI (#8934) 2026-04-10 14:43:29 +02:00
Belén Albeza eb811621a9 🐛 Fix initializing guards in viewport loading 2026-04-10 13:54:06 +02:00
Andrés Moya 3312bfe62c Force current set as active when resolving tokens in sidebar 2026-04-10 13:33:29 +02:00
Pablo AlbaandAndrey Antukh ef6eeb5693 🐛 Fix variants corner cases with selrect and points (#8882)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:23:03 +02:00
Marek HrabeandAndrey Antukh 9785a13e67 🐛 Add webp export format to plugin types (#8870)
* 🐛 Add webp export format to plugin types

Align plugin API typings with runtime export support by including 'webp' in
'Export.type' and updating the exported formats documentation.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 📚 Add plugin-types changelog entry for missing webp export format

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

---------

Signed-off-by: Marek Hrabe <marekhrabe@me.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:22:20 +02:00
DexterityandAndrey Antukh 240e8ce50c 🐛 Use page name for multi-export downloads (#8874)
* 🐛 Use page name for multi-export downloads

* ♻️ Refactor parameter formatting in asset export function

*  Use page name for multi-export ZIP/PDF downloads [Github #8773]

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:21:14 +02:00
Dr. Dominik Jain 8101f58651 📚 Add improvements for mcp readme (#8930)
* 📚 Update description of mcp-remote usage

* Use Streamable HTTP endpoint instead of SSE
* Remove redundant global installation

* 📚 Add recommendations on model selection

* 📚 Use new tags in npx launch commands
2026-04-10 10:49:13 +02:00
Xaviju 9e4c8981be 🎉 Duplicate token group (#8886) 2026-04-10 10:42:35 +02:00
Elena Torró a87552bc45 Merge pull request #8926 from penpot/superalex-wasm-render-performance
🎉 Wasm render performance improvements
2026-04-10 09:11:50 +02:00
Marek Hrabe a803bde2ff 🐛 Fix plugin modal dragging bugs (#8871)
* 🐛 Fix plugin modal drag and close interactions

Switch plugin modal dragging to pointer-capture semantics from the header so drag state remains stable when crossing iframe boundaries. Prevent drag start from close-button pointerdown and add regression tests for both non-draggable close-button interaction and close-event dispatch.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 📚 Update changelog for plugin modal drag fix

Document plugin modal drag and close-button interaction fixes in the unreleased changelog.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 🐛 Simplify plugin modal drag CSS selection rules

Keep user-select disabled at the modal wrapper level and keep touch-action scoped to the header drag handle to remove redundant declarations while preserving drag behavior.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

---------

Signed-off-by: Marek Hrabe <marekhrabe@me.com>
2026-04-09 21:13:10 +02:00
Alejandro Alonso 5eebc17ce2 🎉 Support for debugging cache texture 2026-04-09 19:02:14 +02:00
Alejandro Alonso 434e27bbe8 🎉 Improve panning performance 2026-04-09 19:02:14 +02:00
Dexterity e49b7ce14c 🐛 Fix warnings for unsupported token $type (#8873)
* 🐛 Fix warnings for unsupported token $type

Signed-off-by: Dexterity104 <hatanokanjiro@gmail.com>

* 🐛 Add changelog entry for Github #8790

---------

Signed-off-by: Dexterity104 <hatanokanjiro@gmail.com>
Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com>
2026-04-09 17:09:19 +02:00
Alejandro Alonso 5c67cd0a4b 🐛 Avoid unnecesary text editor pointer movements 2026-04-09 16:18:58 +02:00
Yamila Moreno d2050d5331 🔧 Update tests-mcp.yml
Add a more explicit name for a workflow

Signed-off-by: Yamila Moreno <yamila.moreno@kaleidos.net>
2026-04-09 15:38:34 +02:00
Eva Marco 5b78de3594 🐛 Fix selected colors with tokens (#8889) 2026-04-09 14:10:23 +02:00
Xaviju 666313c2c3 🐛 Close expanded tree when switching or creating sets (#8920) 2026-04-09 12:37:29 +02:00
Eva Marco 290f37425f 🐛 Fix id prop on switch component (#8915) 2026-04-09 12:35:34 +02:00
Andrey Antukh ef39afe9b5 Merge remote-tracking branch 'origin/main' into staging 2026-04-09 12:24:18 +02:00
Pablo Alba d65f3b5396 Add nitrate api endpoints to get an user profile 2026-04-09 12:10:06 +02:00
Pablo Alba fe2023dde5 Add nitrate api endpoints to get an user profile 2026-04-09 12:10:06 +02:00
Elena Torró a88f8f1394 Merge pull request #8918 from penpot/niwinz-main-path-preview-issue
🐛 Fix path drawing preview passing shape instead of content to next-node
2026-04-09 11:48:58 +02:00
Andrey Antukh 388775413e 🐛 Fix path drawing preview passing shape instead of content to next-node
In `preview-next-point`, `st/get-path` was called without extra keys,
which returns the full Shape record. That value was then passed directly
to `path/next-node` as its `content` argument.

`path/next-node` delegates to `impl/path-data`, which only accepts a
`PathData` instance, `nil`, or a sequential collection of segments. A
Shape record matches none of those cases, so `path-data` threw
"unexpected data" every time the user moved the mouse while drawing a
path.

The fix is to call `(st/get-path state :content)` so that only the
`:content` field (a `PathData` instance) is extracted and forwarded to
`path/next-node`.
2026-04-09 09:21:57 +00:00
Marina López 1c68810521 Add can use trial prop in nitrate profile 2026-04-09 11:15:21 +02:00
Eva Marco da6bd7509b 🐛 Fix hot reload on color-row text (#8880) 2026-04-09 10:18:21 +02:00
Eva Marco c1d815f97c 🐛 Fix go to viewer with frame selected (#8878) 2026-04-09 10:05:56 +02:00
Dream 21217c5622 Add per-group add button for typographies (#8895)
*  Add per-group add button for typographies

Add a "+" button to each typography group header, allowing users to
create new typographies directly inside a group instead of only at
the top level. The button only appears for local, editable files.

Closes #5275

* 📚 Add changelog entry for typography group add button

* 🐛 Fix typography group title button layout wrapping

* ♻️ Address review feedback for typography group add button

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-09 09:32:56 +02:00
Juanfran e51e0c7933 Add theme field to nitrate authenticate response 2026-04-09 09:19:36 +02:00
Eva Marco 62b59991a9 🔧 Add guard to apply-token (#8879) 2026-04-09 09:16:28 +02:00
Andrey Antukh 5937a8b0fc Merge remote-tracking branch 'origin/staging' into develop 2026-04-09 09:13:02 +02:00
Alejandro Alonso 27449139ad Merge remote-tracking branch 'origin/staging' into develop 2026-04-09 09:05:02 +02:00
Alejandro Alonso 90fcc9f597 Merge pull request #8903 from penpot/alotor-fix-text-grow-problem
🐛 Fix problem with text auto grow in layouts
2026-04-09 08:26:18 +02:00
Dream 5502fe8df3 📚 Add changelog entry for undo/redo selection state (#8896) 2026-04-08 16:05:53 +02:00
Xaviju 10cfd99525 🐛 Fix lint invalid CSS props (#8907)
* 🐛 Fix lint invalid CSS props

* 🐛 Fix named colors in favor of modern notation or custom properties

* 🐛 Removed multiple combined selectors

* 🐛 Convert alpha value to numeric
2026-04-08 15:44:09 +02:00
Eva Marco b8be89f231 🐛 Update onboarding image (#8902) 2026-04-08 11:00:59 +02:00
alonso.torres 0b0e193b70 🐛 Fix problem with text auto grow in layouts 2026-04-08 10:21:32 +02:00
Andrey Antukh 40dfeb169c Merge remote-tracking branch 'origin/staging' into develop 2026-04-07 21:37:21 +02:00
Andrey Antukh 0cc5f7c63e Merge remote-tracking branch 'origin/staging' into develop 2026-04-07 19:28:23 +02:00
DreamandMihai 0c08dfb13d Add the ability for save and restore selection state in undo/redo (#8652)
*  Capture selection state before changes are applied

Save current selection IDs in commit-changes so undo entries
can track what was selected before each action.

*  Save and restore selection state in undo/redo

Extend undo entry with selected-before and selected-after fields.
On undo, restore selection to what it was before the action.
On redo, restore selection to what it was after the action.
Handles single entries, stacked entries, accumulated transactions,
and undo groups.

Fixes #6007

* ♻️ Wire selected-before through workspace undo stream

Pass the captured selection state from commit data into
the undo entry so it is stored alongside changes.

* 🐛 Fix unmatched delimiter in changes.cljs

* 🐛 Pass selected-before through commit event to undo entry

selected-before was captured in commit-changes but dropped by the
commit function since it was missing from the destructuring and the
commit map. This caused restore-selection to receive nil on undo.

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
Co-authored-by: Mihai <noreply@github.com>
2026-04-07 16:30:47 +02:00
Eva Marco 48e8c0bc65 🐛 Fix show resolved value instead of value (#8883) 2026-04-07 14:27:26 +02:00
Pablo Alba 3c639f41c4 Add option to leave a nitrate organization 2026-04-07 11:26:57 +02:00
Eva Marco a5055af538 🐛 Fix hidden on multiple selection (#8854) 2026-04-07 10:58:34 +02:00
Cheonji Kim d5855f355f 📎 Fix typo in README.md for MCP server description (#8884)
Edited line 7(perfom -> perform)

Signed-off-by: Cheonji Kim <76100119+CheonjiKim@users.noreply.github.com>
2026-04-07 10:45:20 +02:00
Alejandro Alonso 83833896c9 Merge remote-tracking branch 'origin/staging' into develop 2026-04-07 10:18:35 +02:00
Andrey Antukh 650762556f Merge remote-tracking branch 'origin/staging' into develop 2026-04-01 11:30:39 +02:00
Andrey Antukh c097c4a6da Merge remote-tracking branch 'origin/staging' into develop 2026-04-01 09:26:05 +02:00
Eva Marco 28cefa9cba 🐛 Fix delay tokens on typography row (#8851) 2026-03-31 14:06:50 +02:00
Eva Marco 5f474f9536 🎉 Add typography token row (#8749)
* 🔧 Create flag

*  Add typography type on tokens by input

* 🎉 Add typography token row

* ♻️ Update sub-components to use new style

* 🎉 Add disabled option on radio-buttons* component

* 🎉 Add combobox search in a new component

* 🎉 Divide components

* 🐛 Fix placeholder
2026-03-31 13:48:49 +02:00
Xaviju 27313e6add 🐛 Fix error on path and review UI (#8844) 2026-03-31 13:04:47 +02:00
Alejandro Alonso 56b28b5440 Merge remote-tracking branch 'origin/staging' into develop 2026-03-31 11:29:44 +02:00
ElenzakaleidosandLuis de Dios 7ecfe77338 Update README.md (#8833)
* 💄 Update README.md

I updated the two images and removed the fest announcement

Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>

* ♻️ Improve Markdown

---------

Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-03-30 16:17:39 +02:00
Eva Marco 04f6307c69 🐛 Fix radio-buttons component in the DS (#8820) 2026-03-30 13:35:24 +02:00
Andrey Antukh 87bb1b8e74 Merge remote-tracking branch 'origin/staging' into develop 2026-03-30 12:29:43 +02:00
Pablo Alba 06aec4b3a3 Add is-default to nitrate summary (#8814) 2026-03-30 09:39:35 +02:00
Xaviju 1b68318c6b 🐛 Token tree must be expanded by default (#8799) 2026-03-30 08:14:17 +02:00
Alejandro Alonso dff381c4fe Merge remote-tracking branch 'origin/staging' into develop 2026-03-27 13:54:38 +01:00
Alejandro Alonso 508c67c930 Merge remote-tracking branch 'origin/staging' into develop 2026-03-27 12:21:30 +01:00
María Valderrama 7f228e58c6 Update delete team modal when in org 2026-03-27 11:36:42 +01:00
Pablo Alba 8cc6c40b87 Update nitrate organizations dropdown visibility 2026-03-27 10:47:47 +01:00
Marina López 1ecfbef6fb ♻️ Refactor forms file 2026-03-27 10:39:47 +01:00
Marina López abe328973c 💄 Fix focus radio button 2026-03-27 10:39:47 +01:00
Alejandro Alonso 19b1f508d3 Merge remote-tracking branch 'origin/staging' into develop 2026-03-27 10:18:01 +01:00
Alejandro Alonso 9c1f2e9af8 Merge remote-tracking branch 'origin/staging' into develop 2026-03-27 10:06:54 +01:00
Marius 342b07779d 🌐 Add translations for: German
Currently translated at 95.1% (1974 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-03-26 20:09:48 +01:00
Pablo Alba 51b9023640 Show nitrate org name on invitation to join team email (#8802) 2026-03-26 17:25:30 +01:00
Pablo Alba 4b4b99a949 Add response to nitrate request when nitrate is down (#8722) 2026-03-26 17:25:07 +01:00
Pablo Alba 1af2521f64 Add create default team org for nitrate on adding an user to a team 2026-03-26 13:17:05 +01:00
Alejandro Alonso 74af101462 Merge remote-tracking branch 'origin/staging' into develop 2026-03-26 11:42:35 +01:00
Marina López 6fa0c5ceaa Add organization avatar 2026-03-26 10:54:55 +01:00
Xaviju 713ff6190b 🔧 Add SCSS linter (stylelint) (#8592)
* 🔧 Add SCSS linter (stylelint)

*  Fix default standard scss errors with extends - WIP

*  Fix default standard scss errors

*  Update and cleanup

*  Update and cleanup

*  Update and cleanup

* 🐛 Fix broken visual regression tests

* 📎 Add to CHANGES

* ♻️ Remove unused class
2026-03-26 10:09:54 +01:00
Marina López cd67dc42c4 🐛 Fix dates to avoid show them in english when browser is in auto (#8775) 2026-03-26 09:33:13 +01:00
Andrey Antukh 0a98100536 Merge remote-tracking branch 'origin/staging' into develop 2026-03-25 12:07:27 +01:00
Alejandro Alonso d361a2ca6e Merge remote-tracking branch 'origin/staging' into develop 2026-03-25 10:42:24 +01:00
Xaviju a59bd05c4f 🐛 Update visual regression tests (#8730) 2026-03-25 09:51:32 +01:00
Alejandro Alonso caa25c70fc Merge remote-tracking branch 'origin/staging' into develop 2026-03-25 09:38:06 +01:00
Andrey Antukh d4bc1d37f2 Merge remote-tracking branch 'origin/staging' into develop 2026-03-24 18:08:23 +01:00
Eva Marco ccd28140bc 📎 Update changelog (#8744) 2026-03-24 12:03:56 +01:00
Marina López be437fbfa1 💄 Fix styles from select organization 2026-03-24 10:22:09 +01:00
Alejandro Alonso 51fa5a5773 Merge remote-tracking branch 'origin/staging' into develop 2026-03-24 10:18:51 +01:00
Marina López 65ea27cbac 💄 Fix styles between grid layout inputs (#8673) 2026-03-23 20:05:13 +01:00
Andrey Antukh 1442e4c246 📎 Update changelog 2026-03-23 19:16:48 +01:00
Renzo 852f9ce07f 🎉 Add drag-to-change for numeric inputs (#8536)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-03-23 19:01:32 +01:00
Eva Marco 7adac6df40 🐛 Fix review comments (#8708)
* 🐛 Fix focus option only on arrowdown not at open

* 🐛 Fix focus on input when visible focus should be on options

* ♻️ Improve nativation, adding tab control and moving throught options is now cyclic

*  Add selected option when inside cursor is inside option

* 🐛 Dropdown is positioned nex to the input alwais
2026-03-23 16:06:23 +01:00
Pablo Alba 11ed09f431 🐛 Fix link to nitrate create org 2026-03-23 12:32:50 +01:00
Eva Marco 4345cfaec7 🎉 Add natural sort on token names (#8672) 2026-03-23 11:24:59 +01:00
Roland bfb331d230 🐛 Fix pluings API theme.addSet() crash caused by async state race in token-set proxy (#8700)
When `catalog.addSet()` creates a new token set, `st/emit!` is async —
the set is not yet in `@st/state` when the returned proxy is used.
Calling `theme.addSet(proxy)` immediately after reads `.name` from the
proxy, which calls `locate-token-set` on stale state → returns nil →
`enable-set` conjs nil into the theme's `:sets` → backend rejects with
400 (`:sets #{nil}`) → workspace reloads → plugin disconnects.

Fix: store `initial-name` in the proxy at construction time as a
fallback for the `:name` getter during the async propagation window.
Also add nil guards in `addSet`/`removeSet` as defense-in-depth.

Closes #8698

Signed-off-by: rodo <roland@dolltons.com>
2026-03-23 11:24:29 +01:00
Eva Marco 72fd637ec2 ♻️ Refactor small numeric inputs (#8660)
* ♻️ Refactor individual border radius inputs

* ♻️ Refactor layer opacity input

* ♻️ Refactor stroke width inputs and add icon only selects

* ♻️ Fix comments on PR
2026-03-23 11:00:29 +01:00
Andrey Antukh dc56da9662 Merge remote-tracking branch 'origin/staging' into develop 2026-03-23 10:15:30 +01:00
Pablo Alba 8406b5e9f8 Add nitrate api for notify org deletion (#8697) 2026-03-23 09:59:57 +01:00
Andres Gonzalez b637f0a917 🐛 Remove wrong lines from changelog 2026-03-23 09:11:09 +01:00
andrés gonzález 35125dfd79 Update changelog (#8703) 2026-03-23 08:48:22 +01:00
Alejandro Alonso 52496243ac Merge remote-tracking branch 'origin/staging' into develop 2026-03-20 17:00:47 +01:00
Alejandro Alonso c6f3aa4f66 Merge pull request #8710 from penpot/superalex-fix-text-finalize-classic-editor
🐛 Restore correct branches in finalize-editor-state for text
2026-03-20 16:59:53 +01:00
Alejandro Alonso 62b36f0153 🐛 Restore correct branches in finalize-editor-state for text 2026-03-20 16:48:37 +01:00
Juanfran e53ff6d20b Open create org modal in Nitrate 2026-03-20 16:19:29 +01:00
Alejandro Alonso 02afd805ca Merge remote-tracking branch 'origin/staging' into develop 2026-03-20 16:00:24 +01:00
María Valderrama 9c3fbc59b9 🐛 Fix visibility of go to nitrate cc option 2026-03-20 13:42:45 +01:00
Alejandro Alonso f068842a6c Merge remote-tracking branch 'origin/staging' into develop 2026-03-20 10:20:43 +01:00
Eva Marco 71b32b97f0 🔧 Activate flag on dev enviroment (#8706) 2026-03-20 10:13:05 +01:00
Eva Marco fb5ac5cd8b 🐛 Add box shadow to token dropdowns (#8685) 2026-03-20 09:02:27 +01:00
Xaviju ee1dd80b6e Copy token name from contextual menu (#8566) 2026-03-19 23:22:44 +01:00
Xaviju 8ad62c6800 🐛 Add export menu to inspect styles tab (#8645)
* 🐛 Add export menu to inspect styles tab

* 📎 Add to CHANGES
2026-03-19 23:20:18 +01:00
Xaviju f8913c755d 🎉 Rename token group (#8275)
* 🎉 Rename token group

* 📎 Add to CHANGES
2026-03-19 22:54:21 +01:00
Eva Marco 8e7e6ffc2f ♻️ Design review for numeric inputs (#8630)
* ♻️ Update tooltip position on icon buttons

* ♻️ Sort token groups by priority not alphabetically

* ♻️ Add proper padding on text-icon-inputs

* ♻️ Hide detach button when dropdown is open

* 🐛 Fix detach stroke width

* 🐛 Fix strokes applied on all rows

* 🐛 Fix nillable inputs

* 🐛 Fix comments on PR
2026-03-19 16:46:18 +01:00
BitTobyandbittoby b876417d5b Add copy and paste for grid layout rows and columns via co… (#8498)
*  Add copy and paste for grid layout rows and columns via context menu

* 🔧 Use grid-id instead of grid in context menu deps

---------

Co-authored-by: bittoby <bittoby@users.noreply.github.com>
2026-03-18 16:19:15 +01:00
Pablo Alba 2a09f30199 Add nitrate endpoint to delete teams keeping your-penpot projects 2026-03-18 15:59:38 +01:00
Andrey Antukh ca72dcdcbb Merge remote-tracking branch 'origin/staging' into develop 2026-03-18 15:00:40 +01:00
Eva Marco df8194acf5 🐛 Fix several bugs (#8604)
* 🐛 Fix console warning

* ♻️ Use DS buttons and remove deprecated CSS

* 🐛 Fix copy on update library message

* 🐛 Fix id prop on switch component

* 🐛 Fix tooltip shown after tab change
2026-03-18 12:52:58 +01:00
Pablo Alba 04a3e236fe Add a callback-url parameter to login (#8655) 2026-03-18 10:15:31 +01:00
Andrey Antukh 5482ee211e 🐛 Fix unexpected corner case between SES hardening and transit (#8663)
* Revert "🐛 Fix plugin sandbox freezing CLJS Proxy constructor breaking Transit encoding"

This reverts commit 27a934dcfd.

* 🐛 Fix unexpected corner case between SES hardening and transit

The cause of the issue is a race condition between plugin loading
and the first time js/Date objects are encoded using transit. Transit
encoder populates the prototype of the Date object the first time a
Date instance is encoded, but if SES freezes the Date prototype before
transit, an strange exception will be raised on encoding any object
that contains Date instances.

Example of the exception:

Cannot define property transit$guid$4a57baf3-8824-4930-915a-fa905479a036,
object is not extensible
2026-03-18 09:53:22 +01:00
Dr. Dominik Jain 0f24cf26f6 Reduce instructions transferred at MCP connection to a minimum (#8649)
*  Reduce instructions transferred at MCP connection to a minimum

Force on-demand loading of the 'Penpot High-Level Overview',
which was previously transferred in the MCP server's instructions.

This greatly reduces the number of tokens for users who will
not actually interact with Penpot, allowing the MCP server to
remain enabled for such users without wasting too many tokens.

Resolves #8647

* 📎 Update Serena project
2026-03-17 18:48:06 +01:00
Andrey Antukh 4da332a5e2 Merge remote-tracking branch 'origin/staging' into develop 2026-03-17 18:29:08 +01:00
Pablo Alba 5eecd52743 Add get-teams-summary to nitrate api (#8662) 2026-03-17 18:25:18 +01:00
Andrey Antukh 3c92c98c94 Revert several changes to mcp scripts introduced in previous commits 2026-03-17 15:30:26 +01:00
girafic d6cc469027 🐛 Fix permission message and update ruler guide proxy name on plugins api (#8632)
- Updated the error message for missing content write permission in the removeRulerGuide function.
- Renamed the ruler guide proxy from "RuleGuideProxy" to "RulerGuideProxy" for consistency.
- Adjusted variable naming in the addRulerGuide function for clarity.

Signed-off-by: Stas Haas <stas@girafic.de>
2026-03-17 15:05:26 +01:00
Andrey Antukh 7480be0bda 🐛 Fix mcp bundle build issue introduced in previous commits 2026-03-17 14:51:51 +01:00
Andrey Antukh b86898eaf9 Revert "🐛 Fix "Cannot assign to read only property toString" error in plugins runtime"
This reverts commit f796f7ccb9.
2026-03-17 14:45:18 +01:00
Andrey Antukh e018253c6b Make mcp plugin always ready to be in multiuser 2026-03-17 14:45:18 +01:00
David Barragán Merino 1b223359d9 🔧 Remove staging-render bundle github workflow 2026-03-17 11:18:51 +01:00
Andrey Antukh f796f7ccb9 🐛 Fix "Cannot assign to read only property toString" error in plugins runtime
The error "Cannot assign to read only property 'toString' of function"
occurs during React's commit phase after a plugin is loaded. The root
cause is an initialization ordering issue in the SES (Secure EcmaScript)
lockdown sequence.

When loadPlugin() is called, ses.harden(context) runs first, which
transitively freezes everything reachable from the context object —
including Function.prototype and Object.prototype — via prototype chain
traversal of getter functions. Later, createSandbox() calls
ses.hardenIntrinsics(), which attempts to run enablePropertyOverrides()
to convert frozen data properties (like Function.prototype.toString)
into accessor pairs that work around JavaScript's "override mistake".
However, enablePropertyOverrides checks "if (configurable)" before
converting, and since Function.prototype is already frozen (all
properties have configurable: false), the override taming is silently
skipped. This leaves Function.prototype.toString as a frozen
non-writable data property, causing any subsequent code that assigns
.toString to a function instance in strict mode to throw a TypeError.

The fix calls ses.hardenIntrinsics() before ses.harden(context) in
loadPlugin(), ensuring override taming installs the accessor pairs on
prototype properties before they get frozen. The existing
hardenIntrinsics() call in createSandbox() becomes a harmless no-op
thanks to the idempotency guard.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 10:14:12 +01:00
Andrey Antukh 27a934dcfd 🐛 Fix plugin sandbox freezing CLJS Proxy constructor breaking Transit encoding
When the plugin sandbox calls harden() (SES lockdown) on any proxy object
returned from the penpot.* API, SES traverses the prototype chain up to
Proxy.prototype and freezes the CLJS Proxy constructor function. Transit's
typeTag helper later fails with "object is not extensible" when trying to
set its cache property on that frozen constructor.

Fix by deleting the constructor data property from Proxy.prototype so that
harden never traverses to the CLJS Proxy constructor function.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 10:01:45 +01:00
Pablo Alba acc383ba31 Improve nitrate module JSON handling and error management 2026-03-17 09:59:02 +01:00
Andrey Antukh 46f50aab16 Merge remote-tracking branch 'origin/staging' into develop 2026-03-16 16:13:16 +01:00
David Barragán Merino 31696de474 🔧 GitHub Actions worker tasks updated 2026-03-16 15:02:26 +01:00
Marina López 1b8871df8e Update image nitrate modal 2026-03-16 14:23:23 +01:00
Pablo Alba 8cb5c23a29 🐛 Fix nitrate url 2026-03-16 13:35:15 +01:00
Dream ce04780b6c 🐛 Make collapsible sidebar titles clickable to toggle (#8547)
Fixes #5168
2026-03-16 11:03:49 +01:00
andrés gonzález 98e989d7f3 📚 Adjust MCP presence in changelog (#8642) 2026-03-16 10:51:10 +01:00
Dr. Dominik Jain f566c1950f Account for changed interfaces of addToken and addSet (#8614)
Resolves #8613
2026-03-16 10:38:25 +01:00
Pablo Alba 8f35e451e6 Add notification for nitrate when creating a team inside an organization (#8639) 2026-03-16 10:36:32 +01:00
Andrey Antukh 6e19548bac 📎 Update changelog 2026-03-16 09:38:23 +01:00
Denys Kisil 0719e4fa70 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 99.7% (2068 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-03-09 20:09:49 +00:00
deveronica aca63802e1 🌐 Add translations for: Korean
Currently translated at 99.8% (2070 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-03-05 22:13:51 +01:00
deveronica 380d211b4c 🌐 Add translations for: Korean
Currently translated at 48.8% (1013 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-03-04 21:10:10 +01:00
deveronica 0be5119b21 🌐 Add translations for: Korean
Currently translated at 14.1% (294 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-03-03 19:09:49 +00:00
deveronica 8e17f846c9 🌐 Add translations for: Korean
Currently translated at 12.0% (250 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-03-02 19:09:49 +01:00
Denys Kisil 8262b7a3a2 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 99.7% (2068 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-03-02 19:09:47 +01:00
Egor Filatov bce52c6da8 🌐 Add translations for: Russian
Currently translated at 80.0% (1660 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-03-01 10:09:48 +01:00
Alexis Morin cd3a1d6376 🌐 Add translations for: French (Canada)
Currently translated at 99.7% (2068 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-28 01:09:48 +01:00
Denys Kisil c769e782f0 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 99.7% (2068 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-02-26 15:10:03 +01:00
Alexis Morin ed97cdde66 🌐 Add translations for: French (Canada)
Currently translated at 96.4% (2001 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-26 15:09:51 +01:00
Alexis Morin db2689efc9 🌐 Add translations for: French (Canada)
Currently translated at 91.1% (1891 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-24 03:09:48 +01:00
Alexis Morin 5770a1fdc9 🌐 Add translations for: French (Canada)
Currently translated at 86.5% (1796 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-22 16:09:49 +00:00
Alexis Morin 41003310f8 🌐 Add translations for: French (Canada)
Currently translated at 85.3% (1771 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-21 14:09:48 +00:00
Yaron Shahrabani 4c416b7c18 🌐 Add translations for: Hebrew
Currently translated at 99.7% (2068 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-02-20 04:09:52 +01:00
Alexis Morin 7df10e2238 🌐 Add translations for: French (Canada)
Currently translated at 84.2% (1748 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-20 04:09:50 +01:00
Yaron Shahrabani 0e182cff18 🌐 Add translations for: Hebrew
Currently translated at 99.3% (2060 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-02-18 14:10:00 +00:00
Egor Filatov b4db1df62f 🌐 Add translations for: Russian
Currently translated at 78.7% (1634 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-02-18 14:09:55 +00:00
Alexis Morin 010074df4b 🌐 Add translations for: French (Canada)
Currently translated at 73.8% (1532 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-18 14:09:52 +00:00
Edgars Andersons 89f0e282ec 🌐 Add translations for: Latvian
Currently translated at 90.9% (1887 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-02-18 14:09:47 +00:00
Oğuz Ersen 06bb2b98a9 🌐 Add translations for: Turkish
Currently translated at 99.8% (2070 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-02-17 11:10:12 +01:00
Alexis Morin 836616a05b 🌐 Add translations for: French (Canada)
Currently translated at 71.7% (1489 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-17 11:10:07 +01:00
Yaron Shahrabani 53d5fcd8d0 🌐 Add translations for: Hebrew
Currently translated at 97.1% (2015 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-02-17 11:09:59 +01:00
Stephan Paternotte 1c062b4cd0 🌐 Add translations for: Dutch
Currently translated at 99.8% (2070 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-02-17 11:09:56 +01:00
nautilusx 627854fbba 🌐 Add translations for: German
Currently translated at 94.6% (1964 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-02-17 11:09:52 +01:00
Nicola Bortoletto 73b7c0ee5d 🌐 Add translations for: Italian
Currently translated at 99.5% (2065 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-02-17 11:09:49 +01:00
2634 changed files with 270364 additions and 94404 deletions

No files matched your search

+3
View File
@@ -88,6 +88,9 @@
:dynamic-var-not-earmuffed
{:level :off}
:type-mismatch
{:level :off}
:used-underscored-binding
{:level :warning}
+133
View File
@@ -0,0 +1,133 @@
# `.devenv/` — Per-Workspace AI-Client MCP Configs
This directory carries the pieces needed to point an AI coding agent
(currently Claude Code, opencode, VS Code Copilot, and the OpenAI Codex CLI)
at the MCP servers running inside the parallel devenv instance the developer
is currently working in. Every parallel workspace (`ws0`, `ws1`, …) has its
own copy because the Penpot MCP and Serena MCP host ports are
workspace-specific.
## Layout
```
.devenv/
README.md
scripts/
merge-mcp-config.py # generator helper invoked by manage.sh
shared/ # committed; workspace-independent entries
claude-code.json # Playwright — same for every workspace
opencode.json
vscode.json
codex.toml
templates/ # committed; entries with ${...} port placeholders
claude-code.json # Penpot MCP, Serena MCP — port is the only diff
opencode.json
vscode.json
codex.toml
mcp/ # gitignored; written by manage.sh per workspace
claude-code.json # loaded via Claude Code's --mcp-config flag
opencode.json # loaded via OPENCODE_CONFIG env var
```
One more file is generated outside `.devenv/`, in the directory VS Code itself
auto-discovers (gitignored):
```
.vscode/mcp.json # auto-loaded by GitHub Copilot in VS Code
```
Codex is the exception: it has no way to load an MCP config from an arbitrary
path, and its only project-level config file (`.codex/config.toml`) is one a
developer may already own. So we do **not** write a file for Codex at all —
`start-coding-agent codex` injects our servers as `-c` command-line overrides
built fresh from `shared/codex.toml` + `templates/codex.toml` at launch.
* **`shared/`** holds MCP entries that don't depend on the workspace — the
browser-driving Playwright server today, plus any other workspace-independent
servers we add later. Same content in every workspace, so it's a static
checked-in file.
* **`templates/`** holds the workspace-specific entries (Penpot MCP, Serena
MCP) with `${PENPOT_MCP_PORT}` and `${SERENA_MCP_PORT}` placeholders. The
placeholders are resolved per-workspace from the port-base constants in
`manage.sh`.
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
the port-substituted `templates/`. `manage.sh` writes these on every
`run-devenv --agentic` pass. Gitignored, dedicated paths with no developer
content — never edit by hand, your edits will be overwritten on the next
reconcile.
* **`.vscode/mcp.json`** is the same merge, but written to the path VS Code
auto-discovers. Because on `ws0` that path *is* the live repo's own file, the
reconcile **deep-merges** into it: any servers you added yourself are kept,
and only the entries we manage (`penpot`, `serena-devenv`, `playwright`) are
(re)written to the current ports. On `ws1+` the file doesn't exist yet, so it
is created from scratch.
* **`scripts/merge-mcp-config.py`** is the generator. Its `json` mode does the
JSON deep-merge (with `--merge-into-existing` for the VS Code path); its
`codex-args` mode prints the `-c` assignments for Codex. `manage.sh`'s
`_merge-mcp-config-json` helper is a thin shim over the former, and
`start-coding-agent` calls the latter directly. Run
`python3 .devenv/scripts/merge-mcp-config.py --help` for the CLI.
## Launching a coding agent
The easiest path is the wrapper command, which knows the right flags per
client, `cd`'s into the target workspace, and refuses to launch unless the
target instance is running and its MCP config has been generated:
```bash
# Default target is ws0 (the live repo).
./manage.sh start-coding-agent claude [...args to forward]
./manage.sh start-coding-agent opencode [...args to forward]
./manage.sh start-coding-agent vscode [...args to forward to 'code']
./manage.sh start-coding-agent codex [...args to forward]
# Target a parallel workspace with --ws N. N is an integer (non-negative);
# 'main', 'ws1' and similar spellings are rejected.
./manage.sh start-coding-agent claude --ws 1
./manage.sh start-coding-agent opencode --ws 2
```
Equivalents by hand (run from inside the workspace directory):
```bash
claude --mcp-config .devenv/mcp/claude-code.json
OPENCODE_CONFIG=.devenv/mcp/opencode.json opencode
code "$PWD" # VS Code auto-discovers .vscode/mcp.json
# Codex: pass our servers as -c overrides (no config file is written).
codex $(python3 .devenv/scripts/merge-mcp-config.py --format codex-args \
.devenv/shared/codex.toml .devenv/templates/codex.toml \
| sed 's/^/-c /')
```
`start-coding-agent codex` does the `-c` wiring for you (and resolves the
workspace's ports first). Because our servers arrive as command-line
overrides, no "trusted project" prompt is involved for them — that prompt only
gates Codex's own `.codex/config.toml`, which we never write.
## Overriding our entries
Both the auto-discovered configs and the launcher-loaded configs sit *on top
of* the developer's global config (with varying precedence rules). All four
clients offer escape hatches for shadowing entries we ship:
* **Claude Code** — `claude mcp add --scope local …` installs a private entry
that overrides the one in `mcp/claude-code.json`. Local scope wins.
* **opencode** — drop an `opencode.json` at the repo root with the override
entries you need. opencode's precedence chain is *global → `OPENCODE_CONFIG`
→ project*, so the project file always wins. The root `opencode.json` is
gitignored on purpose, since these overrides are personal.
* **VS Code Copilot** — the reconcile deep-merges into `.vscode/mcp.json`, so
any servers you add there yourself are preserved (only `penpot`,
`serena-devenv` and `playwright` are rewritten). To shadow one of *ours*,
put an entry under the same name in your VS Code user-profile MCP config —
it is loaded alongside the workspace file and wins.
* **Codex CLI** — our servers arrive as `-c` overrides, which are Codex's
highest-precedence layer, so they win over a same-named `[mcp_servers.<name>]`
in your `~/.codex/config.toml` or a project `.codex/config.toml`. To override
one of ours, append your own `-c` after the client name — extra args are
forwarded after ours and the later `-c` wins, e.g.
`./manage.sh start-coding-agent codex -- -c 'mcp_servers.penpot.url="…"'`.
See `docs/technical-guide/developer/agentic-devenv.md` for the broader
client-configuration story (browser remote debugging, AI-client config
schemas, manual setup for unsupported clients).
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Combine a shared MCP-server config with a port-substituted template for one
AI coding-agent client.
Invoked per workspace by manage.sh's `write-instance-mcp-configs` (JSON
clients) and by `start-coding-agent` (Codex). Each supported client ships a
`.devenv/shared/<tool>.{json,toml}` (workspace-independent entries, e.g.
Playwright) and a `.devenv/templates/<tool>.{json,toml}` (per-workspace entries
with `${PENPOT_MCP_PORT}` / `${SERENA_MCP_PORT}` placeholders). This script
combines the two for the target client.
Two output modes are supported:
json Deep-merge two JSON documents under a configurable top-level key
(`mcpServers` for Claude Code, `mcp` for opencode, `servers` for
VS Code Copilot) and write the result to <out>. Same-name
entries in the template override entries in shared. With
--merge-into-existing, any pre-existing <out> file is loaded as
the lowest-precedence layer first, so entries the developer
already had are preserved (ours win on name collision). This is
used for VS Code's auto-discovered `.vscode/mcp.json`, which on
ws0 IS the live repo's file and may hold the developer's own
servers; the Claude/opencode outputs live in a dedicated,
gitignored `.devenv/mcp/` path and are written without the flag
(a clean overwrite).
codex-args Deep-merge the two TOML chunks and print one
`dotted.key=<toml-value>` assignment per line to stdout (no
<out> file). The caller wraps each line in a `codex -c` flag.
Codex has no way to load an MCP config from an arbitrary file
path (CODEX_HOME would relocate auth/history too), so rather than
writing the auto-discovered `.codex/config.toml` we inject our
servers as ephemeral per-invocation overrides. This never
touches the developer's project- or user-level Codex config.
In both modes, `${VAR}` placeholders inside *either* chunk are resolved from
the current environment (only template chunks carry placeholders in practice,
but the substitution is uniform either way) using Python's
`os.path.expandvars`. Undefined placeholders are left as `${VAR}` literal text
-- callers (i.e. manage.sh) are responsible for exporting the variables before
invoking the script.
Usage:
merge-mcp-config.py --format json --key <key> [--merge-into-existing] \
<shared> <template> <out>
merge-mcp-config.py --format codex-args <shared> <template>
Exit codes:
0 success
2 argparse error (missing required option, bad value, unreadable input)
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import tomllib
from pathlib import Path
def merge_json(
shared_path: Path,
tpl_path: Path,
out_path: Path,
key: str,
merge_into_existing: bool,
) -> None:
"""Deep-merge JSON documents under a single top-level dict key into out.
Precedence (lowest to highest): an existing <out> file (only when
merge_into_existing is set), then shared, then the template. Entries under
`key` are merged by name, so the template wins on a name collision while
every other entry the lower layers contributed is kept. Top-level keys
other than `key` come from the existing file and shared (shared wins).
"""
shared = json.loads(shared_path.read_text())
tpl = json.loads(os.path.expandvars(tpl_path.read_text()))
base: dict = {}
if merge_into_existing and out_path.exists():
base = json.loads(out_path.read_text())
merged: dict = {**base, **shared}
merged[key] = {**base.get(key, {}), **shared.get(key, {}), **tpl.get(key, {})}
out_path.write_text(json.dumps(merged, indent=2) + "\n")
def _deep_merge(base: dict, overlay: dict) -> dict:
"""Recursively merge overlay into base; overlay wins on scalar/list keys."""
out = dict(base)
for k, v in overlay.items():
if isinstance(out.get(k), dict) and isinstance(v, dict):
out[k] = _deep_merge(out[k], v)
else:
out[k] = v
return out
def _toml_value(value: object) -> str:
"""Serialize a scalar/list as a TOML literal for a `codex -c` value.
bool is checked before int because `isinstance(True, int)` is True. Strings
are emitted as JSON strings, which are valid TOML basic strings for the
ASCII values our configs carry (commands, args, URLs). Tables never reach
here -- they are flattened into dotted keys by _flatten.
"""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return repr(value)
if isinstance(value, str):
return json.dumps(value)
if isinstance(value, list):
return "[" + ", ".join(_toml_value(v) for v in value) + "]"
raise TypeError(f"unsupported TOML value type: {type(value).__name__}")
_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
def _key_segment(seg: str) -> str:
"""A dotted-key segment: bare if TOML-safe, else a quoted key."""
return seg if _BARE_KEY.match(seg) else json.dumps(seg)
def _flatten(obj: dict, prefix: list[str]):
"""Yield (dotted-path-segments, leaf-value) for every non-table leaf.
Lists are leaves (TOML arrays), so we do not recurse into them; nested
tables (e.g. an `env` table) are flattened into further dotted keys.
"""
for k, v in obj.items():
path = prefix + [k]
if isinstance(v, dict):
yield from _flatten(v, path)
else:
yield path, v
def emit_codex_args(shared_path: Path, tpl_path: Path) -> None:
"""Print `dotted.key=<toml-value>` lines from the merged TOML chunks."""
shared = tomllib.loads(os.path.expandvars(shared_path.read_text()))
tpl = tomllib.loads(os.path.expandvars(tpl_path.read_text()))
merged = _deep_merge(shared, tpl)
for path, value in _flatten(merged, []):
dotted = ".".join(_key_segment(s) for s in path)
sys.stdout.write(f"{dotted}={_toml_value(value)}\n")
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description=__doc__.split("\n\n", 1)[0],
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--format",
choices=("json", "codex-args"),
required=True,
help="Output mode: 'json' writes a merged file; 'codex-args' prints -c assignments.",
)
parser.add_argument(
"--key",
help="Top-level JSON key under which MCP entries live (required for --format json).",
)
parser.add_argument(
"--merge-into-existing",
action="store_true",
help="json only: layer the merge on top of an existing <out> file, "
"preserving entries already there (ours still win on name collision).",
)
parser.add_argument("shared", type=Path, help="Path to the shared chunk.")
parser.add_argument("template", type=Path, help="Path to the port-placeholder template chunk.")
parser.add_argument(
"out",
type=Path,
nargs="?",
help="Path the merged result is written to (json only; codex-args writes stdout).",
)
args = parser.parse_args(argv)
if args.format == "json":
if not args.key:
parser.error("--key is required when --format json")
if args.out is None:
parser.error("out is required when --format json")
merge_json(args.shared, args.template, args.out, args.key, args.merge_into_existing)
else: # codex-args
if args.key:
parser.error("--key is not accepted when --format codex-args")
if args.merge_into_existing:
parser.error("--merge-into-existing is not accepted when --format codex-args")
if args.out is not None:
parser.error("out path is not accepted when --format codex-args (result goes to stdout)")
emit_codex_args(args.shared, args.template)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# Workspace-independent MCP servers for the OpenAI Codex CLI.
# This block is concatenated with the port-substituted templates/codex.toml
# by manage.sh's write-instance-mcp-configs to produce .codex/config.toml at
# the workspace root.
[mcp_servers.playwright]
command = "npx"
args = ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
+9
View File
@@ -0,0 +1,9 @@
{
"mcp": {
"playwright": {
"type": "local",
"command": ["npx", "@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"],
"enabled": true
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"servers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"mcpServers": {
"penpot": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:${PENPOT_MCP_PORT}/mcp", "--allow-http"]
},
"serena-devenv": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:${SERENA_MCP_PORT}/mcp", "--allow-http"]
}
}
}
+10
View File
@@ -0,0 +1,10 @@
# Workspace-specific MCP servers for the OpenAI Codex CLI. The PENPOT_MCP_PORT
# and SERENA_MCP_PORT placeholders below are filled in per workspace by
# manage.sh's write-instance-mcp-configs, then the result is concatenated
# with shared/codex.toml to produce .codex/config.toml.
[mcp_servers.penpot]
url = "http://localhost:${PENPOT_MCP_PORT}/mcp"
[mcp_servers.serena-devenv]
url = "http://localhost:${SERENA_MCP_PORT}/mcp"
+14
View File
@@ -0,0 +1,14 @@
{
"mcp": {
"penpot": {
"type": "remote",
"url": "http://localhost:${PENPOT_MCP_PORT}/mcp",
"enabled": true
},
"serena-devenv": {
"type": "remote",
"url": "http://localhost:${SERENA_MCP_PORT}/mcp",
"enabled": true
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"servers": {
"penpot": {
"type": "http",
"url": "http://localhost:${PENPOT_MCP_PORT}/mcp"
},
"serena-devenv": {
"type": "http",
"url": "http://localhost:${SERENA_MCP_PORT}/mcp"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# Penpot API configuration for error-reports CLI tool
PENPOT_API_URI=http://localhost:3450
PENPOT_ACCESS_TOKEN=your-access-token-here
+3 -2
View File
@@ -1,7 +1,8 @@
description: Create a report to help us improve
labels: ["bug"]
name: Bug report
title: "bug: "
type: Bug
labels: ["needs triage"]
body:
- type: markdown
attributes:
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+3 -2
View File
@@ -1,7 +1,8 @@
description: Suggest an idea for this project.
labels: ["needs triage", "enhancement"]
labels: ["needs triage"]
name: "Feature request"
title: "feature: "
type: Enhancement
body:
- type: markdown
attributes:
@@ -1,38 +0,0 @@
---
name: New Render Bug Report
about: Create a report about the bugs you have found in the new render
title: ''
labels: new render
assignees: claragvinola
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or screen recordings**
If applicable, add screenshots or screen recording to help illustrate your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
-1
View File
@@ -15,6 +15,5 @@
- [ ] Add or modify existing integration tests in case of bugs or new features, if applicable.
- [ ] Refactor any modified SCSS files following the refactor guide.
- [ ] Check CI passes successfully.
- [ ] Update the `CHANGES.md` file, referencing the related GitHub issue, if applicable.
<!-- For more details, check the contribution guidelines: https://github.com/penpot/penpot/blob/develop/CONTRIBUTING.md -->
+76
View File
@@ -0,0 +1,76 @@
name: Auto Label and Add to Project
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: triage-app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.TRIAGE_APP_ID }}
private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }}
owner: penpot
- name: Process Issue or PR
uses: actions/github-script@v7
with:
github-token: ${{ steps.triage-app-token.outputs.token }}
script: |
// === 1. CONFIGURATION ===
const PROJECT_NUMBER = 8; // <--- Replace with your project board number
const IS_ORG = true; // <--- Set to false if this is a personal project, true if an organization
const OWNER = context.repo.owner;
const REPO = context.repo.repo;
const issueNumber = context.issue.number;
const isPR = !!context.payload.pull_request;
const contentId = isPR ? context.payload.pull_request.node_id : context.payload.issue.node_id;
// Define your labels here
const labelToApply = 'needs triage';
// === 2. APPLY THE LABEL ===
console.log(`Applying label "${labelToApply}" to ${isPR ? 'PR' : 'Issue'} #${issueNumber}...`);
await github.rest.issues.addLabels({
issue_number: issueNumber,
owner: OWNER,
repo: REPO,
labels: [labelToApply]
});
// === 3. ADD TO PROJECT BOARD ===
console.log(`Fetching Project #${PROJECT_NUMBER} ID...`);
const projectQuery = `
query($owner: String!, $number: Int!) {
${IS_ORG ? 'organization' : 'user'}(login: $owner) {
projectV2(number: $number) {
id
}
}
}
`;
const projectRes = await github.graphql(projectQuery, { owner: OWNER, number: PROJECT_NUMBER });
const projectId = IS_ORG ? projectRes.organization.projectV2.id : projectRes.user.projectV2.id;
console.log(`Adding item to project board...`);
const addToProjectMutation = `
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item {
id
}
}
}
`;
await github.graphql(addToProjectMutation, { projectId, contentId });
console.log("Automation successfully completed!");
+75 -33
View File
@@ -9,16 +9,6 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
workflow_call:
inputs:
gh_ref:
@@ -26,25 +16,21 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
jobs:
build-bundle:
name: Build and Upload Penpot Bundle
# ── 1. Decide whether there is anything to build ───────────────────────
check:
name: Check current bundle
runs-on: penpot-runner-01
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
timeout-minutes: 10
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Checkout repository
@@ -59,10 +45,52 @@ jobs:
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
# The uploaded zip carries its version as S3 metadata. If the
# existing object was already built from this same commit, the
# whole build job is skipped.
- name: Check if this bundle is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
EXISTING_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
--query 'Metadata."bundle-version"' \
--output text 2>/dev/null || echo "none")
if [ "$EXISTING_VERSION" = "${{ steps.vars.outputs.bundle_version }}" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Bundle build skipped"
echo ""
echo "The bundle in S3 was already built from \`${{ steps.vars.outputs.bundle_version }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
# ── 2. Build and upload, only when needed ──────────────────────────────
build:
name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01
timeout-minutes: 90
needs: check
if: needs.check.outputs.exists == 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Build bundle
env:
BUILD_WASM: ${{ inputs.build_wasm }}
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
BUILD_WASM: 'yes'
BUILD_STORYBOOK: 'yes'
run: ./manage.sh build-bundle
- name: Prepare directories for zipping
@@ -76,18 +104,32 @@ jobs:
zip -r zips/penpot.zip penpot
- name: Upload Penpot bundle to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
aws s3 cp zips/penpot.zip \
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-01
timeout-minutes: 5
needs: [check, build]
if: failure()
steps:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 📦 *[PENPOT] Error building penpot bundles.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
📄 Triggered from ref: `${{ needs.check.outputs.gh_ref || inputs.gh_ref }}`
Bundle version: `${{ needs.check.outputs.bundle_version || 'n/a' }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
+6 -2
View File
@@ -11,8 +11,6 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -20,3 +18,9 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "develop"
@@ -0,0 +1,91 @@
name: Admin Console Docker Builder
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
default: 'develop'
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
secrets:
ORG_WORKFLOW_TOKEN:
description: 'Token with Actions write access on penpot-nitrate'
required: true
jobs:
build-nitrate-docker:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.ORG_WORKFLOW_TOKEN }}
REPO: penpot/penpot-nitrate
WORKFLOW: build-docker-admin-console.yml
GH_REF: ${{ inputs.gh_ref }}
DISPATCH_REF: ${{ inputs.dispatch_ref }}
steps:
- name: Trigger nitrate docker build
id: dispatch
run: |
DISTINCT_ID="${{ github.run_id }}-${{ github.run_attempt }}"
CALLER_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
-f gh_ref="$GH_REF" \
-f caller_run_id="$DISTINCT_ID" \
-f caller_run_url="$CALLER_URL"
# Locate the dispatched run using the correlation id embedded in its run-name
RUN_ID=""
for i in $(seq 1 24); do
sleep 5
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" \
--limit 10 --json databaseId,displayTitle \
--jq ".[] | select(.displayTitle | contains(\"$DISTINCT_ID\")) | .databaseId" \
| head -n1)
[ -n "$RUN_ID" ] && break
done
if [ -z "$RUN_ID" ]; then
echo "::error::Could not locate the dispatched run in $REPO"
exit 1
fi
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT"
echo "::notice title=Nitrate docker build::$RUN_URL"
- name: Wait for nitrate docker build
run: |
gh run watch "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" \
--interval 30 \
--exit-status
- name: Report result
if: always() && steps.dispatch.outputs.run_id != ''
run: |
CONCLUSION=$(gh run view "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" --json conclusion --jq '.conclusion')
{
echo "### 🐳 Nitrate docker build"
echo ""
echo "- Result: \`${CONCLUSION:-in_progress}\`"
echo "- Run: ${{ steps.dispatch.outputs.run_url }}"
} >> "$GITHUB_STEP_SUMMARY"
+23 -5
View File
@@ -6,7 +6,6 @@ on:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
environment: release-admins
runs-on: penpot-runner-02
steps:
@@ -19,16 +18,23 @@ jobs:
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
- name: Login to Docker Registry (push destination)
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'penpotapp/devenv'
with:
@@ -36,6 +42,18 @@ jobs:
file: ./docker/devenv/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
tags: ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🚀 *[PENPOT] New devenv available*
📄 You may want to update your devenv.
@alvaro
+214 -115
View File
@@ -16,57 +16,119 @@ on:
required: true
default: 'develop'
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
env:
ALL_IMAGES: backend frontend exporter storybook mcp
# All runner instances live on the same server, so the bundle is
# downloaded from S3 once and shared between build jobs through this
# host-local directory. Each build job falls back to S3 if the file is
# missing (e.g. if runners ever move to separate machines).
BUNDLE_CACHE: /var/tmp/penpot-bundle-cache
jobs:
build-and-push:
name: Build and Push Penpot Docker Images
# ── 1. Resolve the build key and check the whole set at once ───────────
prepare:
name: Prepare
runs-on: penpot-runner-02
timeout-minutes: 15
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
sha: ${{ steps.vars.outputs.sha }}
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short=12 HEAD)" >> $GITHUB_OUTPUT
BUNDLE_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-$GH_REF.zip" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
# The image set is a single block, so a single set-level check is
# enough: `promote` drops a marker object in S3 only after every
# image was built AND every branch tag was moved. Marker present
# means there is nothing at all to do for this commit.
- name: Check if this image set is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "markers/images-sha-${{ steps.vars.outputs.sha }}" \
> /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Image set build skipped"
echo ""
echo "The whole set was already built and promoted for \`sha-${{ steps.vars.outputs.sha }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
# Stage the bundle in the host-local cache, once, for all the
# build jobs. Download to a temp name and mv for atomicity;
# prune stale bundles while at it.
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.bundle_version }}.zip"
if [ ! -f "$ZIP" ]; then
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
fi
# ── 2. One build per image, in parallel, only when needed ──────────────
build:
name: Build ${{ matrix.image }}
runs-on: penpot-runner-02
timeout-minutes: 60
needs: prepare
if: needs.prepare.outputs.exists == 'false'
strategy:
fail-fast: true
# 4 runner slots are available for build jobs on this server; cap the
# matrix at 3 so short jobs (prepare and other workflows' checks)
# never queue behind long builds.
max-parallel: 3
matrix:
image: [backend, frontend, exporter, storybook, mcp]
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
id: bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
tmp=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "$FILE_NAME" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
@@ -76,108 +138,145 @@ jobs:
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Images now build FROM Docker Hardened Images (dhi.io). DHI
# is free (Apache 2.0, no subscription), but pulling from it
# still requires an authenticated login -- a separate `docker
# login` against a different registry host, even though it
# reuses the same PUB_DOCKER_* credentials as the DockerHub
# login above.
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Bundle staged once by `prepare` on this host; the S3 fallback only
# triggers if the cache is unavailable (runners on another machine,
# cache pruned mid-run, ...).
- name: Prepare Penpot bundle
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.bundle_version }}.zip"
if [ ! -f "$ZIP" ]; then
echo "Bundle not found in host cache; falling back to S3."
mkdir -p "$BUNDLE_CACHE"
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.prepare.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
# Extract only the bundle this job needs.
pushd docker/images
unzip -q "$ZIP" "penpot/${{ matrix.image }}/*"
mv "penpot/${{ matrix.image }}" "bundle-${{ matrix.image }}"
popd
- name: Set up QEMU (stable)
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images:
frontend
backend
exporter
storybook
mcp
images: ${{ matrix.image }}
labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }}
bundle_version=${{ needs.prepare.outputs.bundle_version }}
- name: Build and push Backend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.backend
file: ./docker/images/Dockerfile.${{ matrix.image }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
provenance: mode=max
sbom: true
# Immutable tag only; branch tags are moved atomically for the
# whole image set by the `promote` job.
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:sha-${{ needs.prepare.outputs.sha }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
# ── 3. Move the branch tags of ALL images together ─────────────────────
# Runs only when every build succeeded (default `needs` semantics); if
# the set was already complete, `build` is skipped and so is this job —
# the S3 marker guarantees the branch tags were already moved.
promote:
name: Promote image set
runs-on: penpot-runner-02
timeout-minutes: 10
needs: [prepare, build]
steps:
- name: Set common environment variables
run: |
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.frontend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v6
- name: Point branch tags to the new build key
run: |
set -e
for image in $ALL_IMAGES; do
docker buildx imagetools create \
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
"${{ secrets.DOCKER_REGISTRY }}/$image:sha-${{ needs.prepare.outputs.sha }}"
done
# The marker is written LAST: its presence certifies that all five
# images exist and all branch tags point to this build key.
- name: Write set-completed marker
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.exporter
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
echo "${{ github.run_id }}" | aws s3 cp - \
"s3://${{ secrets.S3_BUCKET }}/markers/images-sha-${{ needs.prepare.outputs.sha }}"
{
echo "### ✅ Image set promoted"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`sha-${{ needs.prepare.outputs.sha }}\`."
} >> "$GITHUB_STEP_SUMMARY"
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.storybook
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
# ── 4. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-02
timeout-minutes: 5
needs: [prepare, build, promote]
if: failure()
steps:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🐳 *[PENPOT] Error building penpot docker images.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
❌ 🐳 *[PENPOT] Error building/promoting the penpot docker image set.*
📄 Triggered from ref: `${{ needs.prepare.outputs.gh_ref || inputs.gh_ref }}`
📦 Bundle: `${{ needs.prepare.outputs.bundle_version || 'n/a' }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
@@ -1,15 +0,0 @@
name: _STAGING RENDER
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"
+6 -2
View File
@@ -11,8 +11,6 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -20,3 +18,9 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "staging"
+1 -4
View File
@@ -12,8 +12,6 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -26,10 +24,9 @@ jobs:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+3
View File
@@ -6,12 +6,14 @@ on:
- edited
- reopened
- synchronize
- ready_for_review
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
push:
branches:
- main
@@ -20,6 +22,7 @@ on:
jobs:
check-commit-message:
if: ${{ !github.event.pull_request.draft }}
name: Check Commit Message
runs-on: ubuntu-latest
steps:
+2 -2
View File
@@ -62,7 +62,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
@@ -131,7 +131,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+2 -2
View File
@@ -62,7 +62,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
@@ -114,7 +114,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
@@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- id: filter
uses: dorny/paths-filter@v3
uses: dorny/paths-filter@v4
with:
filters: |
colors_to_tokens:
@@ -60,7 +60,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
@@ -129,7 +129,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+5 -5
View File
@@ -19,7 +19,6 @@ permissions:
jobs:
release:
environment: release-admins
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.vars.outputs.gh_ref }}
@@ -63,14 +62,15 @@ jobs:
echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io
IMAGES=("frontend" "backend" "exporter" "storybook")
IMAGES=("frontend" "backend" "exporter" "mcp" "storybook")
SHORT_TAG=${TAG%.*}
for image in "${IMAGES[@]}"; do
skopeo copy --all \
docker://$DOCKER_REGISTRY/$image:$TAG \
docker://docker.io/penpotapp/$image:$TAG
for alias in main latest; do
for alias in main latest "$SHORT_TAG"; do
skopeo copy --all \
docker://$DOCKER_REGISTRY/$image:$TAG \
docker://docker.io/penpotapp/$image:$alias
@@ -93,7 +93,7 @@ jobs:
# --- Create GitHub release ---
- name: Create GitHub release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -103,7 +103,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+85
View File
@@ -0,0 +1,85 @@
name: "CI: Backend"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'backend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'backend/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-backend:
if: ${{ !github.event.pull_request.draft }}
name: "Backend Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
services:
postgres:
image: postgres:17
# Provide the password for postgres
env:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: valkey/valkey:9
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Fmt
working-directory: ./backend
run: |
cljfmt check --parallel=true src/ test/
- name: Lint
working-directory: ./backend
run: |
clj-kondo --parallel --lint ../common/src/ src/
- name: Tests
working-directory: ./backend
env:
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://redis/1"
run: |
mkdir -p /tmp/penpot;
clojure -M:dev:test
+57
View File
@@ -0,0 +1,57 @@
name: "CI: Common"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-common:
if: ${{ !github.event.pull_request.draft }}
name: "Common Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint
working-directory: ./common
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run check-fmt:js
pnpm run lint:clj
- name: Tests
working-directory: ./common
run: |
./scripts/test
@@ -0,0 +1,69 @@
name: "CI: Composable Test Suite"
# Runs the composable component test suite (it exercises component semantics
# through the real Plugin API against the full frontend, so it needs the
# frontend bundle + the plugin runtime, but no backend): the driver serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
- name: Run composable test suite (mocked)
working-directory: ./plugins
run: pnpm --filter composable-test-suite run test:ci
+58
View File
@@ -0,0 +1,58 @@
name: "CI: Exporter"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'exporter/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'exporter/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-exporter:
if: ${{ !github.event.pull_request.draft }}
name: "Exporter Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run lint:clj
- name: Tests
working-directory: ./exporter
run: |
./scripts/test
+71
View File
@@ -0,0 +1,71 @@
name: "CI: Frontend"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-frontend:
if: ${{ !github.event.pull_request.draft }}
name: "Frontend Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:js
pnpm run check-fmt:clj
pnpm run check-fmt:scss
pnpm run lint:clj
pnpm run lint:js
pnpm run lint:scss
- name: Unit Tests
working-directory: ./frontend
run: |
./scripts/test
- name: Component Tests
working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: |
./scripts/test-components
+124
View File
@@ -0,0 +1,124 @@
name: "CI: Integration"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Build Bundle
working-directory: ./frontend
run: |
./scripts/build
- name: Store Bundle Cache
uses: actions/cache@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
env:
PLAYWRIGHT_REPORTER: list,json
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
run: |
./scripts/test-e2e
- name: Flaky summary
if: always()
working-directory: ./frontend
run: |
if [ ! -f report.json ]; then
echo "No report.json produced (the run failed early)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -r '
[ .. | objects
| select(has("tests") and has("file"))
| select(any(.tests[]; .status == "flaky"))
| "- `\(.file):\(.line)` — \(.title)"
] as $f
| "## Flaky tests: \($f | length)\n"
+ (if ($f | length) == 0 then "_none_" else ($f | join("\n")) end)
' report.json >> "$GITHUB_STEP_SUMMARY"
- name: Upload JSON report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-json-report
path: frontend/report.json
overwrite: true
retention-days: 30
- name: Upload test result
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result
path: frontend/test-results/
overwrite: true
retention-days: 3
+58
View File
@@ -0,0 +1,58 @@
name: "CI: Library"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'common/**'
- 'library/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'common/**'
- 'library/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-library:
if: ${{ !github.event.pull_request.draft }}
name: "Library Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint
working-directory: ./library
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
- name: Tests
working-directory: ./library
run: |
./scripts/test
+10 -3
View File
@@ -1,4 +1,4 @@
name: "MCP CI"
name: "CI: MCP"
on:
pull_request:
@@ -10,6 +10,7 @@ on:
types:
- opened
- synchronize
- ready_for_review
paths:
- 'mcp/**'
@@ -24,8 +25,9 @@ on:
- 'mcp/**'
jobs:
test:
name: "Test"
test-mcp:
if: ${{ !github.event.pull_request.draft }}
name: "Test MCP"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
@@ -43,3 +45,8 @@ jobs:
pnpm run fmt:check;
pnpm -r run build;
pnpm -r run types:check;
- name: Tests
working-directory: ./mcp
run: |
pnpm run test;
@@ -0,0 +1,133 @@
name: "CI: Plugin API Test Suite"
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
# needs a running frontend + the plugin runtime). Two jobs:
#
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
# binding + in-memory store; backend-result-dependent tests are skipped via the
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
#
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
# credentials via repo secrets. Manual because the CI runner has no Docker to
# stand up a full stack.
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
base_url:
description: "Penpot base URL (e.g. https://localhost:3449)"
required: false
default: "https://localhost:3449"
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/src/app/plugins/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
- name: Generate API surface
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite run gen:api
- name: Run API test suite (mocked)
working-directory: ./plugins
env:
MOCK_BACKEND: "1"
run: pnpm --filter plugin-api-test-suite run test:ci
## The following job will launch the whole suite of tests but we need
## to have a full environment in the CI for this to work.
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-runner-02
# container:
# image: penpotapp/devenv:latest
#
# env:
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
#
# steps:
# - uses: actions/checkout@v6
#
# - name: Setup Node
# uses: actions/setup-node@v6
# with:
# node-version-file: .nvmrc
#
# - name: Install deps
# working-directory: ./plugins
# run: |
# corepack enable;
# corepack install;
# pnpm install;
#
# - name: Install Playwright Chromium
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
#
# - name: Generate API surface
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run gen:api
#
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
# - name: Run API test suite
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run test:ci
+77
View File
@@ -0,0 +1,77 @@
name: "CI: Plugins"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'plugins/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-plugins:
if: ${{ !github.event.pull_request.draft }}
name: Plugins Runtime Linter & Tests
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install -r;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build:runtime
- name: Build doc
working-directory: ./plugins
run: pnpm run build:doc
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
+57
View File
@@ -0,0 +1,57 @@
name: "CI: WASM"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'render-wasm/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'render-wasm/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-render-wasm:
if: ${{ !github.event.pull_request.draft }}
name: "Render WASM Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Format
working-directory: ./render-wasm
run: |
cargo fmt --check
- name: Lint
working-directory: ./render-wasm
run: |
./lint
- name: Test
working-directory: ./render-wasm
run: |
./test
-402
View File
@@ -1,402 +0,0 @@
name: "CI"
defaults:
run:
shell: bash
on:
pull_request:
types:
- opened
- synchronize
push:
branches:
- develop
- staging
concurrency:
group: ${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: "Linter"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint Common
working-directory: ./common
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run check-fmt:js
pnpm run lint:clj
- name: Lint Frontend
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:js
pnpm run check-fmt:clj
pnpm run check-fmt:scss
pnpm run lint:clj
pnpm run lint:js
pnpm run lint:scss
- name: Lint Backend
working-directory: ./backend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
- name: Lint Exporter
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
- name: Lint Library
working-directory: ./library
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
test-common:
name: "Common Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run tests
working-directory: ./common
run: |
./scripts/test
test-plugins:
name: Plugins Runtime Linter & Tests
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- uses: actions/checkout@v6
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build:runtime
- name: Build doc
working-directory: ./plugins
run: pnpm run build:doc
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
test-frontend:
name: "Frontend Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: test-render-wasm
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Restore shared.js
uses: actions/cache/restore@v4
with:
key: "render-wasm-shared-js-${{ github.sha }}"
path: frontend/src/app/render_wasm/api/shared.js
- name: Unit Tests
working-directory: ./frontend
run: |
./scripts/test
- name: Component Tests
working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: |
./scripts/test-components
test-render-wasm:
name: "Render WASM Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Format
working-directory: ./render-wasm
run: |
cargo fmt --check
- name: Lint
working-directory: ./render-wasm
run: |
./lint
- name: Test
working-directory: ./render-wasm
run: |
./test
- name: Copy shared.js artifact
working-directory: ./render-wasm
run: |
SHARED_FILE=$(find target -name render_wasm_shared.js | head -n 1);
mkdir -p ../frontend/src/app/render_wasm/api;
cp $SHARED_FILE ../frontend/src/app/render_wasm/api/shared.js;
- name: Cache shared.js
uses: actions/cache@v4
with:
key: "render-wasm-shared-js-${{ github.sha }}"
path: frontend/src/app/render_wasm/api/shared.js
test-backend:
name: "Backend Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
services:
postgres:
image: postgres:17
# Provide the password for postgres
env:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: valkey/valkey:9
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run tests
working-directory: ./backend
env:
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://redis/1"
run: |
clojure -M:dev:test --reporter kaocha.report/documentation
test-library:
name: "Library Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run tests
working-directory: ./library
run: |
./scripts/test
build-integration:
name: "Build Integration Bundle"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Build Bundle
working-directory: ./frontend
run: |
./scripts/build
- name: Store Bundle Cache
uses: actions/cache@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="1/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-1
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-2:
name: "Integration Tests 2/4"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="2/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-2
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-3:
name: "Integration Tests 3/4"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="3/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-3
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-4:
name: "Integration Tests 4/4"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="4/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-4
path: frontend/test-results/
overwrite: true
retention-days: 3
+28 -2
View File
@@ -9,12 +9,22 @@
.clj-kondo
.cpcache
.lsp
.env
.nrepl-port
.nyc_output
.rebel_readline_history
.repl
opencode.json
.opencode/package-lock.json
/*.jpg
/*.md
!CHANGES.md
!CONTRIBUTING.md
!README.md
!AGENTS.md
!CODE_OF_CONDUCT.md
!SECURITY.md
!HIGHLIGHTS.md
/*.png
/*.svg
/*.sql
@@ -24,6 +34,8 @@
/.clj-kondo/.cache
/_dump
/notes
/plans
/prompts
/playground/
/backend/*.md
!/backend/AGENTS.md
@@ -50,6 +62,7 @@
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
/frontend/playwright/ui/visual-specs/
/frontend/text-editor/src/wasm/
/frontend/dist/
/frontend/npm-debug.log
@@ -62,8 +75,6 @@
/frontend/target/
/frontend/test-results/
/frontend/.shadow-cljs
/other/
/scripts/
/nexus/
/tmp/
/vendor/**/target
@@ -78,6 +89,21 @@
/blob-report/
/playwright/.cache/
/render-wasm/target/
/media-processor/dist/
/**/node_modules
/**/.yarn/*
/.pnpm-store
/.vscode
/.idea
*.iml
/.claude
/CLAUDE.md
/.playwright-mcp
/.devenv/mcp/
/opencode.json
/.opencode/plans
/.opencode/reports
/.opencode/prompts
/.ci-logs
/.codex/
/tools/__pycache__
+1 -1
View File
@@ -1 +1 @@
v22.22.0
v24.19.0
-37
View File
@@ -1,37 +0,0 @@
---
name: engineer
description: Senior Full-Stack Software Engineer
mode: primary
---
Role: You are a high-autonomy Senior Full-Stack Software Engineer working on
Penpot, an open-source design tool. You have full permission to navigate the
codebase, modify files, and execute commands to fulfill your tasks. Your goal is
to solve complex technical tasks with high precision while maintaining a strong
focus on maintainability and performance.
Tech stack: Clojure (backend), ClojureScript (frontend/exporter), Rust/WASM
(render-wasm), TypeScript (plugins/mcp), SCSS.
Requirements:
* Read the root `AGENTS.md` to understand the repository and application
architecture. Then read the `AGENTS.md` **only** for each affected module.
Not all modules have one — verify before reading.
* Before writing code, analyze the task in depth and describe your plan. If the
task is complex, break it down into atomic steps.
* When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
`.gitignore` by default.
* Do **not** touch unrelated modules unless the task explicitly requires it.
* Only reference functions, namespaces, or APIs that actually exist in the
codebase. Verify their existence before citing them. If unsure, search first.
* Be concise and autonomous — avoid unnecessary explanations.
* After making changes, run the applicable lint and format checks for the
affected module before considering the work done (see module `AGENTS.md` for
exact commands).
* Make small and logical commits following the commit guideline described in
`CONTRIBUTING.md`. Commit only when explicitly asked.
- Do not guess or hallucinate git author information (Name or Email). Never include the
`--author` flag in git commands unless specifically instructed by the user for a unique
case; assume the local environment is already configured. Allow git commit to
automatically pull the identity from the local git config `user.name` and `user.email`.
-37
View File
@@ -1,37 +0,0 @@
---
name: testing
description: Senior Software Engineer specialized on testing
mode: primary
---
Role: You are a Senior Software Engineer specialized in testing Clojure and
ClojureScript codebases. You work on Penpot, an open-source design tool.
Tech stack: Clojure (backend/JVM), ClojureScript (frontend/Node.js), shared
Cljc (common module), Rust (render-wasm).
Requirements:
* Read the root `AGENTS.md` to understand the repository and application
architecture. Then read the `AGENTS.md` **only** for each affected module. Not all
modules have one — verify before reading.
* Before writing code, describe your plan. If the task is complex, break it down into
atomic steps.
* Tests should be exhaustive and include edge cases relevant to Penpot's domain:
nil/missing fields, empty collections, invalid UUIDs, boundary geometries, Malli schema
violations, concurrent state mutations, and timeouts.
* Tests must be deterministic — do not use `setTimeout`, real network calls, or rely on
execution order. Use synchronous mocks for asynchronous workflows.
* Use `with-redefs` or equivalent mocking utilities to isolate the logic under test. Avoid
testing through the UI (DOM); e2e tests cover that.
* Only reference functions, namespaces, or test utilities that actually exist in the
codebase. Verify their existence before citing them.
* After adding or modifying tests, run the applicable lint and format checks for the
affected module before considering the work done (see module `AGENTS.md` for exact
commands).
* Make small and logical commits following the commit guideline described in
`CONTRIBUTING.md`. Commit only when explicitly asked.
- Do not guess or hallucinate git author information (Name or Email). Never include the
`--author` flag in git commands unless specifically instructed by the user for a unique
case; assume the local environment is already configured. Allow git commit to
automatically pull the identity from the local git config `user.name` and `user.email`.
+39
View File
@@ -0,0 +1,39 @@
---
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
agent: build
---
This command is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context. Follow these steps in order.
## 1. Create the issue
Use the **`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body from the
plan. Capture the new issue's number — call it **NNNN** (needed for the branch
name and the commit reference).
## 2. Create the branch
Create and switch to a branch named after the issue:
```
git checkout -b issue-NNNN
```
(Replace NNNN with the issue number from step 1.)
## 3. Execute the plan
Implement the prepared plan from the session context. Work methodically, keeping
changes focused on what the issue requires. Do not commit — the commit happens in
step 4.
## 4. Commit with the create-commit skill
After the implementation is complete, load the **`create-commit`** skill and
follow its workflow to commit the changes. Provide a brief summary of what was
implemented and why, the issue reference (`issue-NNNN`), and the model name you
are running as so the `AI-assisted-by` trailer is set correctly.
Do not push. Pushing is handled separately by the user.
@@ -0,0 +1,40 @@
---
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
agent: build
---
# Fix Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
+299
View File
@@ -0,0 +1,299 @@
import { tool } from "@opencode-ai/plugin"
import path from "path"
import { spawn } from "child_process"
const penpotPsqlTool = tool({
description:
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
args: {
sql: tool.schema
.string()
.describe("SQL command to execute"),
test: tool.schema
.boolean()
.describe("Use the penpot_test database")
.optional(),
},
async execute(args, context) {
const host = process.env.PENPOT_DB_HOST || "postgres"
const user = process.env.PENPOT_DB_USER || "penpot"
const db = args.test
? "penpot_test"
: process.env.PENPOT_DB_NAME || "penpot"
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql]
return new Promise((resolve) => {
let stdout = ""
let stderr = ""
const proc = spawn("psql", psqlArgs, {
cwd: context.worktree,
env: { ...process.env, PGPASSWORD: password },
})
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "Query executed successfully"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
})
},
})
const parenRepairTool = tool({
description:
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
args: {
// A string is used instead of an array so OpenCode displays it
// in the generic tool invocation.
files: tool.schema
.string()
.describe(
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
)
.optional(),
code: tool.schema
.string()
.describe("Code string to fix via stdin")
.optional(),
},
async execute(args, context) {
const script = path.join(context.worktree, "scripts/paren-repair")
const files = args.files
? args.files
.split(",")
.map((file) => file.trim())
.filter(Boolean)
: []
const paramInfo =
files.length > 0
? `files=[${files.join(", ")}]`
: args.code !== undefined
? `code=(${args.code.length} chars)`
: "none"
return new Promise((resolve) => {
const childArgs =
files.length > 0
? [script, ...files]
: [script]
const proc = spawn("bb", childArgs, {
cwd: context.worktree,
})
let stdout = ""
let stderr = ""
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "No changes needed"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
// Close stdin in all cases so the process cannot wait indefinitely.
if (args.code !== undefined) {
proc.stdin.end(args.code)
} else {
proc.stdin.end()
}
})
},
})
export default async function plugin() {
return {
tool: {
"paren-repair": parenRepairTool,
"penpot-psql": penpotPsqlTool,
},
}
}
// import { tool } from "@opencode-ai/plugin"
// import path from "path"
// import { spawn } from "child_process"
// function formatFiles(files) {
// if (files.length === 0) return "stdin"
// // Keep the visible tool title reasonably short.
// if (files.length <= 3) return files.join(", ")
// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)`
// }
// const parenRepairTool = tool({
// description:
// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.",
// args: {
// files: tool.schema
// .array(tool.schema.string())
// .describe("Array of file paths to fix")
// .optional(),
// code: tool.schema
// .string()
// .describe("Code string to fix via stdin")
// .optional(),
// },
// async execute(args, context) {
// const script = path.join(context.worktree, "scripts/paren-repair")
// const files = (args.files ?? []).map((file) => {
// const absolute = path.isAbsolute(file)
// ? file
// : path.resolve(context.worktree, file)
// return path.relative(context.worktree, absolute)
// })
// const targetSummary =
// files.length > 0
// ? formatFiles(files)
// : args.code !== undefined
// ? `stdin (${args.code.length} chars)`
// : "no input"
// // This updates the tool-call title immediately, while it is running.
// await context.metadata({
// title: `Paren repair: ${targetSummary}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// },
// })
// const childArgs =
// args.files && args.files.length > 0
// ? [script, ...args.files]
// : [script]
// return new Promise((resolve) => {
// const proc = spawn("bb", childArgs, {
// cwd: context.worktree,
// })
// let stdout = ""
// let stderr = ""
// if (args.code !== undefined) {
// proc.stdin.end(args.code)
// }
// proc.stdout.on("data", (data) => {
// stdout += data.toString()
// })
// proc.stderr.on("data", (data) => {
// stderr += data.toString()
// })
// proc.on("close", (exitCode) => {
// const successful = exitCode === 0
// const commandOutput = successful
// ? stdout.trim() || "No changes needed"
// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}`
// const parameterOutput =
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : args.code !== undefined
// ? `Input passed through stdin: ${args.code.length} characters`
// : "No files or stdin input were passed"
// resolve({
// title: `Paren repair: ${targetSummary}`,
// output: `${parameterOutput}\n\n${commandOutput}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// exitCode,
// successful,
// },
// })
// })
// proc.on("error", (error) => {
// resolve({
// title: `Paren repair failed: ${targetSummary}`,
// output: [
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : `Input: ${targetSummary}`,
// `Failed to start bb: ${error.message}`,
// ].join("\n\n"),
// metadata: {
// files,
// codeChars: args.code?.length,
// successful: false,
// },
// })
// })
// })
// },
// })
// export default async function plugin() {
// return {
// tool: {
// "paren-repair": parenRepairTool,
// },
// }
// }
+210
View File
@@ -0,0 +1,210 @@
---
name: bat-cat
description: A cat clone with syntax highlighting, line numbers, and Git integration - a modern replacement for cat.
homepage: https://github.com/sharkdp/bat
metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"id":"brew","kind":"brew","formula":"bat","bins":["bat"],"label":"Install bat (brew)"},{"id":"apt","kind":"apt","package":"bat","bins":["bat"],"label":"Install bat (apt)"}]}}
---
# bat - Better cat
`cat` with syntax highlighting, line numbers, and Git integration.
## Quick Start
### Basic usage
```bash
# View file with syntax highlighting
bat README.md
# Multiple files
bat file1.js file2.py
# With line numbers (default)
bat script.sh
# Without line numbers
bat -p script.sh
```
### Viewing modes
```bash
# Plain mode (like cat)
bat -p file.txt
# Show non-printable characters
bat -A file.txt
# Squeeze blank lines
bat -s file.txt
# Paging (auto for large files)
bat --paging=always file.txt
bat --paging=never file.txt
```
## Syntax Highlighting
### Language detection
```bash
# Auto-detect from extension
bat script.py
# Force specific language
bat -l javascript config.txt
# Show all languages
bat --list-languages
```
### Themes
```bash
# List available themes
bat --list-themes
# Use specific theme
bat --theme="Monokai Extended" file.py
# Set default theme in config
# ~/.config/bat/config: --theme="Dracula"
```
## Line Ranges
```bash
# Show specific lines
bat -r 10:20 file.txt
# From line to end
bat -r 100: file.txt
# Start to specific line
bat -r :50 file.txt
# Multiple ranges
bat -r 1:10 -r 50:60 file.txt
```
## Git Integration
```bash
# Show Git modifications (added/removed/modified lines)
bat --diff file.txt
# Show decorations (Git + file header)
bat --decorations=always file.txt
```
## Output Control
```bash
# Output raw (no styling)
bat --style=plain file.txt
# Customize style
bat --style=numbers,changes file.txt
# Available styles: auto, full, plain, changes, header, grid, numbers, snip
bat --style=header,grid,numbers file.txt
```
## Common Use Cases
**Quick file preview:**
```bash
bat file.json
```
**View logs with syntax highlighting:**
```bash
bat error.log
```
**Compare files visually:**
```bash
bat --diff file1.txt
bat file2.txt
```
**Preview before editing:**
```bash
bat config.yaml && vim config.yaml
```
**Cat replacement in pipes:**
```bash
bat -p file.txt | grep "pattern"
```
**View specific function:**
```bash
bat -r 45:67 script.py # If function is on lines 45-67
```
## Integration with other tools
**As pager for man pages:**
```bash
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
man grep
```
**With ripgrep:**
```bash
rg "pattern" -l | xargs bat
```
**With fzf:**
```bash
fzf --preview 'bat --color=always --style=numbers {}'
```
**With diff:**
```bash
diff -u file1 file2 | bat -l diff
```
## Configuration
Create `~/.config/bat/config` for defaults:
```
# Set theme
--theme="Dracula"
# Show line numbers, Git modifications and file header, but no grid
--style="numbers,changes,header"
# Use italic text on terminal
--italic-text=always
# Add custom mapping
--map-syntax "*.conf:INI"
```
## Performance Tips
- Use `-p` for plain mode when piping
- Use `--paging=never` when output is used programmatically
- `bat` caches parsed files for faster subsequent access
## Tips
- **Alias:** `alias cat='bat -p'` for drop-in cat replacement
- **Pager:** Use as pager with `export PAGER="bat"`
- **On Debian/Ubuntu:** Command may be `batcat` instead of `bat`
- **Custom syntaxes:** Add to `~/.config/bat/syntaxes/`
- **Performance:** For huge files, use `bat --paging=never` or plain `cat`
## Common flags
- `-p` / `--plain`: Plain mode (no line numbers/decorations)
- `-n` / `--number`: Only show line numbers
- `-A` / `--show-all`: Show non-printable characters
- `-l` / `--language`: Set language for syntax highlighting
- `-r` / `--line-range`: Only show specific line range(s)
## Documentation
GitHub: https://github.com/sharkdp/bat
Man page: `man bat`
Customization: https://github.com/sharkdp/bat#customization
+247
View File
@@ -0,0 +1,247 @@
---
name: code-review
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
---
# Code Review and Quality
## Overview
Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance.
**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it.
## When to Use
- Before merging any PR or change
- After completing a feature implementation
- When another agent or model produced code you need to evaluate
- When refactoring existing code
- After any bug fix (review both the fix and the regression test)
## Core Principles
These principles underpin every axis. When in doubt, default to them.
- **DRY (Don't Repeat Yourself):** Every piece of knowledge has one authoritative representation. If the same logic appears in two places, extract it into a shared helper, model, or type. Reviewers: flag duplicated logic as a required change — it's not "just similar," it's drift that will diverge.
- **KISS (Keep It Simple, Stupid):** The simplest solution that works is the best solution. Complexity must earn its place. Reviewers: if you need more than one sentence to explain what a piece of code does, it's too complex — push for simplification before merge.
- **YAGNI (You Aren't Gonna Need It):** Don't add abstractions, hooks, or generalizations for hypothetical future use cases. Generalize on the third occurrence, not the first. Reviewers: delete speculative generality.
- **Don't invent problems:** Do not manufacture issues to produce more feedback. Every finding must be a real risk, a real readability barrier, or a real architectural concern — not a hypothetical or a stylistic preference disguised as a problem.
## The Five-Axis Review
Every review evaluates code across these dimensions.
### 1. Correctness
Does the code do what it claims to do?
- Does it match the spec or task requirements?
- Are edge cases handled (null, empty, boundary values)?
- Are error paths handled (not just the happy path)?
- Does it pass all tests? Are the tests actually testing the right things?
- Are there off-by-one errors, race conditions, or state inconsistencies?
### 2. Readability & Simplicity
Can another engineer (or agent) understand this code without the author explaining it?
- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context)
- Is the control flow straightforward (avoid nested ternaries, deep callbacks)?
- Are there any "clever" tricks that should be simplified?
- **KISS check:** Is this the simplest approach that solves the problem? A 20-line straightforward function beats a 5-line clever one that requires a comment to explain.
- Could this be done in fewer lines? (1000 lines where 100 suffice is a failure)
- Are abstractions earning their complexity? (Don't generalize until the third use case)
- Is a new conditional bolted onto an unrelated flow? Push the logic into its own helper, state, or policy.
- Do repeated conditionals on the same shape appear? They signal a missing model or dispatcher.
- Are there dead code artifacts: no-op variables, backwards-compat shims, or `// removed` comments?
### 3. Architecture
Does the change fit the system's design?
- Does it follow existing patterns or introduce a new one? If new, is it justified?
- Does it maintain clean module boundaries?
- **DRY check:** Is there existing code that does the same thing? Reuse the canonical helper instead of writing a near-duplicate. If two branches do nearly the same thing, collapse them.
- Are dependencies flowing in the right direction (no circular dependencies)?
- Is the abstraction level appropriate (not over-engineered, not too coupled)?
- Does this refactor reduce complexity or just relocate it? Count the concepts a reader must hold. Prefer the restructuring that makes whole branches disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it.
- Is feature-specific logic leaking into a shared or general-purpose module?
- Are type boundaries explicit? Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks.
- **Structural remedies:** When you flag a problem, propose the move — not just the problem. Replace conditionals with dispatchers, collapse duplicate branches, separate orchestration from business logic, extract helpers, split large files. Prefer the remedy that removes moving pieces over one that spreads the same complexity around.
### 4. Security
For detailed security guidance, see `security-and-hardening`.
- Is user input validated and sanitized?
- Are secrets kept out of code, logs, and version control?
- Is authentication/authorization checked where needed?
- Are SQL queries parameterized (no string concatenation)?
- Are outputs encoded to prevent XSS?
- Are dependencies from trusted sources with no known vulnerabilities?
- Is data from external sources (APIs, logs, user content, config files) treated as untrusted?
### 5. Performance
- Any N+1 query patterns?
- Any unbounded loops or unconstrained data fetching?
- Any synchronous operations that should be async?
- Any unnecessary re-renders in UI components?
- Any missing pagination on list endpoints?
- Any large objects created in hot paths?
## Review Process
1. **Understand the intent** — What is this change trying to accomplish? What spec or task does it implement?
2. **Review tests first** — Tests reveal intent and coverage. Do they test behavior, not implementation details? Are edge cases covered?
3. **Review the implementation** — Walk through each file with the five axes in mind.
4. **Categorize findings** — Label every comment with its severity:
| Prefix | Meaning | Author Action |
|--------|---------|---------------|
| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality |
| **High:** | Required change | Must address before merge |
| **Medium:** | Should fix | Strongly recommended, not a blocker |
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
| **Suggestion:** | Worth considering | Not required, but improves the code |
**Unique finding IDs.** Assign every finding a stable identifier: `F1`, `F2`, `F3`, … numbered in order of severity (Critical first, then High, Medium, Low, Suggestion). Use the ID everywhere the finding is mentioned — in section headers, in the verdict, in follow-up discussion. Never renumber within a review. Example: `**F3 (High)** — `app/validate.cljs:42` — duplicate branch logic…`.
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
5. **Verify the verification** — What tests were run? Did the build pass? Was the change tested manually? Screenshots for UI changes?
## Review Output
Structure every review using this format:
### Summary
Briefly explain what the code does and give an overall assessment.
### Critical and High-Priority Issues
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. Each finding gets its unique ID (`F1`, `F2`, …). For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
### Other Findings
List medium- and low-priority issues, including maintainability and design concerns. Continue the ID sequence started above (`F3`, `F4`, …).
### Suggested Refactoring
Provide focused code changes or revised snippets. Preserve existing behavior unless a behavior change is explicitly justified.
### Testing Recommendations
Identify missing tests and describe specific test cases, including edge cases and failure scenarios.
### Positive Observations
Mention implementation choices that are clear, safe, efficient, or well designed. This is not fluff — it reinforces good patterns and tells the author what to keep doing.
### Final Verdict
Choose one:
- **Approve** — Ready to merge
- **Approve with minor changes** — Good to merge after addressing low/medium issues
- **Request changes** — Critical or high issues must be resolved before merge
List the finding IDs the verdict depends on (e.g. "Request changes: F1, F4").
## Change Sizing
Small, focused changes are easier to review, faster to merge, and safer to deploy.
```
~100 lines changed → Good. Reviewable in one sitting.
~300 lines changed → Acceptable if it's a single logical change.
~1000 lines changed → Too large. Split it.
```
**Watch file size, not just diff size.** Around 1000 *total* lines in a single file is a common inspection signal. When a change materially grows an already-large file, decompose first.
**Splitting strategies:**
| Strategy | How | When |
|----------|-----|------|
| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies |
| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns |
| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture |
| **Vertical** | Break into smaller full-stack slices of the feature | Feature work |
**Separate refactoring from feature work.** A change that refactors and adds new behavior is two changes — submit them separately.
## Change Descriptions
- **First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC."
- **Body:** What is changing and why. Include context and reasoning not visible in the code itself.
- **Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Phase 1."
## Dependencies
Before adding any dependency:
1. Does the existing stack solve this? (Often it does.)
2. How large is the dependency? (Check bundle impact.)
3. Is it actively maintained? (Check last commit, open issues.)
4. Does it have known vulnerabilities? (`npm audit`)
5. What's the license? (Must be compatible with the project.)
**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability.
**Upgrading dependencies:**
- Read the changelog, not just the version number. Semver is a promise the maintainer may not have kept.
- One dependency per change. When a bulk bump breaks the build, you've lost which package did it.
- Let the tests decide — a green suite before *and* after, not just "it installed."
- Review the lockfile diff, not just `package.json`. Commit it and never hand-edit it.
For supply-chain risk triage, follow the `security-and-hardening` skill.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. |
| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. |
| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. |
| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. |
| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture, security, or readability problems. |
| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve. |
| "It's only a small addition to this file" | Small diffs still push files past healthy size and bolt branches onto unrelated flows. |
| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog. |
| "I'll upgrade everything in one PR" | A bulk bump hides which package broke the build. One per change. |
| "It's duplicated but it's only two places" | Two becomes three becomes five. Extract now, before the copies diverge. |
| "The abstraction is future-proof" | YAGNI. Delete speculative generality — generalize on the third occurrence, not the first. |
| "It's clever but efficient" | Cleverness is a readability tax. If it needs a comment to understand, simplify it. |
## Red Flags
- PRs merged without any review
- Review that only checks if tests pass (ignoring other axes)
- "LGTM" without evidence of actual review
- Security-sensitive changes without security-focused review
- Large PRs that are "too big to review properly" (split them)
- No regression tests with bug fix PRs
- Accepting "I'll fix it later" — it never happens
- A refactor that moves code around without reducing the number of concepts a reader must hold
- New conditionals scattered into unrelated code paths (a missing abstraction)
- A bespoke helper that duplicates an existing canonical one
- A bulk "bump dependencies" PR with no changelog review
## Verification
Before emitting the verdict, verify the change as it stands. This is the reviewer's own due diligence — it covers the state of the code at review time, not the later resolution of findings (fixing findings is the author's job; confirming them is a new review):
- [ ] Tests pass — run them yourself, don't trust the claim
- [ ] Build succeeds
- [ ] The verification story is documented (what changed, how it was verified)
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
## See Also
- For detailed security review guidance, see `security-and-hardening`
+47
View File
@@ -0,0 +1,47 @@
---
name: create-commit
description: Stage, review, and commit files following Penpot commit conventions.
---
# Skill: create-commit
Produce a git commit that follows Penpot's commit message conventions. This
skill owns the commit format, staging review, and safety checks — it does not
implement features or push.
## When to Use
- After code changes are complete and files need to be committed
- When delegated by a workflow step (e.g. implement-plan) to handle the commit
## Required Reading
Before drafting any commit, read `mem:workflow/creating-commits` end-to-end. It
is the authoritative source for the commit message format, the emoji menu,
subject/body limits, and the `AI-assisted-by` trailer. Follow it exactly.
## Workflow
1. **Stage the files** specified by the calling context. Do not ask for
confirmation.
2. Run `git diff --staged` to review the content. If you see secrets (API keys,
tokens, passwords, private keys, `.env` values), debug prints, or anything
that does not match the stated intent, **STOP** and tell the user before
committing.
3. Draft the message following the format in the memory doc, wrapping the body
at 72 characters per line, and run:
```bash
git commit -m "<subject>" -m "<body>"
```
(or `git commit -F -` if the body has unusual characters).
4. The `AI-assisted-by` trailer value is provided by the calling context — use
it verbatim.
## Constraints
- Do not push. Pushing is a separate workflow handled by the user.
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm`.
- Do not pass `--author`. Author identity comes from the local git config.
- Do not amend a commit you did not create in this session, unless explicitly asked.
- Do not bypass pre-commit hooks (`--no-verify`) unless explicitly asked.
- Do not add untracked files that were not created in this session.
+27
View File
@@ -0,0 +1,27 @@
---
name: create-issue
description: Create or update GitHub issues (from PR, from draft body, retitle existing). Routes to the canonical flow in `mem:workflow/creating-issues`.
---
# Skill: create-issue
Entry point for all GitHub issue work. All rules (title derivation, metadata,
body templates, Issue Type IDs), all flows, and all `gh` / GraphQL commands
live in `mem:workflow/creating-issues` (file:
`.serena/memories/workflow/creating-issues.md`). This skill routes to the
right flow.
## When to Use
- **Create from PR** — PR exists; the issue is the changelog/release unit,
the PR is the implementation. Issue = WHAT, PR = HOW.
→ memory section **Creating Issues from PRs**
- **Create from draft body** — Taiga story, user report, discussion; no PR
yet.
→ memory section **Creating Issues from Draft Body**
- **Retitle existing issue** — current title is vague, prefixed, or stale.
→ memory section **Retitling an Existing Issue**
Everything else (title derivation, metadata policy, body templates, Issue
Type IDs, create/verify/cleanup commands) lives in the memory — go to the
matching section there.
+39
View File
@@ -0,0 +1,39 @@
---
name: create-pr
description: Create or update a GitHub PR following Penpot conventions.
---
# Skill: create-pr
Create or update a GitHub PR. Read and follow:
- `mem:workflow/creating-prs` — title format, description structure, writing principles
- `mem:workflow/creating-commits` — commit type emojis
## When to Use
- Creating a new PR from a feature branch
- Updating an existing PR's title or description to match conventions
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
## Commands
**Create:**
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Update:**
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Verify:**
```bash
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
+194
View File
@@ -0,0 +1,194 @@
---
name: fd-find
description: A fast and user-friendly alternative to 'find' - simple syntax, smart defaults, respects gitignore.
homepage: https://github.com/sharkdp/fd
metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id":"brew","kind":"brew","formula":"fd","bins":["fd"],"label":"Install fd (brew)"},{"id":"apt","kind":"apt","package":"fd-find","bins":["fd"],"label":"Install fd (apt)"}]}}
---
# fd - Fast File Finder
User-friendly alternative to `find` with smart defaults.
## Quick Start
### Basic search
```bash
# Find files by name
fd pattern
# Find in specific directory
fd pattern /path/to/dir
# Case-insensitive
fd -i pattern
```
### Common patterns
```bash
# Find all Python files
fd -e py
# Find multiple extensions
fd -e py -e js -e ts
# Find directories only
fd -t d pattern
# Find files only
fd -t f pattern
# Find symlinks
fd -t l
```
## Advanced Usage
### Filtering
```bash
# Exclude patterns
fd pattern -E "node_modules" -E "*.min.js"
# Include hidden files
fd -H pattern
# Include ignored files (.gitignore)
fd -I pattern
# Search all (hidden + ignored)
fd -H -I pattern
# Maximum depth
fd pattern -d 3
```
### Execution
```bash
# Execute command on results
fd -e jpg -x convert {} {.}.png
# Parallel execution
fd -e md -x wc -l
# Use with xargs
fd -e log -0 | xargs -0 rm
```
### Regex patterns
```bash
# Full regex search
fd '^test.*\.js$'
# Match full path
fd --full-path 'src/.*/test'
# Glob pattern
fd -g "*.{js,ts}"
```
## Time-based filtering
```bash
# Modified within last day
fd --changed-within 1d
# Modified before specific date
fd --changed-before 2024-01-01
# Created recently
fd --changed-within 1h
```
## Size filtering
```bash
# Files larger than 10MB
fd --size +10m
# Files smaller than 1KB
fd --size -1k
# Specific size range
fd --size +100k --size -10m
```
## Output formatting
```bash
# Absolute paths
fd --absolute-path
# List format (like ls -l)
fd --list-details
# Null separator (for xargs)
fd -0 pattern
# Color always/never/auto
fd --color always pattern
```
## Common Use Cases
**Find and delete old files:**
```bash
fd --changed-before 30d -t f -x rm {}
```
**Find large files:**
```bash
fd --size +100m --list-details
```
**Copy all PDFs to directory:**
```bash
fd -e pdf -x cp {} /target/dir/
```
**Count lines in all Python files:**
```bash
fd -e py -x wc -l | awk '{sum+=$1} END {print sum}'
```
**Find broken symlinks:**
```bash
fd -t l -x test -e {} \; -print
```
**Search in specific time window:**
```bash
fd --changed-within 2d --changed-before 1d
```
## Integration with other tools
**With ripgrep:**
```bash
fd -e js | xargs rg "pattern"
```
**With fzf (fuzzy finder):**
```bash
vim $(fd -t f | fzf)
```
**With bat (cat alternative):**
```bash
fd -e md | xargs bat
```
## Performance Tips
- `fd` is typically much faster than `find`
- Respects `.gitignore` by default (disable with `-I`)
- Uses parallel traversal automatically
- Smart case: lowercase = case-insensitive, any uppercase = case-sensitive
## Tips
- Use `-t` for type filtering (f=file, d=directory, l=symlink, x=executable)
- `-e` for extension is simpler than `-g "*.ext"`
- `{}` in `-x` commands represents the found path
- `{.}` strips the extension
- `{/}` gets basename, `{//}` gets directory
## Documentation
GitHub: https://github.com/sharkdp/fd
Man page: `man fd`
+112
View File
@@ -0,0 +1,112 @@
---
name: jq-json-processor
description: Process, filter, and transform JSON data using jq - the lightweight and flexible command-line JSON processor.
homepage: https://jqlang.github.io/jq/
metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id":"brew","kind":"brew","formula":"jq","bins":["jq"],"label":"Install jq (brew)"},{"id":"apt","kind":"apt","package":"jq","bins":["jq"],"label":"Install jq (apt)"}]}}
---
# jq JSON Processor
Process, filter, and transform JSON data with jq.
## Quick Examples
### Basic filtering
```bash
# Extract a field
echo '{"name":"Alice","age":30}' | jq '.name'
# Output: "Alice"
# Multiple fields
echo '{"name":"Alice","age":30}' | jq '{name: .name, age: .age}'
# Array indexing
echo '[1,2,3,4,5]' | jq '.[2]'
# Output: 3
```
### Working with arrays
```bash
# Map over array
echo '[{"name":"Alice"},{"name":"Bob"}]' | jq '.[].name'
# Output: "Alice" "Bob"
# Filter array
echo '[1,2,3,4,5]' | jq 'map(select(. > 2))'
# Output: [3,4,5]
# Length
echo '[1,2,3]' | jq 'length'
# Output: 3
```
### Common operations
```bash
# Pretty print JSON
cat file.json | jq '.'
# Compact output
cat file.json | jq -c '.'
# Raw output (no quotes)
echo '{"name":"Alice"}' | jq -r '.name'
# Output: Alice
# Sort keys
echo '{"z":1,"a":2}' | jq -S '.'
```
### Advanced filtering
```bash
# Select with conditions
jq '[.[] | select(.age > 25)]' people.json
# Group by
jq 'group_by(.category)' items.json
# Reduce
echo '[1,2,3,4,5]' | jq 'reduce .[] as $item (0; . + $item)'
# Output: 15
```
### Working with files
```bash
# Read from file
jq '.users[0].name' users.json
# Multiple files
jq -s '.[0] * .[1]' file1.json file2.json
# Modify and save
jq '.version = "2.0"' package.json > package.json.tmp && mv package.json.tmp package.json
```
## Common Use Cases
**Extract specific fields from API response:**
```bash
curl -s https://api.github.com/users/octocat | jq '{name: .name, repos: .public_repos, followers: .followers}'
```
**Convert CSV-like data:**
```bash
jq -r '.[] | [.name, .email, .age] | @csv' users.json
```
**Debug API responses:**
```bash
curl -s https://api.example.com/data | jq '.'
```
## Tips
- Use `-r` for raw string output (removes quotes)
- Use `-c` for compact output (single line)
- Use `-S` to sort object keys
- Use `--arg name value` to pass variables
- Pipe multiple jq operations: `jq '.a' | jq '.b'`
## Documentation
Full manual: https://jqlang.github.io/jq/manual/
Interactive tutorial: https://jqplay.org/
+37
View File
@@ -0,0 +1,37 @@
---
name: nrepl-eval
description: Evaluate Clojure code via nREPL using the standalone scripts/nrepl-eval.mjs CLI tool.
---
# nREPL Eval
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
`scripts/nrepl-eval.mjs`.
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
## Quick Reference
```bash
./scripts/nrepl-eval.mjs [options] [<code>]
```
| Flag | Description | Default |
|------|-------------|---------|
| `--backend` | Connect to backend nREPL (port 6064) | — |
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
| `-p, --port PORT` | nREPL server port | `6064` |
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
| `--reset-session` | Discard stored session and start fresh | — |
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
| `-h, --help` | Show help message | — |
## Examples
```bash
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
./scripts/nrepl-eval.mjs -e
```
+315
View File
@@ -0,0 +1,315 @@
---
name: plan-review
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
---
# Plan Review
## Overview
Multi-dimensional plan review with quality gates. Every plan gets reviewed before implementation starts — no exceptions. Review covers six axes: completeness, task quality, architecture & sequencing, risk coverage, actionability, and proposed code quality.
**The approval standard:** Approve a plan when it is specific enough that a skilled implementer could execute it without guessing, the task ordering is sound, and risks are acknowledged. Perfect plans don't exist — the goal is confidence that implementation won't derail. Don't block a plan because it isn't exactly how you would have structured it. If it's executable and well-organized, approve it.
## When to Use
- After the planner skill produces a plan
- Before starting implementation on any non-trivial task
- When reviewing a plan written by another agent or a human
- When a plan feels too large, vague, or risky to start
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
## The Six-Axis Review
Every plan gets evaluated across these dimensions:
### 1. Completeness
Does the plan cover everything needed to implement successfully?
- Is the **context** clear? (What problem, why now, what's the goal?)
- Are **affected modules** identified with paths?
- Are **architecture decisions** documented with rationale?
- Is there a **testing strategy**?
- Are **verification commands** explicit (not "run the tests")?
- Are **open questions** listed (not buried in someone's head)?
- Is there a **parallelization** assessment for multi-task plans?
**Missing any of these is a gap, not a nit.**
### 2. Task Quality
Are the tasks well-defined and independently executable?
- Does every task have **acceptance criteria**? (Testable, not vague)
- Does every task have **verification steps**?
- Are tasks **sized appropriately**? (XSM is ideal, L is acceptable, XL must be split)
- Are **dependencies** between tasks explicitly stated?
- Are **files likely touched** listed?
- Is each task a **single, self-contained change**? (Not "implement the whole feature")
- Could a skilled implementer pick up any task and execute it without asking clarifying questions?
### 3. Architecture & Sequencing
Is the plan structured so implementation flows correctly?
- Does implementation order follow the **dependency graph** (foundations first)?
- Are tasks **vertically sliced** (feature paths) rather than horizontally layered?
- Does each task leave the system in a **working state**?
- Are there **checkpoints** between major phases?
- Are **high-risk tasks early** (fail fast)?
- Is the total plan a reasonable number of tasks? (More than ~15 tasks suggests the scope should be split into multiple plans)
### 4. Risk Coverage
Are the hard parts acknowledged and mitigated?
- Are **edge cases** identified?
- Are **breaking changes** or **migration concerns** noted?
- Are **security implications** considered?
- Are **performance implications** considered?
- Are **external dependencies** or integration risks flagged?
- Is there a plan for **rollback** if something goes wrong?
- Are **data integrity** risks addressed (what happens if a migration fails mid-way)?
### 5. Actionability
Can an implementer actually execute this?
- Are **file paths** specific (not "update the relevant files")?
- Are **function/method names** mentioned where applicable?
- Are **verification commands** copy-pasteable (not "run the linter")?
- Are **test commands** project-specific (not generic)?
- Is the **code shape** described where the implementation isn't obvious?
- Are **conventions** referenced (naming, patterns, existing utilities to reuse)?
- Does the plan reference **existing code** the implementer should read first?
### 6. Proposed Code Quality *(when the plan includes implementation details)*
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
- **Readability:** Are proposed names descriptive and consistent with project conventions?
- **Architecture:** Do proposed abstractions follow existing patterns? Are they justified (not over-engineered)?
- **Security:** Do proposed APIs validate input at boundaries? Any injection/XSS vectors in the design?
- **Performance:** Do proposed data structures avoid N+1 patterns? Any unbounded operations in the design?
**When to apply:** Only when the plan includes specific code snippets, type definitions, API contracts, or function signatures. Plans that only describe "what" without showing "how" skip this axis.
## Structural Remedies
When you flag a structural problem in a plan, propose the fix — not just the problem:
- **A task is too large (XL):** Split it into vertical slices. Each slice should be independently testable.
- **Missing acceptance criteria:** Draft 23 specific, testable conditions for the task.
- **Wrong sequencing:** Identify the dependency and propose the correct order.
- **No checkpoints:** Suggest where checkpoints should go (typically after every 23 tasks).
- **Vague verification:** Replace "run tests" with the actual project command.
- **Horizontal slicing:** Restructure into vertical feature paths.
- **Missing risk section:** Draft the risks you can identify from the plan content.
Prefer the remedy that makes the plan immediately actionable over one that just flags the gap.
## Plan Sizing
Plans should be scoped to a single deliverable:
```
15 tasks → Good. A focused feature or bug fix.
610 tasks → Acceptable for a moderate feature.
1115 tasks → Large. Consider splitting into phases.
15+ tasks → Too large. Split into multiple plans.
```
**What counts as "one plan":** A self-contained set of changes that delivers a single coherent capability. If you can describe the goal in one sentence, it's one plan.
## Categorize Findings
Label every comment with its severity so the author knows what's required vs optional:
| Prefix | Meaning | Author Action |
|--------|---------|---------------|
| *(no prefix)* | Required change | Must address before implementation starts |
| **Critical:** | Blocks implementation | Missing security consideration, data integrity risk, fundamentally wrong approach |
| **Nit:** | Minor, optional | Author may ignore — wording, formatting |
| **Optional:** / **Consider:** | Suggestion | Worth considering but not required |
| **FYI** | Informational only | No action needed — context for future reference |
**Lead with what matters.** Order findings by leverage: missing risks and wrong sequencing first, then task quality gaps, then completeness, then nits. If you have one critical sequencing problem and ten nits, the sequencing problem *is* the review.
## Review Process
### Step 1: Understand the Goal
Before evaluating structure, understand intent:
```
- What is this plan trying to accomplish?
- What problem does it solve?
- What does "done" look like?
```
### Step 2: Check Completeness First
Scan for missing sections before diving into content:
```
- Context present?
- Affected modules listed?
- Architecture decisions documented?
- Risks acknowledged?
- Testing strategy defined?
- Verification commands explicit?
```
### Step 3: Review Task Quality
Walk through each task:
```
For each task:
1. Can I tell exactly what to build?
2. Are acceptance criteria specific and testable?
3. Is the size reasonable (not XL)?
4. Are dependencies clear?
5. Would I know which files to touch?
```
### Step 4: Validate Sequencing
Check the dependency graph:
```
- Are foundations built first?
- Does each task leave the system working?
- Are checkpoints placed correctly?
- Are high-risk items early?
- Is it vertically sliced?
```
### Step 5: Assess Actionability
Put yourself in the implementer's shoes:
```
- Could I pick up task 1 and start coding without asking any questions?
- Are the verification commands copy-pasteable?
- Are file paths and function names specific?
- Is existing code referenced where I'd need to read it?
```
### Step 6: Verify the Verification Story
Check that the plan can actually confirm it worked:
```
- What tests should pass after implementation?
- What build/compile commands are relevant?
- What manual checks are needed?
- How do we know the feature works end-to-end?
```
### Step 7: Evaluate Proposed Code Quality *(if applicable)*
If the plan includes code snippets, types, or API designs:
```
- Load code-review skill for criteria
- Check proposed signatures for edge cases
- Verify naming follows project conventions
- Confirm abstractions follow existing patterns
- Scan for security vectors in proposed APIs
- Check for performance issues in proposed data structures
```
## Review Checklist
```markdown
## Review: [Plan title]
### Completeness
- [ ] Context explains the problem and goal
- [ ] Affected modules are listed with paths
- [ ] Architecture decisions have rationale
- [ ] Testing strategy is defined
- [ ] Verification commands are explicit and project-specific
- [ ] Open questions are listed
### Task Quality
- [ ] Every task has acceptance criteria
- [ ] Every task has verification steps
- [ ] Tasks are sized XSM (L acceptable, XL must be split)
- [ ] Task dependencies are stated
- [ ] Files likely touched are listed
### Architecture & Sequencing
- [ ] Order follows dependency graph (foundations first)
- [ ] Vertically sliced (not horizontal layers)
- [ ] Each task leaves system working
- [ ] Checkpoints exist between phases
- [ ] High-risk tasks are early
### Risk Coverage
- [ ] Edge cases identified
- [ ] Breaking changes / migrations noted
- [ ] Security implications considered
- [ ] Performance implications considered
- [ ] Rollback strategy exists (if applicable)
### Actionability
- [ ] File paths are specific
- [ ] Verification commands are copy-pasteable
- [ ] Existing code to read is referenced
- [ ] Conventions and patterns are noted
### Proposed Code Quality *(if plan includes implementation details)*
- [ ] Proposed types/signatures handle edge cases
- [ ] Proposed names follow project conventions
- [ ] Proposed abstractions follow existing patterns
- [ ] No security vectors in proposed APIs
- [ ] No performance issues in proposed structures
### Verdict
- [ ] **Approve** — Ready to implement
- [ ] **Request changes** — Gaps must be addressed
```
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll figure out the details during implementation" | That's how you discover blocking dependencies mid-task. Surface them now. |
| "The tasks are obvious, no need for criteria" | Write them anyway. Explicit criteria surface hidden assumptions. |
| "It's just a small feature, it doesn't need a plan" | Small features have edge cases too. 3 tasks with criteria takes 5 minutes. |
| "The plan is good enough" | "Good enough" without acceptance criteria means the implementer defines "done" — and they might define it differently. |
| "I'll add verification steps later" | Later never comes. The plan is the contract — define verification now. |
| "Risks are minimal" | Every change has risks. If you can't name them, you haven't thought about them. |
| "The file paths are obvious" | They're obvious to the author. The implementer might not know the codebase. |
| "The code in the plan is fine, it'll get reviewed later" | Plan-level code review catches design problems before implementation — fixing them after coding is more expensive. |
## Red Flags
- No acceptance criteria on any task
- Tasks that say "implement the feature" without specifics
- No verification steps anywhere in the plan
- All tasks are XL-sized
- No checkpoints between phases
- Dependency order isn't considered (e.g., API handler before domain model)
- No testing strategy
- Verification commands are generic ("run tests") instead of project-specific
- Plan has 20+ tasks (scope too large for one plan)
- No risk section on a plan with migrations, breaking changes, or security implications
- Horizontal slicing (all domain, then all services, then all API)
- File paths are vague ("update the relevant files")
- Missing open questions section despite stated unknowns
- Proposed code ignores project conventions or existing patterns
- Proposed types use gratuitous `any`/`unknown`/optional without justification
- Proposed APIs don't validate input at boundaries
## See Also
- For producing plans, use the `planner` skill
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
- For security-specific concerns, see `security-and-hardening`
- For testing strategy guidance, see `testing`
+377
View File
@@ -0,0 +1,377 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user and save to .opencode/plans/YYYY-MM-DD-<title>.md.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans with task breakdowns that engineers or other agents can
execute. Never writes or modifies code.
## When to Use
- The user asks for a plan, design, or analysis of a feature or bug.
- The user wants to understand which parts of the codebase a task will touch.
- The user needs a step-by-step implementation plan with file paths, function
names, and test strategy.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
breakdown.
- A task feels too large or vague to start.
- Work needs to be parallelized across multiple agents or sessions.
Do **not** use this skill to actually implement anything — it is read-only.
**When NOT to use:** Single-file changes with obvious scope, or when the spec
already contains well-defined tasks.
## Role
You help users understand the Penpot codebase, design solutions, and produce
implementation plans that other agents or developers can execute. The plan
tells them what to build and how to verify it, task by task.
The implementer reads the project's agent docs (`AGENTS.md`, project memories
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
before working. Reference those memories instead of re-explaining tooling,
conventions, or test design — explain in the plan only what they do not cover.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the implementer's responsibility.
## CRITICAL: Required Reading Before Planning
Before drafting any plan, work through the project's own guidance:
1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
that describes the monorepo structure and module dependency graph.
2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
4. For each affected module, note its lint, format, and test commands so the
plan can include concrete verification steps.
Skipping this step is the #1 cause of incorrect or incomplete plans.
---
## The Planning Process
### Phase 1: Architecture Analysis
1. Read the spec, requirements, or feature request.
2. Analyze the codebase architecture and identify affected modules.
3. Read project conventions (starting with `critical-info` and module core
memories) before drafting.
4. Map dependencies between components (see the dependency graph in
`critical-info`).
5. Identify risks, edge cases, performance implications, and breaking changes.
### Phase 2: Task Breakdown
#### Identify the Dependency Graph
Map what depends on what, following the monorepo's module dependency graph:
```
common (shared types, schemas — no deps)
├── backend (depends common)
│ ├── RPC handlers
│ └── persistence / migrations
├── frontend (depends common, render-wasm)
│ ├── UI components
│ └── state / API integration
├── exporter (depends common)
└── render-wasm (consumed by frontend)
```
Implementation order follows the dependency graph bottom-up: build shared
foundations first, then layer consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
**Bad (horizontal slicing):**
```
Task 1: Build all common types
Task 2: Build all backend handlers
Task 3: Build all frontend components
```
**Good (vertical slicing):**
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
Task 3: frontend UI component + API integration
```
Each vertical slice delivers working, testable functionality.
#### Write Tasks
Each task follows this structure:
```markdown
## Task [N]: [Short descriptive title]
**Description:** One or two paragraphs explaining what this task accomplishes.
Should be clear and concise.
**Rationale:** Why this task exists and why this approach over the obvious
alternatives — design decisions, trade-offs, constraints discovered during
analysis. One or two sentences; skip only if genuinely trivial.
**Code sketch (optional):** Signature-, type-, or shape-level example when the
intended interface is non-obvious. Keep it short — a skeleton that fixes the
contract (function signature, model fields, error shape), never a full
implementation. Omit when the task is mechanical.
**Acceptance criteria:**
- [ ] [Specific, testable condition]
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Relevant tests pass (module-specific test command).
- [ ] Lint/formatter passes (module-specific check command), if applicable.
- [ ] The core flow works end-to-end, if applicable.
**Dependencies:** [Task numbers this depends on, or "None"]
**Files likely touched:**
- `path/to/file.clj`
- `path/to/file_test.clj`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common,
`npx shadow-cljs compile test && npx karma start` for frontend, or the
commands noted in the module's core memory).
When possible, design each task with TDD in mind: acceptance criteria double
as a test list, and the natural first step of the task is writing those tests
before the implementation. Some tasks resist this (config, migrations, pure
wiring) — for those, keep the usual verification steps.
#### Estimate Scope
| Size | Files | Scope | Example |
|------|-------|-------|---------|
| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule |
| **S** | 1-2 | One handler or component method | Add a new RPC endpoint |
| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests |
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
| **XL** | 8+ | **Too large — break it down further** | — |
If a task is XL, it should be broken into smaller tasks. Agents perform best
on S and M tasks.
**When to break a task down further:**
- It would take more than one focused session
- You cannot describe the acceptance criteria in 3 or fewer bullet points
- It touches two or more independent subsystems
- You find yourself writing "and" in the task title (a sign it is two tasks)
#### Order and Checkpoints
Arrange tasks so that:
1. Dependencies are satisfied (build foundation first)
2. Each task leaves the system in a working state
3. Verification checkpoints occur after every 2-3 tasks
4. High-risk tasks are early (fail fast)
Add explicit checkpoints with the relevant module commands:
```markdown
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
```
## Requirements
- Analyze the codebase architecture and identify affected modules.
- Read project conventions before drafting (start with `critical-info` and
affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist after every 2-3 tasks.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code.
- The only file write you may attempt is the plan itself, saved to
`.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`, `bat`).
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill.
Additionally, save the plan to:
```
.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
`.opencode/plans/` directory if it does not exist.
IMPORTANT: The plan agent has write permission specifically for
`.opencode/plans/` — always attempt the write. If the user explicitly provides
a target file path, use that path instead of the default.
### Plan Document Template
```markdown
# Plan: [Feature/Project Name]
## Context
[One paragraph: what is the problem or feature request? Why is it needed?]
## Affected Modules
[Which modules of the monorepo are involved? Reference module paths and any
`mem:` memories that were consulted.]
## Architecture Decisions
- [Key decision 1 and rationale]
- [Key decision 2 and rationale]
## Risks & Considerations
[Edge cases, performance implications, breaking changes, migration concerns,
security implications.]
## Approach
[A short strategy summary: 3-5 sentences describing the overall approach and
the shape of the dependency graph (what depends on what, what gets built
first). High-level only — the task-by-task detail lives in the Task List.]
## Task List
Each task uses the full task structure defined in
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
verification, dependencies, files, estimated scope, and optional code sketch.
Never reduce a task to a one-line checkbox; the plan must be self-contained
and executable without other context.
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
into phases, milestones, or sprints; ordering and dependencies are already
captured per task. Insert a checkpoint after every 2-3 tasks.
## Task 1: [Short descriptive title]
**Description:** [What this task accomplishes.]
**Rationale:** [Why this approach over the alternatives.]
**Acceptance criteria:**
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Relevant tests pass (module-specific command).
**Dependencies:** None
**Files likely touched:**
- `path/to/file`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
**Code sketch (optional):** [Short contract-level example, only if the shape
is non-obvious.]
## Task 2: [Short descriptive title]
[Same structure as Task 1.]
## Task 3: [Short descriptive title]
[Same structure as Task 1.]
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
## Task 4: [Short descriptive title]
[Same structure as Task 1.]
## Task 5: [Short descriptive title]
[Same structure as Task 1.]
## Verification & Testing
[How to verify each task and the whole plan: the project's real test, lint,
build, and run commands (extracted during Required Reading), coverage
expectations, and manual checks. Consult each module's core memory for the
exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features, documentation
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
## Open Questions
- [Question needing human input]
```
When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. |
| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. |
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
## Red Flags
- Delivering prose without a task breakdown
- Tasks that say "implement the feature" without acceptance criteria
- No verification steps in the plan
- All tasks are XL-sized
- No checkpoints between tasks
- Dependency order isn't considered
## Verification Checklist
Before delivering the plan, confirm:
- [ ] Every task has acceptance criteria
- [ ] Every task has a verification step
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task is XL or larger — break it down instead
- [ ] Checkpoints exist after every 2-3 tasks
- [ ] The plan is ready for human review
+134
View File
@@ -0,0 +1,134 @@
---
name: refine-prompt
description: Refine and improve a user-supplied prompt for maximum clarity and effectiveness using prompt-engineering best practices and Penpot project context. Outputs a rewritten prompt (and brief rationale); never executes the prompt.
---
# Refine Prompt
Expert prompt-engineering pass on a user-supplied prompt. Takes a draft prompt
and returns a clearer, more effective, well-structured version — ready to be
used with any AI model. Never executes the prompt itself.
## When to Use
- The user shares a prompt and asks to improve, refine, polish, or rewrite it.
- The user asks "make this prompt better" or "can you clean this up?".
- The user wants to add structure, constraints, examples, or output format to
a vague prompt.
- The user wants a prompt adapted for a specific target model, audience, or
task type.
Do **not** use this skill to actually answer the prompt or do the task — it
only rewrites the prompt.
## Role
You are an expert Prompt Engineer with strong knowledge of Penpot. Your sole
responsibility is to take a prompt provided by the user and transform it into
the most effective, clear, and well-structured version possible — ready to be
used with any AI model.
You do **not** execute tasks. You do **not** write code. You only design and
refine prompts.
## Required Reading Before Refining
Before rewriting, internalize the project context the prompt will likely run
against:
1. Read `AGENTS.md` (root) for the project-level rules and conventions.
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
understand the module layout (`frontend`, `backend`, `common`,
`render-wasm`, `exporter`, `mcp`, `plugins`, `library`).
3. Skim the relevant module's core memory (`mem:frontend/core`,
`mem:backend/core`, etc.) when the prompt targets a specific module — this
lets you inject precise vocabulary, file conventions, and test commands
into the refined prompt.
This step matters most when the user is preparing a prompt *about* the
Penpot codebase. For generic prompts, focus on prompt-engineering principles
and only weave in Penpot context when it is clearly relevant.
## Requirements
- Analyze the original prompt: identify its intent, target audience,
ambiguities, missing context, and structural weaknesses.
- Ask clarifying questions if the intent is unclear or if critical information
is missing (e.g. target model, expected output format, tone, constraints).
Keep questions concise and grouped. Prefer to ask 14 questions at once
rather than one at a time. **Use the `question` tool** to ask them so the
user gets a structured multi-choice UI; reserve a plain `## Clarifying
questions` markdown section for cases where the `question` tool is
unavailable or the question is genuinely open-ended.
- Rewrite the prompt using prompt-engineering best practices (see below).
- Preserve the user's original intent — do not change the underlying task.
- When the user provides Penpot project context, weave in the relevant
conventions, module paths, and tooling.
## Prompt Engineering Principles
Apply these techniques when refining prompts:
- **Be specific and explicit**: Replace vague instructions with precise ones.
- **Set the context**: Include background information the model needs to
perform well.
- **Specify the output format**: State the desired structure, length, tone,
or format (e.g. bullet list, JSON, step-by-step).
- **Add constraints**: Include what the model should avoid or not do.
- **Use examples** (few-shot): When applicable, suggest adding examples to
anchor the model's behaviour.
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
- **Avoid ambiguity**: Remove pronouns and references that could be
misinterpreted.
- **Chain of thought**: For reasoning tasks, include "Think step by step."
- **Role framing**: When helpful, give the model a clear role
("You are a senior backend engineer...").
- **Tool awareness**: When the prompt targets an agentic model, mention
relevant tools (`grep`, `glob`, `read`, `bash`, etc.) so the model uses the
right surface.
## Constraints
- Do **not** execute the prompt yourself.
- Do **not** answer the question inside the prompt.
- Do **not** add unnecessary verbosity — prompts should be as short as they
can be while remaining complete.
- Always preserve the user's original intent.
- If the user provides Penpot project context, prefer Penpot-specific
vocabulary over generic terms (e.g. name actual modules and `mem:`
references instead of "the codebase").
## Output Format
Deliver the result in the response as two clearly separated blocks:
1. **Refined prompt** — a single fenced code block (markdown ```) containing
the rewritten prompt, ready to copy and use.
2. **What changed (brief)** — a short bulleted list of the most important
changes you made and why (37 bullets max). Skip the rationale if the
changes are trivial.
If you asked clarifying questions via the `question` tool, stop and wait for
the answers before producing a refined prompt. If the `question` tool was not
available and you asked the questions in chat, list them in a separate
**Clarifying questions** section above the refined prompt and stop — do not
produce a refined prompt until the user answers. If the user explicitly told
you to proceed without questions (e.g. "just rewrite it"), make reasonable
assumptions and note them under **Assumptions made** in the rationale block.
## File Persistence
Always persist the refined prompt to disk so it can be re-used later, versioned
in git, and shared with other agents. The response still contains the prompt
and rationale blocks; the file is an additional artifact, not a replacement.
- Save the refined prompt (the body inside the fenced code block, **without**
the surrounding ``` fences) to `.opencode/prompts/<descriptive-name>.md`.
- Use a **kebab-case** filename that summarises the task, e.g.
`add-error-reports-management-rpc.md`, `backend-rpc-security-audit.md`. No
spaces, no uppercase, no version numbers or dates in the filename.
- If `.opencode/prompts/` does not exist, create it before writing.
- If a file with the same name already exists, overwrite it (the file is the
refined prompt, not a log).
- Only skip the file write when the user explicitly opts out (e.g. "don't save
this one", "just show it in the chat"). When in doubt, save it.
+150
View File
@@ -0,0 +1,150 @@
---
name: ripgrep
description: Blazingly fast text search tool - recursively searches directories for regex patterns with respect to gitignore rules.
homepage: https://github.com/BurntSushi/ripgrep
metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id":"brew","kind":"brew","formula":"ripgrep","bins":["rg"],"label":"Install ripgrep (brew)"},{"id":"apt","kind":"apt","package":"ripgrep","bins":["rg"],"label":"Install ripgrep (apt)"}]}}
---
# ripgrep (rg)
Fast, smart recursive search. Respects `.gitignore` by default.
## Quick Start
### Basic search
```bash
# Search for "TODO" in current directory
rg "TODO"
# Case-insensitive search
rg -i "fixme"
# Search specific file types
rg "error" -t py # Python files only
rg "function" -t js # JavaScript files
```
### Common patterns
```bash
# Whole word match
rg -w "test"
# Show only filenames
rg -l "pattern"
# Show with context (3 lines before/after)
rg -C 3 "function"
# Count matches
rg -c "import"
```
## Advanced Usage
### File type filtering
```bash
# Multiple file types
rg "error" -t py -t js
# Exclude file types
rg "TODO" -T md -T txt
# List available types
rg --type-list
```
### Search modifiers
```bash
# Regex search
rg "user_\d+"
# Fixed string (no regex)
rg -F "function()"
# Multiline search
rg -U "start.*end"
# Only show matches, not lines
rg -o "https?://[^\s]+"
```
### Path filtering
```bash
# Search specific directory
rg "pattern" src/
# Glob patterns
rg "error" -g "*.log"
rg "test" -g "!*.min.js"
# Include hidden files
rg "secret" --hidden
# Search all files (ignore .gitignore)
rg "pattern" --no-ignore
```
## Replacement Operations
```bash
# Preview replacements
rg "old_name" --replace "new_name"
# Actually replace (requires extra tool like sd)
rg "old_name" -l | xargs sed -i 's/old_name/new_name/g'
```
## Performance Tips
```bash
# Parallel search (auto by default)
rg "pattern" -j 8
# Skip large files
rg "pattern" --max-filesize 10M
# Memory map files
rg "pattern" --mmap
```
## Common Use Cases
**Find TODOs in code:**
```bash
rg "TODO|FIXME|HACK" --type-add 'code:*.{rs,go,py,js,ts}' -t code
```
**Search in specific branches:**
```bash
git show branch:file | rg "pattern"
```
**Find files containing multiple patterns:**
```bash
rg "pattern1" | rg "pattern2"
```
**Search with context and color:**
```bash
rg -C 2 --color always "error" | less -R
```
## Comparison to grep
- **Faster:** Typically 5-10x faster than grep
- **Smarter:** Respects `.gitignore`, skips binary files
- **Better defaults:** Recursive, colored output, line numbers
- **Easier:** Simpler syntax for common tasks
## Tips
- `rg` is often faster than `grep -r`
- Use `-t` for file type filtering instead of `--include`
- Combine with other tools: `rg pattern -l | xargs tool`
- Add custom types in `~/.ripgreprc`
- Use `--stats` to see search performance
## Documentation
GitHub: https://github.com/BurntSushi/ripgrep
User Guide: https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md
@@ -0,0 +1,457 @@
---
name: security-and-hardening
description: Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.
---
# Security and Hardening
## Overview
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
## When to Use
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
## Process: Threat Model First
Controls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker:
1. **Map the trust boundaries.** Where does untrusted data cross into your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output**. Every boundary is attack surface.
2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement.
3. **Run STRIDE over each boundary** — a quick lens, not a ceremony:
| Threat | Ask | Typical mitigation |
|---|---|---|
| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification |
| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |
| **R**epudiation | Can an action be denied later? | Audit logging of security events |
| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors |
| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |
| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
4. **Write abuse cases next to use cases.** For each feature, ask "how would I misuse this?" — then make that your first test.
If you can't name the trust boundaries for a feature, you're not ready to secure it. This is OWASP **A04: Insecure Design** — most breaches begin in design, not code.
## The Three-Tier Boundary System
### Always Do (No Exceptions)
- **Validate all external input** at the system boundary (API routes, form handlers)
- **Parameterize all database queries** — never concatenate user input into SQL
- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it)
- **Use HTTPS** for all external communication
- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)
- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- **Use httpOnly, secure, sameSite cookies** for sessions
- **Run `npm audit`** (or equivalent) before every release
### Ask First (Requires Human Approval)
- Adding new authentication flows or changing auth logic
- Storing new categories of sensitive data (PII, payment info)
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
- Modifying rate limiting or throttling
- Granting elevated permissions or roles
### Never Do
- **Never commit secrets** to version control (API keys, passwords, tokens)
- **Never log sensitive data** (passwords, tokens, full credit card numbers)
- **Never trust client-side validation** as a security boundary
- **Never disable security headers** for convenience
- **Never use `eval()` or `innerHTML`** with user-provided data
- **Never store sessions in client-accessible storage** (localStorage for auth tokens)
- **Never expose stack traces** or internal error details to users
## OWASP Top 10 Prevention Patterns
These are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in `references/security-checklist.md`.
### Injection (SQL, NoSQL, OS Command)
```typescript
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
```
### Broken Authentication
```typescript
// Password hashing
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
// Session management
app.use(session({
secret: process.env.SESSION_SECRET, // From environment, not code
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
```
### Cross-Site Scripting (XSS)
```typescript
// BAD: Rendering user input as HTML
element.innerHTML = userInput;
// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;
// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
```
### Broken Access Control
```typescript
// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// Check that the authenticated user owns this resource
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
});
}
// Proceed with update
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
```
### Security Misconfiguration
```typescript
// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());
// Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
// CORS — restrict to known origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));
```
### Sensitive Data Exposure
```typescript
// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
```
### Server-Side Request Forgery (SSRF)
Any time the server fetches a URL the user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, `localhost`, private IPs).
```typescript
// BAD: fetch whatever the user gives you
await fetch(req.body.webhookUrl);
// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';
const ALLOWED_HOSTS = new Set(['hooks.example.com']);
async function assertSafeUrl(raw: string): Promise<URL> {
const url = new URL(raw);
if (url.protocol !== 'https:') throw new Error('https only');
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
// Resolve ALL records; a single private/reserved address fails the check.
const addrs = await lookup(url.hostname, { all: true });
if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
throw new Error('private/reserved IP');
}
return url;
}
await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });
```
The `range() !== 'unicast'` check covers loopback, link-local `169.254.169.254` (cloud metadata, the #1 SSRF target), private, and unique-local ranges across IPv4 and IPv6.
**Caveat — this still has a TOCTOU gap.** `fetch` resolves DNS again after the check, so an attacker using a short-TTL record can rebind to an internal IP between validation and connection. For high-risk surfaces, resolve once and connect to the pinned IP, or put a filtering agent in front (`request-filtering-agent` / `ssrf-req-filter`).
## Input Validation Patterns
### Schema Validation at Boundaries
```typescript
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200).trim(),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
dueDate: z.string().datetime().optional(),
});
// Validate at the route handler
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: result.error.flatten(),
},
});
}
// result.data is now typed and validated
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
```
### File Upload Safety
```typescript
// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateUpload(file: UploadedFile) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('File type not allowed');
}
if (file.size > MAX_SIZE) {
throw new ValidationError('File too large (max 5MB)');
}
// Don't trust the file extension — check magic bytes if critical
}
```
## Triaging npm audit Results
Not all audit findings require immediate action. Use this decision tree:
```
npm audit reports a vulnerability
├── Severity: critical or high
│ ├── Is the vulnerable code reachable in your app?
│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency)
│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
│ └── Is a fix available?
│ ├── YES --> Update to the patched version
│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
├── Severity: moderate
│ ├── Reachable in production? --> Fix in the next release cycle
│ └── Dev-only? --> Fix when convenient, track in backlog
└── Severity: low
└── Track and fix during regular dependency updates
```
**Key questions:**
- Is the vulnerable function actually called in your code path?
- Is the dependency a runtime dependency or dev-only?
- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)?
When you defer a fix, document the reason and set a review date.
### Supply-Chain Hygiene
`npm audit` catches known CVEs; it won't catch a malicious or typosquatted package. Also:
- **Commit the lockfile** and install with `npm ci` (not `npm install`) in CI — reproducible builds, no silent version drift.
- **Review new dependencies before adding them** — maintenance, download counts, and whether they truly earn their place. Every dependency is attack surface (OWASP **A06: Vulnerable Components**, **LLM03: Supply Chain**).
- **Be wary of `postinstall` scripts** in unfamiliar packages — they run arbitrary code at install time.
- **Watch for typosquats** — `cross-env` vs `crossenv`, `react-dom` vs `reactdom`.
## Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 10 attempts per 15 minutes
}));
```
## Secrets Management
```
.env files:
├── .env.example → Committed (template with placeholder values)
├── .env → NOT committed (contains real secrets)
└── .env.local → NOT committed (local overrides)
.gitignore must include:
.env
.env.local
.env.*.local
*.pem
*.key
```
**Always check before committing:**
```bash
# Check for accidentally staged secrets
git diff --cached | grep -i "password\|secret\|api_key\|token"
```
**If a secret is ever committed, rotate it.** Deleting the line or rewriting history is not enough — assume it's compromised the moment it reaches a remote. Revoke and reissue the key first, then purge it from history.
## Securing AI / LLM Features
If your app calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to the [OWASP Top 10 for LLM Applications (2025)](https://genai.owasp.org/llm-top-10/):
- **Treat all model output as untrusted input (LLM05: Improper Output Handling).** Never pass LLM output straight into `eval`, SQL, a shell, `innerHTML`, or a file path. Validate and encode it exactly as you would raw user input.
- **Assume prompts can be hijacked (LLM01: Prompt Injection).** Untrusted text in the context window — a user message, a fetched web page, a PDF — can carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt.
- **Keep secrets and other users' data out of prompts (LLM02 / LLM07).** Anything in the context can be echoed back. Don't put API keys, cross-tenant data, or the full system prompt where the model can repeat it.
- **Constrain tool and agent permissions (LLM06: Excessive Agency).** Scope tools to the minimum, require confirmation for destructive or irreversible actions, and validate every tool argument.
- **Bound consumption (LLM10: Unbounded Consumption).** Cap tokens, request rate, and loop/recursion depth so a crafted input can't run up cost or hang the system.
- **Isolate retrieval data (LLM08: Vector and Embedding Weaknesses).** In RAG, treat the vector store as a trust boundary: partition embeddings per tenant so one user can't retrieve another's data, and validate documents before indexing so poisoned content can't steer answers.
```typescript
// BAD: trusting model output as a command or as markup
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql); // arbitrary query execution
container.innerHTML = await llm.reply(userMessage); // stored XSS, via the model
// GOOD: model output is data — parse defensively, then validate, then encode
let intent;
try {
intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
throw new ValidationError('unexpected model output'); // JSON.parse or schema failed
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);
```
## Security Review Checklist
```markdown
### Authentication
- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- [ ] Session tokens are httpOnly, secure, sameSite
- [ ] Login has rate limiting
- [ ] Password reset tokens expire
### Authorization
- [ ] Every endpoint checks user permissions
- [ ] Users can only access their own resources
- [ ] Admin actions require admin role verification
### Input
- [ ] All user input validated at the boundary
- [ ] SQL queries are parameterized
- [ ] HTML output is encoded/escaped
- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services)
### Data
- [ ] No secrets in code or version control
- [ ] Sensitive fields excluded from API responses
- [ ] PII encrypted at rest (if applicable)
### Infrastructure
- [ ] Security headers configured (CSP, HSTS, etc.)
- [ ] CORS restricted to known origins
- [ ] Dependencies audited for vulnerabilities
- [ ] Error messages don't expose internals
### Supply Chain
- [ ] Lockfile committed; CI installs with `npm ci`
- [ ] New dependencies reviewed (maintenance, downloads, postinstall scripts)
### AI / LLM (if used)
- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)
- [ ] Secrets and other users' data kept out of prompts
- [ ] Tool/agent permissions scoped; destructive actions require confirmation
```
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. |
| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. |
## Red Flags
- User input passed directly to database queries, shell commands, or HTML rendering
- Secrets in source code or commit history
- API endpoints without authentication or authorization checks
- Missing CORS configuration or wildcard (`*`) origins
- No rate limiting on authentication endpoints
- Stack traces or internal errors exposed to users
- Dependencies with known critical vulnerabilities
- Server fetches user-supplied URLs without an allowlist (SSRF)
- LLM/model output passed into a query, the DOM, a shell, or `eval`
- Secrets, PII, or the full system prompt placed inside an LLM context window
## Verification
After implementing security-relevant code:
- [ ] `npm audit` shows no critical or high vulnerabilities
- [ ] No secrets in source code or git history
- [ ] All user input validated at system boundaries
- [ ] Authentication and authorization checked on every protected endpoint
- [ ] Security headers present in response (check with browser DevTools)
- [ ] Error responses don't expose internal details
- [ ] Rate limiting active on auth endpoints
- [ ] Server-side URL fetches validated against an allowlist (no SSRF)
- [ ] LLM/model output validated and encoded before use (if AI features present)
+78
View File
@@ -0,0 +1,78 @@
---
name: ste
description: Write or rewrite text in ASD-STE100 Simplified Technical English. ONLY use this skill when the user explicitly invokes it by name — i.e. they type "/ste" or literally write "use the ste skill" / "apply ASD-STE100". Do NOT trigger it on paraphrased intent such as "simplify this", "make it clearer", "write technical documentation", or "shorter sentences please" — the user has deliberately scoped this skill to explicit invocation only. For those requests, respond normally without loading this skill unless they name it.
---
# ASD-STE100 Simplified Technical English
Apply the ASD-STE100 standard to all prose you produce in this task. Do not announce that you use STE, do not name the standard, and do not explain the style unless the user asks. If the user later asks you to "write more naturally," ask one short question to confirm they want to leave STE before you drop it.
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
## Step 0 — Classify the text
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
## Core rules
### Sentences
- Procedural: maximum **20 words** per sentence.
- Descriptive: maximum **25 words** per sentence.
- Maximum **6 sentences** per paragraph. One topic per paragraph.
- One instruction per sentence. Two actions in one sentence only if they occur at the same time.
- Put a condition BEFORE its command: "If the pressure decreases, close the valve."
- Do not omit articles, subjects, or verbs to save words. "Ensure file exists" is wrong; "Make sure that the file exists" is correct. Keep the word "that" after verbs like "make sure."
- Numbers, units with numbers, abbreviations, quoted strings, code identifiers, and proper nouns each count as one word.
### Verbs
- Allowed forms only: infinitive, imperative, simple present, simple past, simple future, and past participle used as an adjective.
- Never use present perfect or continuous forms. "We have received" → "We received." "is being tested" → a simple form.
- Never use an -ing form as a verb. An -ing word is allowed only inside a technical name ("the mounting bracket," "logging").
- Active voice. Passive is allowed only in descriptive text when the agent is unknown or unimportant.
- Instructions use the imperative: "Open the panel," not "You must open the panel" or "The panel should be opened."
- Express actions as verbs, not nouns: "compress the file," not "perform compression of the file."
- Modals: use **can** (possibility), **will** (future), **must** (requirement). Do not use should, would, could, may, might. A hedge becomes a fact or a "can": "an explosion can occur."
- No phrasal verbs: "go down" → "decrease," "set up" → "install," "carry out" → "do."
### Words
- One word, one meaning, one part of speech, used consistently. Never rotate synonyms: pick one name for a thing and repeat it.
- Before drafting, replace unapproved vocabulary. Read `references/word-substitutions.md` and apply it; it is the working dictionary for this skill.
- Domain-specific nouns (part names, tool names, product names, UI labels) and domain verbs (drill, ream, boot, compile) are your **technical nouns/verbs** — keep them as-is, use each consistently, and do not verb a noun or noun a verb.
- Noun clusters: maximum **3 words** ("overhead panel light" is the limit). Longer clusters get decomposed with prepositions or hyphenated on first use: "main-gear-door retraction-winch handle."
- American English spelling.
- No Latin abbreviations: "e.g." → "for example," "i.e." → "that is," delete "etc."
### Punctuation
- No semicolons — write two sentences.
- Parentheses only for references, abbreviations, and item numbers.
- Hyphenate words that act as one unit; a hyphenated word counts as one word.
- No contractions.
### Warnings, cautions, notes
- **WARNING** = risk of injury or death. **CAUTION** = risk of damage. **NOTE** = information only, never an instruction.
- Start a warning or caution with the command or condition, then give the risk:
"WARNING: Do not touch the terminal. The terminal has a dangerous voltage."
- Notes obey the 25-word descriptive limit.
## Step 2 — Self-check pass
After drafting, scan your text once for each of these and fix every hit before you respond:
1. Any sentence over the 20/25-word limit for its type
2. Contractions, semicolons
3. "should," "would," "could," "may," "might"
4. "has been," "have been," "had been," "is being," "was being"
5. -ing words used as verbs
6. Missing articles (a/an/the/this) before nouns
7. Synonym rotation (the same object under two names)
8. Any word in the unapproved column of `references/word-substitutions.md`
9. Warnings that state the risk before the command
## Reference files
- `references/word-substitutions.md` — unapproved → approved word mappings and one-meaning rulings. Read it before drafting; it is short.
- `references/examples.md` — worked before/after rewrites (procedural, descriptive, warnings, common mistakes). Read it when rewriting existing text or when unsure how a rule applies.
## What NOT to touch
Code blocks, command strings, file paths, error messages, quoted UI text, and proper nouns stay exactly as written. STE applies to the prose around them.
@@ -0,0 +1,67 @@
# Worked before/after examples
## Verb forms
| Before | After |
|---|---|
| We have received the technical reports from HQ. | We received the technical reports from HQ. |
| This device has been being used at Boeing since 2005. | Boeing started to use this device in 2005. |
| The test is continued by the operator. | Continue the test. |
| The screws should be replaced. | Replace the screws. |
| The system is currently running diagnostics. | The system does diagnostic tests now. |
## Vocabulary and phrasing
| Before | After |
|---|---|
| Ensure file exists before running. | Make sure that the file exists before you run the command. |
| Rotate the cover until the jacks are accessible. | Turn the cover until you can get access to the jacks. |
| Extend the jack until the wheels are clear of the ground. | Extend the jack until the wheels do not touch the ground. |
| Make sure that these steps are followed. | Obey these steps. |
| Utilize approximately 3 liters of water. | Use about 3 liters of water. |
| Perform a compression of the log files prior to upload. | Compress the log files before you upload them. |
## Noun clusters
| Before | After |
|---|---|
| Main gear door retraction winch handle | Main-gear-door retraction-winch handle |
| Runway light connection resistance calibration | Calibration of the resistance of the runway light connection |
| Remove the engine transmission housing attachment bolts. | Remove the bolts that attach the transmission housing to the engine. |
## Procedural rewrite (condition first, one instruction per sentence)
Before:
> You should check that the reservoir has been filled with the correct fluid and, after attaching a clear tube to the bleed screw while ensuring its free end is submerged, the pedal can be pumped and held so that opening the screw releases trapped air.
After:
> 1. Fill the reservoir with the correct fluid.
> 2. Attach a clear tube to the bleed screw.
> 3. Put the free end of the tube in a container of fluid.
> 4. Push the pedal three times. Hold the pedal down.
> 5. Open the bleed screw one half turn. Air and fluid flow into the tube.
> 6. Close the bleed screw. Release the pedal.
> 7. If air continues to come out, do steps 4 thru 6 again.
## Warnings and cautions (command first, then risk)
Before:
> Note that serious data loss may potentially occur if the --force flag is used against production.
After:
> CAUTION: Do not use the --force flag on the production database. The flag deletes the rows that do not match the source.
Before:
> Touching the terminal could result in electrocution.
After:
> WARNING: Do not touch the terminal. The terminal has a dangerous voltage.
## Common mistakes checklist
- Dropped articles: "Insert pin in bracket" → "Insert the pin in the bracket."
- Synonym rotation: check/verify/confirm for the same action → one term, everywhere.
- Hedges: "you may want to," "it is recommended that" → an imperative or "must."
- Instruction buried in a NOTE: notes never instruct. Move the instruction to a numbered step.
- Semicolon joining two clauses → two sentences.
- "There are three bolts on the panel" → "The panel has three bolts."
@@ -0,0 +1,68 @@
# Word substitutions and one-meaning rulings
Compiled from public secondary sources (STEMG/ASD public pages, TechScribe, Acrolinx, training materials). This is a working approximation, not the official ASD dictionary. When a word is not listed here and feels formal or Latin-derived, prefer the shortest common alternative.
## Unapproved → approved
| Do not use | Use instead |
|---|---|
| utilize, leverage, employ | use |
| commence, initiate, begin, originate | start |
| terminate, cease, conclude | stop, end |
| ensure, verify, confirm, validate, check | make sure (that), examine |
| perform, conduct, execute, carry out | do |
| facilitate, assist | help |
| obtain, acquire, procure | get |
| sufficient, adequate | enough |
| approximately | about |
| prior to | before |
| subsequent to, following (prep.) | after |
| adjacent to | near |
| accomplish | do |
| additional, supplementary | more |
| attempt | try |
| require, necessitate | need, must |
| mandatory | necessary |
| indicate, signify | show |
| observe (=watch) | look at, examine |
| rotate | turn |
| deactivate | turn off, set to off |
| activate, energize (unless technical verb) | turn on, start |
| toxic | poisonous |
| in order to | to |
| via, by means of | through, with |
| due to, owing to | because of |
| in the event of/that | if |
| accessible | (rewrite: "you can get access to") |
| remainder | rest |
| demonstrate | show |
| modify, alter | change |
| construct, fabricate, build | assemble, make |
| retain | keep |
| locate (=find) | find |
| depress (a button) | push, press |
| proceed | continue, go |
## One meaning, one part of speech (canonical rulings)
- **close** — verb only: to move to a position that stops flow, or to operate a circuit breaker. The adjective is unapproved → use **near** ("do not go near the propeller").
- **test** — noun only: "do a test," never "test the system."
- **check** — do not use as a verb for verification → "make sure that" or "examine."
- **follow** — means only "come after." For rules and steps use **obey**: "Obey the safety instructions."
- **fall** — means only "move down by gravity." For quantities use **decrease**. Never the season.
- **oil** — noun only. "Oil the bearing" → "Put oil on the bearing" / "Lubricate the bearing."
- **right** — direction only, never "correct."
- **clear** — "without blockage." "Wheels are clear of the ground" → "wheels do not touch the ground."
- **help** — verb only; the noun is **aid** ("with the aid of a mirror").
- **above / below** — physical position only. For quantities: **more than / less than**.
- **about** — two approved senses: "approximately" and "on the subject of." Use carefully.
- **turn** — the general verb for rotation; "turn on / turn off" for power state is standard.
- **level** — approved as noun and adjective (documented exception to the one-POS rule).
## Frequent-offender function words
- **should / would / could / may / might** — never. Requirement → **must**. Possibility → **can**. Future → **will**.
- **etc.** — delete, or write the full list.
- **e.g. / i.e.** — "for example" / "that is."
- **any / appropriate / applicable / relevant** as hedges — replace with the specific thing meant.
- **there is / there are** openers — rewrite with a real subject: "There are three bolts on the panel" → "The panel has three bolts."
+110
View File
@@ -0,0 +1,110 @@
---
name: taiga
description: Fetch information from Taiga public API for the Penpot project (id 345963) — issues, user stories, and tasks, without authentication.
metadata: {"clawdbot":{"requires":{"bins":["python3"]}}}
---
# Taiga API Skill
Fetch information from Taiga public API for the **Penpot** project
(project id: `345963`, slug: `penpot`).
**No authentication required** — only public project data is accessed.
## Prerequisites
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
## Quick Start
The easiest way is to use the bundled Python script:
```bash
# Pass a Taiga URL directly
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
# Or use "<type> <ref>" syntax
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648
# Add --json for raw output
python3 scripts/taiga.py --json issue 13714
# See full usage
python3 scripts/taiga.py --help
```
## URL Pattern Reference
Taiga web URLs follow these patterns:
| Type | Web URL Pattern |
|------|----------------|
| Issue | `https://tree.taiga.io/project/penpot/issue/<REF>` |
| User Story | `https://tree.taiga.io/project/penpot/us/<REF>` |
| Task | `https://tree.taiga.io/project/penpot/task/<REF>` |
To extract the **type** and **ref** from a URL:
- `issue/13714` → type=`issue`, ref=`13714`
- `us/14128` → type=`us`, ref=`14128`
- `task/13648` → type=`task`, ref=`13648`
## Python Script Reference
The `scripts/taiga.py` script wraps the Taiga API into a single convenient CLI
with sensible defaults.
### Usage
```
python3 scripts/taiga.py <taiga-url>
python3 scripts/taiga.py <type> <ref>
python3 scripts/taiga.py [--json] <taiga-url>
python3 scripts/taiga.py [--json] <type> <ref>
```
### Examples
```bash
# By URL (recommended — no need to think about type/ref)
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
# By type and ref
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648
# Raw JSON output
python3 scripts/taiga.py --json issue 13714
```
### Output
The script prints a clean, structured summary:
```text
User Story #11964 — 🔴 [DESIGN TOKENS] Typography Composite Input
================================
Status: Defining
Milestone: design-systems-sprint-26
Points: 3 role(s)
Assignee: Natacha Menjibar
Author: Natacha Menjibar
Created: 2025-09-01
Tags: iop-design-tokens
URL: https://tree.taiga.io/project/penpot/us/11964
================================
<full description text, unmodified>
```
The fields section includes type-specific information:
- **Issues:** Status, Type ID, Severity ID, Priority ID
- **User Stories:** Status, Milestone, Points
- **Tasks:** Status, Milestone, Parent US
## Reference
- API docs: https://docs.taiga.io/api.html
- Taiga instance: https://tree.taiga.io
- API base: https://api.taiga.io/api/v1
- Penpot project id: `345963`
- Penpot project slug: `penpot`
+58
View File
@@ -0,0 +1,58 @@
---
name: testing
description: Enforce TDD workflow and testing best practices for Penpot. Use when implementing features, fixing bugs, or modifying behavior. Reads testing memory for full guidance.
---
# Testing Skill
Enforces test-driven development and Penpot testing conventions.
## When to Use
- Implementing new logic or behavior
- Fixing any bug (reproduction test required)
- Modifying existing functionality
- Adding edge case handling
**Skip:** Pure configuration changes, documentation updates, or static content with no behavioral impact.
## Workflow
Follow TDD (Red → Green → Refactor) whenever practical:
1. **RED** — Write a failing test first
2. **GREEN** — Write minimal code to pass
3. **REFACTOR** — Clean up while tests stay green
For bug fixes, use the Prove-It Pattern: write a test that reproduces the bug, confirm it fails, implement the fix, confirm it passes.
## Required Reading
Before writing any test, read:
1. `.serena/memories/testing.md` — cross-cutting testing principles, TDD workflow, anti-patterns, execution discipline
2. Module-specific testing memory for the affected module:
- `mem:common/testing` — CLJC unit tests
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
- `mem:backend/testing` — JVM clojure.test conventions
- `mem:exporter/testing` — exporter unit tests
## Key Rules
- Every behavior change needs a test
- Test state, not interactions
- DAMP over DRY — tests are specifications; duplication is OK if each test is self-contained and readable
- Prefer Real > Fake > Stub > Mock
- Arrange-Act-Assert structure
- One assertion per concept
- Never pipe test output to filters — redirect to file first
- Register new test files in the module's runner/entrypoint
## Verification
After completing implementation:
- [ ] Every new behavior has a test
- [ ] All tests pass for touched modules
- [ ] Bug fixes include a reproduction test
- [ ] Lint/formatter passes
+825
View File
@@ -0,0 +1,825 @@
---
name: update-changelog
description: Update the project CHANGES.md with issues from a given GitHub milestone, with correct categorization and references.
---
# Skill: update-changelog
Update `CHANGES.md` with entries for all issues and PRs in a given GitHub
milestone. Each entry references the user-facing issue (not the PR) as the
primary link, with the fix PR inline on the same line.
## When to Use
- Before a new release, to populate the changelog with all fixed issues
- When new issues are added to an existing milestone and the changelog needs
to be refreshed
- To ensure every entry follows the correct format for the changelog
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
- Python 3.8+
- `scripts/gh.py` helper script available
## Workflow
### 1. Determine the target version
The version is typically a semver string like `2.15.3`. Confirm with the user
if not specified.
### 2. Fetch all issues in the milestone
Use the helper script. It uses GraphQL for efficient single-pass fetching
(closing PRs are included in the same query — no N+1):
```bash
# All closed issues (default)
python3 scripts/gh.py issues "2.16.0"
# Include open issues too
python3 scripts/gh.py issues "2.16.0" --state all
# Exclude entries that should not go in the changelog
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
```
**Exclusion rules (issue-level):**
- `no changelog` label — Chore/refactor work that doesn't need a changelog entry
- `release blocker` label — Blocked issues not yet ready for changelog
- `Task` issue type — Internal chores are not user-facing; automatically excluded by `gh.py`. Use `--include-tasks` to override.
- **Rejected project status** — Issues with a "Rejected" status in the "Main" project board are automatically excluded by `gh.py`. This project-level status (independent of the GitHub issue `state`) indicates the issue was rejected from the release. Use `--include-rejected` to override.
**Exclusion rules (PR-level):**
In addition to issue-level exclusions, PRs with these labels should be
excluded regardless of their linked issue's labels:
- `release blocker` — PR is part of a pending release blocker batch
- `no issue required` — Trivial fix not tracked as an issue
The script outputs JSON with each entry containing `number`, `title`, `state`,
`issue_type`, `labels`, `closing_prs` (the PRs that fix each issue), and
`project_status` (the "Main" project board status, e.g. "Done", "Rejected",
or `null` if not tracked in a project).
### 3. Identify missing entries (optional)
If updating from an existing `CHANGES.md`, find issues in the milestone that
are NOT yet referenced in the changelog:
```bash
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
```
This returns a filtered JSON array with only the missing issues.
> **Note:** The `--compare` flag checks **issues** only (via issue number
> references in the changelog). To find merged **PRs** not yet referenced,
> use the milestone PR cross-reference described in step 10 below.
### 4. Fetch additional PR details when needed
When you need more context for specific PRs (e.g. to find the PR author for
community contribution attribution, or to read the PR body for
"Fixes/Closes #NNN" patterns):
```bash
# One or more PR numbers
python3 scripts/gh.py prs 9179 9204 9311
# From a file
python3 scripts/gh.py prs --file prs.txt
# From stdin
cat prs.txt | python3 scripts/gh.py prs --stdin
```
The `prs` command also supports listing all PRs in a milestone in one call:
```bash
# All merged PRs in a milestone (default)
python3 scripts/gh.py prs --milestone "2.16.0"
# All states (merged, open, closed)
python3 scripts/gh.py prs --milestone "2.16.0" --state all
```
The `prs` command returns JSON with `number`, `title`, `body`, `state`,
`merged_at`, `author`, `labels`, and `closing_issues`. PRs are fetched in
batches of 50 via GraphQL to stay within API limits (milestone mode uses
paginated GraphQL on the milestone's `pullRequests` connection).
You can also list all PRs in a milestone in a single call:
```bash
# All merged PRs in a milestone (default)
python3 scripts/gh.py prs --milestone "2.16.0"
# All states (merged, open, closed)
python3 scripts/gh.py prs --milestone "2.16.0" --state all
# Open PRs only
python3 scripts/gh.py prs --milestone "2.16.0" --state open
```
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
connection (100 per page), avoiding one-by-one fetches.
### 5. Categorize entries — strictly by issue type, never by labels or emoji
Use the **Issue Type** field (GitHub's native issue type, exposed as
`issue_type` in the `gh.py` JSON output) to determine which section an entry
belongs to.
> **⚠️ CRITICAL: Never use labels or title emoji prefixes for categorization.**
> Labels like `bug` and `enhancement`, as well as title prefixes like `:bug:`
> and `:sparkles:`, are frequently inaccurate, missing, or contradictory to the
> actual issue type. The `issue_type` field from `gh.py` is the single source
> of truth.
| `issue_type` value | Changelog section |
|--------------------|-------------------|
| `Bug` | `### :bug: Bugs fixed` |
| `Feature` or `Enhancement` | `### :sparkles: New features & Enhancements` |
| `Task` | **Exclude** — internal chores are not user-facing |
| `null` (not set) | Check labels as a fallback: `bug` label → bugs, otherwise enhancements |
The `gh.py` issues command already includes `issue_type` in every entry's
output. **No separate GraphQL query is needed.**
**Preserve highlighted entries:** If an entry is already featured in
`### :rocket: Epics and highlights`, keep it in that section when refreshing a
changelog version. Do not remove a highlighted entry just because issue type
categorization would otherwise place it under `### :sparkles: New features &
Enhancements`.
**Community contribution attribution:** If the issue or its fix PR has the
`community contribution` label, add an attribution `(by @<github_username>)`
on the changelog entry line, **before** the GitHub issue/PR references.
The attribution should reference the **PR author**, not the issue author.
The `prs` subcommand includes the `author` field — use that:
```bash
python3 scripts/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
```
Placement in the entry line:
```markdown
- Fix description of the bug (by @username) [#<ISSUE>](...) (PR: [#<PR>](...))
```
**Only closed issues are included.** An issue must have `state: "closed"` to
appear in the changelog. Open/unresolved issues are omitted, even if they are
tracked in the milestone.
**Pairing rules:**
| Pattern | Changelog format |
|---------|-----------------|
| Closed issue + one or more PRs fix it | Primary link = issue, PR inline comma-separated |
| PR exists with no linked issue | If a corresponding closed issue exists in the same milestone, link the issue. Otherwise, skip the entry (the issue must be the changelog unit). |
| Closed issue with no fix PR in milestone | Link the issue directly, without a PR reference. |
> **False-positive associations:** A PR may incorrectly claim to close an issue
> from a different context (e.g., a very old PR referencing a modern issue, or a
> cross-project reference). If the PR title and issue title are clearly unrelated,
> or the PR was created years before the issue, treat it as a data glitch and
> skip it. PR [#3](https://github.com/penpot/penpot/pull/3) (ancient License PR
> claiming to close a plugin API issue) is a known example.
### 5a. ⚠️ Verify PR merge status before writing
A closed issue may list closing PRs that were **closed without merging**
(e.g., a community PR that was superseded by another). The changelog must
only reference **merged** PRs. Verify before writing:
```bash
# Collect all PR numbers from the candidate entries and check them
python3 scripts/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
import json, sys
for pr in json.load(sys.stdin):
if pr['state'] != 'MERGED':
print(f'WARNING: #{pr[\"number\"]} is {pr[\"state\"]} (not merged)')
"
```
If a closing PR is closed-unmerged, find the actual merged PR that
superseded it:
1. Check the issue's closing PRs list for other PRs (there may be multiple)
2. Look for other PRs with similar titles or descriptions referencing the same issue
3. Inspect the closed PR's conversation timeline for a pointer to the replacement
Replace the reference in the changelog entry with the correct merged PR number.
### 5b. Security advisory (GHSA) entries
Security advisories fixed in a release are documented in the changelog even
though they are **neither milestone issues nor PRs**. The GHSA ID and its
description are supplied by the user or the release notes — they never come
from the milestone fetch in step 2.
**Format** (matches the existing precedent in `CHANGES.md`, e.g. the
`create-font-variant` arbitrary file read advisory):
```markdown
- Fix <user-facing description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Rules:
- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link**
only the advisory URL.
- The advisory may be **draft/unpublished** at changelog time (the URL 404s
publicly). Do **not** web-fetch or verify the URL, and do **not** drop the
entry because of that. Rely on the GHSA ID provided by the user.
- Derive the description from the supplied advisory title, imperative mood and
user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`).
- These entries are **invisible to the automation**: they are not returned by
`gh.py issues`, not matched by `--compare` (step 3), not part of the PR
cross-reference (step 10), and not scanned by the anomaly-report regexes
(step 11, which only match `issues/` and `pull/` links). Add them manually.
- During pre-flight checks (step 6a) apply only the **backport/duplicate**
check: if the same GHSA already appears in an earlier version section, remove
it from the current section. Their absence from milestone cross-references
is expected, not an anomaly.
### 6. Read the current CHANGES.md
Read the top of `CHANGES.md` to understand the existing format and find the
insertion point (newest version goes at the top, after the `# CHANGELOG`
header).
Key format rules from the existing file:
```markdown
## <VERSION>
### :bug: Bugs fixed
- Fix description of the bug [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- Fix another bug (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
### :sparkles: New features & Enhancements
- Add new feature description [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
```
Format details:
- Entries start with `- ` followed by a short description in imperative mood
- Primary link is **always the issue** (user-facing artifact)
- PR references are inline on the same line: `(PR: [#<N>](<url>))`
If an issue has multiple fix PRs, they are comma-separated:
`(PR: [#<N>](<url>), [#<M>](<url>))`
- The description should describe the fix/feature from the user's perspective
- Community contributions get `(by @<username>)` **before** the issue link
- Sections are separated by a blank line between the last entry and the next
section title
- Only include a section if there are entries for it
- When an entry already exists in an earlier version section, it must be removed
from the current version to avoid duplicates
### 6a. Pre-flight checks — fix rule violations in the changelog
**The LLM must apply these checks during the workflow and fix any
violations directly in `CHANGES.md`. They are not anomalies — they are
process errors that should be corrected before writing the new section.**
The changelog is a *snapshot* of the milestone at a point in time, but
milestones and changelog entries can drift. The LLM must reconcile the
existing changelog against the current state of the milestone and the
existing changelog entries.
For each entry that already exists in `CHANGES.md` (in any version
section) or in the candidate set for the current milestone, check:
1. **Duplicate across versions.** Is the same issue already documented
in another (older) version section? If yes, this is a *backport*:
- The user-facing fix was already released. Remove the duplicate
from the current section. The earlier version is the canonical
reference.
2. **Stale milestone assignment.** Has the issue been moved out of the
current milestone since the changelog was last updated (e.g., a fix
arrived late and the issue was reassigned to a future milestone)?
- Verify the issue is still in the current milestone via
`python3 scripts/gh.py issues <MILESTONE> --state all`. If it's no
longer there, remove the entry from the current section. (If the
target section doesn't exist yet, the entry is simply dropped.)
3. **Exclusion labels newly applied.** Did the issue acquire a
`no changelog` or `release blocker` label since the changelog was
last updated? If yes, remove the entry from the current section.
4. **Issue state changed.** Is the issue still closed? Has it been
reopened, deleted, or moved to a `Rejected` project status? If yes,
remove the entry.
5. **Unmerged or removed PR references.** For every PR referenced in
the entry, is the PR still merged? Was the PR closed without
merging (superseded)? Was the PR moved to a different milestone?
If the only referenced PR is no longer merged, fix the reference
(find the actual merged fix PR) or remove the entry. A PR that is
merged in a *different* milestone is reported as an anomaly in
step 11 — do not silently remove it.
6. **Issue type changed.** Did the issue type change (e.g., from Bug to
Task)? If the new type is `Task`, the issue is internal and should
be removed.
7. **Cross-section completeness.** For every closed, non-excluded
milestone issue that is *not* referenced in any version section of
the changelog, add it to the current section (per the categorization
rules in step 5).
After these checks, the changelog should be internally consistent with
the milestone. **Do not defer these fixes to step 11 — they are
workflow errors, not anomalies.** Step 11 only reports milestone
mismatches that require human judgment about the team's release
intent.
### 7. Build the description text
Derive the description from the issue title, not the PR title. Strip leading
emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`) and focus on the
user-facing behavior.
Examples:
| Issue title | Changelog description |
|-------------|----------------------|
| `Plugin API token methods fail with schema validation error on PRO` | `Fix Plugin API token methods failing with schema validation error on PRO` |
| `Comment content is not sanitized before rendering, enabling stored XSS` | `Sanitize comment content on rendering` |
| `Custom uploaded font family names are not sanitized` | `Sanitize font family names on custom uploaded fonts` |
### 8. Insert the section into CHANGES.md
Insert the new version section right after the `# CHANGELOG` header (before
the previous version entry). Use the `edit` tool with enough context to make
a unique match.
### 9. Verify
Read the top of `CHANGES.md` and confirm:
- The version header is correct
- Every entry has a GitHub link
- Entries with a fix PR have the PR sub-line
- The section ordering is correct (newest first)
- Formatting matches the surrounding entries
### 10. Cross-reference milestone PRs against the changelog
Issues can be fixed by PRs that aren't in the milestone, and merged PRs in
the milestone may not close any tracked issue. After writing, run a full
cross-reference to catch gaps:
```bash
# List all merged PRs in the milestone
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
# Extract PR numbers from the changelog section
python3 -c "
import json, re
with open('CHANGES.md') as f:
content = f.read()
# Extract the version section (adjust regex to match the actual version)
match = re.search(r'## <MILESTONE> \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
section = match.group(1)
# Collect all PR numbers referenced
changelog_prs = set()
for m in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
changelog_prs.add(int(m))
# Collect all milestone PRs (filtered)
with open('/tmp/milestone-prs.json') as f:
milestone_prs = json.load(f)
milestone_merged = {pr['number'] for pr in milestone_prs}
# PRs in milestone but not in changelog
missing = sorted(milestone_merged - changelog_prs)
print(f'Milestone merged PRs: {len(milestone_merged)}')
print(f'Changelog referenced PRs: {len(changelog_prs)}')
print(f'PRs in milestone but NOT in changelog: {len(missing)}')
for num in missing:
pr = next(p for p in milestone_prs if p['number'] == num)
print(f' #{num} {pr[\"title\"][:80]}')
"
```
For each missing PR found, decide whether it should be added to the
changelog or is legitimately excluded (check its labels).
Also verify that no closed-unmerged PRs remain in the changelog:
```bash
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
import json, sys
data = json.load(sys.stdin)
closed = [p for p in data if p['state'] == 'CLOSED']
if closed:
print('WARNING: CLOSED (unmerged) PRs in milestone:')
for p in closed:
print(f' #{p[\"number\"]} {p[\"title\"][:80]}')
"
```
**Post-edit audit checklist:**
- ✅ All referenced PRs are merged (no closed-unmerged artifacts)
- ✅ Every merged milestone PR is either in the changelog or excluded by label
- ✅ PR and issue counts are internally consistent
- ✅ No false-positive PR-to-issue associations
- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the
cross-reference is intentional (see step 5b)
## Version section template
```markdown
## <VERSION>
### :bug: Bugs fixed
- <fix description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See
step 5b.
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
After all edits and cross-referencing are complete, generate a structured
report and save it to `CHANGES-ISSUES.md` (overwriting if exists).
This provides a persistent record of any discrepancies between the milestone
and the changelog.
**Every issue and PR number in the report must be rendered as a full GitHub
Markdown link** using the same URL format as `CHANGES.md`:
- Issue N → `[#N](https://github.com/penpot/penpot/issues/N)`
- PR N → `[#N](https://github.com/penpot/penpot/pull/N)`
The titles and notes should also link to the corresponding issue/PR page
where applicable, so the report is self-contained and clickable from any
Markdown viewer.
## What is an anomaly
**An anomaly is a milestone-mismatch between an issue and its referenced
PR.** It indicates that the changelog claim "this issue is fixed by this PR,
all in milestone M" is inconsistent with the actual milestone assignments.
There are exactly two types:
1. **Issue is in the milestone, but its referenced PR is in a different
milestone (or has no milestone).** The changelog claims a fix in this
release, but the PR is being released elsewhere — the fix may not
actually ship here.
2. **PR is in the milestone, but the issue it closes is in a different
milestone.** The PR is being released here, but the issue it fixes is
being released in a different version — the changelog pairing is
misleading.
**Exception — issue with no milestone is NOT an anomaly.** Milestones
are only required for issues tracked in the "Main" project. A milestone
PR that closes an issue with no milestone references an issue from
another (probably private) project; that is expected and the issue is
not part of this changelog. Do not report it.
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
references, duplicates across versions, stale milestone assignments)
are **rule violations** that the LLM must fix directly in `CHANGES.md`
during step 6a (pre-flight checks). They should not appear in this
report — if they do, the LLM has skipped the pre-flight step and
needs to re-run the workflow.
The changelog's primary unit is the **issue**, not the PR, so a missing or
mismatched PR only matters when its issue is part of this milestone.
Run this self-contained script:
```bash
python3 << 'PYEOF'
import json, re, subprocess, sys
from datetime import datetime, timezone
MILESTONE = "<MILESTONE>"
CHANGES_MD = "CHANGES.md"
OUTPUT = "CHANGES-ISSUES.md"
REPO = "penpot/penpot"
# --- URL helpers (match CHANGES.md format exactly) ---
def issue_url(n): return f"https://github.com/{REPO}/issues/{n}"
def pr_url(n): return f"https://github.com/{REPO}/pull/{n}"
def issue_link(n): return f"[#{n}]({issue_url(n)})"
def pr_link(n): return f"[#{n}]({pr_url(n)})"
def issue_link_title(n, title):
url = issue_url(n)
if title:
return f"[#{n}]({url}) — [{title}]({url})"
return f"[#{n}]({url})"
def pr_link_title(n, title):
url = pr_url(n)
if title:
return f"[#{n}]({url}) — [{title}]({url})"
return f"[#{n}]({url})"
def fmt_pr_list(nums):
return ", ".join(pr_link(n) for n in nums)
def fmt_issue_list(nums):
return ", ".join(issue_link(n) for n in nums)
# --- Fetch milestone data ---
result = subprocess.run(
["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"],
capture_output=True, text=True)
all_issues = json.loads(result.stdout)
issue_by_num = {i['number']: i for i in all_issues}
result = subprocess.run(
["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
capture_output=True, text=True)
all_prs = json.loads(result.stdout)
pr_by_num = {p['number']: p for p in all_prs}
# --- Read changelog section ---
with open(CHANGES_MD) as f:
content = f.read()
m = re.search(rf'## {re.escape(MILESTONE)}(?:\s*\([^)]*\))?\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
section = m.group(1) if m else ""
changelog_issues = set()
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)', section):
changelog_issues.add(int(num))
for num in re.findall(r'\[Github #(\d+)\]', section):
changelog_issues.add(int(num))
changelog_prs = set()
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
changelog_prs.add(int(num))
for num in re.findall(r'PR:\[(\d+)\]', section):
changelog_prs.add(int(num))
# --- Milestone lookup caches ---
# PRs and issues returned by milestone queries are KNOWN to be in MILESTONE.
# For everything else, fall back to `gh` per-item lookups.
pr_milestone_cache = {p['number']: MILESTONE for p in all_prs}
issue_milestone_cache = {i['number']: MILESTONE for i in all_issues}
def get_pr_milestone(pr_num):
"""Return the milestone title for a PR, or None if unassigned / unknown."""
if pr_num in pr_milestone_cache:
return pr_milestone_cache[pr_num]
try:
r = subprocess.run(
["gh", "pr", "view", str(pr_num), "--json", "milestone"],
capture_output=True, text=True, check=True)
data = json.loads(r.stdout)
ms = data.get('milestone')
pr_milestone_cache[pr_num] = (ms or {}).get('title')
except (subprocess.CalledProcessError, json.JSONDecodeError):
pr_milestone_cache[pr_num] = None
return pr_milestone_cache[pr_num]
def get_issue_milestone(issue_num):
"""Return the milestone title for an issue, or None if unassigned / unknown."""
if issue_num in issue_milestone_cache:
return issue_milestone_cache[issue_num]
try:
r = subprocess.run(
["gh", "issue", "view", str(issue_num), "--json", "milestone"],
capture_output=True, text=True, check=True)
data = json.loads(r.stdout)
ms = data.get('milestone')
issue_milestone_cache[issue_num] = (ms or {}).get('title')
except (subprocess.CalledProcessError, json.JSONDecodeError):
issue_milestone_cache[issue_num] = None
return issue_milestone_cache[issue_num]
# --- Exclusion rules (shared) ---
EXCLUDED_LABELS = {'release blocker', 'no changelog'}
EXCLUDED_ISSUE_TYPES = {'Task'}
EXCLUDED_PROJECT_STATUS = {'Rejected'}
def issue_excluded(issue):
if not issue: return True
if issue.get('state') != 'CLOSED': return True
if issue.get('issue_type') in EXCLUDED_ISSUE_TYPES: return True
if issue.get('project_status') in EXCLUDED_PROJECT_STATUS: return True
if EXCLUDED_LABELS & set(issue.get('labels', [])): return True
return False
# --- ANOMALIES: milestone mismatches between issues and their referenced PRs ---
# These are the ONLY items that should appear in the report. All other
# discrepancies (exclusion labels, missing valid issues, unmerged PRs,
# duplicates, stale milestone assignments) are workflow errors that the
# LLM must fix in step 6a (pre-flight checks) — they are not anomalies.
# Type A: issue in MILESTONE, referenced PR in different milestone or no milestone
anomalies_a = [] # list of dicts: {issue, issue_title, pr, pr_milestone}
for issue_num in sorted(changelog_issues):
issue = issue_by_num.get(issue_num)
if not issue: continue
if get_issue_milestone(issue_num) != MILESTONE: continue
for pr_num in issue.get('closing_prs', []):
pr_ms = get_pr_milestone(pr_num)
if pr_ms != MILESTONE:
anomalies_a.append({
'issue': issue_num,
'issue_title': issue.get('title', ''),
'pr': pr_num,
'pr_milestone': pr_ms, # may be None
})
# Type B: PR in MILESTONE, the issue it closes is in different milestone or no milestone
anomalies_b = [] # list of dicts: {pr, pr_title, issue, issue_milestone}
for pr_num in sorted(changelog_prs):
pr = pr_by_num.get(pr_num)
if not pr: continue
if get_pr_milestone(pr_num) != MILESTONE: continue
for issue_num in pr.get('closing_issues', []):
issue_ms = get_issue_milestone(issue_num)
# No milestone = issue from another (probably private) project —
# milestones are only required for the "Main" project. Not an
# anomaly, and the issue never belongs in this changelog.
if issue_ms is None: continue
if issue_ms != MILESTONE:
anomalies_b.append({
'pr': pr_num,
'pr_title': pr.get('title', ''),
'issue': issue_num,
'issue_milestone': issue_ms, # may be None
})
# --- Write report ---
def fmt_ms(ms):
return ms if ms else "_none_"
with open(OUTPUT, 'w') as f:
f.write(f'# Changelog Anomaly Report — {MILESTONE}\n\n')
f.write(f'Generated: {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")}\n\n')
f.write('---\n\n')
n_a = len(anomalies_a)
n_b = len(anomalies_b)
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
# --- Anomalies section ---
if n_a or n_b:
f.write('## Anomalies\n\n')
f.write('These are milestone mismatches between an issue in the changelog '
'and its referenced PR (or vice-versa). The changelog claim '
'"this issue is fixed by this PR, all in this milestone" is '
'inconsistent with the actual milestone assignments. '
'Resolve by either updating the milestone on the issue/PR or '
'removing the misleading entry from the changelog.\n\n')
if n_a:
f.write(f'### Issue in {MILESTONE}, PR in different milestone or no milestone\n\n')
by_issue = {}
for a in anomalies_a:
by_issue.setdefault(a['issue'], []).append(a)
for issue_num in sorted(by_issue):
entries = by_issue[issue_num]
title = entries[0]['issue_title']
f.write(f'- {issue_link_title(issue_num, title[:80])}\n')
for e in entries:
ms_label = fmt_ms(e['pr_milestone'])
badge = '🔴' if e['pr_milestone'] is None else '⚠️'
f.write(f' - {badge} Referenced {pr_link(e["pr"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
if n_b:
f.write(f'\n### PR in {MILESTONE}, closing issue in a different milestone\n\n')
by_pr = {}
for b in anomalies_b:
by_pr.setdefault(b['pr'], []).append(b)
for pr_num in sorted(by_pr):
entries = by_pr[pr_num]
title = entries[0]['pr_title']
f.write(f'- {pr_link_title(pr_num, title[:80])}\n')
for e in entries:
ms_label = fmt_ms(e['issue_milestone'])
badge = '🔴' if e['issue_milestone'] is None else '⚠️'
f.write(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
else:
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
# --- Context ---
f.write('---\n\n')
f.write('## Context\n\n')
f.write(f'- Milestone: **{MILESTONE}**\n')
f.write(f'- Milestone total issues (all states): {len(all_issues)}\n')
f.write(f'- Closed issues in milestone: {sum(1 for i in all_issues if i.get("state") == "CLOSED")}\n')
f.write(f'- Valid issues after exclusions (after step 5/6a): {len([i for i in all_issues if not issue_excluded(i)])}\n')
f.write(f'- Issues referenced in changelog: {len(changelog_issues)}\n')
f.write(f'- PRs referenced in changelog: {len(changelog_prs)}\n')
print(f"Anomaly report written to {OUTPUT}")
PYEOF
```
This generates `CHANGES-ISSUES.md` containing **only the anomalies**
milestone mismatches between issues and their referenced PRs:
1. **Issue in milestone, referenced PR in different milestone or no milestone**
the changelog claims a fix here, but the PR is released elsewhere.
2. **PR in milestone, closing issue in a different milestone**
the PR is released here, but the issue it fixes belongs to another version.
(An issue with *no* milestone belongs to another, probably private,
project — milestones are only required on the "Main" project — so it is
neither an anomaly nor a changelog candidate.)
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
If the report contains a rule violation, the LLM has skipped the pre-flight
step and needs to re-run the workflow before re-generating the report.
The report is overwritten each time it's generated, reflecting the current
state of the milestone and changelog. Every number is rendered as a full
`[#N](https://github.com/penpot/penpot/issues/N)` or
`[#N](https://github.com/penpot/penpot/pull/N)` link so the report is
self-contained and clickable in any Markdown viewer.
## Key Principles
- **Issue = changelog unit.** The primary link always points to the
user-facing issue, not the implementation PR.
- **PR = implementation detail.** Reference the PR inline so readers
can find the code changes.
- **Latest version first.** New sections are inserted at the top of the
changelog, below the `# CHANGELOG` header.
- **Issue Type determines section — exclusively.** Use the `issue_type` field from `gh.py` output (Bug → `:bug:`, Feature/Enhancement → `:sparkles:`). **Do not** use labels (`bug`, `enhancement`) or title emoji prefixes (`:bug:`, `:sparkles:`) — they are frequently wrong or contradictory. The `issue_type` is the single source of truth.
- **User-facing descriptions.** Write from the user's perspective — describe
what broke and what was fixed, not internal implementation details.
- **Community attribution.** When the issue or fix PR has the
`community contribution` label, add `(by @<username>)` on the entry line
between the description and the issue link. Use the **PR author** (not the
issue author) for the attribution.
- **Only closed issues.** An issue must have `state: "closed"` to appear in
the changelog. Open/unresolved issues are omitted.
- **Rejected project status.** Issues marked as "Rejected" in the "Main"
project board are automatically excluded by `gh.py`, even if they are
closed. The project status is distinct from the GitHub issue state.
Use `--include-rejected` to override this behavior.
- **Excluded issues.** Issues with `no changelog` label must be excluded.
Issues with `issue_type: "Task"` must also be excluded — they are internal
chores, not user-facing changes.
- **Multiple PRs per issue.** If multiple PRs fix the same issue, list them
comma-separated inline: `(PR: [#A](url), [#B](url))`.
- **Duplicate removal.** If an entry already exists in a prior version section,
remove it from the current version. Check for text-level duplicates (after
stripping links and attributions) across version sections.
- **Taiga references.** If a changelog entry references a Taiga URL
(`tree.taiga.io`), attempt to find a corresponding GitHub issue via the
Taiga description text or by searching GitHub PRs that reference the Taiga
URL. Replace the Taiga reference with the GitHub issue link and add the PR
reference if applicable.
- **Security advisory (GHSA) entries.** Advisories fixed in the release are
listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or
PR link**, even though they are not in the milestone. The GHSA ID and
description come from the user — do **not** fetch or verify the URL, and do
not drop a draft (unpublished) advisory. Precedent:
`- Fix arbitrary file read security issue on create-font-variant rpc method
(https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`.
See step 5b.
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
before making edits, don't rely on cached data.
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
milestone issue listing and PR detail fetching. It handles GraphQL
pagination, batching, and label filtering automatically.
- **Verify PR merge status.** Not all closing PRs are merged — community PRs
can be superseded and closed without merging. Always check that every PR
referenced in the changelog has `state: MERGED`.
- **PR-level exclusions apply.** A PR can carry its own exclusion labels
(`release blocker`, `no issue required`) independent of its linked issue's
labels. Check both.
- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on
the `issues` command only compares issue numbers. Merged PRs not linked to
any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone`
for a full PR cross-reference.
- **False-positive PR-to-issue associations.** A PR may claim to close an
issue from a different project or context. If the PR title and issue title
are clearly unrelated, or the PR predates the issue by years, treat it as a
data glitch and skip it.
- **Anomaly = milestone mismatch only.** The report contains only milestone
mismatches: (1) the issue is in this milestone but the referenced PR is
in a different milestone (or unassigned), and (2) the PR is in this
milestone but the issue it closes is in a different milestone. An
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
an anomaly: milestones are required only for the "Main" project, so such
issues come from another (probably private) project and are not changelog
candidates. These anomalies are reported because the changelog pairing is
*misleading* — the human needs to decide whether the milestone or the
changelog is wrong. All other discrepancies (exclusion labels, missing
valid issues, unmerged PR references, duplicates, stale milestone
assignments) are **rule violations** that the LLM must fix directly in
`CHANGES.md` during step 6a (pre-flight checks). They never appear in
the report — if they do, the pre-flight step was skipped.
+2
View File
@@ -0,0 +1,2 @@
/cache
/project.local.yml
@@ -0,0 +1,40 @@
# Backend Auth, Permissions, and Product Domain Subtleties
## Auth and sessions
- Main auth RPC commands live in `app.rpc.commands.auth`; LDAP and OIDC provider logic live in `app.auth.ldap` and `app.auth.oidc`, with LDAP-specific RPC checks in `app.rpc.commands.ldap`.
- Public auth endpoints must explicitly set `::rpc/auth false`; RPC auth defaults to enabled. Session cookie creation/deletion is usually attached as an RPC response transform.
- Basic Penpot registration is token staged: prepare/register creates or verifies temporary tokens, then profile creation/session setup is reused by other auth backends. The frontend `/auth/verify-token` flow is a hub for registration confirmation, email change, and invitation tokens.
- OIDC-compatible providers share a generic flow: redirect to provider, validate callback/request token, fetch identity data, then login an existing profile or register a new one. Known providers may have hardcoded endpoints; generic OIDC can use discovery/configured endpoints.
- LDAP login validates credentials against the external directory, fetches identity data, then logs in or registers a matching Penpot profile. LDAP registration is not a separate Penpot signup flow.
- Logout may return an OIDC provider redirect URI when the session claims include provider/session data and the provider has a logout URI.
- Invitation tokens are verified through token issuers and only accepted when the token member id/email matches the authenticated profile; otherwise login proceeds without consuming the invitation.
- HTTP/session parsing details such as cookie/header precedence, JWT session token versions, and SameSite behavior are in `mem:backend/http-storage-filedata-subtleties`.
## Permission model
- `app.rpc.permissions` provides predicate/check factories. Failed permission checks intentionally raise `:not-found` / `:object-not-found`, not an authorization-specific error, to avoid leaking object existence.
- Team role flags are normalized as owner > admin > editor > viewer. Owner/admin imply edit; any membership row implies read.
- File/project/comment checks are implemented in the owning command namespaces, often via helpers imported from `files`, `teams`, or `projects`; do not bypass those helpers with direct DB lookups unless preserving their not-found semantics.
- Comment permission includes both logged-in state and the file/team comment policy. Shared viewer paths may pass `share-id`; preserve that path when changing comment queries.
## Teams, projects, and invitations
- Team/project commands mix DB changes, email, message bus notifications, media/storage cleanup, feature flags, quotas, and audit metadata. Keep mutations transactional when the existing command does so.
- Invitation flows validate muted/bounced emails before sending and use tokenized invitation state. Accepting an invitation is tied to the invited member identity, not just possession of a token.
- Logical deletion is used for many product objects; prefer existing logical-deletion helpers over hard deletes unless the command already performs permanent cleanup.
- Bounced/spam-complaint emails can mute/block a profile for login/registration and email sending. Devenv MailCatcher is the normal local path for registration/email-flow testing.
## Comments, webhooks, and audit
- Comment thread queries join file/project/profile state and exclude deleted files/projects. Unread comment counts depend on `comment_thread_status.modified-at` and profile notification preferences.
- Webhook edits are allowed for team editors/admins or the webhook creator. Webhook validation performs a synchronous HEAD request with a short timeout; validation errors are mapped through `app.loggers.webhooks`.
- Audit events are prepared from RPC metadata, result metadata, params, request context, and selected auth identifiers. Webhook event batching can be controlled through audit/webhook metadata on commands or results.
- Webhook and audit logging are cross-cutting side effects of product commands; when adding a command, check nearby command metadata and result metadata patterns before inventing a new event shape.
## Local testing notes
- Enable LDAP login locally with frontend flag `enable-login-with-ldap`; the devenv includes a configured test LDAP service.
- OIDC testing requires external provider app credentials plus matching backend/frontend config.
- Backend domain tests usually live under `backend/test/backend_tests/rpc/commands/*_test.clj` or nearby backend test namespaces. Use focused `clojure -M:dev:test --focus ...` from `backend/` when possible.
- For auth/session or HTTP behavior, combine backend tests with the HTTP/session notes in `mem:backend/http-storage-filedata-subtleties` because RPC-level tests may not exercise cookie/header transforms.
+106
View File
@@ -0,0 +1,106 @@
# Backend Architecture and Workflow
Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; mail; audit/logging; workers.
## Focused memories
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
- Storage abstraction, logical buckets, object lifecycle, deduplication, access, and garbage collection: `mem:backend/storage`.
- HTTP sessions, config, media processing, and file data persistence: `mem:backend/http-storage-filedata-subtleties`.
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
## Stable namespace map
- `app.rpc.commands.*`: RPC command implementations exposed under `/api/rpc/command/<cmd-name>`.
- `app.rpc.permissions`: permission predicate/check helper factories.
- `app.http.*`: HTTP routes and middleware.
- `app.auth.*`: provider-specific authentication helpers such as LDAP/OIDC.
- `app.loggers.*`: audit, webhook, database, and external log integrations.
- `app.db.*` / `app.db`: next.jdbc wrapper and SQL helpers.
- `app.tasks.*`: background task handlers.
- `app.worker`: task execution/cron plumbing.
- `app.main`: Integrant system map and component wiring.
- `app.config`: `PENPOT_*` env config and feature flags.
- `app.srepl.*`: development REPL helpers for manual backend operations (data inspection, migration helpers, one-off admin tasks).
- `app.nitrate`, `app.rpc.commands.nitrate`, and `app.rpc.management.nitrate`: external Nitrate subscription/organization integration, gated by the `:nitrate` feature flag and shared-key HTTP calls.
## RPC conventions
RPC commands are defined with `app.util.services/defmethod` and schemas. Use `get-` prefixes for read operations. Command metadata usually includes auth, docs version, params schema, and result schema. Return plain maps/vectors or raise structured exceptions from `app.common.exceptions`.
Backend RPC command areas without focused memories include access tokens, binfile, demo, feedback, file snapshots, fonts, management, Nitrate, and webhooks beyond the notes in `mem:backend/auth-permissions-product-domains`; inspect nearby command tests and command metadata before changing them.
## DB conventions
`app.db` helpers accept cfg, pool, or conn in most places and convert kebab-case to snake_case:
- `db/get`, `db/get*`, `db/query`, `db/insert!`, `db/update!`, `db/delete!`.
- Use `db/run!` for multiple operations on one connection.
- Use `db/tx-run!` for transactions.
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
For interactive PostgreSQL access with correct dev defaults, use `scripts/psql`; to dump
the current DDL schema, use `scripts/db-schema` (see `mem:scripts/psql`).
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
## Background tasks
A task handler is an Integrant component with `ig/assert-key`, `ig/expand-key`, and `ig/init-key`, returning the function run by the worker. New tasks also need wiring in `app.main`: handler config, worker registry entry, and cron entry if scheduled.
For worker dispatch, cron, retry semantics, deduplication, and queue internals: `mem:backend/rpc-db-worker-subtleties`.
## REPL
In devenv, backend nREPL is exposed on port 6064.
### Non-interactive eval (preferred for agents)
`./scripts/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
```bash
./scripts/nrepl-eval.mjs '(+ 1 2)' # single expression
./scripts/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
./scripts/nrepl-eval.mjs -e # inspect last exception (*e)
./scripts/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
./scripts/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
(def x 10)
(+ x 20)
EOF
```
Default port is 6064. Use `-p <PORT>` for a different port. Use `-t <MS>` to override the 120s timeout. Do not start the nREPL server — assume it is already running.
### Interactive REPL
`backend/scripts/nrepl` starts a REPLy client connected to the running nREPL.
For an in-process backend REPL (where you control the JVM lifecycle), stop the running backend first so port 9090 is free, then run `backend/scripts/repl`. Useful top-level helpers include `(start)`, `(stop)`, `(restart)`, `(run-tests)`, and `(repl/refresh-all)`. Many `app.srepl.main` helpers accept the global `system` var, e.g. manual email or maintenance operations.
## Fixtures
Fixtures can populate local data for manual testing/perf work. From the backend REPL, run `(app.cli.fixtures/run {:preset :small})`; fixture users conventionally look like `profileN@example.com` with password `123123`. Standalone fixture aliases may exist, but check current `backend/deps.edn` before relying on old command names.
## Performance
* **Type Hinting:** Use explicit JVM type hints (e.g. `^String`, `^long`) in performance-critical paths to avoid reflection overhead.
* **Batch inserts:** Use `db/insert-many!` for bulk row inserts — generates a single SQL with multiple parameter tuples. Avoid on very large datasets (SQL length / parameter count limits).
* **Server-side cursors:** Use `db/plan` (fetch-size 1000, forward-only, read-only) or `db/cursor` for large result sets. Never fetch large collections into memory at once.
* **Transaction discipline:** Use `tx-run!` for writes (opens a transaction), `run!` for reads (single connection, no transaction). Set `:read-only` on `tx-run!` when applicable to let PostgreSQL optimize.
## Lint and Format
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
* **Linting:** `pnpm run lint:clj`.
* **Formatting:** `pnpm run check-fmt:clj` to check, `pnpm run fmt:clj` to fix. After running `fmt:clj`, `check-fmt:clj` is redundant. Avoid unrelated whitespace diffs.
**Before linting:** if delimiter errors are suspected (after LLM edits), run
`scripts/paren-repair` on the affected files first. Delimiter errors produce
misleading linter/compiler output. See `mem:scripts/paren-repair`.
## Testing
Backend test commands, coverage rules, and conventions: `mem:backend/testing`.
Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
@@ -0,0 +1,28 @@
# Backend HTTP, Storage, Media, and File Data Subtleties
## Config and HTTP/session middleware
- `app.config/config` and `flags` are dynamic `defonce` vars populated from `PENPOT_*` env vars through the shared schema string transformer. Tests and tooling can bind them.
- `parse-flags` automatically adds `:disable-secure-session-cookies` when `public-uri` is plain HTTP and not localhost. This changes cookie defaults without an explicit env flag.
- The backend sets Clojure `*assert*` globally from the `:backend-asserts` feature flag. Assertion-dependent checks can therefore differ by runtime flags.
- Request body parsing is mostly POST-oriented and supports Transit JSON plus plain JSON. Plain JSON request keys are kebab-decoded before being merged into `:params`.
- Response formatting negotiates with `Accept` or `_fmt=json`. Transit is the default for collection/boolean bodies; JSON encoding has special pointer-map handling.
- Auth prefers the session cookie token before the `Authorization` header. Headers may be `Token` or `Bearer`; JWTs with `kid=1` and `ver=1` are decoded as v1 session tokens, otherwise they are treated as legacy tokens.
- Shared-key auth requires `x-shared-key` as `<key-id> <key>` and stores the lowercased key id on the request. If no shared keys are configured it always rejects.
- Session management uses DB storage unless the DB pool is read-only, then falls back to the in-memory manager. DB sessions support both legacy string ids and v2 UUID session ids.
- Session cookies are renewed when using a legacy string id or when `modified-at` is older than the renewal interval. SameSite is `none` for CORS, otherwise strict/lax based on config.
## Storage and media
- Storage abstraction, backend configuration, logical buckets, object lifecycle, deduplication, access rules, and garbage collection: `mem:backend/storage`.
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.
- Font processing shells out to FontForge and WOFF conversion tools and can derive TTF/OTF/WOFF variants from uploaded fonts.
## File data persistence
- File data backends are `legacy-db`, `db`, and `storage`. The storage backend keeps encoded file data in storage bucket `file-data`; the DB row stores metadata with `storage-ref-id` and nil data.
- `fdata/upsert!` touches any storage object referenced by incoming metadata before storing the new row/blob.
- Pointer-map fragments are persisted separately as type `fragment`, and only modified pointer maps are written.
- `fdata/realize` combines pointer realization and object-map realization. Use it before operations that need complete in-memory file data instead of pointer placeholders.
@@ -0,0 +1,26 @@
# Backend RPC/DB/Worker Subtleties
## RPC exposure and wrappers
- RPC commands are discovered from vars created by `app.util.services/defmethod`; adding a command namespace is not enough unless `backend/src/app/rpc.clj` includes it in `resolve-methods`.
- `GET`/`HEAD` RPC calls are only allowed for method names starting with `get-`. Other methods are method-not-allowed even if they are read-only internally.
- RPC auth defaults to enabled. Public endpoints must set `::auth false` metadata explicitly.
- The wrapper stack does auth before params validation, then auditing/rate/concurrency/metrics/retry/condition handling, with DB transaction handling inside that stack. `::db/transaction` metadata controls transaction wrapping.
- Params with `::sm/params` are decoded/conformed through the JSON transformer and successful IObj results get `:encode/json` metadata. Legacy spec conforming only applies when no Malli params schema exists.
- Nil RPC bodies become HTTP 204 unless explicit status metadata is present. Stream bodies default to `application/octet-stream` when no content type is set.
## DB helpers
- Most `app.db` helpers accept a pool, connection, or map containing `::db/pool` / `::db/conn`; preserve that convention in shared code.
- `db/tx-run!` uses `next.jdbc.transaction/*nested-tx* :ignore`: nested transaction calls reuse the outer transaction, not a savepoint. Use explicit savepoints when nested rollback semantics matter.
- `db/run!` opens/reuses one connection but does not create a transaction.
- `db/tjson` is Transit JSON for jsonb storage; `db/json` is plain JSON. Worker task props use Transit and are decoded with `decode-transit-pgobject`.
- Advisory transaction locks accept UUIDs or ints. UUID locks are hashed using a zero-UUID seeded siphash.
## Workers and cron
- Task queues are tenant-prefixed. Submit dedupe only removes not-yet-due `new` tasks with the same name/queue/label; it does not dedupe due, scheduled, retry, running, or completed work.
- The dispatcher selects `new`/`retry` tasks with `FOR UPDATE SKIP LOCKED`, marks them `scheduled`, and publishes Redis payload `[id scheduled-at]`. The runner skips Redis messages whose scheduled timestamp no longer matches DB state.
- Lost `scheduled` tasks are rescheduled after about 5 minutes; `running` tasks older than about 24 hours are marked failed as orphans.
- A task handler that is missing or returns an invalid result currently defaults to completed after warning. Throwing with `ex-data :type ::retry` controls retry behavior; `:strategy ::noop` retries without incrementing retry count.
- Cron jobs lock their `scheduled_task` row with `FOR UPDATE SKIP LOCKED`, disable statement/idle-in-transaction timeouts locally, and reschedule themselves in `finally` unless interrupted. Worker, dispatcher, and cron components do not start when the DB pool is read-only.
+83
View File
@@ -0,0 +1,83 @@
# Backend Storage
## Abstraction
- `app.storage` stores binary objects.
- Each object has a `storage_object` database row.
- The row stores the UUID, size, backend, timestamps, and Transit metadata.
- The backend stores the binary content.
- Supported backends are `:fs` and `:s3`.
- FS uses one root directory and a UUID-derived path.
- S3 uses one configured bucket and an optional prefix.
- A Penpot bucket is metadata. It is not an S3 bucket or a filesystem directory.
- FS and S3 use the same UUID-derived object path. The bucket does not change the path.
- `PENPOT_OBJECTS_STORAGE_*` configures the current object backend.
- Deprecated asset-storage config keys remain supported for migration.
- Database rows keep the backend name. Keep the legacy `:assets-fs` and `:assets-s3` aliases.
## Object Lifecycle
- `put-object!` creates the database row before it writes backend content.
- Backend content is written only when the row is new.
- A failed backend write can leave an unreferenced database row.
- Callers often set `:touched-at` so garbage collection can remove such rows.
- `get-object` excludes rows with `deleted_at`.
- Existing object values can remain readable until physical deletion.
- `:expired-at` blocks reads after the expiration time.
- `del-object!` sets `deleted_at`. It does not remove backend content.
- `storage-gc-deleted` removes the database row and backend content after the deletion delay.
- `storage-gc-touched` finds references before it sets `deleted_at`.
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
## Deduplication
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
- The lookup does not include file ID, profile ID, team ID, or organization ID.
- Objects can therefore share content across users and files within one bucket.
- Deleted objects are not reused.
- `tempfile` objects never use deduplication, even when the caller requests it.
- Use `sto/wrap-with-hash` when the caller already calculated the content hash.
## Bucket Rules
| Bucket | Content and references | Dedup | Direct `/assets/by-id` access | Cleanup |
| --- | --- | --- | --- | --- |
| `file-media-object` | Original file images and generated media thumbnails. References: `file_media_object.media_id` and `thumbnail_id`. | Yes | Public | Reference scan. |
| `team-font-variant` | Font variants in `team_font_variant`. References: `woff1_file_id`, `woff2_file_id`, `otf_file_id`, and `ttf_file_id`. | Yes | Public | Reference scan. |
| `file-object-thumbnail` | Frame and component thumbnails in `file_tagged_object_thumbnail.media_id`. | Yes | Public | Reference scan. |
| `file-thumbnail` | File grid thumbnails in `file_thumbnail.media_id`. | Yes | Authentication required | Reference scan. |
| `profile` | User and team profile photos. References: `profile.photo_id` and `team.photo_id`. | Yes | Authentication required | Reference scan. |
| `organization` | Organization logos uploaded by the Nitrate management API. | Yes | Public | No reference scan. A touched object is deleted. |
| `tempfile` | Export files, chunked-upload chunks, and temporary font downloads. | No | Authentication required | No reference scan. A touched object uses a two-hour deletion delay. |
| `file-data` | Encoded file data when `file-data-backend` is `storage`. Reference metadata has `storage-ref-id`, `file-id`, and the `file_data` row ID. | Yes | Authentication required | Reference scan. |
| `file-data-fragment` | Compatibility value for file-data fragments. The current backend has no dedicated producer for this bucket. | No current write semantics | Public | No touched-object collector case. |
| `file-change` | Compatibility value for file changes. Current snapshots store data in `file_data`, not this bucket. | No current write semantics | Authentication required | No touched-object collector case. |
- The valid bucket set lives in `app.storage/valid-buckets`.
- `file-media-object` is the default bucket for old rows without bucket metadata.
- Do not assign a new bucket without adding its access and cleanup behavior.
- The touched-object collector raises an internal error for an unknown bucket.
- It supports `file-media-object`, `team-font-variant`, `file-object-thumbnail`, `file-thumbnail`, `profile`, `file-data`, `tempfile`, and `organization`.
- It does not support `file-data-fragment` or `file-change`.
## Access Rules
- `app.http.assets` decides direct object authentication from the bucket.
- Public buckets are `file-media-object`, `file-object-thumbnail`, `team-font-variant`, `file-data-fragment`, and `organization`.
- Other valid buckets require a session or access-token profile ID.
- File-media routes also require file read permission.
- Non-public direct responses set `content-disposition: attachment`.
- FS responses use `x-accel-redirect` for the configured asset path.
- S3 responses use a presigned URL and an HTTP redirect.
## File Data
- `file-data-backend` accepts `legacy-db`, `db`, or `storage`.
- `legacy-db` stores main data in `file.data` and snapshots in `file_change.data`.
- `db` stores encoded data in `file_data.data`.
- `storage` stores encoded data in storage subsystem with `file-data` bucket and keeps `data` nil in `file_data` table.
- The `file_data.metadata.storage-ref-id` value points to the storage object.
- `fdata/upsert!` touches a storage object from incoming metadata before it stores the new row.
- File snapshots use `file_data` for snapshot data and `file_change` for snapshot metadata.
+11
View File
@@ -0,0 +1,11 @@
# Backend Testing
JVM `clojure.test` (kaocha runner) under `backend/test/backend_tests/`.
- READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all JVM test runs.
- All CLI commands must be executed from the `backend/` subdirectory.
- Tests are invoked directly via `clojure -M:dev:test` (kaocha) — there is no pnpm wrapper. Kaocha auto-discovers test namespaces, so no runner registration is needed.
- Coverage: if code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
- Isolated run: `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace, or `clojure -M:dev:test --focus backend-tests.my-ns-test/my-test-var` for a specific test var.
- Regression run: `clojure -M:dev:test` to ensure no regressions in related functional areas.
- If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`.
@@ -0,0 +1,39 @@
# File Mutations: Changes and Undo Architecture
Penpot mutates file data through change records. A change set is both the persistence payload and the basis for undo/redo, so UI actions, tests, backend file updates, and library/file tooling should drive the production change pipeline instead of ad hoc object-map mutation.
## Change shape
Each change is a map such as `{:type ... :id ... :page-id ...}`. Common families:
- `:add-obj`, `:mod-obj`, `:del-obj`: shape lifecycle. `:mod-obj` contains `:operations`, commonly `{:type :set :attr ... :val ... :ignore-geometry ... :ignore-touched ...}` or `{:type :set-touched ...}`.
- `:add-component`, `:mod-component`, `:del-component`: component/library metadata.
- `:add-children`, `:remove-children`, `:reg-objects`: tree and object-map edits.
- `:set-option`, `:add-page`, `:mov-page`, and related file/page metadata changes.
Each transaction carries `:redo-changes` and inverse `:undo-changes`. The undo stack stores transactions and can move its index backward/forward.
## changes-builder API
`common/src/app/common/files/changes_builder.cljc` (usually alias `pcb`) is the fluent builder. Start from `(pcb/empty-changes <it> <page-id>)` or `(pcb/empty-changes nil <page-id>)` for tests.
High-value builder operations:
- `pcb/with-page-id`, `pcb/with-objects`, `pcb/with-library-data`: set context for following operations.
- `pcb/update-shapes ids update-fn`: emits `:mod-obj` with diff-derived `:set` ops. Options include `{:with-objects? true}`, `{:ignore-touched true}`, and `{:attrs #{...}}`.
- `pcb/add-objects`, `pcb/change-parent`, `pcb/remove-objects`, `pcb/resize-parents`: shape/tree edits.
- `pcb/add-component`, `pcb/update-component`, `pcb/mod-component`: component/library edits.
- `pcb/set-translation? true`: marks the whole change set as translation-only, which lets component sync skip expensive work.
## Applying changes in tests
`thf/apply-changes` in `app.common.test-helpers.files` is the test analog of the production applier. It validates by default; pass `:validate? false` only for intentionally-invalid intermediate states.
The applier uses the same `process-operation` multimethod as production (`common/src/app/common/files/changes.cljc`), so tests that use it exercise production behavior.
## :touched and geometry
For component touched semantics and sync groups, read `mem:common/component-data-model`. For the exact `set-shape-attr` / second-pass behavior during change application, read `mem:common/file-change-validation-migration-subtleties`. For transform-specific ignore-geometry behavior, read `mem:frontend/workspace-transform-subtleties`.
## Inspection
To inspect what a UI action emitted, use `mem:frontend/cljs-repl` with the snippets in `mem:common/component-debugging-recipes` rather than adding temporary source instrumentation.
@@ -0,0 +1,47 @@
# Component and Variant Data Model
## Shape roles relative to components
A shape can occupy multiple roles at once:
1. Master/main instance: defines a component and has `:main-instance true` plus `:component-id`.
2. Copy/non-main instance: produced by instantiating a component and carries `:shape-ref` pointing at the master shape. `(ctk/in-component-copy? shape)` is essentially `(some? (:shape-ref shape))`.
3. Component root: topmost shape of an instance, marked `:component-root true` and carrying surface attrs such as `:component-id` and `:component-file`.
Variant masters are main instances and component roots. Their descendants may themselves be component copies, so master/copy logic must handle nested instances rather than assuming those roles are exclusive.
## :shape-ref chains
`:shape-ref` walks up the inheritance hierarchy and can cross files for remote libraries. `find-ref-shape` and `get-ref-chain-until-target-ref` in `app.common.types.file` follow this chain.
`find-shape-ref-child-of` in `app.common.logic.variants` walks the chain looking for the first ref-shape whose ancestors include a specific parent. Variant switch uses this to locate the equivalent master child in the target variant.
## :touched flags
`:touched` is a set of override-group keywords such as `:geometry-group`, `:fill-group`, and `:text-content-group`. It means a copy diverged from its master for attrs in that sync group.
`sync-attrs` in `app.common.types.component` maps attrs to groups. `set-touched-group` is the legitimate setter; the central `set-shape-attr` path calls it only for copies and only when ignore flags allow it.
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
## Swap slots and positional matching
- A swap slot (stored via `ctk/set-swap-slot`, a `:touched` group `swap-slot-<uuid>`) marks a copy sub-head that was SWAPPED to another component; `compare-children` then pairs it to the main child by slot instead of by `shape-ref`.
- Copy sub-heads without a slot are paired to main children by `shape-ref` (seek, not index). `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
## Cloning paths
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
`duplicate-component` in `app.common.logic.libraries` creates a new component master by cloning existing component shapes, setting component metadata, and applying a position delta. It does not have the same clean-copy semantics as `make-component-instance`, so inherited attrs on the source can matter.
When a bug depends on touched state, identify which cloning path produced the shape before changing sync logic.
## Variant containers
A variant container is a frame with `:is-variant-container true`. Its children are variant masters with `:variant-id` pointing at the container and `:variant-name` naming the variant value. Component records in the library carry `:variant-properties`.
Predicates are broad: `ctk/is-variant?` checks `:variant-id` and applies to both variant master shapes and component rows; `ctk/is-variant-container?` checks the container shape flag.
Moving/dropping a shape into a variant container through the move-to-frame path can auto-convert it into a variant via `generate-make-shapes-variant`, which may duplicate the underlying component. Treat drag/drop into variant containers as a component/variant operation, not a plain reparent.
@@ -0,0 +1,58 @@
# Common Component and Change Debugging Recipes
Keep source changes out of these recipes unless the task requires a durable fix.
## Inspect recent workspace changes
From `cljs_repl` after triggering an action:
```clojure
(let [items (get-in @app.main.store/state [:workspace-undo :items])
n (count items)]
(->> items
(drop (max 0 (- n 5)))
(map-indexed (fn [i it]
{:idx (+ i (max 0 (- n 5)))
:tags (:tags it)
:n (count (:redo-changes it))
:types (frequencies (map :type (:redo-changes it)))
:ids (mapv :id (:redo-changes it))}))))
```
To inspect operations within the latest `:mod-obj`:
```clojure
(let [items (get-in @app.main.store/state [:workspace-undo :items])
mod-obj (->> (:redo-changes (last items))
(filter #(= :mod-obj (:type %)))
first)]
(:operations mod-obj))
```
## Trace variant switch attribute copying
To capture what `update-attrs-on-switch` saw during a real UI swap, patch it temporarily in `cljs_repl`:
```clojure
(def orig (deref #'app.common.logic.libraries/update-attrs-on-switch))
(def trace-buf (atom []))
(set! app.common.logic.libraries/update-attrs-on-switch
(fn [& args]
(swap! trace-buf conj
(let [[_ curr prev _ _ origin _] args]
{:curr (select-keys curr [:name :x :y :selrect :points :touched])
:prev (select-keys prev [:name :x :y :selrect :points :touched])
:origin-ref (select-keys origin [:id :name :x :y :width :height :selrect])}))
(apply orig args)))
;; trigger UI action, then inspect @trace-buf
(set! app.common.logic.libraries/update-attrs-on-switch orig)
```
Runtime patching is faster than adding temporary source instrumentation and avoids recompilation cleanup. Restore the var or reload the frontend when finished.
## Test-side helpers
- Use `thf/dump-file file :keys [...]` to print a shape tree with selected keys during common tests.
- Prefer production-path helpers such as `cls/generate-update-shapes` plus `thf/apply-changes` for shape mutations.
- For component swaps with keep-touched behavior, use `tho/swap-component-in-shape` with `{:keep-touched? true}`.
- Temporary `prn` calls in production code are acceptable while investigating but should be removed before committing.
@@ -0,0 +1,42 @@
# Component Swap and Variant Switch Pipeline
## Entry points
Frontend entry points under `frontend/src/app/main/data/workspace/`:
- `variants.cljs`: `variants-switch` and `variant-switch` events feed property-toggle UI and Plugin API `switchVariant` behavior into `dwl/component-swap`.
- `libraries.cljs`: `component-swap` is the single-swap workhorse; `component-multi-swap` batches swaps and calls `component-swap` with `keep-touched? = false`.
`keep-touched? = true` is the discriminator for preserving user overrides during variant switch. Batch/multi-swap paths intentionally bypass that logic.
## Common-side pipeline
For a single swap with `keep-touched? = true`:
1. `cll/generate-component-swap` in `common/src/app/common/logic/libraries.cljc` builds the base changes: remove old shape and instantiate the target component in its place through `generate-new-shape-for-swap`, `generate-instantiate-component`, and `make-component-instance`.
2. `clv/generate-keep-touched` in `common/src/app/common/logic/variants.cljc` walks pre-swap children, augments each with chain-derived touched flags through `add-touched-from-ref-chain`, finds the equivalent target shape through `find-shape-ref-child-of`, then calls `update-attrs-on-switch`.
3. `update-attrs-on-switch` in `app.common.logic.libraries` decides which touched attrs from the previous shape should be copied onto the freshly instantiated target shape.
## update-attrs-on-switch hazards
The routine compares `current-shape` (fresh target copy), `previous-shape` (pre-swap shape with chain-derived touched), and `origin-ref-shape` (source variant master's equivalent shape). It loops over sync attrs except `swap-keep-attrs` and copies only attrs that pass several guards:
- skip equal previous/current values;
- skip equal composite geometry for selected attrs;
- require the corresponding touched group;
- for most attrs, require source and target masters to agree;
- for fixed-size selrect/points/width/height, use dedicated fixed-layout geometry handling;
- text and path shapes have specialized value conversion paths.
The generic fallback branch copies from `previous-shape`. It represents the intended "carry user override through switch" behavior, but bugs usually appear when guards fail to reject incompatible geometry or master differences before reaching that branch.
## Known sharp edges
- Composite `:selrect` and `:points` bypass the simple different-master skip; width/height checks catch some but not all positional differences.
- `previous-shape` may be repositioned by destination-root minus origin-root before copying. For normal variant switch this is often zero, but do not assume it for all swap entry points.
- Touched flags can be inherited through ref chains, so a shape that looks untouched locally may still behave as touched after `add-touched-from-ref-chain`.
## Test harness
`common/src/app/common/test_helpers/compositions.cljc` has `swap-component-in-shape`, which drives `generate-component-swap` plus `generate-keep-touched` with the production `keep-touched?` flag. Use it for focused common tests of variant-switch behavior.
`common/test/common_tests/logic/variants_switch_test.cljc` is the canonical reference suite for swap+touched scenarios. Read nearby tests before adding another case.
+56
View File
@@ -0,0 +1,56 @@
# Common Architecture and Workflow
`common/` intro: shared CLJC for frontend, backend, exporter, library/file tooling, tests. Small semantic changes can affect multiple runtimes.
## Stable namespace map
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
- `app.common.geom.*`: geometry helpers and transformations.
- `app.common.schema` / `app.common.schema.*`: Malli abstraction layer.
- `app.common.math`, `app.common.time`, `app.common.uuid`, `app.common.json`, etc.: cross-runtime utilities.
- `app.common.test_helpers.*`: test builders and production-path helpers.
## Layering and cross-runtime rules
Use reader conditionals for platform-specific code. Because CLJC runs on JVM and CLJS targets, avoid assuming browser-only or JVM-only behavior unless the reader conditional isolates it.
Respect the intended abstraction direction in new/refactored code:
- generic data utilities should not know Penpot domain concepts;
- `types.*` should preserve invariants for a single domain entity or ADT;
- `files.*` can coordinate several entities inside a file and preserve referential integrity;
- `changes*` should adapt serializable change records to lower-level operations and avoid embedding broad business algorithms;
- `logic.*` and frontend/backend event layers own higher workflow/business behavior.
Some legacy code violates this layering; do not copy those violations into new code when a focused refactor is practical.
## Focused memory routing
Model, schema, and persistence shape:
- File/page/shape/component attr changes, import/export surfaces, inspector/codegen, and cross-module checklist: `mem:common/data-model-change-checklist`.
- Token data structures, token import/export, active theme/set semantics, and schema/coercion behavior: `mem:common/tokens-schema-subtleties`.
Geometry and layout:
- Shape geometry invariants, redundant geometry fields, and geometry-sensitive tests: `mem:common/geometry-invariants`.
- Coordinate drift and approximate float comparisons: `mem:common/decimals-and-coordinates`.
- Layout/grid assignment, deassignment, metadata cleanup, and auto-positioning: `mem:common/layout-grid-subtleties`.
Change pipeline, validation, and migrations:
- Change records, undo/redo architecture, changes-builder API, and production-path mutation guidance: `mem:common/changes-architecture`.
- Change application, shape-tree edits, validation/repair, migrations, and second-pass touched behavior: `mem:common/file-change-validation-migration-subtleties`.
Components, variants, and debugging:
- Component/variant data model, ref chains, touched override semantics, and cloning paths: `mem:common/component-data-model`.
- Component swap, variant switch, and keep-touched pipeline: `mem:common/component-swap-pipeline`.
- Live inspection snippets, temporary runtime patching, and test-side debugging helpers for common change/component behavior: `mem:common/component-debugging-recipes`.
Text and tests:
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
## Areas without focused memories
Common areas with little or no dedicated memory include colors, media/SVG helpers, path operations, thumbnail helpers, generic pools, weak refs, and some utility namespaces. Treat work there as source/test-led unless a focused memory exists.
@@ -0,0 +1,25 @@
# Common Data Model Change Checklist
## Attribute conventions
- Prefer optional page/shape attrs with default behavior when absent. Reverting to default should usually remove the attr instead of storing nil.
- Do not treat nil as a distinct persisted state from absence. Import/export and cleanup paths may filter nil attrs away.
- Avoid Clojure-special naming in exported object attrs, especially boolean names ending in `?`; exported/imported data must survive JSON/SVG/Transit and external tooling.
- Any new shape attr that participates in component sync must be listed in `app.common.types.component/sync-attrs` with the correct touched group. Attrs absent from `sync-attrs` are ignored by component synchronization.
## Cross-module update checklist
When changing the file data model, check the relevant paths:
- Schema/type definitions under `common/src/app/common/types*` and helpers under `common/src/app/common/files*` / `logic*`.
- File migrations in `common/src/app/common/files/migrations.cljc` when old files cannot safely use absence/default behavior.
- Frontend edit forms under `frontend/src/app/main/ui/workspace/sidebar/options/`; multi-selection behavior is usually in `multiple.cljs` and must handle `:multiple` values.
- SVG/file render and export metadata under `frontend/src/app/main/ui/shapes/*`, especially `export.cljs` when an attr is not a native SVG property.
- SVG import/parser paths under `frontend/src/app/worker/import/parser.cljs`; attrs not exported and imported will be lost on reimport.
- Viewer inspect and code generation under `frontend/src/app/main/ui/viewer/inspect/*` and `frontend/src/app/util/code_gen.cljs` / markup/style helpers when handoff output should expose the attr.
- Exporter/library consumers when the change affects file construction, rendering, or packaged `.penpot` archives.
## Migrations
Existing files should keep working unchanged when possible. If absence cannot preserve old behavior, add a migration and preserve append/order semantics described in `mem:common/file-change-validation-migration-subtleties`.
Model changes can also require file feature flags or migration metadata updates; check nearby migrations and `common/src/app/common/features.cljc` before inventing a new pattern.
@@ -0,0 +1,54 @@
# Decimals and Coordinates in Penpot
Penpot stores all geometry as JS numbers (doubles in CLJS, doubles in
CLJ for the JVM-side common code). Several Penpot-specific facts
about how this plays out are not obvious from reading the code.
## Sub-pixel drift is routine
Coordinate values that "should" be integers are routinely off by ~1e-5
in production data. A `:width` of 107 will frequently appear as
`107.00001275539398` after the value has passed through:
- the modifier propagation pipeline (`apply-wasm-modifiers` and the
Rust WASM transform engine)
- any rotation/scale composition
- repeated translations
This drift is invisible in the UI (the renderer rounds at draw time)
but defeats exact equality comparisons in business logic. It does NOT
appear in JVM-only test setups because the WASM pipeline isn't
involved — tests that build shapes via `setup-shape` and `add-sample-shape`
get clean integer values. Bugs that depend on drift will pass tests
but fire in production unless tests explicitly inject drift.
## Use the close? helpers, not `=`
For comparing coordinate-like floats, the established convention is:
- `app.common.math/close?` — scalar tolerance comparison.
Default precision 0.001 (sub-pixel; tight enough to keep distinct
shapes distinct, loose enough to absorb arithmetic noise).
Two-arity uses default precision; three-arity takes a custom one.
- `app.common.geom.point/close?` — element-wise close on `gpt/Point`
records. Compares :x and :y via `mth/close?`.
- `app.common.geom.matrix/close?` — close on transform matrices.
- `app.common.geom.shapes/close-attrs?` — used inside `set-shape-attr`
to decide whether a re-assigned `:width`/`:height` should be treated
as a no-op (suppresses spurious touched marking from drift).
Treat `=` on `:x`, `:y`, `:width`, `:height`, `:selrect`, or `:points`
fields as a code smell when the inputs may have flowed through any
transform. The `set-shape-attr`-style precedent (already using
`close-attrs?`) is the right model.
## The redundancy multiplies failure modes
A shape's position lives in `:x/:y`, `:selrect`, AND `:points` (see
`mem:common/geometry-invariants` memory). Each is a separate set of float
values. After any operation that touches geometry, all three should
agree, but each is computed by a different path and accumulates
its own drift. Comparing `:selrect.width` from shape A to
`:selrect.width` from shape B is comparing two values that
"semantically" should be equal but were computed via different
operation chains — exact equality will often be false.
@@ -0,0 +1,31 @@
# Common File Change, Validation, and Migration Subtleties
## Change application
- `process-changes` validates the whole change vector once by default, reduces changes, then performs a second pass for collected touched changes. Callers that already validated can pass `verify? false`.
- `process-operation :set` delegates to `ctn/set-shape-attr`; `:assign` first decodes attrs with the shape-attrs JSON transformer and then emits per-attr set operations.
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
- Copy structure is guarded at change application: `:mov-objects` (`is-valid-move?`) and `:reorder-children` both refuse to alter children of shapes inside component copies unless the change carries `allow-altering-copies` (sync/swap flows set it). New structural change types must follow the same rule.
- `cls/generate-delete-shapes` propagates deletions from INSIDE a component main to the copy shapes referencing them (transitively, all pages of the file) so no dangling `shape-ref`s remain; skipped when the main root itself is deleted (copies then resolve into the deleted component) and for `allow-altering-copies` flows (swap replaces the shape; sync reconciles).
## Shape tree edits
- `shape-tree/add-shape` falls back invalid/missing parent or frame ids to root (`uuid/zero`), ensures parent `:shapes` is a vector, avoids duplicate child ids, and clears `:remote-synced` on copy parents unless `ignore-touched` is true.
- `shape-tree/delete-shape` removes the shape and all descendants from the objects map and removes the id from its parent. This is different from render-wasm deletion, which may keep deleted children for undo/redo internals.
- Page object maps can carry metadata indexes such as cached frame lists. `start-page-index` / `update-page-index` rebuild those metadata indexes; `frontend` commit application calls `ctst/update-object-indices` after page changes.
## Validation and repair
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule.
## Migrations
- Prefer optional attrs/default behavior so old files continue working without migration. If absence cannot preserve old behavior, add a migration.
- Migrations are an ordered set mixing legacy version-derived ids and newer named ids. Keep append order stable; `migrate` applies the set difference between available migrations and file migrations.
- `migrate-file` synthesizes legacy migration ids from old numeric versions when `:migrations` is absent, migrates legacy features, and records feature flags created through `cfeat/*new*`.
- When a file had no previous `:migrations`, `migrate-file` marks all migrations as migrated in metadata so callers persist the complete migration set, not only transformations that changed data.
@@ -0,0 +1,48 @@
# Geometry Invariants in Penpot Shapes
Core invariant: shape position is stored redundantly, and all geometry fields must stay coherent.
## Redundant fields
For a shape at `(x, y)` with width `w` and height `h`:
- `:x`, `:y`, `:width`, `:height`: top-left and dimensions.
- `:selrect`: `{:x :y :width :height :x1 :y1 :x2 :y2}`, where `x2 = x + w` and `y2 = y + h`.
- `:points`: four corners for an axis-aligned rect, clockwise from top-left.
- `:transform` and `:transform-inverse`: identity for axis-aligned shapes; populated for transformed shapes.
After a geometric mutation, equivalent fields such as `:y`, `(:y :selrect)`, and the first point's `:y` should agree. The renderer and hit-testing read `:selrect` / `:points`, so a shape can render or select incorrectly even when `:x` / `:y` look right.
## Helpers that preserve the invariant
- `gsh/move`: translates by delta and updates geometry consistently.
- `gsh/absolute-move`: moves to an absolute position by computing a delta from the current selrect.
- `gsh/transform-shape`: applies a full transform.
- `cts/setup-shape`: initializes geometry for new shapes; variant test helpers such as `thv/add-variant-with-child` use it.
## Edits that break the invariant
- `(assoc shape :x ...)` or `(assoc shape :y ...)`: updates only one field and leaves `:selrect` / `:points` stale.
- `ths/update-shape file label :y val`: goes through `set-shape-attr`, but does not repair all position fields for `:y` alone.
- Direct `update-in` edits to `:selrect`, `:points`, or dimensions.
## Test setup warning
When positioning test shapes, use `gsh/absolute-move`, `gsh/move`, or production change helpers. Do not set only `:x` / `:y`.
```clojure
(cls/generate-update-shapes
(pcb/empty-changes nil page-id)
#{(:id child)}
#(gsh/absolute-move % (gpt/point (:x %) 101))
(:objects page)
{})
```
Using `(ths/update-shape file label :y 101)` leaves `:selrect.y` stale. Downstream code that reads `:selrect` can then fail in ways that look like product bugs but are only invalid test setup.
## :touched and geometry mutation
When a copy shape changes geometry through the proper pipeline (`set-shape-attr` via `process-operation :set`), `:touched` gains `:geometry-group` unless ignored. Tests can either drive the production update with `cls/generate-update-shapes`, or inject `(assoc shape :touched #{:geometry-group})` when only touched state matters.
If a test needs both a new position and touched state, move the shape first with geometry-preserving helpers, then inject or assert touched state.
@@ -0,0 +1,16 @@
# Common Layout and Grid Subtleties
## Layout metadata
- Layout container data and child layout-item data are removed by different helpers. Do not assume clearing a layout frame also clears all child layout metadata.
- Layout data can affect both container attrs and immediate child attrs; validate behavior for both sides when changing cleanup or propagation.
## Grid assignment
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
- `position-absolute?` counts HIDDEN shapes as absolute: hiding a grid child frees its cell on the next `assign-cells`.
- `reorder-grid-children` rewrites the parent's `:shapes` to the REVERSE of the sorted cell order, but children with no cell (hidden/absolute) keep their original index — do not "fix" this into moving them to an end; that broke copy/main positional slot alignment (referential-integrity crash).
- The `:reorder-children` change it emits is refused on parents inside component copies unless `allow-altering-copies` (same rule as `:mov-objects`); `pcb/reorder-grid-children` also skips copy grids producer-side. Copy child order is owned by the component sync engine.
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
+53
View File
@@ -0,0 +1,53 @@
# Common Testing and Verification
`common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved.
## Unit tests
READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all CLJS/JS and JVM test runs.
Common tests live under `common/test/common_tests/` and use `clojure.test`.
They are CLJC and run on both JVM and JS.
From `common/`:
- Full JVM test run: `clojure -M:dev:test`
- Full JS test run (always builds, suppressed output): `pnpm run test:quiet`
- Full JS test run (always builds, build output visible): `pnpm run test`
- Focus a JVM test namespace: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test`
- Focus a JVM test var: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch`
- Focus a JS test namespace: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test`
- Focus a JS test var: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute`
- Quiet logging during a JS run: append `--log-level warn` (or `trace|debug|info|warn|error`)
- Build JS test target only (no run): `pnpm run build:test`
- After `build:test` has been run, run the compiled runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
New common JS test namespaces must be required/listed in `common_tests/runner.cljc`;
new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags
compose as a union.
## Test helpers
Helpers live under `common/src/app/common/test_helpers/` and are usually aliased with short `th*` prefixes. Test namespaces using label->uuid helpers should start with `(t/use-fixtures :each thi/test-fixture)` so labels reset between tests.
Useful builders:
- `thf/sample-file` creates a base file.
- `tho/add-simple-component` creates a simple component.
- `thc/instantiate-component` instantiates a component copy.
- `thv/add-variant-with-child` creates a variant container with two child variants.
- `thv/add-variant-with-copy` creates variants whose children are component instances.
`add-variant-with-copy` does not accept position params for children; use `gsh/absolute-move` after creation if positions matter.
## Driving production paths
For shape mutations, prefer production-path helpers such as `cls/generate-update-shapes` plus `thf/apply-changes`. For component swaps with keep-touched behavior, use `tho/swap-component-in-shape` with `{:keep-touched? true}`.
`thf/apply-changes` validates by default and usually gives the most useful invariant failure. Pass `:validate? false` only for intentionally malformed intermediate state.
## Geometry setup caution
For geometry-sensitive tests, read `mem:common/geometry-invariants` before positioning shapes. Use geometry-preserving helpers or production change helpers rather than direct single-field edits.
## Debugging
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
@@ -0,0 +1,14 @@
# Common Text Subtleties
## DraftJS compatibility
- `app.common.text` is legacy DraftJS conversion support. New text work should prefer the newer text type namespaces unless specifically touching DraftJS conversion.
- DraftJS style values are encoded as Transit strings under `PENPOT$$$<key>$$$<encoded>` style names. `PENPOT_SELECTION` is a special marker.
- Text conversion uses Unicode code points on both CLJ and CLJS paths, not UTF-16 code units. This matters for offsets around emoji and astral characters.
- Draft conversion fixes gradient type strings back to keywords.
## Modern text content
- Modern text content schema is narrow: root -> paragraph-set -> paragraph -> text nodes.
- `position-data` is derived layout/geometry/font fragment data and should be treated as generated state, not source-of-truth file data.
- Token propagation and some non-current-page text updates drop `:position-data` so it can be regenerated in the right runtime context.
@@ -0,0 +1,17 @@
# Common Tokens and Schema Subtleties
## Tokens
- `TokensLib` always ensures an internal hidden theme exists and defaults active themes to that hidden theme path. That hidden theme represents the UI state where active sets are controlled without modifying a user-created theme.
- `get-tokens-in-active-sets` merges tokens from sets selected by active themes in set order, so later active sets with the same token name override earlier ones.
- Activating a real theme in `common.logic.tokens` removes the hidden theme from the active-theme set unless it is the only active theme. Toggling active sets directly copies current active sets into the hidden theme.
- DTCG import/export deliberately hides the internal hidden theme: exports omit it from `$themes` and activeThemes, while `activeSets` records the hidden/current active sets.
- Single-set DTCG/legacy imports throw if no supported tokens are found. Multi-set import normalizes set names, keeps `tokenSetOrder`, rejects conflicting token path names, discards unsupported token types, and validates theme sets against existing sets.
- Token values stored on shapes are token names in `:applied-tokens`, not token ids. Renames and group renames must update those name paths.
- Token serialization has both Transit handlers for frontend/backend transport and Fressian handlers for internal file-data storage, with migrations for older token-lib internal versions.
## Schema
- `app.common.schema/json-transformer` has custom map-of key decoding/encoding, so map keys can be transformed based on the key schema instead of only the value schema.
- `check-fn` throws `ex-info` with default `:type :assertion`, `:code :data-validation`, and `::explain`. Prefer reusable `check-fn`/lazy validators in hot or repeated paths; `sm/check` creates a checker every call.
- `coercer` decodes with the JSON transformer and then checks. This is the common pattern for accepting external JSON-shaped data into internal types.
+91
View File
@@ -0,0 +1,91 @@
You are working on the GitHub project `penpot/penpot`, a monorepo.
# Memory system
- Memories are the primary project guidance (not docs or other readme files).
- A section's top-level memory is `<section>/core`. When a section is relevant, read the core memory
before focused memories.
- Edits/stale refs/duplication cleanup: `mem:memory-maintenance`.
- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`.
# Development workflow
- Commit/PR/issue creation is **on explicit request only**. Before any of these actions, read the relevant memory — don't infer format from prior examples:
- Before `git commit``mem:workflow/creating-commits` (subject format, body, `AI-assisted-by: model-name` trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
*After making changes, run the applicable lint and format checks for the affected module before considering the work done (per example `mem:backend/core` or `mem:frontend/core`).
- Align `let` binding values: when a `let` form has multiple bindings spanning
several lines, align the value forms to the same column with spaces.
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
fix them with `scripts/paren-repair` BEFORE running lint/format checks.
See `mem:scripts/paren-repair` for usage.
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
# Project modules
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
- `frontend/`: ClojureScript + SCSS SPA/design editor; core conventions: `mem:frontend/core`.
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers; core conventions: `mem:backend/core`. Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities; core conventions: `mem:common/core`.
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend; core conventions: `mem:render-wasm/core`.
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export; core conventions: `mem:exporter/core`.
- `mcp/`: TypeScript Model Context Protocol integration; core conventions: `mem:mcp/core`.
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types; core conventions: `mem:plugins/core`.
- `library/`: design library workflows; core conventions: `mem:library/core`.
- `docs/`: documentation site; core workflow and conventions: `mem:docs/core`.
- `media-processor/`: TypeScript/Node.js HTTP service for image (sharp) and font (FontForge) processing; core conventions: `mem:media-processor/core`.
The memory is structured in a way that you can get the critical information about the
module. You can read it from `mem:<MODULE>/core`
# Low-centrality project paths
- `docker/` contains devenv related code, not needed unless specifically instructed.
When working on devenv startup, compose layout, instance config (`defaults.env`),
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s
`*-devenv` commands, read `mem:devenv/core`.
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
# Dev Scripts (scripts/)
- `scripts/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
See `mem:scripts/nrepl-eval`.
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
See `mem:scripts/paren-repair`.
- `scripts/psql` — PostgreSQL client wrapper with devenv defaults.
Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`.
- `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the
Penpot Taiga project without authentication. See `mem:scripts/taiga`.
- `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`.
- `scripts/error-reports.mjs` — Query error reports via RPC API with token
authentication. Supports list/get operations with filtering and pagination.
See `mem:scripts/error-reports`.
# Dependency graph
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can
affect frontend, backend, exporter, file migrations, and design-library behavior; validate across consumers when
semantics change.
# Working with Penpot designs
- Before automating or inspecting Penpot designs through the Plugin API, call the Penpot MCP `high_level_overview` tool.
- connection between the JavaScript plugin API and the ClojureScript code: `mem:frontend/plugin-api-to-cljs-binding`.
- executing ClojureScript code in the frontend: `mem:frontend/cljs-repl`.
- handling Clojure compiler errors, runtime patching and debug helpers: `mem:frontend/handling-errors-and-debugging`.
## Detecting Crashes
The Penpot frontend can crash silently from the JS API's perspective: `execute_code` calls return successfully, but 1-2s later the workspace becomes unusable (Internal Error page).
The `execute_code` tool then stops working, but `cljs_repl` still works. Use it to detect a crash via `(some? (:exception @app.main.store/state))`.
For details on handling crashes, read memory `mem:frontend/handling-crashes`.
+76
View File
@@ -0,0 +1,76 @@
# Devenv startup and configuration
Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Parallel instances share infra + Postgres + MinIO; each instance has its own `main` container, Valkey, source checkout, tmux session.
## Compose project layout
- `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`.
- `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer.
- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount.
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
## Source-of-truth files
- `docker/devenv/defaults.env`: ws0 baseline — container/volume names, runtime env, published host ports, tmux defaults. `manage.sh` aborts if unreadable.
- For ws1+, `instance-env-overrides` computes the per-instance overrides (container/volume names, host ports offset `10000·N`, `PENPOT_PUBLIC_URI`, `PENPOT_REDIS_URI`, `PENPOT_BACKEND_WORKER=false`) and `instance-compose` injects them as env vars at compose time — never written to disk, recomputed each call so they can't drift. ws0 uses `defaults.env` as-is.
- `backend/scripts/_env`: backend-internal only — secret keys, `PENPOT_FLAGS` (with `enable-backend-worker` gated on `PENPOT_BACKEND_WORKER`), `JAVA_OPTS`, `setup_minio()`. Never duplicates `defaults.env`.
- Compose files use pure `${VAR}` substitution; missing var = compose fails.
## Invariants
- `infra-compose` / `instance-compose` wrap `docker compose` with `env -i`, then re-inject what compose needs. Stripping is required because `defaults.env` is sourced into manage.sh's shell at startup (stale values would leak); the ws1+ overrides are deliberately re-injected as shell env vars precisely because Compose gives shell precedence over `--env-file`, so they override the `defaults.env` baseline.
- Volume names pinned via `name:` (PENPOT_*_VOLUME), decoupled from the compose project name. ws1+ inject distinct per-instance volume names; ws0 keeps the historical `penpotdev_*` physical names so project renames never require data migration.
- Network aliases (`- main`, `- redis`) are not declared in main.yml. Compose's auto-service-alias still registers `redis` on the shared network, so DNS for `redis` is non-deterministic with multiple instances. Backend uses `PENPOT_REDIS_URI=redis://penpot-devenv-wsN-valkey/0` (container_name) instead.
- No cross-project `depends_on`. `manage.sh ensure-infra-up` `docker wait`s on the `minio-setup` one-shot.
- `JAVA_OPTS` in `manage.sh` is shadowed inside the container by `_env`. The `-e JAVA_OPTS=...` flag only matters for processes that don't source `_env`.
## Worker policy
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running.
## Port layout
Container-internal ports fixed; host side offset `10000·N`.
| ws0 | ws1 | wsN | container | role |
|---|---|---|---|---|
| 3449 | 13449 | 3449+10000·N | 3449 | public HTTPS (Caddy; `/mcp/ws` same-origin) |
| 3449/udp | 13449/udp | … | 3449/udp | HTTP/3 |
| 4401 | 14401 | … | 4401 | MCP HTTP stream |
| 4403 | 14403 | … | 4403 | MCP REPL |
| 14181 | 24181 | … | 14281 | Serena MCP |
| 14182 | 24182 | … | 24282 | Serena dashboard |
Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin dev, MCP inspector/WebSocket) is in-process or same-origin via Caddy/nginx. Infra publishes: mailer 1080, ldap 10389/10636 (singletons, not offset).
## Tmux + MCP routing
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv --agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv --agentic` — the conditional windows are only added at session create time.
MCP plugin routing is same-origin: frontend uses `<public-uri>/mcp/ws`, per-instance nginx proxies to MCP port 4401 in-container. For the plugin↔MCP server wiring (how the browser plugin discovers the URL, the in-memory connection registry, why DB-mediated routing isn't needed), see `mem:mcp/core`.
## Workspace orchestration (ws1+)
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv --agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
`sync-workspace wsN`:
1. `assert-clean-git-state` — refuses on `.git/{rebase-apply,rebase-merge,MERGE_HEAD,CHERRY_PICK_HEAD,index.lock}`. No `--sync-force` escape.
2. `rsync -a --delete $PWD/.git/ $workspace/.git/`.
3. `git ls-files -z --cached --others --exclude-standard``rsync --files-from` (Git is the authority on tracked files; rsync's gitignore filter would drop committed files under gitignored parents like `.clj-kondo/config.edn`).
4. Initial-only copy of `frontend/resources/public/js/config.js` (gitignored, but agentic mode needs it). After the first sync the workspace's copy belongs to the developer — subsequent syncs leave it alone.
5. `git switch -C "wsN/<current-branch>"` inside the workspace.
No `--delete` on the working-tree pass: gitignored caches in the workspace survive. Workspace dir + named volumes survive `compose down`.
## CLI surface
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX] [--opencode-config-dir DIR]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). `--opencode-config-dir DIR` bind-mounts DIR at `~/.config/opencode` in-container via the optional overlay above; mount applies at container creation, so changing it requires stop + re-run.
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv`: legacy alias, ws0 non-agentic attached.
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)
- `start-devenv` / `log-devenv` / `drop-devenv`: legacy paths around ws0 + shared infra. `drop-devenv` never removes volumes.
`exporter/scripts/run` and `wait-and-start.sh` source `backend/scripts/_env` then `_env.local` if present.
+19
View File
@@ -0,0 +1,19 @@
# Docs
`docs/`: Penpot documentation site; Eleventy.
## Layout and Tooling
- `docs/package.json`: Eleventy commands and documentation-site dependencies.
- `docs/README.md`: local setup and tooling notes.
- Diagrams may use PlantUML, svgbob, mermaid, and C4/arc42 conventions depending on the existing page.
## Commands
From `docs/`:
- Install deps if needed: `pnpm install`.
- Local server: `pnpm start` or `pnpm run serve` (serves on `http://localhost:8080`).
- Build: `pnpm run build`.
- Watch: `pnpm run watch`.
Documentation changes should follow the existing page structure and rendered Help Center conventions rather than inventing a new style locally.
+35
View File
@@ -0,0 +1,35 @@
# Exporter Architecture and Workflow
`exporter/`: CLJS/Node headless export service. Depends on `common/`; uses Playwright plus export JS/CLJS deps for SVG/PDF/assets.
## Layout and commands
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; test bundle `pnpm run build:test`; tests `pnpm run test` or `pnpm run test:quiet`; lint `pnpm run lint:clj`; format check/fix `pnpm run check-fmt:clj` / `pnpm run fmt:clj`.
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Exporter test conventions and CI: `mem:exporter/testing`.
## HTTP and browser pool
- POST body limit is about 60 MB. Exporter supports `application/transit+json`; request params merge query params and body params.
- Map response bodies are Transit JSON and force HTTP 200; nil 200 bodies become 204.
- Auth token comes from cookie `auth-token`, then uploads use Bearer auth plus the management shared key.
- Each export job gets a fresh Playwright browser context. On success, the context closes and the browser returns to the pool; on error, the browser is destroyed instead of reused.
- Borrow validates browser connection. Pool acquire timeout is about 10s; font loading timeout logs a warning and continues after about 15s.
## Export batching and async behavior
- `prepare-exports` groups entries by `[scale type]` and partitions groups into chunks of 50. Each partition uses file/page/share/name from its first item, so be careful if entries might cross those boundaries.
- Single-export response is used only when multiple export is not forced and there is exactly one prepared export containing exactly one object.
- Multi-object export can run async: when `wait` is false it returns a resource immediately and publishes progress/end/error to Redis by profile topic; when `wait` is true it waits for upload and returns the uploaded resource.
- Frame export returns a resource immediately and publishes Redis updates; it does not follow the same `wait` option path.
- ZIP entry names are sanitized and duplicates receive numeric suffixes.
## Render details
- Bitmap export differs for WASM vs non-WASM render paths: WASM forces Playwright `deviceScaleFactor` to 1 and passes scale through the render URL; non-WASM uses `deviceScaleFactor = scale`.
- WebP is produced by taking a PNG screenshot and converting it with ImageMagick.
- SVG export rasterizes text foreignObjects to PNG, converts through PPM/color masks/potrace, and reassembles SVG paths. It also replaces non-breaking spaces for SVG compatibility and drops empty defs/paths.
- PDF export injects `@page` sizing through raw browser `evaluate` JavaScript; that code cannot rely on CLJS runtime helpers.
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
+16
View File
@@ -0,0 +1,16 @@
# Exporter Testing
- READ `mem:testing` first.
- Tests use `cljs.test` and live under `exporter/test/exporter_tests/`.
- Register every test namespace in `exporter-tests.runner`.
- From `exporter/`: `pnpm run build:test` builds the Node test bundle without running tests.
- From `exporter/`: `pnpm run test` builds and runs tests with full output.
- From `exporter/`: `pnpm run test:quiet` builds and runs tests with reduced build output.
- After `build:test`, reuse the compiled bundle with `node target/tests/test.js`.
- For iterative focused runs, build once and reuse the compiled bundle.
- Focus a test namespace with `node target/tests/test.js --focus exporter-tests.renderer-svg-test`.
- Focus a test var with `node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element`.
- Set app log level by appending `--log-level warn` (or `trace|debug|info|warn|error`).
- `test:quiet` accepts forwarded options but rebuilds the bundle; prefer the direct runner after `build:test` for focused runs.
- From `exporter/`: `pnpm run check-fmt:clj` checks ClojureScript formatting.
- From `exporter/`: `pnpm run lint:clj` runs ClojureScript linting.
+94
View File
@@ -0,0 +1,94 @@
# ClojureScript REPL and Frontend Debugging
Execute code in the live frontend via the Penpot MCP `cljs_repl` tool. For browser-console debugging, the frontend also exports a `debug` JS namespace in development builds.
## Accessing app state
The main store is `app.main.store/state`. It contains workspace metadata, selection, UI state, profile, route, etc. Page objects are not under a `:workspace-data` key; use derived refs.
```clojure
;; Current selection
(mapv str (get-in @app.main.store/state [:workspace-local :selected]))
;; Current page objects
(let [objects @app.main.refs/workspace-page-objects
shape (get objects (parse-uuid "some-uuid-here"))]
(select-keys shape [:name :type :x :y :width :height :fills :strokes :rotation :opacity :frame-id :parent-id]))
```
Shape keys use kebab-case keywords. Internal `:rect` corresponds to "rectangle" in the JS Plugin API, and `:frame` corresponds to "board".
Component instance shapes carry `:component-id` and `:component-file` directly; `:component-root` flags the root of an instance. Use `app.common.types.container/get-head-shape` for nearest head and `get-instance-root` for outermost root; they differ for nested instances.
## Navigation recipe
To programmatically open a workspace file, all three ids are required:
```clojure
(do (require '[app.main.data.common :as dcm])
(app.main.store/emit! (dcm/go-to-workspace
:team-id (parse-uuid "<team-id>")
:file-id (parse-uuid "<file-id>")
:page-id (parse-uuid "<page-id>"))))
```
Get `team-id` from `(:current-team-id @app.main.store/state)`. Get file ids from `(vals (:files @app.main.store/state))`. Get page ids by fetching file data, e.g. through `rp/cmd! :get-file` with current features.
## Reload the live runtime
`(.reload js/location)` (alias `app.util.dom/reload-current-window`) from `cljs_repl` reloads the browser page: clears `set!` runtime patches, re-fetches file state, and is the simplest crash recovery while the repl is live (`mem:frontend/handling-crashes`). To re-fetch only the current file's data without a full page reload, emit `(app.main.store/emit! (potok.v2.core/event :app.main.data.workspace/reload-current-file))`.
## Useful lookup helpers
`app.plugins.utils` contains state lookup helpers that are useful from any CLJS, despite living under `plugins/`:
- `locate-shape`, `locate-objects`, `locate-file`.
- `locate-component` resolves through the outermost instance root.
- `locate-head-component` resolves through the nearest component head.
- `locate-library-component` does direct file-id/component-id lookup.
## Runtime patching with `set!`
Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching. From `cljs_repl`, use `set!` for temporary debugging of CLJS vars such as `app.main.store/on-event`, `app.main.errors/reload-file`, `app.main.errors/is-plugin-error?`, `app.main.errors/last-report`, or `app.main.errors/last-exception`. These patches affect only the live browser runtime and disappear on reload or recompilation.
```clojure
;; Log non-noisy Potok events temporarily.
(set! app.main.store/on-event
(fn [event]
(when (potok.v2.core/event? event)
(.log js/console (potok.v2.core/repr-event event)))))
```
Restore mutable hooks after debugging, or reload the frontend. Use JVM `alter-var-root` only for JVM Clojure; it is not the normal way to patch live CLJS browser vars.
## Browser-console debug namespace
In development, the JS console exposes `debug` helpers from `frontend/src/debug.cljs`:
```javascript
debug.set_logging("namespace", "debug");
debug.dump_state();
debug.dump_buffer();
debug.get_state(":workspace-local :selected");
debug.dump_objects();
debug.dump_object("Rect-1");
debug.dump_selected();
debug.dump_tree(true, true);
```
Visual workspace debug overlays can be toggled with `debug.toggle_debug("bounding-boxes")`, `"group"`, `"events"`, or `"rotation-handler"`; `debug.debug_all()` and `debug.debug_none()` toggle all visual aids.
For temporary source traces, prefer existing logging (`app.common.logging` / `app.util.logging`) or short-lived `prn`, `app.common.pprint/pprint`, `js/console.log`, or `js-debugger` calls. Remove temporary source instrumentation before committing.
## Runtime targeting
`cljs_repl` may connect to the wrong runtime when several are attached, such as workspace plus rasterizer. Verify with `(.-title js/document)`; it should show the workspace file name, not "Penpot - Rasterizer".
To list or target shadow-cljs runtimes, run from `/home/penpot/penpot/frontend`:
```bash
printf '(shadow.cljs.devtools.api/repl-runtimes :main)\n' | timeout 10 npx shadow-cljs clj-eval --stdin
printf '(shadow.cljs.devtools.api/cljs-eval :main "<cljs-code>" {:client-id 5})\n' | timeout 10 npx shadow-cljs clj-eval --stdin
```
Use command timeouts so a disconnected browser does not hang the session.
@@ -0,0 +1,22 @@
# Frontend Compile Diagnostics
Separate from runtime crash recovery.
## First check the shadow-cljs build
Use the Penpot MCP `cljs_compiler_output` tool to inspect the latest shadow-cljs `:main` build status. This is the fastest way to distinguish a bad build from a runtime error in the browser.
Recommended order after CLJ/CLJC/CLJS source edits:
1. Run `cljs_compiler_output`.
2. If the compiler reports a Clojure syntax problem, especially unmatched delimiters or a confusing location, run `clj_check_parentheses` on the absolute path of the suspect `.clj`, `.cljc`, or `.cljs` file.
3. After the build is healthy, use `mem:frontend/cljs-repl`, browser tools, or runtime crash checks for behavior.
## Parentheses checker
`clj_check_parentheses` analyzes one Clojure/ClojureScript source file and reports the area likely responsible for unclosed parentheses/brackets/braces. Use it when compiler output points near EOF, points at a misleading later form, or says delimiter-related syntax errors.
## Hot reload notes
When the frontend shadow-cljs watch process is running, edits to CLJC files in `common/` are normally recompiled into the browser automatically. Do not restart the frontend before checking `cljs_compiler_output`; stale behavior is often a failed build.
For production/minified stack traces, build the production bundle from `frontend/` with `pnpm run build:app`. Output and source maps are generated under `frontend/resources/public/js`; inspect source maps or shadow-cljs reports using build ids from `shadow-cljs.edn`.
@@ -0,0 +1,152 @@
# Composable component tests
A framework concept for systematically testing Penpot's component subsystem
(synchronisation/propagation, swaps, variant switches, nesting, overrides), implemented in TWO test
suites that share the principles below:
1. **ClojureScript suite** — in the frontend test tree (`frontend/test/frontend_tests/
composable_tests/`), driving a minimally-assembled real app headlessly. The original.
2. **TypeScript suite** — a Penpot plugin (`plugins/apps/composable-test-suite/`), driving the FULL
production app end-to-end through the Plugin API, with a slightly more elaborate set of
abstractions. Runs interactively (panel), remotely (Playwright), and headlessly in CI. Its
README is the authoritative operational reference.
## Shared core idea
A test is a **composition of operations** over a starting configuration, plus assertions. You
describe a test as data (a setup + a sequence of operations) rather than writing bespoke imperative
code, and coverage grows by COMPOSITION: a new variation is one combinator wrapped around existing
pieces, not a copied test. Choice points (one-of alternatives, optional steps) EXPAND the
composition into a full sweep of variants — one written case stands for a whole matrix of concrete
tests.
## Shared principles
- **Every producing object is the accessor interface to what it produces downstream.** An
operation — and related objects such as content-creation strategies — is not merely an action:
the SAME object instance the case holds is the typed interface through which everything it
created or changed is later retrieved, checked, and asserted, parameterized by the situation. A
foundation operation exposes accessors for the participants it built; an edit operation exposes
its dual check (`assertHasChangedProperty` / `has-property-of`); a choice is recovered by asking
the one-of object (`getChoice`/`get-choice`); "did this step run" is asked of the step
(`wasApplied`/`applied?`). NEVER reach into a situation (or the document) for something an
upstream object produced — ask the producer. This is what keeps sweeps sound (object identity
ties the question to the exact node that ran) and what keeps retrieval logic in exactly one
place. Particularly explicit in the TS OOP implementation, where these accessors are methods on
the operation/strategy classes; repeatedly violating it (reading the document directly,
duplicating retrieval) was the most common review correction while building the suites.
- **Operations are data with identity.** Each operation node has a unique id at construction and
records what it did under that id; interrogation is by identity. Bind an operation to a value
ONCE and reuse it in the composition and in every query about it.
- **Drive the real production pipeline.** Operations route through genuine Penpot logic — real
change functions / real workspace events / the real Plugin API, never raw field writes — so the
production watcher's AUTOMATIC propagation is what's under test.
- **Roles, not internals.** A starting configuration names its participants (roles). Role→id
capture happens when the configuration is built; operation TARGETS resolve at apply-time and may
be re-bound, so an operation targeting a role follows it as state-building ops re-point it —
which lets a single operation be swept across depth.
- **Enumeration is authored, not exhaustive.** Compose only VALID cases, so outcomes are just
pass / fail / error — no not-applicable cells.
- **Naming discipline.** Penpot domain nouns ("component", "variant") must not name framework
abstractions; an operation may name the domain ACTION it performs.
- **Operator algebra** (same in both suites): sequence (cartesian product of the steps' variants),
one-of (union, choice recorded), optional(X) = one-of([X, skip]), inline assertion ops, trailing
asserters.
- **Case authoring:** a case carries a CamelCase identifier and a plain-terms description in three
parts — situation setup, actions/variations, asserted requirement.
---
# ClojureScript suite (frontend test tree)
Test-only `.cljs` code in the frontend test tree (nothing "common" about it). A **situation** =
the in-memory file value + named roles + `:vars` + an ordered applied-log. Operations are records
implementing `IOperation`/`apply-to` (`apply` collides with core). Assertions = inline `Test` ops
and/or a trailing asserter; the runner makes no judgment. Failures carry `describe-applied` (the
transcript), which is what makes a failing variant in a sweep identifiable.
Layout: `core.cljs` (the domain-agnostic engine: situation, identity/transcript, roles/targets,
operators, runners), `comp/setups.cljs` (setups + role accessors), `comp/nodes.cljs` (the component
operations and their check duals), `interpreter.cljs` (runs cases against the real frontend),
`comp/sync_test.cljs` (the cases; registered in `frontend_tests/runner.cljs`). Case letters B..N;
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
geometry, on the #10109 fix branch until merged) are the flagship pattern — read them before
writing a new sweep.
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
objects under `:vars`, each holding the FIXED deepest origin (`:remote-*`), the ADVANCING outer
main (`:main-*`), and per-nesting-level data whose `:nested-head` (the deepest instance at that
level, found by descending the `:shape-ref` chain — matching chain MEMBERSHIP, not terminus) is
the swap/switch target, anchored by its swap-stable parent. Nesting seeks the FIXED origin, not
the advancing main — that is what makes each level's `:nested-head` land on the deepest instance.
A variant nesting re-points the lineage's remote to the chosen member. Construction lesson:
cross-level propagation requires progressively NESTED levels (one variant + plain wraps); sibling
nestings do not propagate between each other.
**Interpreter:** installs the situation's files into the global `st/state` (aux files tagged
`:library-of`), starts the real `watch-component-changes` (+ harness `watch-undo-stack`), maps
event-ops to REAL workspace events (`dwsh/update-shapes`, `dwl/component-swap`,
`dwv/variants-switch`, `dwt/increase-rotation` — which runs the `check-delta` placement
classification — `dwt/update-dimensions`, `dwu/undo`, `dwl/sync-file`, …) and runs sync-ops'
`apply-to` against the live store file; awaits settlement (idle-gap heuristic + per-op grace) and
re-reads `:file` each step so the shared accessors keep working.
STORE-SWAP IMMUNITY: other test namespaces `set!` `st/state`/`st/stream` and never restore, while
the `app.main.refs` lenses stay bound to the ORIGINAL atoms — propagation then dies silently. The
interpreter captures the atoms at namespace-load time and re-`set!`s them per variant.
Running: `cd frontend && pnpm run build:test`, then
`node target/tests/test.js --focus frontend-tests.composable-tests.comp.sync-test`
(var-level focus for one case).
**Fidelity warning:** the harness drives a MINIMALLY-ASSEMBLED app — only some
`initialize-workspace` subscriptions are wired. Risk = SILENT UNDER-WIRING (e.g. undo needs the
harness `watch-undo-stack`). When a case needs app behaviour beyond a raw edit, check for an
unwired subscription and verify by PROBING store state, not by trusting a green assertion.
**Caveats:** inline `Test` exceptions are UNCAUGHT on the frontend (crash the runner — assert in
the trailing asserter). `(optional (in-sequence …))` is not flattened for the interpreter — use
independent optionals. The Serena/clj-kondo cache for `nodes.cljs` goes stale (phantom symbols) —
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
that fails headless (benign; absorbed by per-op grace).
---
# TypeScript suite (the plugin) — full e2e
`plugins/apps/composable-test-suite/` — same principles against the FULL production app through the
Plugin API (real frontend, real propagation). Continuation of the CLJS suite per issue #10584.
Operational details (build/run, connect URL, remote control, reading logs, auto-reload, CI): the
plugin README.
Distinguishing abstractions (the OOP articulation of the shared principles):
- `TestCase {identifier, description, operation}` with the three-part description mandated in the
constructor docstring.
- The accessor-interface principle is class-level: foundation operations (e.g.
`OpCreateSimpleComponentWithCopy`) expose the roles they build; **content-creation strategies**
(pluggable: what content a foundation builds around) expose accessors for the content they
created; edit operations expose their checks (`OpChangeProperty.assertHasChangedProperty`);
`OpOneOf`/`OpOptional` are queried for what ran. Tests never grope the document for something a
producer can be asked for.
- `ShapeProp` model: property duals with numeric tolerance; rotation is a writable attr, height
goes via resize (readonly in the Plugin API).
- `TestSuite` enumerates cases into a `TestTree` with stable per-test ids;
`run(ids, TestRunObserver)` is the ONLY output channel — the framework is UI-free by
construction. `plugin.ts` (panel adapter), `main.ts` (panel UI) and `src/ci/headless.ts`
(CI adapter) are three thin consumers.
- Cases live in `src/composable-tests/cases/` as `case<Identifier>.ts` (e.g. `MainEditSyncs` — the
sweep that found #10109).
- Panel checkboxes carry stable DOM ids (case identifier / `Identifier-N` composites) for remote
control via Playwright; recipe in the README.
## CI
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
NOT a limitation for this suite (everything asserted is frontend store logic; empirically
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
Details: README, "Running in CI".
## Substrate
`mem:common/test-setup`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
`mem:frontend/testing`.
+60
View File
@@ -0,0 +1,60 @@
# Frontend Architecture and Workflow
Frontend: CLJS SPA; React/Rumext; Potok; RxJS; okulary refs; SCSS modules; shared `common/`; JS/TS workspace packages.
## Stable namespace map
- `app.main.ui.*`: Rumext/React UI components for workspace, dashboard, viewer, settings, auth, nitrate, etc.
- `app.main.data.*`: Potok event handlers and side effects.
- `app.main.refs`: reactive refs/lenses over store and derived workspace data.
- `app.main.store`: Potok store and `emit!`.
- `app.plugins.*` and `app.plugins`: CLJS implementation of Plugin JS API proxies.
- `app.render_wasm.*`: frontend bridge to Rust/WASM renderer.
- `app.util.*`: DOM, HTTP, i18n, keyboard, codegen, and general frontend utilities.
- `frontend/packages/*` and `frontend/text-editor`: JS/TS workspace packages consumed by the app.
- Nitrate subscription/organization UI and flows live under `app.main.data.nitrate` and `app.main.ui.nitrate*`; backend/API behavior is covered by backend memories, and shared permission rules are in `common/src/app/common/types/nitrate_permissions.cljc`.
## Lint and Format
From `frontend/`:
- CLJ/CLJS lint: `pnpm run lint:clj`.
- JS lint currently no-ops via `pnpm run lint:js`.
- SCSS lint: `pnpm run lint:scss`.
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. After running `fmt:*`, `check-fmt:*` is redundant.
- Translation formatting after i18n edits: `pnpm run translations`.
**Before linting:** if delimiter errors are suspected (after LLM edits, or
lint/compiler reports syntax errors), run `scripts/paren-repair` on the
affected files first. Delimiter errors produce misleading linter output.
See `mem:scripts/paren-repair`.
## Focused memory routing
UI and packages:
- App UI components, SCSS modules, style-system boundaries, accessibility, i18n, and render performance: `mem:frontend/ui-conventions-and-style-system`.
- JS/TS packages, shared UI package, text editor, Storybook, and package builds: `mem:frontend/ui-packages-text-editor-workflow`.
Workspace behavior:
- Workspace state, commits, persistence, undo, repo calls, and refs: `mem:frontend/workspace-state-persistence-subtleties`.
- Workspace transforms, modifier previews, WASM modifier integration, and transform commits: `mem:frontend/workspace-transform-subtleties`.
- Workspace token application/propagation: `mem:frontend/workspace-token-subtleties`; shared token data/schema: `mem:common/tokens-schema-subtleties`.
App shell and product flows:
- Routing, root app shell, websocket, and global errors: `mem:frontend/routing-app-shell-subtleties`.
- Dashboard and viewer flows: `mem:frontend/dashboard-viewer-subtleties`.
- Plugin JS API runtime inside the frontend app: `mem:frontend/plugin-api-to-cljs-binding`.
Diagnostics and validation:
- Runtime inspection and navigation: `mem:frontend/cljs-repl`.
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
- Runtime crash recovery: `mem:frontend/handling-crashes`.
- Tests and live verification: `mem:frontend/testing`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
## Areas without focused memories
These frontend areas currently have no dedicated Serena memory beyond this architecture entry and nearby source/tests: clipboard, drawing tools, boolean/path operations, interactions/prototyping, color/style asset management, grid-layout editing UI, comments UI, fonts UI, and many dashboard/settings subflows. Treat work there as less memory-covered and inspect source/tests more carefully.
@@ -0,0 +1,16 @@
# Frontend Dashboard and Viewer Subtleties
## Dashboard
- Dashboard initialization fetches projects and fonts for the team, then listens to websocket messages only for global topic `uuid/zero` or the current profile id.
- Project fetch replaces each project map completely instead of merging, so fields such as `deleted-at` can disappear cleanly.
- Dashboard file/project mutations are often optimistic local updates with fire-and-forget RPC watchers. Bulk permanent delete/restore paths use SSE progress and progress notifications.
- File creation/duplication strips file `:data` before putting file summaries into dashboard state.
## Viewer
- Viewer initialization sets `:current-file-id`, `:current-share-id`, and `:viewer-local`, then fetches the view-only bundle. Comment threads are fetched only for logged-in users.
- Viewer bundle fetch sends the full supported feature set because anonymous shared viewers may not know team-enabled features.
- View-only bundles can contain pointer values in `:pages-index` and file data. Viewer resolves those fragments with `:get-file-fragment` before storing the bundle.
- `bundle-fetched` indexes pages and precomputes viewer frames/all-frames, stores libraries/users/thumbnails/permissions under `:viewer`, then navigates to frame id, query index, or auto-selected frame.
- Viewer zoom and interaction mode changes update both `:viewer-local` and the `:viewer` route query params.
Loaded 100 of 2634 files, more files were not shown because too many files have changed in this diff. Show more