* 🐛 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>
Preserve real size overrides during variant switches without copying stale absolute composite geometry from the source variant.
Signed-off-by: Codex <codex@openai.com>
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.
* 📎 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?
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>
* 🔧 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>
* 🎉 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 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
* ✨ 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