318 Commits

Author SHA1 Message Date
Andrey Antukh
b5fbc4fd8c
✨ Add size limits to profile props and plugin registry (#11596)
* ✨ Add size limits to profile props and plugin registry

Bound the total serialized size of profile settings to 2 MiB
(:profile-props-max-size), checked on the merged result before
persisting, with a controlled :props-too-large error. Profiles
that already exceed the limit can still shrink but cannot grow.

Cap plugin registry entries in the shared schema (code 1 MiB, 50
plugins max, bounded name/host/description/icon) and restore rate
limiting on the plugin RPCs (profile-mutations bucket, one write
at a time per profile). The plugin manager now asks for
confirmation before removal and ignores repeated clicks while a
persist request is in flight.

Closes #11592

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

* 🐛 Enforce plugin count cap, byte sizes and removal guard

Enforce the declared 50-plugin cap in add-profile-plugin with a
specific :too-many-plugins error (updates of existing entries
still pass); the cap lives in a shared max-plugins constant.

Measure profile props size in UTF-8 bytes instead of chars so
multibyte content cannot slip past the limit.

Cover install/remove persist logic with mocked-RPC frontend tests
(release semantics, in-flight dedupe, validation vs rollback
split) and add the missing boundary tests in common.

Expose the in-flight persist set from the plugin registry and
disable the remove button of entries being saved.

Closes #11592

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

* 🐛 Fix rollback loops and restore paths in plugin registry

Restore the previous plugin version instead of dropping the entry
when a validation error rejects an update of an installed plugin.

Make compensating writes one-shot with terminal callbacks so a
persistent failure cannot ping-pong between install and remove.
Restores keep the original list position; the unused public
plugin-persisting? predicate is removed.

Pin count-before-size precedence with a dedicated test and fix
translation source refs to their canonical lines.

Closes #11592

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

* 🐛 Guard notifications write and fix restore ordering

Route update-profile-notifications through check-props-size! so
oversized profiles cannot grow through that path; document the
exempt system writers. Remove the duplicated stale entries in
en.po, keeping the canonical translation refs.

Restore rejected plugin updates at their original list position
instead of leaving the optimistic move in place.

Closes #11592

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

* 🐛 Skip no-op plugin removal and clarify size comments

Return early from remove-profile-plugin when the id is absent:
no wasted write, no size check, and no manufactured :plugins key
that could spuriously fail on oversized profiles.

Clarify that per-field string caps count chars while the byte
budget is enforced by profile-props-max-size.

Closes #11592

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

* 📎 Fix formatting in rlimit.edn for profile operations

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

* 📎 Fix formatting of import-binfile/global entry

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

* ♻️ Simplify props size check and tighten plugin entry caps

Measure props with transit bytes directly instead of the
PGobject string roundtrip.

Rename check-props-size! to check-props-size: single hard limit
on the merged props, no growth comparison, and return props so
writers thread the check into the update.

Move the 2 MiB default into default-props-max-size on the
profile namespace, still overridable with the optional
:profile-props-max-size config entry.

Tighten registry-entry :code and :icon to 500 chars: they hold
manifest paths, not content.

Closes #11592

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

* 🐛 Fix compatibility problems

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-01 14:31:30 +02:00
Alonso Torres
0509e2b9d2
🐛 Fix problem with connect library (#12002) 2026-09-30 17:56:01 +02:00
Alejandro Alonso
ec5a1edbed
✨ Make export follow the active renderer only (#11910)
Drop the separate :wasm-export flag and wasm-export/v1 feature. Single
export, clipboard PNG, plugins, and batch :is-wasm now key off
render-wasm/v1 alone. The exporter trusts :is-wasm for headless WASM
and always keeps a worker pool ready.
2026-09-25 12:40:56 +02:00
Alonso Torres
cbb9e5d971
✨ Add end-to-end tests for plugins validation (#11587)
* ✨ Add missing plugin data validations

* ✨ Add migration to fix the new schema validations

* ✨ Add end-to-end tests for plugins validation

* 🐛 Fix unit tests after merge

* 🐛 Change normalize behavior
2026-09-23 19:59:22 +02:00
Andrey Antukh
476e25e371 Merge remote-tracking branch 'origin/staging' into develop 2026-09-22 13:44:56 +02:00
Alonso Torres
efb10c82a8
🐛 Fix silent fail in plugin API interactions (#11781) 2026-09-22 12:56:35 +02:00
Andrés Moya
3cd9bfa9de
✨ Add sync with design tokens in external libraries (#10293)
* ✨ Auto link tokens when adding external libraries (provisional)

* 🔧 Refactor tokens-lib initialization

* 🔧 Add separated TokenStatus to store status apart of TokensLib

* 🔧 Make all status operations use the new data structure

* 🔧 Normalize status helper functions and access token sets by id

* 🔧 Rename :tokens-file to :tokens-source

* 🎉 Allow the user to choose the tokens-source of a file

* 🎉 Make tokens library readonly when it's in an external file

* 🎉 Show tokens in library summaries

* 🎉 Show source info in sidebar

* 🔧 Fix integration tests

* 🐛 Propagate changes of token values in external library

* 🎉 Layout updates

* 🔧 Refactor tokens source calculations

* 🔧 Add harder checks for nil or empty values in everything

* 🐛 Fix some integration tests

* 🔧 Add integration tests for tokens in external libs

* 🔧 Validate and repair missing tokens status

* 🎉 Make ui changes optional with config flag

* 🐛 Propagate tokens after synchronizing components in ext library

* 🐛 Propagate tokens after creating new instances

* 🐛 Propagate tokens after synchronizing tokens in ext library

* 🐛 Add a tokens source icon to libraries section (#11439)

* 🐛 Add a tokens source icon to libraries section

* 🐛 Fix ellipsis on library names

* ♻️ Remove code under flag on legacy component

* 🐛 Fix token theme name on inspect tab

* 🎉 Add changes notification (#11476)

* 🎉 Add changes notification

* ♻️ Change fn names

* 🐛 Fix tokens source label truncation and missing translations (#11533)

* 🐛 Fix tokens source label truncation and missing translations

The tokens source file name always showed, even for the current file,
and long names wrapped onto a second line instead of truncating
because the header used flex-wrap and overflow-wrap: break-word
instead of single-line ellipsis.

Show the source row unconditionally (it now displays "This file" when
the source is the current file, matching the connected-library case),
truncate the file name to one line with an ellipsis, and only attach a
tooltip with the full name when the text is actually truncated.

Replace the hardcoded UI strings with translated ones and add their
English and Spanish entries.

AI-assisted-by: claude-sonnet-5

* 🐛 Remove redundant effect dependency in tokens source

file-name-truncated? was listed as a dependency of the with-effect
that checks and observes label truncation, even though it isn't
read inside the effect body. Since the effect itself flips that
state via check-file-name-truncated, including it as a dependency
caused the ResizeObserver to be needlessly disconnected and
reconnected on every truncation change.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix small visual error

* 🐛 Fix problem with plugins

* 🐛 Fix playwright tests

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-21 15:37:45 +02:00
Alonso Torres
d642fcbf5c
🐛 Fix problem with plugins api event handler (#11787) 2026-09-20 11:55:23 +02:00
makesomethingshit
edf146db7b
🐛 Preserve source order when changing grid flow (#11662)
* 🐛 Reflow auto grid cells on flow direction change

Remap only single-span auto cells to the new
:layout-grid-dir traversal order, keeping source
order, manual and area placements untouched.

Update both grid direction controls to use the
new change-grid-direction event and refresh the
stale active button on persisted direction.

Add a RED-to-GREEN model regression covering a
2x2 row-to-column transition and source-order.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Keep source order on grid flow change with areas

Skip the generic grid cell pass for the
direction event, since reflowing already
places every eligible auto item and a blind
reorder rewrites shapes around pinned areas.

Pin area/span grids with a regression test
covering direction change and source order.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Scope grid skip to direction changes only

Replace the translation flag with a narrow
skip-grid-reassignment option so component
sync and reflow metadata stay intact while
the generic grid cell pass is skipped.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Clear leftover auto cells on grid flow change

Write remapped shapes to every target auto cell and
empty leftover cells so sparse grids cannot duplicate
a child across target cells. Manual, area and
spanned cells stay untouched; source order is kept.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Pin grid flow invariants with span and manual regressions

Keep the direction-change design unchanged and lock the
claimed invariants with tests: a real 2x1 manual span
cell and an occupied manual cell stay byte-identical,
row->column->row round-trips to the original cells,
and a mixed auto/manual/span/area grid shows no shape
loss or duplication. Also drop the unused page-objects
binding from the direction-change watcher.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Unoverlap mixed grid fixture and assert movement

Move auto C to (1,3) so it no longer overlaps the 2x1
manual span at (1,2). Row auto order A,C,B,E becomes
column order A,B,E,C; assert the exact placement
while keeping pinned, source-order and no-loss
checks. Test-only change.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Unify plugin dir setter and normalize missing direction

Route GridLayoutProxy.dir through change-grid-direction so the
plugin API shares the UI direction-change path with its reflow
and source-order guarantees. Normalize a missing
:layout-grid-dir to :row at the change-grid-direction entry
point and cover it with a missing-direction regression plus a
plugin setter routing regression.

AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* 🐛 Fix grid plugin dir setter syntax

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
AI-assisted-by: opencode-go/muse-spark-1.3-contributor

* 🐛 Fix comments and tests

---------

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-17 14:09:32 +02:00
Andrey Antukh
ba608f8c77 Merge remote-tracking branch 'origin/staging' into develop 2026-09-14 14:50:32 +02:00
Alonso Torres
b598d7d72e
🐛 Fix problem in plugins api when removing interactions (#11621) 2026-09-11 13:19:55 +02:00
makesomethingshit
30849babcc
🐛 Fix fontFamilies token property mapping in Plugin API (#11566)
* 🐛 Fix fontFamilies token property mapping in Plugin API

The Plugin API exposes the font-family token property as `fontFamilies`,
while Penpot stores the canonical applied-token attribute as
`:font-family`. The bidirectional plugin/internal attribute map did not
contain that alias, so explicit `applyToken(..., ["fontFamilies"])`
validation rejected the property and applied-token readback exposed the
undocumented singular `fontFamily`.

Add `:font-family -> :font-families` to the existing canonical alias
map. The reverse mapping is derived automatically, keeping application
and readback symmetric without introducing a font-specific code path.

Closes #11405

AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>

* 🐛 Fix fontFamilies e2e test to target a text shape

The fontFamilies end-to-end regression created a flex layout frame,
whose attribute set (frame-with-layout-attributes) excludes
:font-family. The workspace token application filters such shapes,
so the internal binding and readback assertions would pass vacuously
without exercising the alias.

Target an actual `:text` shape (ctho/add-text) instead, so the test
verifies the full JS "fontFamilies" -> schema -> alias -> canonical
:font-family -> camelCase readback path.

AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>

* 🐛 Fix fontFamilies test WASM error and add changelog entry

The text-shape fontFamilies e2e applies a layout-affecting token via
wasm renderer path, hitting missing WASM exports under Node. Merge
thw/setup-wasm-mocks! into the :each fixture and add plugins
CHANGELOG entry for the fontFamilies alias fix.

AI-assisted-by: muse-spark-1.3-contributor
Related to #11566

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

---------

Signed-off-by: 최준수 <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
2026-09-10 13:43:41 +02:00
Andrey Antukh
f695553469 Merge remote-tracking branch 'origin/staging' into develop 2026-09-08 21:15:58 +02:00
Andrey Antukh
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 Antukh
a8e0b3c1f8 🐛 Add dedicated RPC methods for plugin registry operations
Add `add-profile-plugin` and `remove-profile-plugin` RPC methods for
atomic plugin registry operations, preventing manipulation via the
broader `update-profile-props` endpoint.

- Close the `:plugins` field in `update-profile-props` schema to
  eliminate the mass assignment attack vector for plugin data.
- Define `valid-permissions` and a closed `schema:permissions` enum to
  restrict plugin permissions to known values.
- Migrate the frontend to use the new granular RPC methods with
  optimistic updates and rollback on failure.
- Add comprehensive backend tests covering valid/invalid permissions,
  updates, removal, and rejection via old endpoint.

AI-assisted-by: qwen3.7-plus
2026-09-07 09:30:01 +02:00
Elena Torró
b458dc764e
🔧 Set wasm export by flag instead of team feature (#11449) 2026-09-01 12:47:05 +02:00
Andrey Antukh
f5aad7b1ae Merge remote-tracking branch 'origin/staging' into develop 2026-09-01 12:45:17 +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
Andrey Antukh
03cd3fa70f
♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity (#9958)
* ✨ Auto-link libraries during import based on slugified name

When a Penpot file is exported without bundled libraries and then
imported into a different environment, external library links are
broken because library UUIDs differ across environments.

This feature adds a heuristic to auto-relink libraries by matching
slugified library names against shared files in the target team:

- Export: embed external library metadata (id, name, slug, used-by)
  in the manifest when libraries are not included in the export.
- Import: resolve external libraries by slugifying shared file names
  in the destination team and matching against manifest slugs.
- Single match: auto-link silently (creates file-library-rel row).
- Multiple matches: emit SSE event so the frontend shows a selection
  dialog for the user to pick the correct library.
- No match: import continues without linking (current behavior).

Backend changes:
- Extended manifest schema with optional :external-libraries field
- Added slugify-name, get-files-names, get-shared-files-for-team,
  find-shared-files-by-slug helpers in app.binfile.common
- Threaded team-id into import cfg from RPC layer
- Added resolve-external-libraries and auto-link-libraries in v3
- Emit :library-candidates SSE event for multi-match cases

Frontend changes:
- Worker captures library-candidates SSE events and forwards them
- Import dialog shows auto-link notification and multi-match
  selection UI with select dropdowns
- Added link-files-to-library! RPC helper for user selections
- Added en/es translations for new UI strings

Closes #9263

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

* ✨ Add UI for the auto-link plumbing

* ♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity

Consolidates the auto-link libraries feature into a single coherent implementation:

- Unify boolean flags (embed-assets, include-libraries, link-later)
  into single ::bfc/export-type parameter
- Fix critical reference-integrity bug: pre-resolution no longer remaps
  :component-file refs when no link is created (multi-match / no-permission)
- compute-link-decisions as single source of truth for auto-link logic
- 80+ backend tests covering round-trip, cross-team, permissions,
  edge cases, and reference integrity

AI-assisted-by: longcat-2.0

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-08-27 10:09:52 +02:00
Andrey Antukh
81c3b3cd56
📎 Update copyright name on file header (#11346) 2026-08-25 11:55:10 +02:00
Alejandro Alonso
2dcf1a8a0a Merge remote-tracking branch 'origin/staging' into develop 2026-08-20 09:15:03 +02:00
Alonso Torres
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
Elena Torró
30bc2a4bc3
🔧 Add FF to enable wasm export at team level (#11130) 2026-08-07 12:36:52 +02:00
Andrey Antukh
a60b648c6c 🐛 Fix issues with draft-js tests 2026-08-06 08:55:58 +02:00
Andrey Antukh
e741313add Merge remote-tracking branch 'origin/staging' into develop 2026-07-31 19:21:07 +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
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
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
AK
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
Alonso Torres
6c5d01283a
🐛 Add more test for reported bugs (#10765) 2026-07-21 12:18:40 +02:00
Elena Torró
c11d3aaaa7
🐛 Add :stroke-image support to plugins API (#10683) 2026-07-14 10:26:19 +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
Andrey Antukh
f84a8687f6 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 19:37:16 +02:00
Alonso Torres
d6c50cc40b
🐛 Fix problems with plugins api setPluginData (#10632) 2026-07-09 18:00:26 +02:00
Andrey Antukh
0e25f7d8ad Merge remote-tracking branch 'origin/staging' into develop 2026-07-08 10:25:25 +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
Dr. Dominik Jain
c88d9f568b
🐛 Preserve component order when combining components as variants (#10562) 2026-07-06 16:49:29 +02:00
Alejandro Alonso
b82ab0c830 Merge remote-tracking branch 'origin/staging' into develop 2026-07-02 13:52:25 +02:00
RenzoMXD
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 Sajdak
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 Sajdak
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 Sajdak
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
ab623cc02d 🐛 Make overlay position optional in plugin open-overlay/toggle-overlay interactions 2026-07-02 13:32:17 +02:00
Filip Sajdak
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 Sajdak
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
Andrey Antukh
c4e72fd7f9 Merge remote-tracking branch 'origin/staging' into develop 2026-06-30 16:23:23 +02:00
Alonso Torres
8823f7ac4d
✨ Make v2 plugins default throw on error (#10433) 2026-06-30 14:50:50 +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
Filip Sajdak
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
Alonso Torres
bbf63e1136
🐛 Fix array format in plugins properties (#10246) 2026-06-19 00:59:47 +02:00