* 🔧 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
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
* 🐛 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
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
* 🐛 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
* 🐛 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
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
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
* 🐛 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
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>
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>
* ✨ 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>
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>
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>
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>
* 🐛 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>
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>