- Fix 'conten' typo to 'content' in path.cljc docstring
- Fix 'curvle' typo to 'curve' in shape_to_path.cljc docstring
- Replace confusing XOR-style filter with readable
(contains? #{:line-to :curve-to} ...) in bool.cljc
- Align handler-indices and opposite-index docstrings with
matching API in path.cljc
The CLJS implementation of PathData's -nth protocol method had
swapped arguments in the 3-arity version (with default value).
The call (d/in-range? i size) should be (d/in-range? size i)
to match the CLJ implementation. With swapped args, valid indices
always returned the default value, and invalid indices attempted
out-of-bounds buffer reads.
Add normalize-coord helper function that clamps coordinate values to
max-safe-int and min-safe-int bounds when reading segments from PathData
binary buffer. Applies normalization to read-segment, impl-walk,
impl-reduce, and impl-lookup functions to ensure coordinates remain
within safe bounds.
Add corresponding test to verify out-of-bounds coordinates are properly
clamped when reading PathData.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The backtrace-tokens-tree function used a namespaced keyword :temp/id
which clj->js converted to the JS property "temp/id". The sd-token-uuid
function then tried to access .id on the sd-token top-level object,
which was undefined, causing "Cannot read properties of undefined
(reading uuid)".
Fix by using the existing token :id instead of generating a temporary
one, and read it from sd-token.original (matching sd-token-name pattern).
* 🐛 Add webp export format to plugin types
Align plugin API typings with runtime export support by including 'webp' in
'Export.type' and updating the exported formats documentation.
Signed-off-by: Marek Hrabe <marekhrabe@me.com>
* 📚 Add plugin-types changelog entry for missing webp export format
Signed-off-by: Marek Hrabe <marekhrabe@me.com>
---------
Signed-off-by: Marek Hrabe <marekhrabe@me.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 📚 Update description of mcp-remote usage
* Use Streamable HTTP endpoint instead of SSE
* Remove redundant global installation
* 📚 Add recommendations on model selection
* 📚 Use new tags in npx launch commands
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`.
The text editor's SelectionController threw 'TypeError: Invalid text
node' when:
- Pressing Backspace to delete the only character of the **first** text
span in a paragraph that contains multiple spans.
- Pressing Delete to delete the only character of the **last** text
span in the same situation.
- Pressing a word-backward shortcut that empties the first span of a
multi-span paragraph.
In all three cases the tree-iterator (previousNode / nextNode) returned
null because no sibling text node existed in that direction, and that
null was subsequently passed to getTextNodeLength() which calls
isTextNode() — which unconditionally throws when given a falsy value.
Fix: use the null-coalescing fallback to the first/last remaining
child's text node of the paragraph before calling collapse() /
getTextNodeLength().
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>
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.