2926 Commits

Author SHA1 Message Date
Andrey Antukh
b6524881e0 🐛 Fix crash in apply-text-modifier with nil selrect or modifier (#8762)
* 🐛 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>
2026-03-30 11:04:54 +02:00
Andrey Antukh
a149f31d56 Add comprehensive tests for shape layout namespace (#8759)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-30 11:03:53 +02:00
Andrey Antukh
e4cc7d72da 🐛 Fix incorrect attrs references on generate-sync-shape (#8776)
For :component
2026-03-30 11:03:34 +02:00
Andrey Antukh
4174d6a05b 🎉 Add tests for undo-stack helper function on common (#8766) 2026-03-26 19:44:49 +01:00
Andrey Antukh
737e04fe2c 🐛 Fix nil deref on missing bounds in layout modifier propagation (#8735)
* 🐛 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>
2026-03-25 15:36:21 +01:00
Andrey Antukh
0dfac801a4 Improve error handling and exception formatting (#8757)
*  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>
2026-03-24 19:55:23 +01:00
Andrey Antukh
cc73a768d5 Add comprehensive tests for path and descendant namespaces (#8755)
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.
2026-03-24 19:53:22 +01:00
Andrey Antukh
3ef100427b 🎉 Add tests for app.common.data namespace (#8750)
*  Add tests for predicates and ordered data structures

Adds tests for boolean-or-nil?, in-range?, ordered-set/map creation
and ordering, oassoc/oassoc-in/oupdate-in/oassoc-before, and the
ordered collection index helpers (adds/inserts/addm/insertm-at-index).

*  Add tests for lazy and sequence helpers

Adds tests for concat-all, mapcat, zip, zip-all, enumerate,
interleave-all, add-at-index, take-until, safe-subvec and domap.

*  Add tests for collection lookup and map manipulation

Adds tests for group-by, seek, index-by, index-of-pred/of,
replace-by-id, getf, vec-without-nils, without-nils,
without-qualified, without-keys, deep-merge, dissoc-in, patch-object,
without-obj, update-vals, update-in-when, update-when, assoc-in-when,
assoc-when, merge, txt-merge, mapm, removev, filterm, removem,
map-perm, distinct-xf and deep-mapm.

*  Add tests for parsing, numeric and utility helpers

Adds tests for nan?, safe+, max, min, parse-integer, parse-double,
parse-uuid, coalesce-str, coalesce, read-string, name, prefix-keyword,
kebab-keys, regexp?, nilf, nilv, any-key?, tap, tap-r, map-diff,
unique-name, toggle-selection, invert-map, obfuscate-string,
unstable-sort, opacity-to-hex, format-precision, format-number
and append-class.

*  Add tests for remaining untested helpers in data ns

Cover percent?, parse-percent, num-string?, num?, not-empty?,
editable-collection?, oreorder-before, oassoc-in-before,
lazy-map and reorder.

Platform-specific assertions use reader conditionals where
CLJS and JVM behaviour differ (js/isFinite string coercion,
js/isNaN empty-string coercion).
2026-03-24 19:52:52 +01:00
Andrey Antukh
7461c5304c Add comprehensive tests for app.common.colors ns (#8758)
Cover all public functions: valid-hex-color?, parse-rgb,
valid-rgb-color?, rgb->str, hex->rgb, rgb->hex, rgb->hsv,
hsv->rgb, rgb->hsl, hsl->rgb, hex->hsl, hex->hsv, hex->rgba,
hex->hsla, hex->lum, hsl->hex, hsl->hsv, hsv->hex, hsv->hsl,
format-hsla, format-rgba, expand-hex, prepend-hash, remove-hash,
color-string?, parse, next-rgb, reduce-range, interpolate-color,
uniform-spread, uniform-spread? and interpolate-gradient.

Tests pass on both JVM and JS (ClojureScript) platforms.
Platform differences (NaN saturation for achromatic colors,
integer vs float return types) are handled with mth/close?.
2026-03-24 19:10:44 +01:00
Andrey Antukh
cc03f3f884 📚 Add minor improvements to ai agents documentation 2026-03-24 18:00:39 +01:00
Andrey Antukh
2d616cf9c0 📚 Add better organization for AGENTS.md file (#8675) 2026-03-18 14:59:38 +01:00
Andrey Antukh
0779c9ca61 🐛 Fix TypeError in get-points when content is not PathData (#8634)
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>
2026-03-17 09:31:10 +01:00
Andrey Antukh
e7e6303184 🐛 Make ct/format-inst nil safe (#8612)
Prevent JS TypeError when date is nil in date formatting.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 19:55:51 +01:00
Alejandro Alonso
4b330e7b50 Merge pull request #8596 from penpot/niwinz-staging-fix-max-recursion
🐛 Fix RangeError (stack overflow) in find-component-main
2026-03-12 13:30:16 +01:00
Alejandro Alonso
1487386fbb Merge pull request #8582 from penpot/niwinz-staging-bugfix-path-plain-content
🐛 Fix plain vector leaking into shape :content from shape-to-path
2026-03-12 13:15:14 +01:00
Alejandro Alonso
1680be33ef Merge pull request #8568 from penpot/niwinz-staging-bugfix-1-path-get-points
🐛 Fix TypeError when path content is nil in get-points calls
2026-03-12 12:31:58 +01:00
Andrey Antukh
11a1ac2a09 🐛 Fix RangeError (stack overflow) in find-component-main
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>
2026-03-11 16:15:47 +01:00
Andrey Antukh
7ec9261475 Add improvements to AGENTS.md (#8586) 2026-03-11 15:24:40 +01:00
Andrey Antukh
7939cb045b 🐛 Fix plain vector leaking into shape :content from shape-to-path conversions
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>
2026-03-10 19:26:03 +01:00
Andrey Antukh
f566a2adfd 🐛 Fix ITransformable error when path content is a plain vector
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>
2026-03-10 18:06:44 +00:00
Andrey Antukh
0f47c30349 Merge branch 'main' into staging 2026-03-10 14:39:16 +01:00
Andrey Antukh
68fbacf8b3 Merge tag '2.14.0-RC2' 2026-03-10 14:38:58 +01:00
Andrey Antukh
7ab5f241da 🐛 Fix TypeError when path content is nil in get-points calls
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>
2026-03-10 12:21:13 +00:00
Andrey Antukh
32cf95265a 📚 Add GitHub Copilot instructions (#8548) 2026-03-10 13:12:15 +01:00
Pablo Alba
34d29328e6 🐛 Fix bad size on switching a layout with fixed sizing (#8504) 2026-03-09 12:12:03 +01:00
Andrey Antukh
0ceadada35 🐛 Fix invalid data on layout flex dir shape property 2026-03-09 10:09:07 +01:00
Andrey Antukh
591d63e470 Add better error report on wrong input on logging helpers 2026-03-09 10:01:09 +01:00
Xaviju
e1d556f4aa 🐛 Sort tokens by name (#8488) 2026-03-04 10:33:29 +01:00
Andrey Antukh
86e851f408 🐛 Fix incorrect version visibility on workspace (#8463)
* 🐛 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>
2026-03-04 09:27:51 +01:00
Andrey Antukh
a4351d133b Add minor improvements to error reporting (#8402) 2026-03-04 09:12:19 +01:00
Andrés Moya
ba87ea1a44 🔧 Add tokenscript flag and more validations to token values 2026-02-25 14:04:20 +01:00
Andrés Moya
c626634610 🐛 Detect empty font-family 2026-02-12 16:04:23 +01:00
Andrey Antukh
12e5d8d8c4 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-12 11:00:56 +01:00
Elena Torro
2b525f0f48 🔧 Set up embedded editor 2026-02-12 09:34:20 +01:00
Andrey Antukh
11a283916d Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-10 11:58:27 +01:00
Andrey Antukh
f08700945a Merge remote-tracking branch 'origin/staging' into develop 2026-02-10 11:58:09 +01:00
Andrey Antukh
06e5825c8a 🐛 Add proper input checking to font related RCP method 2026-02-10 10:36:57 +01:00
Pablo Alba
5d3ccbc8b4 Add managed profiles endpoint to nitrate api (#8292) 2026-02-09 15:52:18 +01:00
Andrés Moya
5b5f22a8c6 🎉 Add tokens to Penpot Plugins API (#7756)
* 🎉 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>
2026-02-09 14:18:31 +01:00
Alejandro Alonso
8acd031ab2 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-06 11:23:50 +01:00
alonso.torres
53c2acb3e6 🐛 Fix several problems with layouts and texts 2026-02-05 17:29:43 +01:00
Eva Marco
dda3377596 🐛 Allow detach broken token from input (#8242)
* 🐛 Allow detach broken token from input

* 🐛 Fix multiselection on multiple token applied

* ♻️ Remove detach-token new fn
2026-02-05 11:28:47 +01:00
Florian Schroedl
150d57b1eb Add tokenscript MVP 2026-02-05 09:45:55 +01:00
Andrey Antukh
1656fefdc9 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-04 16:23:46 +01:00
Andrey Antukh
7f318bb110 Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-04 16:22:13 +01:00
Andrey Antukh
06afd94a74 ⬆️ Update backend dependencies (mainly bugfixes) 2026-02-04 16:21:19 +01:00
Andrey Antukh
d80ba1856a Add several improvements to frontend error reporting
*  Add major improvement on error handling

*  Add the ability to store frontend reports

* 📎 Add PR feedback changes
2026-02-04 12:45:38 +01:00
Andrey Antukh
c99fac000a Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-03 09:30:16 +01:00
Andrey Antukh
1325584e1a Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-03 08:24:04 +01:00
Alejandro Alonso
8c25fb00ac 🐛 Fix auto width/height texts on variant swithching 2026-01-29 12:25:38 +01:00