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`.
Replace unsafe std::mem::transmute calls in Rust WASM path code with
validated TryFrom conversions to prevent undefined behavior from invalid
enum discriminant values. This was the most likely root cause of the
"No matching clause: -19772" production crash -- corrupted bytes flowing
through transmute could produce arbitrary invalid enum variants.
Fix byteOffset handling throughout the CLJS PathData serialization
pipeline. DataView instances created via buf/slice carry a non-zero
byteOffset, but from-bytes, transit write handler, -write-to,
buf/clone, and buf/equals? all operated on the full underlying
ArrayBuffer, ignoring offset and length. This could silently produce
PathData with incorrect size or content.
Rust changes (render-wasm):
- RawSegmentData: From<[u8; N]> -> TryFrom<[u8; N]> with discriminant
validation (must be 0x01-0x04) before transmuting
- RawBoolType: From<u8> -> TryFrom<u8> with explicit match on 0-3
- Add #[wasm_error] to set_shape_path_content, current_to_path,
convert_stroke_to_path, and set_shape_bool_type so panics are caught
and routed through the WASM error protocol instead of crashing
- set_shape_path_content: replace .expect() with proper Result/? error
propagation per segment
- Remove unused From<BytesType> bound from SerializableResult trait
CLJS changes (common):
- from-bytes: use DataView.byteLength instead of ArrayBuffer.byteLength
for DataView inputs; preserve byteOffset/byteLength when converting
from Uint8Array, Uint32Array, and Int8Array
- Transit write handler: construct Uint8Array with byteOffset and
byteLength from the DataView, not the full backing ArrayBuffer
- -write-to: same byteOffset/byteLength fix
- buf/clone: copy only the DataView byte range using Uint8Array with
proper offset, not Uint32Array over the full ArrayBuffer
- buf/equals?: compare DataView byte ranges using Uint8Array with
proper offset, not the full backing ArrayBuffers
Frontend changes:
- shape-to-path, stroke-to-path, calculate-bool*: wrap WASM call and
buffer read in try/catch to ensure mem/free is always called, even
when an exception occurs between the WASM call and the free call
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Add nil defaults to all case expressions that match binary segment
type codes so that corrupted/unknown values are skipped instead of
throwing 'No matching clause'. This prevents a React render crash
(triggered via shape-with-open-path? -> get-subpaths -> reduce)
when a PathData buffer contains invalid bytes, e.g. from a WASM
data transfer or deserialization of damaged stored data.
Affected functions: read-segment, impl-walk, impl-reduce,
impl-lookup, to-string-segment*, and the seq/reduce protocol
implementations on both JVM and CLJS PathData types.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The impl-walk, impl-reduce, and impl-lookup functions had the binary
type codes for move-to and line-to swapped (1 mapped to :line-to and
2 to :move-to). This is inconsistent with from-plain, read-segment,
to-string-segment*, and the Rust RawSegmentData enum which all use
1=move-to and 2=line-to. The swap caused incorrect command types to
be reported to callers like get-handlers and get-points.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix non-integer row/column values in grid cell position inputs
The numeric-input component allows Alt+arrow key increments of 0.1x the
step value, which could produce float values (e.g. 4.5, 0.5) when users
adjusted grid cell row/column/row-span/column-span positions. The schema
requires these fields to be integers, causing backend validation errors.
Round the input values to integers in the on-grid-coordinates callback
before passing them to update-grid-cell-position.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Enforce integer-only values in grid cell numeric inputs
Add an `integer` prop to the legacy `numeric-input*` component that
rounds parsed values in `parse-value`, ensuring all input paths (typed
text, arrow keys, Alt+arrow, mouse wheel, expressions) produce integers.
Use it for all six row/column inputs in the grid cell options panel.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Add newsletter opt-in checkbox to registration validation form
Add accept-newsletter-updates support through the full registration
token flow. The newsletter checkbox is now available on the
registration validation form, allowing users to opt-in during the
email verification step.
Backend changes:
- Refactor prepare-register to consolidate UTM params and newsletter
preference into props at token creation time
- Add accept-newsletter-updates to prepare-register-profile and
register-profile schemas
- Handle newsletter-updates in register-profile by updating token
claims props on second step
Frontend changes:
- Add newsletter-options component to register-validate-form
- Add accept-newsletter-updates to validation schema
- Fix subscription finalize/error handling in register form
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ♻️ Refactor auth register components to modern style
Migrate all components in app.main.ui.auth.register and
app.main.ui.auth.login/demo-warning to use the modern * suffix
convention, removing deprecated ::mf/props :obj metadata and
updating all invocations from [:& name] to [:> name*] syntax.
Components updated:
- terms-and-privacy -> terms-and-privacy*
- register-form -> register-form*
- register-methods -> register-methods*
- register-page -> register-page*
- register-success-page -> register-success-page*
- terms-register -> terms-register*
- register-validate-form -> register-validate-form*
- register-validate-page -> register-validate-page*
- demo-warning -> demo-warning*
Also remove unused old context-notification import in login.cljs.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🔥 Remove unused onboarding-newsletter component
The newsletter opt-in is now handled directly in the registration
form via the newsletter-options* component, making the standalone
onboarding-newsletter modal obsolete.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix register test for UTM params to use prepare-register step
UTM params are now extracted and stored in token props during the
prepare-register step, not at register-profile time. Move utm_campaign
and mtm_campaign from the register-profile call to the
prepare-register-profile call in the test.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix nil path content crash by exposing safe public API
Move nil-safety for path segment helpers to the public API layer
(app.common.types.path) rather than the low-level segment namespace.
Add nil-safe wrappers for get-handlers, opposite-index, get-handler-point,
get-handler, handler->node, point-indices, handler-indices, next-node,
append-segment, points->content, closest-point, make-corner-point,
make-curve-point, split-segments, remove-nodes, merge-nodes, join-nodes,
and separate-nodes. Update all frontend callers to use path/ instead of
path.segment/ for these functions, removing the path.segment require
from helpers, drawing, edition, tools, curve, editor and debug.
Replace ad-hoc nil checks with impl/path-data coercion in all public
wrapper functions in app.common.types.path. The path-data helper
already handles nil by returning an empty PathData instance, which
provides uniform nil-safety across all content-accepting functions.
Update the path-get-points-nil-safe test to expect empty collection
instead of nil, matching the new coercion behavior.
* ♻️ Clean up path segment dead code and add missing tests
Remove dead code from segment.cljc: opposite-handler (duplicate of
calculate-opposite-handler) and path-closest-point-accuracy (unused
constant). Make update-handler and calculate-extremities private as
they are only used internally within segment.cljc.
Add missing tests for path/handler-indices, path/closest-point,
path/make-curve-point and path/merge-nodes. Update extremities tests
to use the local reference implementation instead of the now-private
calculate-extremities. Remove tests for deleted/privatized functions.
Add empty-content guard in path/closest-point wrapper to prevent
ArityException when reducing over zero segments.
The get-frame-ids function could enter infinite recursion when:
1. There's a circular reference in the frame hierarchy
2. A shape's frame-id points to itself (corrupt data)
The fix uses the cached version (get-frame-ids-cached) in recursive calls
and adds a guard to prevent self-referencing.
The stale-asset-error? predicate only matched keyword-constant
cross-build mismatches ($cljs$cst$). Protocol dispatch failures
($cljs$core$I prefix, e.g. IFn/ISeq) and V8's 'Cannot read
properties of undefined' phrasing were not covered, so the handler
fell through to a generic toast instead of triggering a hard reload.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Zone.js (injected by browser extensions such as Angular DevTools) patches
addEventListener by wrapping it and assigning a custom .toString to the
wrapper via Object.defineProperty with writable:false. When the same
element is processed a second time, the plain assignment in strict mode
(libs.js is built with a "use strict" banner) throws a native TypeError:
"Cannot assign to read only property 'toString' of function '...'".
This error escapes the React tree through the window error/unhandledrejection
events and was surfacing the exception page to users even though Penpot itself
is unaffected.
The fix:
- Extract the private ignorable-exception? helpers from the letfn block into
top-level defn/defn- forms so the predicate can be reused elsewhere.
- Add the Zone.js toString TypeError to the ignorable-exception? predicate so
the global uncaught-error handler silently suppresses it.
- The React error boundary is intentionally left unchanged: anything that
reaches it has executed inside React's reconciler and must not be ignored.
Cache in-progress frame traversals before following parent frame links so thumbnail updates stop recursing forever on cyclic or transiently inconsistent shape graphs.
Add a regression test that covers cyclic frame-id chains and keeps the expected frame/component extraction behavior intact.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Expand interaction helper test coverage
Add coverage for interaction destination and flow helpers,
including nil handling and removal helpers. Document the
intent of the new assertions so future interaction changes
keep the helper contract explicit.
* ✨ Cover interaction validation edge cases
Exercise the remaining interaction guards and overlay
positioning edge cases, including invalid state
transitions and nested manual offsets. Keep the test
comments focused on why each branch matters for editor
behavior.
Clamp the frame index to the valid range in zoom-to-fit and
zoom-to-fill events before accessing the frames vector. When the
URL query parameter :index exceeds the number of frames on the
page (e.g. index=1 with a single frame), nth would throw
"No item 1 in vector of length 1". Also adds unit tests covering
the boundary condition.
Extend the telemetry payload with a sorted list of unique email domains
extracted from all registered profile email addresses. The new
:email-domains field is populated via a single SQL query using
split_part and DISTINCT, and is included in the stats sent when
telemetry is enabled.
Also update the tasks-telemetry-test to assert the new field is present
and contains the expected domain values.
Return nil from get-prev-sibling when the shape is no longer present in
the parent ordering so delete undo generation falls back to index-based
restore instead of crashing on invalid vector access.
The options stored in options-ref is a delay (lazy value). In
on-token-key-down, it was passed raw to next-focus-index without being
dereferenced first, causing count to be called on a JS object that does
not implement ICounted.
Fix: dereference the delay in on-token-key-down (matching the existing
pattern in on-key-down), and make next-focus-index itself also handle
delays defensively. Add unit tests covering the delay case.
* ✨ Use update-when for update dashboard state
This make updates more consistent and reduces possible eventual
consistency issues in out of order events execution.
* 🐛 Detect stale JS modules at boot and force reload
When the browser serves cached JS files from a previous deployment
alongside a fresh index.html, code-split modules reference keyword
constants that do not exist in the stale shared.js, causing TypeError
crashes.
This adds a compile-time version tag (via goog-define / closure-defines)
that is baked into the JS bundle. At boot, it is compared against the
runtime version tag from index.html (which is always fresh due to
no-cache headers). If they differ, the app forces a hard page reload
before initializing, ensuring all JS modules come from the same build.
* 📎 Ensure consistent version across builds on github e2e test workflow
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ♻️ Handle fetch-error gracefully with toast instead of full-page error
Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.
Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.
* ✨ Add automatic retry with backoff for idempotent RPC requests
Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs. Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).
Mutation (POST) requests are never retried to avoid unintended
side-effects. Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.
* ♻️ Make retry helpers public with configurable parameters
Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map. with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.
* ✨ Add tests for RPC retry mechanism
Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
bad-gateway, service-unavailable, offline) and non-retryable types
(validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
after transient failures, max-retries exhaustion, no retry for
non-retryable errors, fetch-error retry, custom config, and mixed
error scenarios
* ♻️ Introduce :network error type for fetch-level failures
Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch. This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.
Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}
* 📚 Add comment explaining the use of bit-shift-left
* ⬆️ Update opencode and copilot deps
* 🐛 Decouple workspace-content from workspace-local to reduce scroll re-renders
Move workspace-local subscription from workspace-content* (parent) into
viewport* and viewport-classic* (children). workspace-content* now only
subscribes to the new workspace-vport derived atom, which changes only on
window resize — not on every scroll event. This prevents the sidebar,
palette and other workspace-content children from re-rendering on scroll.
* 🐛 Throttle wheel events to one state update per animation frame
Accumulate wheel event deltas in a mutable ref and flush them via
requestAnimationFrame, so that multiple wheel events between frames
produce a single state mutation instead of one per event. This prevents
the cascade of synchronous React re-renders (via useSyncExternalStore)
that can exceed the maximum update depth on rapid scrolling.
Both panning (scroll) and zoom (ctrl/mod+wheel) are throttled. Scroll
deltas are summed additively; zoom scales are compounded multiplicatively
with the latest cursor point used as the zoom center.
* ♻️ Extract schedule-zoom! and schedule-scroll! from on-mouse-wheel
* ♻️ Avoid zoom dep on on-mouse-wheel by using a ref
* 🐛 Fix crash in apply-text-modifier with nil selrect or modifier
Guard apply-text-modifier against nil text-modifier and nil selrect
to prevent the 'invalid arguments (on pointer constructor)' error
thrown by gpt/point when called with an invalid map.
- In text-wrapper: only call apply-text-modifier when text-modifier is
not nil (avoids unnecessary processing)
- In apply-text-modifier: handle nil text-modifier by returning shape
unchanged; guard selrect access before calling gpt/point
* 📚 Add tests for apply-text-modifier in workspace texts
Add exhaustive unit tests covering all paths of apply-text-modifier:
- nil modifier returns shape unchanged (identity)
- modifier with no recognised keys leaves shape unchanged
- :width / :height modifiers resize shape correctly
- nil :width / :height keys are skipped
- both dimensions applied simultaneously
- :position-data is set and nil-guarded
- position-data coordinates translated by delta on resize
- shape with nil selrect + nil modifier does not throw
- position-data-only modifier on shape without selrect is safe
- selrect origin preserved when no dimension changes
- result always carries required shape keys
* 🐛 Fix zero-dimension selrect crash in change-dimensions-modifiers
When a text shape is decoded from the server via map->Rect (which
bypasses make-rect's 0.01 minimum enforcement), its selrect can have
width or height of exactly 0. change-dimensions-modifiers and
change-size were dividing by these values, producing Infinity scale
factors that propagated through the transform pipeline until
calculate-selrect / center->rect returned nil, causing gpt/point to
throw 'invalid arguments (on pointer constructor)'.
Fix: before computing scale factors, guard sr-width / sr-height (and
old-width / old-height in change-size) against zero/negative and
non-finite values. When degenerate, fall back to the shape's own
top-level :width/:height so the denominator and proportion-lock base
remain consistent.
Also simplify apply-text-modifier's delta calculation now that the
transform pipeline is guaranteed to produce a valid selrect, and
update the test suite to test the exact degenerate-selrect scenario
that triggered the original crash.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ♻️ Simplify change-dimensions-modifiers internal logic
- Remove the intermediate 'size' map ({:width sr-width :height sr-height})
that was built only to be assoc'd and immediately destructured back into
width/height; compute both values directly instead.
- Replace the double-negated condition 'if-not (and (not ignore-lock?) …)'
with a clear positive 'locked?' binding, and flatten the three-branch
if-not/if tree into two independent if expressions keyed on 'attr'.
- Call safe-size-rect once and reuse its result for both the fallback
sizes and the scale computation, eliminating a redundant call.
- Access :transform and :transform-inverse via direct map lookup rather
than destructuring in the function signature, consistent with how the
rest of the let-block reads shape keys.
- Clean up change-size to use the same destructuring style as the updated
function ({sr-width :width sr-height :height}).
- Fix typo in comment: 'havig' -> 'having'.
* ✨ Add tests for change-size and change-dimensions-modifiers
Cover the main behavioural contract of both functions:
change-size:
- Scales both axes to the requested target dimensions.
- Sets the resize origin to the shape's top-left point.
- Nil width/height each fall back to the current dimension (scale 1 on
that axis); both nil produces an identity resize that is optimised away.
- Propagates the shape's transform and transform-inverse matrices into the
resulting GeometricOperation.
change-dimensions-modifiers:
- Changing :width without proportion-lock only scales the x-axis (y
scale stays 1), and vice-versa for :height.
- With proportion-lock enabled, changing :width adjusts height via the
inverse proportion, and changing :height adjusts width via the
proportion.
- ignore-lock? true bypasses proportion-lock regardless of shape state.
- Values below 0.01 are clamped to 0.01 before computing the scale.
- End-to-end: applying the returned modifiers via gsh/transform-shape
yields the expected selrect dimensions.
* ✨ Harden safe-size-rect with additional fallbacks
The previous implementation could still return an invalid rect in several
edge cases. The new version tries four sources in order, accepting each
only if it passes a dedicated safe-size-rect? predicate:
1. :selrect – used when width and height are finite, positive
and within [-max-safe-int, max-safe-int].
2. points->rect – computed from the shape corner points; subject to
the same predicate.
3. Top-level shape fields (:x :y :width :height) – present on all rect,
frame, image, and component shape types.
4. grc/empty-rect – a 0,0 0.01×0.01 unit rect used as last resort so
callers always receive a usable, non-crashing value.
The out-of-range check (> max-safe-int) is new: it rejects coordinates
that pass d/num? (finite) but exceed the platform integer boundary defined
in app.common.schema, which previously slipped through undetected.
Tests cover all four fallback paths, including the NaN, zero-dimension,
and max-safe-int overflow cases.
* ⚡ Optimise safe-size-rect for ClojureScript performance
- Replace (when (some? rect) ...) with (and ^boolean (some? rect) ...)
to keep the entire predicate as a single boolean expression without
introducing an implicit conditional branch.
- Replace keyword access (:width rect) / (:height rect) with
dm/get-prop calls, consistent with the hot-path style used throughout
the rest of the namespace.
- Add ^boolean type hints to every sub-expression of the and chain in
safe-size-rect? (d/num?, pos?, <=) so the ClojureScript compiler emits
raw JS boolean operations instead of boxing the results through
cljs.core/truth_.
- Replace (when (safe-size-rect? ...) value) in safe-size-rect with
(and ^boolean (safe-size-rect? ...) value), avoiding an extra
conditional and keeping the or fallback chain free of allocated
intermediate objects.
* ✨ Use safe-size-rect in apply-text-modifier delta-move computation
safe-size-rect was already used inside change-dimensions-modifiers to
guard the resize scale computation. However, apply-text-modifier in
texts.cljs was still reading (:selrect shape) and (:selrect new-shape)
directly to build the delta-move vector via gpt/point.
gpt/point raises "invalid arguments (on pointer constructor)" when
given a nil value or a map with non-finite :x/:y, which can happen when
a shape's selrect is missing or degenerate (e.g. decoded from the server
via map->Rect, bypassing make-rect's 0.01 floor).
Changes:
- Promote safe-size-rect from defn- to defn in app.common.types.modifiers
so it can be reused by consumers outside the namespace.
- Replace the two raw (:selrect …) accesses in the delta-move computation
with (ctm/safe-size-rect …), which always returns a valid, finite rect
through the established four-step fallback chain.
- Add two frontend tests covering the delta-move path with a fully
degenerate (zero-dimension) selrect, ensuring neither a bare
position-data modifier nor a combined width+position-data modifier
throws.
* ♻️ Ensure all test shapes are proper Shape records in modifiers-test
All shapes in safe-size-rect-fallbacks tests now start from a proper
Shape record built by cts/setup-shape (via make-shape) instead of plain
hash-maps. Each test that mutates geometry fields (selrect, points,
width, height) does so via assoc on the already-initialised record,
which preserves the correct type while isolating the field under test.
A (cts/shape? shape) assertion is added to each fallback test to make
the type guarantee explicit and guard against regressions.
The unused shape-with-selrect helper (which built a bare map) is
removed.
* 🔥 Remove dead code and tighten visibility in app.common.types.modifiers
Dead functions removed (zero callers across the entire codebase):
- modifiers->transform-old: superseded by modifiers->transform; only
ever appeared in a commented-out dev/bench.cljs entry.
- change-recursive-property: no callers anywhere.
- move-parent-modifiers, resize-parent-modifiers: convenience wrappers
for the parent-geometry builder functions; never called.
- remove-children-modifiers, add-children-modifiers,
scale-content-modifiers: single-op convenience builders; never called.
- select-structure: projection helper; only referenced by
select-child-geometry-modifiers which is itself dead.
- select-child-geometry-modifiers: no callers anywhere.
Functions narrowed from defn to defn- (used only within this namespace):
- valid-vector?: assertion helper called only by move/resize builders.
- increase-order: called only by add-modifiers.
- transform-move!, transform-resize!, transform-rotate!, transform!:
steps of the modifiers->transform pipeline.
- modifiers->transform1: immediate helper for modifiers->transform; the
doc-string describing it as 'multiplatform' was also removed since it
is an implementation detail.
- transform-text-node, transform-paragraph-node: leaf helpers for
scale-text-content.
- update-text-content, scale-text-content, apply-scale-content: internal
scale-content pipeline; all called only by apply-modifier.
- remove-children-set: called only by apply-modifier.
- select-structure: demoted to defn- rather than deleted because it is
still called by select-child-structre-modifiers, which has external
callers.
* ✨ Add more tests for modifiers
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Include request URI and status in frontend handle-response error data,
and add request path/context to backend IOException handler logs and
response body. Previously these errors had no identifying information
about which endpoint or request caused the failure.
When AbortController.abort(reason) is called with a custom reason (a
ClojureScript ExceptionInfo), modern browsers (Chrome 98+, Firefox 97+)
reject the fetch promise with that reason object directly instead of with
the canonical DOMException{name:'AbortError'}. The ExceptionInfo has
.name === 'Error', so both the p/catch guard and is-ignorable-exception?
failed to recognise it as an abort, letting it surface to users as an
error toast.
Fix by calling .abort() without a reason so the browser always produces
a native DOMException whose .name is 'AbortError', which is correctly
handled by all existing guards.
Also add a defense-in-depth check in is-ignorable-exception? that
filters errors whose message matches the 'fetch to \'' prefix, guarding
against any future re-introduction of a custom abort reason.
Co-authored-by: Penpot Dev <dev@penpot.app>
* 🐛 Fix nil deref on missing bounds in layout modifier propagation
When a parent shape has a child ID in its shapes vector that does
not exist in the objects map, the layout modifier code crashes
because it derefs nil from the bounds map.
The root cause is that children from the parent shapes list are
not validated against the objects map before being passed to the
layout modifier pipeline. Children with missing IDs pass through
unchecked and reach apply-modifiers where bounds lookup fails.
Fix by adding nil guards in apply-modifiers to skip children
without bounds, and changing map to keep to filter them out.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 📎 Add tests for nil bounds in layout modifier propagation
Tests cover flex and grid layout scenarios where a parent
frame has child IDs in its shapes vector that do not exist
in the objects map, verifying that set-objects-modifiers
handles these gracefully without crashing.
Tests:
- Flex layout with normal children (baseline)
- Flex layout with non-existent child in shapes
- Flex layout with only non-existent children
- Grid layout with non-existent child in shapes
- Flex layout resize propagation with ghost children
- Nested flex layout with non-existent child in outer frame
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix TypeError when token error map lacks :error/fn key
Guard against missing :error/fn in token form control resolve streams.
When schema validation errors are produced they may not carry an
:error/fn key; calling nil as a function caused a TypeError crash.
Apply an if-let guard at all 7 affected sites across input.cljs,
color_input.cljs and fonts_combobox.cljs, falling back to :message
or returning the error map unchanged.
* ♻️ Extract token error helpers and add unit tests
Extract resolve-error-message and resolve-error-assoc-message helpers
into errors.cljs, replacing the seven duplicated inline lambdas in
input.cljs, color_input.cljs and fonts_combobox.cljs with named
function references. Add frontend-tests.tokens.token-errors-test
covering both helpers for the normal path (:error/fn present) and the
fallback path (schema-validation errors that lack :error/fn).
Signed-off-by: Penpot Dev <dev@penpot.app>
---------
Signed-off-by: Penpot Dev <dev@penpot.app>
* ✨ Improve error handling and exception formatting
- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules
* 🐛 Properly handle AbortError on fetch request unsubscription
When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.
This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.
Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Add tests for app.common.types.path.subpath, helpers, segment,
bool operations (union/difference/intersection/exclude), top-level
path API, and shape-to-path conversion. Covers previously untested
functions across all path sub-namespaces. Tests pass on both JVM
and JS (ClojureScript/Node) platforms.
* 🐛 Fix dissoc error when detaching stroke color from library
The detach-value function in color-row was only passing index to
on-detach, but the stroke's on-color-detach handler expects both
index and color arguments. This caused a protocol error when trying
to dissoc from a number instead of a map.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix crash when detaching color asset from stroke
The color_row detach-value callback calls on-detach with (index, color),
but stroke_row's local on-color-detach wrapper only took a single argument
(fn [color] ...), so it received index as color and passed it to
stroke.cljs which then called (dissoc index :ref-id :ref-file), crashing
with 'No protocol method IMap.-dissoc defined for type number'.
Fix the wrapper to accept (fn [_ color] ...) so it correctly ignores the
index passed by color_row (it already has index in the closure) and
forwards the actual color map to the parent handler.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
When pasting an image (with no text content) into the text editor,
Draft.js calls handlePastedText with null/empty text. The previous fix
guarded splitTextIntoTextBlocks against null, but insertText still
attempted to build a fragment from an empty block array, causing
Modifier.replaceWithFragment to crash with 'Cannot read properties of
undefined (reading getLength)'.
Fix insertText to return the original state unchanged when there are no
text blocks to insert. Also guard handle-pasted-text in the ClojureScript
editor to skip the insert-text call entirely when text is nil or empty.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The splitTextIntoTextBlocks function in @penpot/draft-js called
.split() on the text parameter without a null check. When pasting
content without text data (e.g., images only), Draft.js passes null
to handlePastedText, causing a TypeError.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The save-path-content function only converted content to PathData when
there was a trailing :move-to command. When there was no trailing
:move-to, the content from get-path was stored as-is, which could be
a plain vector if the shape was already a :path type with non-PathData
content. This caused segment/get-points to fail with 'can't access
property "get", cache is undefined' when the with-cache macro tried
to access the cache field on a non-PathData object.
The fix ensures content is always converted to PathData via path/content
before being stored in the state.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The get-parent-with-data function traverses the DOM using parentElement
to find an ancestor with a specific data-* attribute. When the current
node is a non-Element DOM node (e.g. Document node reached from event
handlers on window), accessing .-dataset returns undefined, causing
obj/in? to throw "right-hand side of 'in' should be an object".
This adds a nodeType check to skip non-Element nodes during traversal
and continue up the parent chain.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
When the browser denies clipboard read permission (NotAllowedError),
the unhandled exception handler was showing a generic 'Something wrong
has happened' toast. This change adds proper error handling for
clipboard permission errors in paste operations and shows a
user-friendly warning message instead.
Changes:
- Add error handling in paste-from-clipboard for NotAllowedError
- Improve error handling in paste-selected-props to detect permission errors
- Mark clipboard NotAllowedError as ignorable in the uncaught error handler
to prevent duplicate generic error toasts
- Add translation key for clipboard permission denied message
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
PostHog recorder throws errors like 'Cannot assign to read only property
'assert' of object' which are unrelated to the application and should be
ignored to prevent noise in error reporting.
Add a nil guard before subscribing to the stream in the use-stream
hook. When a nil/undefined stream is passed (e.g., from a conditional
expression or timing edge case during React rendering), the subscribe
call on undefined causes a TypeError. The guard ensures we only
subscribe when the stream is defined.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The with-cache macro in impl.cljc assumed the target was always a
PathData instance (which has a cache field). When content was a plain
vector, (.-cache content) returned undefined in JS, causing:
TypeError: Cannot read properties of undefined (reading 'get')
Fix:
- path/get-points (app.common.types.path) is now the canonical safe
entry point: converts non-PathData content via impl/path-data and
handles nil safely before delegating to segment/get-points
- segment/get-points remains a low-level function that expects a
PathData instance (no defensive logic at that level)
- streams.cljs: replace direct call to path.segm/get-points with
path/get-points so the safe conversion path is always used
- with-cache macro: guards against nil/undefined cache, falling back
to direct evaluation for non-PathData targets
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
- Change the default for the newWindow param from true to false, so
openPage() navigates in the same tab instead of opening a new one
- Accept a UUID string as the page argument in addition to a Page object,
avoiding the need to call penpot.getPage(uuid) first
- Add validation error when an invalid page argument is passed
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
When clipboard items have types that don't match the allowed types
list, the filtering results in an empty array. Calling getType with
undefined throws a NotFoundError. This change adds a check for null/undefined
types and filters them from the result.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Fixes a crash where plugins sending messages via 'penpot.ui.sendMessage()'
could fail if their message payload contained non-serializable values like
functions or closures.
The fix adds validation using 'structuredClone()' to catch these messages
early with a helpful error message, and adds a defensive try/catch in the
modal's message handler as a safety net.
Fixes the error: 'Failed to execute postMessage on Window: ... could not
be cloned.'
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Guard against transit-decoded clipboard content that is not a map
before calling assoc, which caused a runtime crash ('No protocol
method IAssociative.-assoc defined for type number').
Also route :copied-props paste data to paste-transit-props instead
of incorrectly sending it to paste-transit-shapes.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Identify and silence "signal is aborted without reason" errors by:
- Providing an explicit reason to AbortController when subscriptions are disposed.
- Updating the global error handler to ignore AbortError exceptions.
- Ensuring unhandled rejections use the ignorable exception filter.
The root cause was RxJS disposal calling .abort() without a reason, combined
with the on-unhandled-rejection handler missing the ignorable error filter.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Guard get-option fallback with (when (seq options) ...) to avoid
"No item 0 in vector of length 0" when options is an empty vector.
Also guard the selected-option memo in select* to mirror the same
pattern already present in combobox*.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Replace int? with number? in on-change handlers for layout item margins,
min/max sizes, and layer opacity. Using int? caused float values like 8.5
to fall into the design token branch, calling (first 8.5) and crashing.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix error when creating guides without frame
The error 'Cannot read properties of undefined (reading
$cljs$core$IFn$_invoke$arity$0$)' occurred when creating a new
guide. It is probably a race condition because it is not reproducible
from the user point of view.
The cause is mainly because of use incorrect jsx handler :& where :>
should be used. This caused that some props pased with incorrect casing
and the relevant callback props received as nil on the component and
on the use-guide hook.
The fix is simple: use correct jsx handler
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 💄 Add cosmetic changes to viewport guides components
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Refactor find-component-main to use an iterative loop/recur pattern instead of direct recursion and added cycle detection for malformed data structures.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
group-to-path was storing a raw concatenated vector into :content after
flattening children's PathData instances via (map vec). bool-to-path
was storing the plain-vector result of bool/calculate-content directly.
Both now wrap through path.impl/path-data at the assignment site so the
:content invariant (always a PathData instance) is upheld.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Coerce content to PathData in transform-content before dispatching
the ITransformable protocol, so shapes carrying a plain vector in
their :content field (legacy data, bool shapes, SVG imports) no
longer crash with 'No protocol method ITransformable.-transform
defined for type object'.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Two related issues that could cause crashes during fast navigation
in the dashboard:
1. grid.cljs: On drag-start, a temporary counter element is appended
to the file card node for the drag ghost image, then scheduled for
removal via requestAnimationFrame. If the user navigates away before
the RAF fires, React unmounts the section and removes the card node
from the DOM. When the RAF fires, item-el.removeChild(counter-el)
throws because counter-el is no longer a child. Fixed by guarding
the removal with dom/child?.
2. sidebar.cljs: Keyboard navigation handlers used ts/schedule-on-idle
(requestIdleCallback with a 30s timeout) to focus the newly rendered
section title after navigation. This left a very wide window for the
callback to fire against a stale DOM after a subsequent navigation.
Additionally, the idle callbacks were incorrectly passed as arguments
to st/emit! (which ignores non-event values), making the scheduling
an accidental side effect. Fixed by replacing all occurrences with
ts/schedule (setTimeout 0), which is sufficient to defer past the
current render cycle, and moving the calls outside st/emit!.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The previous implementation passed document.body directly as the
React portal containerInfo. During unmount, React's commit phase
(commitUnmountFiberChildrenRecursively, case 4) sets the current
container to containerInfo and then calls container.removeChild()
for every DOM node inside the portal tree.
When two concurrent state updates are processed — e.g. navigating
away from a dashboard section while a file-menu portal is open —
React could attempt document.body.removeChild(node) twice for the
same node, the second time throwing:
NotFoundError: Failed to execute 'removeChild' on 'Node':
The node to be removed is not a child of this node.
The fix allocates a dedicated <div> container per portal instance
via mf/use-memo. The container is appended to body on mount and
removed in the effect cleanup. React then owns an exclusive
containerInfo and its unmount path never races with another
portal or the modal container (which also targets document.body).
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Setting horizontalSizing/verticalSizing on a FlexLayoutProxy was
dispatching update-layout-child instead of update-layout, so the
frame's auto-sizing (hug content) was never triggered even though
the getter read back the value correctly.
Also restricts accepted values to #{:fix :auto} (matching shape.cljs)
since frames cannot use :fill, and fixes a copy-paste error that
reported :horizontalPadding instead of :horizontalSizing in error messages.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The download-image function in app.media silently succeeded when the
remote image URL was unreachable or returned an error status code,
causing create-file-media-object-from-url to report success with no
actual image stored.
Add exception handling for connection refused, timeouts, and I/O errors
around the HTTP request, and validate the HTTP status code in
parse-and-validate before processing the response body.
Fixes#8499
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Use nil-safe path/get-points wrapper (some-> based) instead of
direct path.segment/get-points calls in edition.cljs to prevent
'Cannot read properties of undefined (reading get)' crash.
Add nil-safety test to verify path/get-points returns nil without
throwing when content is nil.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Add missing order by clause to snapshot query
This fixes the incorrect snapshot visibility when file
has a lot of versions.
* ⚡ Reduce allocation on milestone-group* component
* 🐛 Fix milestone group timestamp formatting
* 📎 Update changelog
* 🐛 Fix scroll on history panel
---------
Co-authored-by: Eva Marco <evamarcod@gmail.com>
* ✨ Add notification tag to media uploading
This avoid hidding error messages once the upload
is finished.
* 🐛 Don't throw exception when picker is closed and image is still uploading
The main idea behind this, is move all plugin related stuff from
app.main.data.plugins into app.plugins.* and make them more consistent.
Also the intention that put all plugins related state under specific
prefix on the state.
Eliminate duplicated on-change-opacity and on-key-down-opacity handlers
by routing alpha through apply-property-change, and extract shared
stepping logic into on-key-down-step.
Signed-off-by: eureka928 <meobius123@gmail.com>
Color picker numeric inputs (R, G, B, H, S, V, Alpha) now support
Shift+Arrow for ×10 steps and Alt+Arrow for ×0.1 steps, matching
the behavior of numeric inputs elsewhere in the application.
Signed-off-by: eureka928 <meobius123@gmail.com>
This matches the behaviour of findShape, more closely aligning with
the LLM's expectations (given the lack of concrete information in
the instructions)
The types build is not part of the bootstrap, and it is not
relevant to regular users (only to developers).
Information on how to apply it is now in types-generator/README.md
* 🎉 Add tokens to plugins API documentation
And add poc plugin example
* 📚 Document better the tokens value in plugins API
* 🔧 Refactor token validation schemas
* 🔧 Use automatic validation in token proxies
* 🔧 Use schemas to validate token creation
* 🔧 Use multi schema for token value
* 🔧 Use schema in token api methods
* 🐛 Fix review comments
---------
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
- Add incremental tile update that preserves cache during pan
- Only invalidate tile cache when zoom changes
- Force visible tiles to render synchronously (no yielding)
- Increase interest area threshold from 2 to 3 tiles
Partition pending tiles into 4 groups by visibility and cache status.
Visible tiles are processed first to eliminate empty squares during
pan/zoom. Cached tiles within each group are processed before uncached.
Use HashSet for layout_reflows to avoid processing the same
layout multiple times. Also use std::mem::take instead of
creating a new Vec on each iteration.
Avoid Vec allocation + reverse for reversed flex layouts.
The new children_ids_iter_forward returns children in original order,
eliminating the need to collect and reverse.
* ✨ Move devtools perf logging helpers to util.perf ns
* 💄 Move flag check to the entry point instead of initialize event
* ♻️ Make performance events consistent with other events
When tiles are invalidated (during shape updates or page loading), the old tile
content is now kept visible until new content is rendered to replace it. This
provides a smoother visual experience during updates.
When loading large pages with many shapes, the UI now remains responsive by
processing shapes in chunks (100 shapes at a time) and yielding to the browser
between chunks. Preview renders are triggered at 25%, 50%, and 75% progress to
give users visual feedback during loading.
Before the changes on this commit, the team object is used for
retrieve the id, where we already have team-id. Additionally, the
team object resolution is async operation and is not available on
the first render which causes strange issues on automated flows
(playwright) where an option is clicked when the async flow is
still pending and we have no team object loaded.
When creating a token with a name that conflicts with existing
hierarchical token names (e.g., 'accent-color' when 'accent-color.blue.dark'
exists), the validation throws an error via rx/throw. However, the
rx/subs! subscriber in generic_form.cljs had no error handler, causing
an unhandled exception that resulted in an 'Internal Error' crash.
This fix adds an error handler that:
1. Catches validation errors from the reactive stream
2. Uses humanize-errors to convert them to user-friendly messages
3. Displays the error in the form's extra-errors field
Before: Crash with 'Internal Error' dialog
After: Form shows validation error message
Fixes#8110
---
This is a Gittensor contribution.
gittensor:user:GlobalStar117
This was intended to be changed on 13fcf3a9bb. However only the menu
order changed, not the default option.
Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
This was intended to be changed on 13fcf3a9bb. However only the menu
order changed, not the default option.
Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
- Batch hover highlights using RAF to avoid long tasks from rapid events
- Run parent expansion asynchronously to not block selection
- Lazy-load children in layer items using IntersectionObserver
- Clarify expand-all-parents logic with explicit bindings
- Batch hover highlights using RAF to avoid long tasks from rapid events
- Run parent expansion asynchronously to not block selection
- Lazy-load children in layer items using IntersectionObserver
- Clarify expand-all-parents logic with explicit bindings
* 🎉 Add ability to remap tokens when renamed ones are referenced by other child tokens
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
* 🐛 Fix remap skipping tokens with same name in different sets
* 📚 Update CHANGES.md
* 🔧 Fix css styles
---------
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
Co-authored-by: Akshay Gupta <gravity.akshay@gmail.com>
This patches makes the default Tokens importing option to match the
current default Tokens exporting option (single JSON value). This way it
is more obvious and quick to export the tokens from a file and import
in new one,
---
While testing our design system we are often re-exporting and
re-importing the Tokens to the files using the design system components.
I'm aware that this may be addressed in the future so the Tokens are
brought in together with the library. Meanwhile (and even in the future)
I think it is sensible to have a symmetry between the export and import
defeault options.
Co-authored-by: Dalai Felinto <dalai@blender.org>
* ♻️ Replace shadow form
* ♻️ Rename files and components
* ♻️ Replace offsetx and offsety names
* ♻️ Replace form file for new form component using new form system
* ♻️ Rename files and props
* 🐛 Fix unpublish library modal not scrolling when the linked files list is too long
* 💄 Remove deprecated tokens in unpublish library modal
* 🔧 Update CHANGELOG
The color tokens in grid view have a tooltip which is a map.
This is done so the frontend can render:
```
Name: foo
Resolved value: #000000
```
However the validation scheme for tooltips was only accepting functions
and strings.
---
How to reproduce the original (unreported) crash:
* Create a color token
* Create a shape, add a fill
* Pick a color, chose the Token options
* Click on the Grid View
Crash: `{:hint "invalid props on component tooltip*\n\n -> 'content'
should be a string\n"}`
Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
The logic to swap a component would delete the swapped out component
first before bringing in the new one.
In the process of doing so, the sanitization code would unmask the
group, now orphan of its mask shape component, when it was the first
element of the group.
The fix was to pass an optional argument to the generate-delete-shapes
function to ignore mask in special cases like this.
Signed-off-by: Dalai Felinto <dalai@blender.org>
This fixes the html email rendering on gmail. Other clients (like proton,
emailcatcher) properly renders html independently of the order of parts
on the multipart email structure but gmail requires that html should be
the last one.
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463)
- Fix warning when clicking on number token pills [Taiga #13661](https://tree.taiga.io/project/penpot/issue/13661)
- Fix 'not ISeqable' error when entering float values in layout item and opacity inputs [Github #8569](https://github.com/penpot/penpot/pull/8569)
- Fix crash in select component when options vector is empty [Github #8578](https://github.com/penpot/penpot/pull/8578)
- Fix scroll on colorpicker [Taiga #13623](https://tree.taiga.io/project/penpot/issue/13623)
- Fix crash when pasting non-map transit clipboard data [Github #8580](https://github.com/penpot/penpot/pull/8580)
- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520)
## 2.13.3
### :bug: Bugs fixed
- Revert yetti (http server) update, because that caused a regression on multipart uploads
## 2.13.2
### :bug: Bugs fixed
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
## 2.12.1
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations
@@ -11,7 +166,6 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
@@ -44,7 +198,6 @@ This update standardizes all authentication flows under the single URL
and makis it more modular, enabling the ability to configure SSO auth
provider dinamically.
#### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small
@@ -61,6 +214,8 @@ example. It's still usable as before, we just removed the example.
### :heart: Community contributions (Thank you!)
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements
@@ -71,6 +226,7 @@ example. It's still usable as before, we just removed the example.
- Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696)
- Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353)
- Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313)
- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571)
### :bug: Bugs fixed
@@ -85,6 +241,15 @@ example. It's still usable as before, we just removed the example.
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
Loaded 100 of 1460 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.