Files older than the feature components/text-overrides mark a copy text
with :text-content-attribute (any style) or :text-content-structure
(letters and styles). Migration 0033 compares each such copy with its
near main and gives it the groups its old flags allowed where it
differs; an old flag where nothing differs sets nothing. A copy whose
near main cannot be reached keeps every group its flags allowed, so it
loses no override. The migration adds the feature, so a client on the
old code cannot write the old groups into a migrated file.
Binfile import leaves the copies whose near main is in a library not
linked yet, as it does with swap slots, and converts them once the
libraries are linked: the deferred pass of the import now runs both
conversions (convert-deferred!, *defer-unreachable*).
Validation reports the old groups (:legacy-text-touched), and its
repair recomputes them against the near main; a text override group
without :content-group is still :invalid-text-touched. A shape with no
letters in it and its main no longer counts as differing in its styles.
Benchmark (lower quantile, ms; baseline / now / ratio):
deepest-edit-through-three-levels 35.2 / 34.8 / 0.99
edit-through-a-chain-of-dependents 68.9 / 65.0 / 0.94
library-update-into-copies 83.1 / 86.9 / 1.05
main-edit-into-10-copies 1.8 / 1.5 / 0.86
main-edit-into-100-copies 15.4 / 15.0 / 0.97
main-edit-into-1000-copies 153.1 / 150.6 / 0.98
variant-switch-into-copies 32.1 / 26.8 / 0.84
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a copy head took the place of a main shape and its component was
the one of that place, it stopped being a swap, but its children kept
referring to the main of its own component instead of to the children
of that place. Validation did not see it, and the next sync deleted
those children and added them again, losing their overrides, or the
head did not follow later changes to that place. It happened in:
- a variant switch that carries a swapped child into a variant whose
child there has the same component;
- update main of a copy that swapped a nested copy back to the
component a deeper main puts in that place, after the main in between
swapped it for another one; the clone in the main also kept
:swap-group;
- the repair of :ref-not-in-near-main, which also never cleared
:swap-group.
The swap in a copy, the switch, update main and the repair now share
ctf/take-position: the head refers to the main shape of its position,
is swapped while its component differs from that shape's, and else its
descendants refer to the descendants of that shape.
The switch also read the swapped child from the objects of the swap,
where it was already gone with its old parent. It now reads it once it
is moved into the new copy, so its placement turns its children too,
and the undo takes it back to the root as the move to the root left it.
The swap finds the new shapes in its own objects, not in its changes.
The component memory says how a head takes a position.
Benchmark (lower quantile, ms; baseline / now / ratio):
deepest-edit-through-three-levels 35.2 / 37.1 / 1.05
edit-through-a-chain-of-dependents 68.9 / 70.5 / 1.02
library-update-into-copies 83.1 / 77.3 / 0.93
main-edit-into-10-copies 1.8 / 1.7 / 0.94
main-edit-into-100-copies 15.4 / 16.0 / 1.04
main-edit-into-1000-copies 153.1 / 160.6 / 1.05
variant-switch-into-copies 32.1 / 28.7 / 0.89
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The migration 0032 stores every swap of a file as an override group.
A swap made in a copy is paired with the child of the near main that
holds the same position, as sync paired it; any other slot is dropped.
All pairings are found on the data as it was, so swaps nested in swaps
pair against the content of the outer one, and libraries pair the same
whether they are migrated or not. A head that refers to its own slot
only inherited it, and drops it even when its near main cannot be
reached. Any other slot that matches no child, or whose near main is in
a library that is not loaded, keeps the slot as its ref, so validation
reports it. An import, which migrates its files before it links their
libraries, leaves those slots and converts them once the libraries are
linked. A migrated head that sits elsewhere than the child it replaces
would place it gets its geometry marked as touched, so the first sync
does not move it. Migrated and new files carry the feature
components/swap-group, so a client that still writes slots cannot open
them.
Validation checks the new form: :misplaced-slot reports any slot,
:misplaced-swap-group the group anywhere but on a nested copy head in a
copy, :ref-not-in-near-main a nested copy head whose ref is not a child
of the near main of its parent, and :duplicate-ref two children of a
copy that refer to the same main shape. Each has a repair.
Nothing but the migrations reads slots any more: the slot branches go
from sync, swap, switch, reset, detach, paste, duplicate and move. The
helpers that read and write a slot on a shape move to files/swap-slots,
and the ones that follow refs to files/comp-processors. The graph
projection drops the FillsSwapSlot edge, the components debugger shows
the component a swapped head takes its content from, and the plugin API
documents componentRefShape for a swapped copy. The swap tests are
named after what they check.
Benchmark (lower quantile, ms; baseline / now / ratio):
deepest-edit-through-three-levels 35.2 / 34.6 / 0.98
edit-through-a-chain-of-dependents 68.9 / 63.5 / 0.92
library-update-into-copies 83.1 / 76.4 / 0.92
main-edit-into-10-copies 1.8 / 1.6 / 0.87
main-edit-into-100-copies 15.4 / 15.1 / 0.98
main-edit-into-1000-copies 153.1 / 148.8 / 0.97
variant-switch-into-copies 32.1 / 26.4 / 0.82
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A swapped copy sub-head now keeps the shape-ref of the main child it
replaces (its position), takes its content from its own component, and
carries the touched group :swap-group. It used to point at the root of
the new component and store a swap slot naming the replaced child.
Every path reads and writes that form:
- sync pairs a swapped head with the child it replaces and syncs it
from its own component; it takes from the replaced child only its
placement: position, constraints and layout item attributes;
- reset undoes a swap in place, keeping the head's id and the other
overrides of the copy;
- detach, paste and duplicate make a head that leaves every copy level
a plain copy of its own component, and advance a deeper one by
position; the children of a swapped head never advance;
- a variant switch keeps a swap stored this way, and counts only the
copy's own swap, not one made in a main of the chain;
- update main replaces the main child with a clone of the swapped one,
and from a swapped head updates its own component;
- a swap inside a copy keeps the old shape-ref and adds :swap-group;
swapping back to the component of that position gives a plain nested
copy whose children refer to the position's children; a swap inside
a main is a plain nested copy.
Files that hold swap slots keep working until a migration converts
them. The component memories describe the model.
This fixes what came from the slot: reset of a swapped child cleared
the other overrides of the copy (F12) and gave copies of a main new
ids (F66); update main left a swap mark (F16); a second variant switch
lost the overrides (F29); grid cells did not pair a swapped child
(F44); a swapped child did not follow its position (F65). F49 keeps
failing, in another form.
The tests that asserted slots now assert the override group; the tests
of the slot code build the old form with :as-slot?.
Benchmark (lower quantile, ms; baseline / now / ratio):
deepest-edit-through-three-levels 35.2 / 35.4 / 1.01
edit-through-a-chain-of-dependents 68.9 / 68.7 / 1.00
library-update-into-copies 83.1 / 77.5 / 0.93
main-edit-into-10-copies 1.8 / 1.6 / 0.92
main-edit-into-100-copies 15.4 / 15.4 / 1.00
main-edit-into-1000-copies 153.1 / 151.1 / 0.99
variant-switch-into-copies 32.1 / 32.4 / 1.01
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No components v1 file reaches the code any more: opening one fails the
feature check, and imports reject it or give it the feature. What only
served v1 goes:
- validate-file and validate-file-affected no longer skip a file
without components/v2;
- the root of a deleted component is found by its main instance id
only, in the type helper and the component sprite;
- generate-sync-library, which synced shapes inside live components,
and the live components in containers-seq, so asset usages walk the
pages only (deleted components keep their objects and still sync);
- the unused :components-v2 file option, from the schema, the file
data and the test fixtures; stored files that carry it still
validate;
- the team migration-in-progress check and its frontend alert, which
leaves ephimeral features out of the team comparison;
- the components v2 introduction dialog, its styles and icons, and the
:v2-info-shown profile prop;
- the graphic asset events and builders and the reads of graphics in
the selection (the :add-media, :mod-media and :del-media changes stay,
so stored changes still apply), and the uncalled render-page and
render-components helpers.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A review found tests that would pass with what they test broken.
- Runners: both judge one variant at a time, fail a variant with no
assertion and a known-failure mark that applies nowhere, and give
the asserter the state the operations produced. The pure runner
validates the final file and rejects operations of unknown kind.
The frontend interpreter fails a step when the store reports an
event error (Potok only logs it), reports asserter errors against
their case, and stops the watchers that install! starts.
- Nodes drive the real code: a main root rename goes through
end-rename-shape, and the clipboard's advance of nested copies is
one common function, cll/advance-copied-objects, that copy-selected
and both test paths call.
- Cases N, AH, AI, AK, AD, AE, K, C, H, S, AY, I, Y, AX and F check
what they claim; O, R and T get real sizes.
- Stricter common unit tests found F60 (a repair detaches healthy
copies), F61 (a switch drops a fixed-sizing size), F62 (the ref
chain never stops at its target) and F63 (latent); all pinned.
- Frontend library tests: the RPC double records on subscribe, the
dropped commits check that nothing syncs, and the link and share
tests check the events sent.
- Plugin suite: OpSwap and OpDetach throw when the API call did
nothing, and case AB checks that the reset head is an instance of
the original component.
- The composable suite memory records the new rules.
Fill the unit test gaps in common and frontend component code:
- One test per component error code: break a file in one way, check
the codes validation reports, repair it as the backend does (until
valid, at most ten passes) and check what the repair changed.
Pinned: F46 (a dangling ref in a copy child is never reported), F47
and F48 (two repairs that never converge). :component-not-main,
:should-be-component-root and :not-component-not-allowed have no
test, as no file can produce them.
- Library color and typography sync must not touch copies. Both do
(F3): the color sync touches :fill-group, the typography sync
:content-group and :text-content-attribute, and a later main edit of
that attribute then no longer reaches the copy. Pinned.
- Twenty live thf/dump-file calls in common tests go; they printed
whole file trees on every run and asserted nothing.
- The disabled copy-nested-3 frontend test runs again: a stray
backquote quoted its run-store call, so it never called done.
Setting the local file back as the tokens source, or unlinking the
library that was the source, stored the file's own id in
:tokens-source. ensure-tokens-lib only created a tokens-lib when
:tokens-source was nil, so a file without a tokens-lib never got one,
and every token change was dropped without any error.
Now ensure-tokens-lib also creates the tokens-lib when the source is
the file itself, which fixes files already in that state, and
set-tokens-source removes the attribute instead of storing the file's
own id.
Closes#12180
AI-assisted-by: claude-opus-5-5
* 🐛 Gate pastes on page load and harden base-shape lookup
Pasting while the workspace is still opening crashed the
session: the layer-order lookup called rseq on a missing
root children list.
Ignore paste events until the page objects are loaded (the
clipboard keeps its content, so retrying works), return empty
instead of throwing from the shared layer-order helpers, and
fall back to pasting at the pointer position when the selection
is detached from the shape tree. Selecting the page root keeps
working as before through the frame branches.
Closes#11666
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Cover props paste and root-plus-other in paste guards
Address review follow-ups on the paste-before-init fix: gate
props pasting on page readiness like the shape entries, and
route root-plus-other selections without a base shape to the
pointer fallback instead of the unguarded else branch. Pin
single-root selection to the frame path with a regression test.
Closes#11666
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Hoist page lookup out of paste-shapes gate
Bind page and page-objects once in an outer let instead of
calling lookup-page twice (once for the readiness gate and
once inside the body). No behavior change.
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Reuse bound ids in paste-shapes page lookup
Bind file-id and page-id once and pass them to the lookup-page
arity that takes both, instead of resolving the page twice and
rebinding file-id in the inner let. No behavior change.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Auto link tokens when adding external libraries (provisional)
* 🔧 Refactor tokens-lib initialization
* 🔧 Add separated TokenStatus to store status apart of TokensLib
* 🔧 Make all status operations use the new data structure
* 🔧 Normalize status helper functions and access token sets by id
* 🔧 Rename :tokens-file to :tokens-source
* 🎉 Allow the user to choose the tokens-source of a file
* 🎉 Make tokens library readonly when it's in an external file
* 🎉 Show tokens in library summaries
* 🎉 Show source info in sidebar
* 🔧 Fix integration tests
* 🐛 Propagate changes of token values in external library
* 🎉 Layout updates
* 🔧 Refactor tokens source calculations
* 🔧 Add harder checks for nil or empty values in everything
* 🐛 Fix some integration tests
* 🔧 Add integration tests for tokens in external libs
* 🔧 Validate and repair missing tokens status
* 🎉 Make ui changes optional with config flag
* 🐛 Propagate tokens after synchronizing components in ext library
* 🐛 Propagate tokens after creating new instances
* 🐛 Propagate tokens after synchronizing tokens in ext library
* 🐛 Add a tokens source icon to libraries section (#11439)
* 🐛 Add a tokens source icon to libraries section
* 🐛 Fix ellipsis on library names
* ♻️ Remove code under flag on legacy component
* 🐛 Fix token theme name on inspect tab
* 🎉 Add changes notification (#11476)
* 🎉 Add changes notification
* ♻️ Change fn names
* 🐛 Fix tokens source label truncation and missing translations (#11533)
* 🐛 Fix tokens source label truncation and missing translations
The tokens source file name always showed, even for the current file,
and long names wrapped onto a second line instead of truncating
because the header used flex-wrap and overflow-wrap: break-word
instead of single-line ellipsis.
Show the source row unconditionally (it now displays "This file" when
the source is the current file, matching the connected-library case),
truncate the file name to one line with an ellipsis, and only attach a
tooltip with the full name when the text is actually truncated.
Replace the hardcoded UI strings with translated ones and add their
English and Spanish entries.
AI-assisted-by: claude-sonnet-5
* 🐛 Remove redundant effect dependency in tokens source
file-name-truncated? was listed as a dependency of the with-effect
that checks and observes label truncation, even though it isn't
read inside the effect body. Since the effect itself flips that
state via check-file-name-truncated, including it as a dependency
caused the ResizeObserver to be needlessly disconnected and
reconnected on every truncation change.
AI-assisted-by: claude-sonnet-5
* 🐛 Fix small visual error
* 🐛 Fix problem with plugins
* 🐛 Fix playwright tests
---------
Co-authored-by: Eva Marco <evamarcod@gmail.com>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* ⚡ Improve performance and fix orphan detection in validate-file
- Add `*ref-shape-cache*` dynamic var to memoize `find-ref-shape`
lookups per page, avoiding repeated O(depth) ancestor walks.
- Add `*children-sets*` pre-computed maps for O(1) parent-child
containment checks, replacing linear `some` scans.
- Short-circuit `inside-component-main?` when the shape context
already implies a main component.
- Use single-pass reduce with early exit for duplicate detection
(children, swap slots) instead of count/distinct or frequencies.
- Guard `check-missing-slot` to skip expensive `find-near-match`
when the shape already has a swap slot.
- Refactor variant-set validation to use `run!` with direct `get`.
- Refactor `check-ref-cycles` to use a single `reduce-kv` pass.
- Fix `get-orphan-shapes`: the original `map` pipeline produced
nils so orphan shapes were never validated; rewrite with
`reduce-kv` for correct results.
- Add `validate-file-affected!` for change-scoped validation,
replacing full file validation in `process-changes-and-validate`
to only validate pages and components touched by the changes.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Improved validation
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Steps to reproduce: paste an SVG authored in Inkscape (or any editor
that follows the inkscape:label convention) into a penpot file. The
group/element names visible in the source editor are dropped — penpot
shows generic auto-ids like 'g1234' or 'path5678' instead.
Root cause: parse-svg-element in common/src/app/common/files/shapes_
builder.cljc derived the shape name from (or (:id attrs) (tag->name
tag)). Inkscape stores user-given element labels in the inkscape:label
and sodipodi:label namespaced attributes while id holds an auto-
generated technical id, so the operator's chosen name was always
overridden by the technical id when present.
tubax/xml->clj (the SVG parser the import pipeline already uses for
upload, paste, and library import) keeps namespaced attributes as
:prefix:name keywords — the same shape this file already reads
:xlink:href from on line 134, and that app.common.svg uses for the
xlink: namespace at lines 300-307.
Fix: extract the name-resolution logic into a public resolve-element-
name helper that prefers :inkscape:label, then :sodipodi:label, then
:id, then (tag->name tag). Existing SVGs that don't carry either label
namespace fall through the same chain as before, so the behaviour for
non-Inkscape-authored SVGs is unchanged.
This restores the behaviour dfelinto's penpot-icon-generator-plugin
relies on (linked from the issue body) — that plugin reads element
names from the imported SVG to map Blender icons to penpot components.
Tests: 6 deftest blocks in common/test/common_tests/files/shapes_
builder_test.cljc covering the priority order (inkscape > sodipodi >
id > tag), each fallback in isolation, and the empty-attrs case.
Registered in common-tests.runner.
Closes#7869
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>
Return nil from get-prev-sibling when the shape is no longer present in
the parent ordering so delete undo generation falls back to index-based
restore instead of crashing on invalid vector access.
* 🎉 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>
* ✨ Update function implementation
* ✨ Add tests for a new function implementation
* ✨ Update function usages according to new signature
* ✨ Update docstrings
* ✨ Use simpler assertion
* 💄 Replace concat with cons on name-seq
* 🐛 Fix incorrect error handling on legacy workspace redirect
---------
Co-authored-by: Andrey Fedorov <oran9e.red@gmail.com>