3058 Commits

Author SHA1 Message Date
Dexterity
e1d3106f61
Add color customization for ruler guides (#8986)
*  Add customizable colors for ruler guides

*  Update CHANGES.md

* 💄 Move guide color menu styles to SCSS

* 💄 Fix trailing whitespace in guides.cljs

---------

Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 17:31:39 +02:00
Alejandro Alonso
0d17debde7 Merge remote-tracking branch 'origin/staging' into develop 2026-04-21 08:24:29 +02:00
Andrey Antukh
b5922d32ca Merge remote-tracking branch 'origin/main' into staging 2026-04-16 10:59:36 +02:00
Andrey Antukh
146219a439 Add tests for app.common.geom namespaces 2026-04-15 23:37:53 +02:00
Andrey Antukh
fa89790fd6 🐛 Fix grid layout case dispatch, divide-by-zero, and add set-auto-multi-span tests
Three critical fixes for app.common.geom.shapes.grid-layout.layout-data:

1. case dispatch on runtime booleans in get-cell-data (case→cond fix)
   In get-cell-data, column-gap and row-gap were computed with (case ...)
   using boolean locals auto-width? and auto-height? as dispatch values.
   In Clojure/ClojureScript, case compares against compile-time constants,
   so those branches never matched at runtime. Replaced both case forms
   with cond, using explicit equality tests for keyword branches.

2. divide-by-zero guards in fr/auto/span calc (JVM ArithmeticException fix)
   Guard against JVM ArithmeticException when all grid tracks are fixed
   (no flex or auto tracks):
   - (get allocated %1) → (get allocated %1 0) in set-auto-multi-span
   - (get allocate-fr-tracks %1) → (get allocate-fr-tracks %1 0) in set-flex-multi-span
   - (/ fr-column/row-space column/row-frs) guarded with (zero?) check
   - (/ auto-column/row-space column/row-autos) guarded with (zero?) check
   In JS, integer division by zero produces Infinity (caught by mth/finite),
   but on the JVM it throws before mth/finite can intercept.

3. Exhaustive tests for set-auto-multi-span behavior
   Cover all code paths and edge cases:
   - span=1 cells filtered out (unchanged track-list)
   - empty shape-cells no-op
   - even split across multiple auto tracks
   - gap deduction per extra span step
   - fixed track reducing budget; only auto tracks grow
   - smaller children not shrinking existing track sizes (max semantics)
   - flex tracks causing cell exclusion (handled by set-flex-multi-span)
   - non-spanned tracks preserved via (get allocated %1 0) default
   - :row type symmetry with :column type
   - row-gap correctly deducted in :row mode
   - documents that (sort-by span -) yields ascending order (smaller spans
     first), correcting the misleading code comment

All tests pass on both JS (Node.js) and JVM environments.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
71904c9ab6 🐛 Fix CLJS bounds-map deduplication and update intersect test
In the CLJS branch of resolve-modif-tree-ids, get-parent-seq returns
shape maps, but the js/Set was populated with UUIDs. As a result,
.has and .add were passing full shape maps instead of their :id
values, so parent deduplication never worked in ClojureScript.
Fixed both .has and .add calls to extract (:id %) from the shape map.

Also update the collinear-overlap test in geom-shapes-intersect-test
to expect true now that the ::coplanar keyword fix (commit 847bf51)
makes on-segment? collinear checks actually reachable.
2026-04-15 23:37:53 +02:00
Andrey Antukh
d13e464ed1 🐛 Fix three flex layout bugs in drop-area, positions and layout-data
drop_area.cljc: v-end? was guarded by row? instead of col?, making
vertical-end alignment check fire under horizontal layout conditions.
Aligned with v-center? which correctly uses col?.

positions.cljc: In get-base-line, the col? around? branch passed 2 as
a third argument to max instead of as a divisor in (/ free-width
num-lines 2). This made the offset clamp to at least 2 pixels rather
than computing half the per-line free space. Fixed parenthesization.

layout_data.cljc: The second cond branch (and col? space-evenly?
auto-height?) was permanently unreachable because the preceding branch
(and col? space-evenly?) is a strict superset. Removed the dead branch.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
7e9fac4f35 🐛 Fix constraint-modifier :default arity mismatch
All concrete constraint-modifier methods accept 6 arguments
(type, axis, child-before, parent-before, child-after, parent-after)
but the :default fallback only declared 5 parameters. Any unknown
constraint type would therefore receive 6 args and throw an arity
error at runtime. Added the missing sixth underscore parameter.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
80124657b8 🐛 Fix double rotation negation in adjust-shape-flips
When both dot-x and dot-y were negative (both axes flipped),
(update :rotation -) was applied twice which cancelled itself out,
leaving rotation unchanged. The intended behaviour is to negate
rotation once per flip, but flipping both axes simultaneously is
equivalent to a 180° rotation and should not alter the stored angle.

Replaced the two separate conditional rotation updates with a single
one gated on (not= (neg? dot-x) (neg? dot-y)) so the rotation is
negated only when exactly one axis is flipped.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
cf47d5e53e 🐛 Fix coplanar keyword mismatch in intersect-segments?
orientation returns the auto-qualified keyword ::coplanar
(app.common.geom.shapes.intersect/coplanar) but intersect-segments?
was comparing against the plain unqualified :coplanar keyword, which
never matches. This caused all collinear/on-segment edge cases to be
silently skipped, potentially missing valid segment intersections.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
adfe4c3945 🐛 Fix update-rect :size and unqualified abs in corners->rect
update-rect with :size type was only updating :x2 and :y2 but not
:x1 and :y1, leaving the Rect record in an inconsistent state (x1/y1
would not match x/y). Aligned its behaviour with update-rect! which
correctly updates all four corner fields.

corners->rect was calling unqualified abs which is not imported in
app.common.geom.rect namespace. Replaced with mth/abs which is
the proper namespaced version already available in the ns require.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
179bb51c76 🐛 Fix gpt/multiply docstring and gpt/abs Point record downcast
gpt/multiply had a copy-paste docstring from gpt/subtract claiming it
performs subtraction; corrected to accurately describe multiplication.

gpt/abs was using clojure.core/update on a Point record, which returns
a plain IPersistentMap instead of a Point instance, causing point?
checks on the result to return false. Replaced with a direct pos->Point
constructor call using mth/abs on each coordinate.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
3d4c914daa 🐛 Fix trailing comma in matrix->str and remove duplicate dispatch
matrix->str was producing malformed strings like '1,0,0,1,0,0,'
instead of '1,0,0,1,0,0', breaking string serialization of matrix
values used in transit and print-dup handlers.

Also remove the first pp/simple-dispatch registration for Matrix at
line 362 which was dead code shadowed by the identical registration
further down in the file.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Elena Torró
9cd1542dd9
Merge pull request #9000 from penpot/niwinz-main-bugfixes
🐛 Add several fixes for app.common.types namespace
2026-04-15 11:51:14 +02:00
Andrey Antukh
2e97f01838 🐛 Fix safe-subvec 2-arity rejecting start=0
The guard used (> start 0) instead of (>= start 0), so
(safe-subvec v 0) returned nil instead of the full vector.
2026-04-15 11:42:49 +02:00
Andrey Antukh
176edadb6f 🐛 Fix nan? returning false for ##NaN on JVM
Clojure's = uses .equals on doubles, and Double.equals(Double.NaN)
returns true, so (not= v v) was always false for NaN. Use
Double/isNaN with a number? guard instead.
2026-04-15 11:42:49 +02:00
Andrey Antukh
b26ef158ef 📚 Fix typos in vec2, zip-all, and map-perm docstrings 2026-04-15 11:42:49 +02:00
Andrey Antukh
95d4d42c91 🐛 Add missing string? guard to num-string? on JVM
The CLJS branch of num-string? checked (string? v) first, but the
JVM branch did not. Passing non-string values (nil, keywords, etc.)
would rely on exception handling inside parse-double for control
flow. Add the string? check for consistency and to avoid using
exceptions for normal control flow.
2026-04-15 11:42:49 +02:00
Andrey Antukh
bba3610b7b ♻️ Rename shadowed 'fn' parameter to 'pred' in removev
The removev function used 'fn' as its predicate parameter name,
which shadows clojure.core/fn. Rename to 'pred' for clarity and
to follow the naming convention used elsewhere in the namespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh
83da487b24 🐛 Fix append-class producing leading space for empty class
When called with an empty string as the base class, append-class
was producing " bar" (with a leading space) because (some? "")
returns true. Use (seq class) instead to treat both nil and empty
string as absent, avoiding invalid CSS class strings with leading
whitespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh
da8e44147c Remove redundant str call in format-number
format-precision already returns a string, so wrapping its result
in an additional (str ...) call was unnecessary.
2026-04-15 11:42:49 +02:00
Andrey Antukh
69e25a4998 📚 Fix typo in namespace docstring ('if' -> 'of') 2026-04-15 11:42:49 +02:00
Andrey Antukh
eca9b63d68 Remove redundant map lookups in map-diff
The :else branch of diff-attr was calling (get m1 key) and
(get m2 key) again, but v1 and v2 were already bound to those
exact values. Reuse the existing bindings to avoid the extra
lookups.
2026-04-15 11:42:49 +02:00
Andrey Antukh
29ea1cc495 📚 Fix misleading without-obj docstring
The docstring claimed the function removes nil values in addition to
the specified object, but the implementation only removes elements
equal to the given object. Fix the docstring in both data.cljc and
the local copy in files/changes.cljc.
2026-04-15 11:42:49 +02:00
Andrey Antukh
d73ab3ec92 🐛 Fix safe-subvec 3-arity evaluating (count v) before nil check
The 3-arity of safe-subvec called (count v) in a let binding before
checking (some? v). While (count nil) returns 0 in Clojure and does
not crash, the nil guard was dead code. Restructure to check (some? v)
first with an outer when, then compute size inside the guarded block.
2026-04-15 11:42:49 +02:00
Andrey Antukh
1cc860807e Use seq/next idiom in enumerate instead of empty?/rest
Replace (empty? items) + (rest items) with (seq items) + (next items)
in enumerate. The seq/next pattern is idiomatic Clojure and avoids
the overhead of empty? which internally calls seq and then negates.
2026-04-15 11:42:49 +02:00
Andrey Antukh
92dd5d9954 🐛 Fix index-of-pred early termination on nil elements
The index-of-pred function used (nil? c) to detect end-of-collection,
which caused premature termination when the collection contained nil
values. Rewrite using (seq coll) / (next s) pattern to correctly
distinguish between nil elements and end-of-sequence.
2026-04-15 11:42:49 +02:00
Andrey Antukh
057c6ddc0d 🐛 Fix deep-mapm double-applying mfn on leaf entries
The deep-mapm function was applying the mapping function twice on
leaf entries (non-map, non-vector values): once when destructuring
the entry, and again on the already-transformed result in the else
branch. Now mfn is applied exactly once per entry.
2026-04-15 11:42:49 +02:00
Andrey Antukh
a2e6abcb72 🐛 Fix spurious argument to dissoc in patch-object
The patch-object function was calling (dissoc object key value) when
handling nil values. Since dissoc treats each argument after the map
as a key to remove, this was also removing nil as a key from the map.
The correct call is (dissoc object key).
2026-04-15 11:42:49 +02:00
Andrés Moya
a3ea9fbecb
🔧 Add more validations for components, to avoid some crashes (#7820)
* 🔧 Validate only after propagation in tests

* 💄 Enhance some component sync traces

* 🔧 Add fake uuid generator for debugging

* 🐛 Remove old feature of advancing references when reset changes

Since long time ago, we only allow to reset changes in the top copy
shape. In this case the near and the remote shapes are the same, so
the advance-ref has no effect.

* 🐛 Fix some bugs and add validations, repair and migrations

Also added several utilities to debug and to create scripts that
processes files

* 🐛 Fix misplaced parenthesis passing propagate-fn to wrong function

The :propagate-fn keyword argument was incorrectly placed inside the
ths/get-shape call instead of being passed to tho/reset-overrides.
This caused reset-overrides to never propagate component changes,
making the test not validate what it intended.

* 🐛 Accept and forward :include-deleted? in find-near-match

Callers were passing :include-deleted? true but the parameter was not
in the destructuring, so it was silently ignored and the function
always hardcoded true. This made the API misleading and would cause
incorrect behavior if called with :include-deleted? false.

* 💄 Use set/union alias instead of fully-qualified clojure.set/union

The namespace already requires [clojure.set :as set], so use the alias
for consistency.

* 🐛 Add tests for reset-overrides with and without propagate-fn

Add two focused tests to comp_reset_test to cover the propagate-fn
path in reset-overrides:

- test-reset-with-propagation-updates-copies: verifies that resetting
  an override on a nested copy inside a main and supplying propagate-fn
  causes the canonical color to appear in all downstream copies.

- test-reset-without-propagation-does-not-update-copies: regression
  guard for the misplaced-parenthesis bug; confirms that omitting
  propagate-fn leaves copies with the overridden value because the
  component sync never runs.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 08:42:42 +02:00
Andrey Antukh
6d1d044588 ♻️ Move app.common.types.color tests to their own namespace
Tests that exercise app.common.types.color were living inside
common-tests.colors-test alongside the app.common.colors tests. Move
them to common-tests.types.color-test so the test namespace mirrors
the source namespace structure, consistent with the rest of the
types/ test suite.

The [app.common.types.color :as colors] require is removed from
colors_test.cljc; the new file is registered in runner.cljc.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
1e0f10814e 🔥 Remove duplicate gradient helpers from app.common.colors
The five functions interpolate-color, offset-spread, uniform-spread?,
uniform-spread, and interpolate-gradient duplicated the canonical
implementations in app.common.types.color. The copies in colors.cljc
also contained two bugs: a division-by-zero in offset-spread when
num=1, and a crash on nil idx in interpolate-gradient.

All production callers already use app.common.types.color. The
duplicate tests that exercised the old copies are removed; their
coverage is absorbed into expanded tests under the types-* suite,
including a new nil-idx guard test and a single-stop no-crash test.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
db7c646568 Add missing tests for session bug fixes and uniform-spread?
Add indexed-access-with-default in fill_test.cljc to cover the two-arity
(nth fills i default) form on both valid and out-of-range indices, directly
exercising the CLJS Fills -nth path fixed in 593cf125.

Add segment-content->selrect-multi-line in path_data_test.cljc to cover
content->selrect on a subpath with multiple consecutive line-to commands
where move-p diverges from from-p, confirming the bounding box matches
both the expected coordinates and the reference implementation; this
guards the calculate-extremities fix in bb5a04c7.

Add types-uniform-spread? in colors_test.cljc to cover
app.common.types.color/uniform-spread?, which had no dedicated tests.
Exercises the uniform case (via uniform-spread), the two-stop edge case,
wrong-offset detection, and wrong-color detection.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
caac452cd4 🐛 Fix wrong extremity point in calculate-extremities for line-to
In the :line-to branch of calculate-extremities, move-p (the subpath
start point) was being added to the extremities set instead of from-p
(the actual previous point). For all line segments beyond the first one
in a subpath this produced an incorrect bounding-box start point.

The :curve-to branch correctly used from-p; align :line-to to match.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
30931839b5 🐛 Fix reversed d/in-range? args in CLJS Fills -nth with default
In the ClojureScript Fills deftype, the two-arity -nth implementation
called (d/in-range? i size) but the signature is (d/in-range? size i).
This meant -nth always fell through to the default value for any valid
index when called with an explicit default, since i < size is the
condition but the args were swapped.

The no-default -nth sibling on line 378 and both CLJ nth impls on
lines 286 and 291 had the correct argument order.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
6da39bc9c7 🐛 Fix ObjectsMap CLJS negative cache keyed on 'key' fn instead of 'k'
In the CLJS -lookup implementation, when a key is absent from data the
negative cache entry was stored under 'key' (the built-in map-entry
key function) rather than the 'k' parameter.  As a result every
subsequent lookup of any missing key bypassed the cache and repeated
the full lookup path, making the negative-cache optimization entirely
ineffective.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
2b67e114b6 🐛 Fix inside-layout? passing id instead of shape to frame-shape?
`(cfh/frame-shape? current-id)` passes a UUID to the single-arity
overload of `frame-shape?`, which expects a shape map; it always
returns false. Fix by passing `current` (the resolved shape) instead.
Update the test to assert the correct behaviour.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
8b08c8ecc9 🐛 Fix wrong mapcat call in collect-main-shapes
`(mapcat collect-main-shapes children objects)` passes `objects` as a
second parallel collection instead of threading it as the second
argument to `collect-main-shapes` for each child. Fix by using an
anonymous fn: `(mapcat #(collect-main-shapes % objects) children)`.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh
8253738f01 🐛 Fix reversed get args in convert-dtcg-shadow-composite
\`(get "type" shadow)\` always returns nil because the map and key
arguments were swapped. The correct call is \`(get shadow "type")\`,
which allows the legacy innerShadow detection to work correctly.
Update the test expectation accordingly.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:03 +02:00
Andrey Antukh
c30c85ff07 🐛 Remove duplicate font-weight-keys in typography-keys union
font-weight-keys was listed twice in the set/union call for
typography-keys, a copy-paste error. The duplicate entry has no
functional effect (sets deduplicate), but it is misleading and
suggests a missing key such as font-style-keys in its place.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh
ff41d08e3c 🐛 Fix stale accumulator in get-children-in-instance recursion
get-children-rec passed the original children vector to each recursive
call instead of the updated one that already includes the current
shape. This caused descendant results to be accumulated from the wrong
starting point, losing intermediate shapes. Pass children' (which
includes the current shape) into every recursive call.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh
08ca561667 🐛 Add better nil handling in interpolate-gradient when offset exceeds stops
When no gradient stop satisfies (<= offset (:offset %)),
d/index-of-pred returns nil. The previous code called (dec nil) in
the start binding before the nil check, throwing a
NullPointerException/ClassCastException. Guard the start binding with
a cond that handles nil before attempting dec.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh
7b0ea5968d 🚑 Fix typo :podition in swap-shapes grid cell
The key :podition was used instead of :position when updating the
id-from cell in swap-shapes, silently discarding the position value
and leaving the cell's :position as nil after every swap.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh
a81cded0aa
Make the common fressian module more testable (#8859)
*  Add exhaustive unit tests for app.common.fressian encode/decode

Add a JVM-only test suite (41 tests, 172 assertions) for the fressian
serialisation layer, covering:

- All custom handlers: char, clj/keyword, clj/symbol, clj/vector,
  clj/set, clj/map, clj/seq, clj/ratio, clj/bigint, java/instant,
  OffsetDateTime, linked/map (order preserved), linked/set (order preserved)
- Built-in types: nil, boolean, int, long, double (NaN, ±Inf, boundaries),
  String, byte[], UUID
- Edge cases: empty collections, nil values, ArrayMap/HashMap size boundary,
  mixed key types
- Penpot-domain structures: shape maps with UUID keys, nested objects maps
- Correctness: encode→decode→encode idempotency, independent encode calls

* ♻️ Extract fressian handler helpers to private top-level functions

Extract adapt-write-handler, adapt-read-handler, and merge-handlers
out of the letfn in add-handlers! into reusable private functions.
Also creates xf:adapt-write-handler and xf:adapt-read-handler
transducers and adds overwrite-read-handlers and overwrite-write-handlers
for advanced handler override use cases.
2026-04-14 10:48:58 +02:00
Statxc
d90e7f8164
Add Find & Replace for text content and layer names (#8899)
*  Add Find & Replace for text content and layer names

* 💄 Fix cross-browser styling for Find & Replace radio buttons and action buttons

* 💄 Fix stylelint empty line before declaration in layers.scss

*  Improve match-filters and match-ids efficiency

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 10:41:31 +02:00
Dream
4703fe6e3b
Add visibility toggle for strokes (#8913)
*  Add visibility toggle for strokes

* ♻️ Use single emit! call for stroke visibility toggle

* 💄 Disable stroke controls when hidden, matching shadow/blur pattern

When a stroke is hidden, the alignment/style selects, cap selects, and
cap switch button are now disabled. A .hidden CSS class dims the
options area with reduced opacity. This matches the existing behavior
in shadow_row and blur menu where controls are disabled when the
effect is hidden.

* 💄 Move stroke hide button before remove button

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-04-14 10:08:13 +02:00
Andrey Antukh
9106a994f1 Merge remote-tracking branch 'origin/staging' into develop 2026-04-13 18:31:50 +02:00
Andrey Antukh
bc47b992eb Merge remote-tracking branch 'origin/main' into staging 2026-04-13 18:31:32 +02:00
Alejandro Alonso
dfc5a256b4 Merge remote-tracking branch 'origin/staging' into develop 2026-04-13 16:47:18 +02:00
Andrey Antukh
e46b34efc7 📎 Fix formatting issues 2026-04-13 15:41:38 +02:00