23222 Commits

Author SHA1 Message Date
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 Sajdak
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
Alonso Torres
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 Marco
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
Andrey Antukh
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
2.17.1-RC4
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 Alonso
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