112 Commits

Author SHA1 Message Date
Alonso Torres
014ec34249
🐛 Fix system crash on grid element deletion of a component (#10956)
* 🐛 Add regression test: main-side component edits break copy swap slots

Reproduces the :missing-slot referential-integrity failure ("Shape has been
swapped, should have swap slot") that crashes files with component copies.

Root cause: reordering or deleting a nested sub-head IN THE MAIN of a component,
while copies exist, does not propagate swap slots to the copies. find-near-match
matches a copy's sub-heads to the main's children by POSITION, so once the main's
order changes the copies' shape-refs no longer match their position and, lacking a
swap slot, fail referential-integrity validation.

- Copy-side edits are handled correctly (characterization tests, pass today).
- The two main-side tests fail today with :missing-slot and go green once the
  sync assigns swap slots to copies on a main reorder/delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 Fix integrity crashes from copy/main child-order divergence

Copy sub-heads were matched to their main's children purely by position
(find-near-match), while several code paths reorder or mutilate one side
only. Any of them made file validation fail with :missing-slot ("Shape
has been swapped, should have swap slot"), crashing the workspace on the
next validated commit, or persisting a corrupt file whose later edits
crash. Reproduced live: deleting a sub-head inside a copy (which only
hides it) and then reflowing the copy's grid moved the hidden (cell-less)
child to the front of :shapes, knocking every sibling out of its
positional slot.

Four fixes, one per divergence path:

- validate/check-required-swap-slot: a sub-head whose shape-ref is still
  a child of the near main parent is a REORDER (the component sync
  realigns it), not a swap; a swap slot is required only when the ref
  points outside the near main parent (a real swap). This matches how
  the sync engine itself pairs children (by shape-ref, not by position).
  comp-processors/fix-missing-swap-slots (migration 0019) is aligned:
  adding slots to merely-reordered sub-heads would freeze them out of
  normal synchronization.

- changes/:reorder-children now refuses to alter the child structure of
  component copies unless allow-altering-copies is set, mirroring the
  is-valid-move? rule of :mov-objects; that structure is owned by the
  component sync engine. Grid reflows emitted this change type with no
  guard. pcb/reorder-grid-children also skips copy grids producer-side.

- layout/reorder-grid-children keeps children that participate in no
  cell (hidden or absolute positioned) at their original index instead
  of lumping them at the front: moving them gratuitously changed their
  z-order and, in copies, broke the positional matching. Note :shapes
  stays reversed relative to the sorted cell order for in-cell children.

- logic/generate-delete-shapes: deleting shapes from inside a main
  (without deleting the main root, whose copies keep working against the
  deleted component) now also deletes the copy shapes that reference
  them, transitively (copies of copies) and across all pages of the
  file, so no dangling shape-refs remain. Skipped for
  allow-altering-copies flows (component swap replaces the shape and the
  sync reconciles copies via swap slots). Cross-page removals build
  redo/undo changes against that page's objects directly, since the
  changes-builder mounts only the current page; their undo mov-objects
  carry allow-altering-copies so restoring inside a copy is not rejected
  by the new guard.

The regression tests assert the fixed semantics: main-side reorders and
deletes keep copies valid, the previously crashing full chain (unvalidated
main reorder + later copy edit) stays healthy, :reorder-children cannot
scramble copies, and reorder-grid-children keeps cell-less children in
place. The namespace is now also registered in the JS test runner.

AI-assisted-by: Claude Opus 4.8 (1M context)

*  Extend composable slot cases to the grid-reflow crash sweep

Case D (CopySubheadDeletePreservesSlots) now sweeps which copy sub-head
is deleted (first or last) and whether the copy root is resized
afterwards, forcing a grid reflow: the reflow used to move the hidden
(cell-less) child to the front of the copy's children, shifting every
sibling out of its positional slot. Deleting the FIRST sub-head masked
the bug (moving it to the front is a no-op), which is why the case
passed before this sweep. The foundation layout is grid accordingly.

Case E (MainReorderKeepsCopySlots) no longer hangs the app now that a
main-side reorder leaves a valid file, so its warning docstring is
replaced: it runs as a routine test (verified headless, passing) and is
safe in a "run all".

SlotIntegrity's doc is updated to the new validator semantics (a slot is
required only for real swaps, not reorders); the positional alignment it
asserts remains the correct, stronger steady-state invariant for these
cases. The lockfile change materializes the playwright devDependency
already declared in package.json.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 📚 Record copy/main order-divergence invariants in memories

Swap-slot semantics (membership, not positional; slots only for real
swaps), the copy-structure guards on :mov-objects/:reorder-children, the
grid reorder stability for cell-less children, and the main-side delete
propagation in generate-delete-shapes.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 🐛 Add adjustements to the code

---------

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 13:25:03 +02:00
Andrés Moya
f2bf5a3111 🔧 Add more tests for all cases and fix text token application in tests 2026-06-22 12:54:40 +02:00
Andrés Moya
5f8d9740d6 🔧 Normalize text nodes comparison, to be used in tokens detach 2026-06-19 14:58:20 +02:00
alonso.torres
b9dfa0c607 🐛 Fix problem when adding a variant children 2026-06-18 14:24:40 +02:00
Codex
744c1b98c0 🐛 Anchor variant switch geometry to target
Preserve real size overrides during variant switches without copying stale absolute composite geometry from the source variant.

Signed-off-by: Codex <codex@openai.com>
2026-06-09 12:20:15 +02:00
Andrey Antukh
4a8fb5af53 Merge remote-tracking branch 'origin/staging' into develop 2026-06-01 13:15:57 +02:00
Andrey Antukh
c5de4c27b0 Merge remote-tracking branch 'origin/main' into staging 2026-06-01 12:57:39 +02:00
Yamila Moreno
ddba2ffa75
📎 Update Kaleidos Copyright (#9929) 2026-05-29 11:24:58 +02:00
Pablo Alba
aa996c5118
🐛 Fix nested components becomes flat after reset (#9841) 2026-05-28 15:57:35 +02:00
Andrey Antukh
3858993a57 Merge remote-tracking branch 'origin/staging' into develop 2026-05-27 13:37:02 +02:00
Pablo Alba
3cecc29276 🐛 Fix update library dialog when a component position changes
Do not show the library sync popup when the only differences are global x/y changes on library components. We now generate the actual sync changes and only notify if there are real redo-changes to apply.
Run cll/generate-sync-file-changes for candidate libraries and filter out those with empty :redo-changes. The expensive check is deferred via rx/timer 0 so it runs asynchronously and does not block the UI.
Why: Position-only changes are normalized during sync (via reposition-shape) and never propagate to copies; showing the popup in that case was a false positive.
Performance: The check is deferred to the next tick to avoid UI stutter on large files with many libraries.
2026-05-27 11:22:57 +02:00
Alonso Torres
a7b17f54f1
🐛 Fix problem of path position on variant change (#9801) 2026-05-21 16:39:26 +02:00
Andrey Antukh
6f41a2b729 Merge remote-tracking branch 'origin/staging' into develop 2026-05-18 15:24:02 +02:00
Andrés Moya
82169bc0a3
🐛 Fix loss of swap slot in some cases of variant switch (#9147)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-18 14:25:32 +02:00
Michael Panchenko
7fb19fc1a2
🐛 Fix float comparison #14070 (#9434)
* 📎 Add test that surfaces the bug described in #14070

The bug: alt-drag-duplicating a variant master into the variant container
auto-creates a new variant component cloned from the source
via duplicate-component, which preserves :touched on every cloned shape.
The resulting copy's children inherit those :geometry-group touched flags
through add-touched-from-ref-chain on switch.

variants-switch -> component-swap (keep-touched? true) -> generate-keep-touched
then runs update-attrs-on-switch for each touched child. The new shape's
:y is correctly skipped by the per-attr "different masters" guard, but
:selrect/:points fall through to a width/height-based safety check that
uses exact equality. In practice, the alt-drag modifier path leaves a
sub-pixel drift in :width on the copy, so equal-geometry? returns false,
the safety check is bypassed, and the :else branch copies the source
variant's :selrect verbatim onto the freshly instantiated target shape.
The shape ends up with :y from the target master but :selrect.y from the
source — the renderer reads :selrect, so the child appears at the source
position inside a parent that has resized to the target's dimensions:
the visible "button cut off" symptom.

The new test sets up a variant container whose children are themselves
component copies (matching production: variant masters' children carry
:touched), introduces the same kind of width drift on the copy that the
alt-drag path produces, and runs the swap directly via the existing
test harness. It asserts (a) :y matches the target, (b) :selrect.y
matches the target, and (c) :y and :selrect.y agree. With the current
code (a) passes and (b)/(c) fail — capturing both the wrong value and
the internal inconsistency that causes the visible regression.

* 🐛 Fix #14070 by no longer comparing floats for exact equality in equal-geometry?
2026-05-11 15:17:48 +02:00
FairyPiggyDev
9c2a80bfa1
🐛 Fix crash pasting component with variants from shared library (#9136)
Copying a component with variants from a shared library file ("Lib")
and pasting it into a file that uses that library ("Using Lib") would
crash the destination file with the referential-integrity validator
error:

    {:code :component-main-external
     :hint "Main instance should refer to a component in the same file"}

Root cause
----------

Paste goes through `generate-duplicate-shape-change` in
`common/src/app/common/logic/libraries.cljc`. When the shape is a
main instance of a known component and the copy set includes its
variant container, dispatch lands in `duplicate-variant`, then
`generate-duplicate-component`, and finally `duplicate-component`,
which clones the main-instance shape tree. Its `update-new-shape`
helper already re-links the new outer main's `:component-id` to the
freshly created local component (`new-component-id`), but it never
touches `:component-file`. The cloned shape therefore inherits
`:component-file` from the source library while the new component is
registered in the destination's local library
(`:apply-changes-local-library? true`), leaving the main-instance
dangling.

Fix
---

Extend `update-new-shape` with a second clause, sibling to the
existing `:component-id` rewrite: when a destination file id is
provided and differs from the new main's current `:component-file`,
re-root the shape. The same `(= (:component-id new-shape) (:id
component))` guard already used for the id rewrite ensures only the
outer main-instance is touched; nested shapes are unaffected.

The destination file id is threaded from the paste entry point
through the two orchestration functions that already knew the
source/destination distinction:

- `generate-duplicate-shape-change` — supplies the destination
  `file-id` it already has in scope when dispatching to
  `generate-duplicate-component-change`.
- `generate-duplicate-component-change` — accepts `:new-component-file`
  as a kwarg; renames its internal `file-id` binding to
  `source-file-id` for clarity (it was always the component's
  originating library file); forwards `new-component-file` to
  `duplicate-variant`.
- `duplicate-variant` — takes and forwards the `new-component-file`
  positional arg.
- `generate-duplicate-component` — accepts `:new-component-file` kwarg
  and passes it to `duplicate-component`.
- `duplicate-component` — applies the rewrite inside
  `update-new-shape`. The `new-component-file` parameter is placed
  right after `new-component-id` since component-id and component-file
  are typically managed together.

Same-file duplication is not affected: without `:new-component-file`
the new clause is skipped, and when source and destination match the
`(not= new-component-file (:component-file new-shape))` guard fails.

Tests
-----

Added in `common/test/common_tests/logic/comp_creation_test.cljc`:

- `test-duplicate-component-rewrites-component-file-to-destination`
  asserts that passing `:new-component-file` to
  `generate-duplicate-component` produces a main-instance with
  `:component-file` equal to the destination id.
- `test-duplicate-component-keeps-component-file-without-dest`
  baseline: without `:new-component-file`, `:component-file` is left
  untouched, matching pre-existing same-file behavior.

Github #8144

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 15:54:24 +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
Pablo Alba
ef6eeb5693
🐛 Fix variants corner cases with selrect and points (#8882)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:23:03 +02:00
Pablo Alba
34d29328e6
🐛 Fix bad size on switching a layout with fixed sizing (#8504) 2026-03-09 12:12:03 +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
Andrey Antukh
0ab126748f
💄 Add format rule for code comments (#8211)
* 💄 Add format rule for code comments

* ⬆️ Update linter and formatter on devenv
2026-01-27 15:07:18 +01:00
Pablo Alba
69bbdad570 🐛 Fix nested variant in a component doesn't keep inherited overrides (3) 2025-11-03 15:36:37 +01:00
Pablo Alba
df4279bdee Revert "🐛 Fix nested variant in a component doesn't keep inherited overrides (2)"
This reverts commit 7c6515aa7b578dc69c3a0424fef60950a1406dee.
2025-11-03 15:36:37 +01:00
Andrey Antukh
f53935f5df Merge remote-tracking branch 'origin/staging' into develop 2025-10-31 12:13:29 +01:00
Pablo Alba
c6b9954af8 🐛 Fix nested variant in a component doesn't keep inherited overrides 2025-10-22 13:35:22 +02:00
Pablo Alba
220c27c354 🐛 Fix nested variant in a component doesn't keep inherited overrides (2) 2025-10-20 18:28:10 +02:00
Pablo Alba
b3cb7df33c
🐛 Fix nested variant in a component doesn't keep inherited overrides 2025-10-17 14:01:54 +02:00
Eva Marco
045aa7c788
🎉 Add tokens to color selection (#7447)
* 🎉 Add tokens to color row

* 🎉 Add color-token to stroke input

* 🐛 Fix change token on multiselection with groups

* 🎉 Create token colors on selected-colors section

* ♻️ Fix comments
2025-10-14 11:17:14 +02:00
Andrés Moya
4c35571336 🔧 Read and modify token themes by id 2025-09-29 12:16:42 +02:00
Andrés Moya
4e607d8da2 💄 Clarify and reorder interfaces 2025-09-29 12:16:42 +02:00
Andrés Moya
f5fd978a07 🔧 Retrieve tokens from library and not from set 2025-09-29 12:16:42 +02:00
Andrés Moya
b28be62845 🔧 Fix rebase problems 2025-09-29 12:16:42 +02:00
Andrés Moya
d76a5c615c 🔧 Modify token sets by id instead of name and review usage 2025-09-29 12:16:42 +02:00
Andrés Moya
021b8f81ca 🔧 Read token sets by id instead of name 2025-09-29 12:16:42 +02:00
Andrés Moya
ed3fc5b8b2
🐛 Fix detaching a nested copy inside a main component (#7304)
* 🐛 Fix detaching a nested copy inside a main component

* 💄 Rename functions for more semantic precission
2025-09-12 16:00:01 +02:00
Andrés Moya
6e6749f42e 🔧 Add unit tests to apply layout tokens 2025-09-05 11:11:48 +02:00
Andrés Moya
7b6aa0c12a 🐛 Unapply layout item tokens when moving out of a layout 2025-09-05 11:11:48 +02:00
Pablo Alba
dfe0f64c7c
Add variants advanced retrieve tests (#7183) 2025-08-26 16:40:27 +02:00
Luis de Dios
a3557a81e4
🐛 Fix add space between the name and the index of new properties (#7068) 2025-08-07 07:41:33 +02:00
luisδμ
ff1d26294a
🐛 Fix create properties with a default value instead of an empty one (#7033) 2025-07-31 15:01:51 +02:00
Pablo Alba
9db67cc5e8
🐛 Fix bad swap slot after two swaps (#6962)
* 🐛 Fix bad swap slot after two swaps

*  MR changes
2025-07-30 12:35:27 +02:00
Andrés Moya
57330f53e2 🔧 Use id instead of name for tokens crud 2025-07-24 15:21:18 +02:00
Florian Schroedl (aider)
d788a4d252 Implement new token-type :font-families 2025-07-23 11:26:28 +02:00
Andrey Antukh
9ee488009f ♻️ Add substantial refactor on how types are organized
This mainly affects types related to colors, fills and texts, moving library
based operations from color namespace.
2025-07-23 08:03:23 +02:00
Andrey Antukh
4e2998a366 ♻️ Rename fill to fills namespace 2025-07-23 07:32:11 +02:00
Pablo Alba
ea5521485a
♻️ Remove redundant flag on text overrides (#6933) 2025-07-22 12:32:24 +02:00
Pablo Alba
0cfd70da2e 🐛 Fix corner cases on variants text overrides 2025-07-11 15:28:55 +02:00
Florian Schrödl
21746144b7
Add letter spacing token (#6814)
* 🐛 Fix merge schema not working with key generation

*  Add letter-spacing token

* ♻️ Remove comments

* ♻️ Inline line-height for now
2025-07-03 16:00:58 +02:00
Florian Schrödl
7dd61968b5
Implement object type specific tokens (#6816)
*  Allow token applying for supported shape types only

* 🐛 Remove x/y attribute keys from spacing token

*  Shape specific context-menu

*  Only apply tokens to supported shapes when doing multi selection apply

*  Handle groups not supported by tokens yet

* 🐛 Fix outdated tests

* ♻️ Commentary

*  Add helper functions for attribute applicability checks

* ♻️ Groups don't have own attributes

* ♻️ Remove unused function

* ♻️ Move attribute logic to common.types.token
2025-07-03 12:22:04 +02:00
Andrés Moya
f0f01af55c 🔧 Make TokenSet an abstract data type 2025-06-30 16:59:00 +02:00