Cover the callers of the variant generators that had no tests with the
new validation: restoring a deleted variant into its container, restoring
it after its container was deleted, and adding a property and then
setting its value, as the workspace properties panel does. Each case
runs with valid data and expects no validation error on JS.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When generate-relocate moves a variant inside a component, the variant
is no longer a root, so relocate left it out and validated nothing in
its place. Validate the root of the enclosing component instead. The
workspace does not allow this move, but relocate can be called
directly.
Document that the default call of generate-make-shapes-no-variant
requires the shapes to be outside their variant container.
Add tests for moving a variant into another container, and into a
component with a corrupted root.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validating a variant container already checks each of its main
instances, so listing the mains next to it repeated the work. Each id
costs a pass over the whole page, so a variant with N components cost
N+1 passes per call, and each error was reported twice.
The generators in variant_properties now validate only the container.
generate-update-property-value and generate-set-variant-error fall back
to the main instance when the component is not a variant. Before, they
emitted a nil id, which fails the :validate-shapes schema.
Add a test that generate-set-variant-error on a component that is not a
variant produces valid changes.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
duplicate-variant calls generate-make-shapes-variant before it moves the
new shape into the target container. When a variant is pasted into a
different variant container, the new shape already has the target
container's data but still sits in the old one, so the validation in
generate-make-shapes-variant reported errors on valid data.
Pass :skip-validation? to generate-make-shapes-variant, and validate the
target container once the shape is inside it. That also covers the
pasted shape, which the closing validation of
generate-duplicate-changes missed.
Add tests for pasting a variant into another container and for
duplicating it inside the same container.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-make-shapes-variant now validates the new variants and their
variant container, and accepts a :skip-validation? option.
generate-relocate calls it before the shapes are inside the container,
so it passes :skip-validation?. Relocate already validates the new
container once the shapes are moved.
Add tests that corrupt the new variant or the container and expect a
validation error, and a test for :skip-validation?.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-make-shapes-no-variant now validates the shapes and their old
variant containers, and accepts a :skip-validation? option.
generate-relocate calls it before moving the shapes, while they are
still inside the container. Validating the container at that point
reported :main-instance-not-a-variant on valid data. Relocate now passes
:skip-validation? and validates at the end: the old and new variant
containers, and the moved variants when they end up outside any
component. Inside a component they are no longer roots, and the
validator can't check them on their own.
Add tests for the function and for moving a variant out of its
container, with valid and corrupted data.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-add-new-property now validates the main instances and the
variant container. Add a :skip-validation? option to it.
generate-make-shapes-variant calls it several times before the new
shapes become variants. Validating the container at that point reported
:main-instance-not-a-variant on valid data, so it now passes
:skip-validation? and will validate once the shapes are variants.
Add tests that corrupt a main instance or the container and expect a
validation error. Add a test that moves into a variant a component whose
name needs more properties, which runs that intermediate path. Register
variants-test in the runner so that it also runs on JS.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-reorder-variant-poperties now validates the main instances and
the variant container, but only when the reorder changed something, so
a no-op still returns the same changes. Add tests that corrupt a main
instance or the container and expect a validation error.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-set-variant-error now validates the main instance of the
component and its variant container. The validator has no rule for
:variant-error itself, so the new tests corrupt the main instance or the
container and expect a validation error.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-update-property-value now validates the main instance of the
component and its variant container once the new value is set. Add tests
that corrupt the main instance or the container and expect a
validation error.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-instantiate-component validated the new instance on its own.
When the parent is inside another component, the new instance is a
nested copy. The validator starts with a :not-component context, so it
reported :nested-copy-not-allowed. Validate the root of the enclosing
component instead, which walks the new copy with the right context.
During a component swap, generate-new-shape-for-swap sets the swap slot
after the instance is created. Validating at instantiation time then
reported :missing-slot. Add a :skip-validation? option to
generate-instantiate-component. The swap sets it and validates the
enclosing root once the swap slot is in place.
These false errors broke 34 common tests on JS.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply-changes-local replays the pending redo changes on a local copy of
the data, so that later builder calls can read up-to-date values. It
replayed :validate-shapes too, so the validator ran on that copy, which
has no real file id and no libraries. Any chain of two generators that
apply local changes then failed with false errors, such as
:component-not-found. Skip :validate-shapes when applying changes
locally.
Add the variant container to the shapes that
generate-update-property-name and generate-remove-property validate.
Validating only the main instances misses errors that only the container
check finds, such as siblings with different property names.
Register variant-properties-test in the runner so that it runs on JS,
where validation takes place. Add tests that corrupt a main instance or
the container and expect a validation error. Remove two debug prn calls
from the validator.
AI-assisted-by: claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ✨ Show color tokens toggle on canvas background picker
* 🐛 Apply token click on canvas background with no shape selected
* ✨ Apply and persist color tokens on canvas background
* 🌐 Translate canvas background section label
* 🐛 Fix shadows derived from imported SVG filters
* 🐛 Fix inherited group fills in exports and group drop shadows
* 🔧 Run exporter tests on render-wasm changes
* ♻️ Derive per-side stroke widths from the side values
The per-stroke `:stroke-per-side` boolean only gated whether the
renderer looked at the four side widths, and the CSS generator used it
to decide whether to emit one `border-width` or four. Comparing the
sides is enough, so drop the attribute from the shape schema and from
the stroke attribute list.
The WASM property and upload bridges and `stroke-per-side-widths` now
derive the per-side widths from the values alone.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Declare per-side stroke width token attributes
Replace the single `:stroke-width` token attribute with
`:stroke-width-top`, `:stroke-width-right`, `:stroke-width-bottom`
and `:stroke-width-left`, add `per-side-stroke-width-keys`, and map
the new attributes to the strokes shape attribute and to the
dimensions token type.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Unapply only the token of the changed stroke side
A stroke change that reports a single per-side sub-attribute now
resolves to that side's token only. A plain `:stroke-width` change
still resolves to every side, and a change with no sub-attribute
resolves to all width keys plus the color.
Add a regression test that tokens on untouched sides survive a change
to another side.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add a predicate for per-side stroke shapes
Boards and rectangles support independent stroke widths per side.
Expose `per-side-stroke-shape?` so callers can gate the per-side UI,
and cover the supported and unsupported shape types with a test.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add stroke side width materialization helper
`materialize-stroke-side-widths` concretizes the four per-side width
keys from a stroke: edited sides take the new value, the others keep
their current width (0 when there is no stroke), and `:stroke-width`
mirrors the top side for legacy consumers.
This pulls the logic already duplicated in the token apply path into a
shared helper, ready for the direct-edit path.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Apply a stroke width token to every side
`update-stroke-width` now writes the four per-side width keys, both
when the shape already has a stroke and when it gets a new default
one, so the applied-token bookkeeping matches the stroke attributes.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Apply a stroke width token to a single side
Add `update-stroke-width-side`, which changes only the sides named in
`attributes` on the first stroke of each shape. The remaining sides
keep their current width (0 when the shape had no stroke) and all side
keys are materialized through the shared helper, so consumers never
fall back to `:stroke-width`.
Route the per-side token keys to the new function and update the
apply, remap and component tests.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Complete a partially applied per-side token on toggle
When explicit attributes come from an input or a plugin call, toggle
the token off only if it already covers every target attribute on every
selected shape. A partial per-side application is completed instead of
removed. The token pill keeps the previous any-attribute behavior.
Add tests for both the completion and the full removal.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Offer per-side stroke width actions in the token menu
Boards and rectangles get a stroke width submenu with an all-sides
action and one action per side; other shapes keep the single global
action. The global action targets every per-side attribute so the
design tab keeps showing the token on each side.
Add the pill labels for the new attributes, the menu test, and the
`workspace.tokens.stroke-width` string.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Persist the per-side stroke preference
Add `:stroke-per-side` to the user profile props schema and expose it
through a derived `stroke-per-side` ref. The design tab will read the
preference from here instead of a per-stroke attribute.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add per-side stroke width helpers to the stroke menu
Add `stroke-width-all-attrs` and `per-side-stroke-available?`, which
checks the feature flag and that a single board or rectangle, or a
uniform multi-selection of them, is selected. Use it instead of the
inline shape-type check and drop the old per-side toggle handler.
Cover both helpers with a test.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add per-side stroke width token inputs to the design tab
Turn the four side width fields into token inputs with detach actions
and a `:multiple` mixed value when the sides differ. The per-side
toggle now reads the persisted profile preference instead of a
per-stroke attribute, so previous per-side edits survive.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Materialize stroke sides on direct width edit
The stroke menu per-side handler only wrote the edited side key and,
for the top side, the global `:stroke-width`. A stroke holding just
`:stroke-width` made every consumer fall back to the global value, so
editing one side changed all the others.
Add a `change-stroke-side-width` event that materializes the four side
keys through the shared helper and reports only the edited key as
changed, so tokens on untouched sides are not unapplied. Route the menu
handler through it.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Make stroke width fields non nullable
Drop `:nillable` from the global and per-side stroke width inputs and
use `:multiple` for the mixed state, so an empty field no longer
represents a null width.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix the numeric-input props schema key
The schema declared `:applied-token`, but the component body and every
caller use `:applied-token-name`, so the prop was never validated.
Rename the schema entry to match.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add token-disabled support to the numeric input
The design-system numeric input accepts `:token-disabled` and
`:token-tooltip`; the token button is disabled and shows the reason.
Scope the disabled input style to `input:disabled` so a disabled token
button no longer dims the whole field. Pass both props through the
token wrapper.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Disable token controls below the first fill or stroke
Design tokens only apply to the first fill or stroke of a shape. Add
`tokens-allowed-position?` and mark the fill and stroke lists with
`tokens-first-only`, so later entries disable their token controls and
explain why. The colorpicker opens on the direct color tab and disables
the token tab for those rows.
Cover the helper with a test and add the new translation.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Refactor colorpicker style switcher to DS radio buttons
Replace the legacy `components/radio-buttons` markup in the colorpicker
with the design system `radio-buttons*`, using its declarative options
API. Switching between direct color and token mode now passes string
values, as the DS component expects.
The previous keyword values broke the round trip back to color mode:
the DOM stringifies keywords with a leading colon, so the value never
matched `:direct-color`. Using plain strings keeps the conversion
clean.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add playwright tests
* ✨ Scope per-side stroke controls to each stroke
Give every stroke row its own expanded state instead of sharing one
profile-wide preference. The state lives in `:workspace-local`, keyed by
`[ids index]`, so it survives selecting another shape and coming back but
resets on reload.
Remove the `:stroke-per-side` profile prop and its ref. The ref now derives
from `:workspace-local`.
Update the Playwright spec to expand the controls per stroke through the
toggle, and assert that strokes toggle independently, that the state resets
on reload, and that it survives switching shapes.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Keep stroke tokens when editing or removing later strokes
The token unapply logic decided which tokens to clear from the shape
using only the changed sub-attributes, without knowing which stroke was
edited. Since stroke tokens only live on the first stroke, editing or
removing a later stroke cleared the first stroke's tokens.
Add a `:changed-item-index` option to `generate-update-shapes` and skip
unapplying fill/stroke tokens when the changed item is not the first.
The stroke color, attrs, side-width and remove events now report the
index they touch.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Ignore token shortcuts when tokens are disabled for input
The numeric input opened the token dropdown on `{` regardless of
`token-disabled?`, so inputs that cannot hold tokens (for example,
strokes after the first one) still opened it, and typing `{token}` plus
`}` could apply a token there.
Extract the key handling into `token-shortcut`, which returns nil when
tokens are disabled, and use it for both `{` and `}`.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Gate per-side stroke tokens on the WASM renderer
The token context menu offered per-side stroke width actions whenever
the feature flag was on and the shape was a board or rectangle, without
checking the renderer. The classic renderer only draws the single
`:stroke-width`, so applying a per-side token there wrote inert data,
the token pill reported it, and the stroke changed appearance when the
WASM renderer was later enabled.
Add `per-side-stroke-enabled?` (flag + WASM renderer) and use it from
both the design tab and the token context menu. Thread the renderer
flag into the context menu through `:render-wasm`.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Keep first-stroke tokens when reordering later strokes
Fill and stroke tokens only ever live on the first item of the
collection. When a stroke update arrives without a changed item
index (for example reordering the second and third strokes), the
unapply logic assumed the first item had been edited and removed
every stroke token from the shape.
Compare the first item before and after the update instead: when
no item index is given, unapply only if the first item actually
changed. Reordering later strokes now leaves the first stroke and
its tokens untouched, while moving the first stroke away still
detaches them. Explicit item edits keep their previous behavior.
AI-assisted-by: deepseek-v4-flash
Drop the separate :wasm-export flag and wasm-export/v1 feature. Single
export, clipboard PNG, plugins, and batch :is-wasm now key off
render-wasm/v1 alone. The exporter trusts :is-wasm for headless WASM
and always keeps a worker pool ready.
* ♻️ Share structural batch upload through common helper
- Merge svg-filters and svg-fills to app.common.render-wasm.svg-derived
- Add serialize-shapes-batch! in common, shared by the sync and chunked
workspace paths
- Add a routing test for the helper and wires the svg-filters test.
AI-assisted-by: muse-spark, GLM 5.3
* 🐛 Derive SVG effects inside single-shape serializer
- Single and batch paths: one svg effect derivation step
owned by shared serializers.
- set-object forwards the derived shape to its host attrs,
and the exporter reads the derived fills, so SVG-attr
fills, blur and shadow render as in the frontend.
- Adds regression test to the exporter.
AI-assisted-by: muse-spark, GLM 5.3, deepseek-flash
Drop 277 keys nothing references from en.po (verified against
frontend/src and common/src) and let sync propagate the
deletions to every locale. Clear all 10 fuzzy entries: fill
the 5 empty translations, keep the 4 valid ones, and drop the
duplicated max-quote-reached in favor of max-quota-reached
(the backend code stays, the UI maps it to the quota text).
Recover 22 used-but-missing keys with translations: the 19
shortcuts section/subsection labels plus connected-to,
pixel-grid-color and tokens.add-set. Make the rest
statically visible to rehash instead: :label fns on shortcut
commands, sections and subsections (one debug-only and one
colorpicker-local id exempt); case branches in place of
dm/str-built keys (export modal, text decoration and
transform, undo history with raw-key fallback); hoist
conditionals out of tr calls; pre-translate modal props and
role labels; replace the lone (i18n/tr ...) site with tr.
Turn static :error/code data into eager :error/fn calls in
the common schemas and the auth/password forms. Rename the
two keys containing spaces and point team leave at
max-quota-reached. Backend-driven keys stay dynamic by
design, declared with (tr ...) comments: the five
weak-password details, team and organization notifications.
Tooling: rehash also scans common/src and no longer treats
a missing -l as no locale; new clj-kondo tr-dynamic warning
flags non-literal tr args (lint scripts use --fail-level
error so it never fails CI); tr docstring states the
literal-only rule. Tests cover the shortcut label wiring,
the undo-history fallback and the :error/fn schemas.
Translations memory rewritten to match; es check word list
gains three entries.
Rebased onto develop: adopt the register field-error UX
(the weak-password declarations move onto the :options
code), keep develop's newer keys (connection-error,
account-locked, save-retrying, tokens-source strings) with
fresh references, and reword the shortcuts.cljs prose
comment so rehash does not invent a "literal" key.
AI-assisted-by: muse-spark-1.3-contributor
Before background blur got its own shape attribute, the `:blur` attribute
accepted both `:layer-blur` and `:background-blur` types, so the editor and
the plugin API could save a background blur under `:blur`. The shape schema
was later tightened to only allow `:layer-blur` on `:blur`, but no migration
moved the existing values, so those files fail server schema validation.
Add migration 0029: when a shape has a `:blur` map with `:type
:background-blur`, move it to the `:background-blur` attribute. When the
shape already has a `:background-blur`, keep it and drop the mis-typed
`:blur`. The migration walks both pages and components.
Closes#11904
AI-assisted-by: deepseek-v4.1-flash
Introduce a shared xf:add-index transducer in app.common.data that
attaches the position to each item, and cover it with unit tests.
Use it in the workspace interactions menu: the indexed interactions
list is now derived in a memoized step keyed on the interactions
prop, so it is not rebuilt when the section is collapsed or
expanded. The previous code called d/enumerate on every render.
Update the frontend UI conventions memory with the pattern and the
constraint that the transducer only works on associative items.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix error when reset overrides on a swapped copy
* 🐛 Add regression test for reset overrides inside group
Cover the case where a nested copy lives inside a group (not directly
under the instance root). After a swap, reset overrides must undo the
swap without error (#11656).
---------
Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
* ✨ Add account lockout after failed login attempts
Implement per-account brute-force protection using a Redis-backed
failed-login counter. After 5 failed attempts within 15 minutes, the
account is temporarily locked out and all login attempts (including
with the correct password) are rejected with a 429 response.
Closes#11397
AI-assisted-by: longcat-2.0
* 🐛 Bind LDAP session to directory-verified profile
The account-lockout change added a shortcut that preferred the
profile matching the typed email over the one returned by the LDAP
directory. These can differ with aliases, UPNs, or multi-valued mail
attributes, letting a user with valid LDAP credentials bind a session
to another Penpot account.
Keep the typed-email profile only for lockout checks. After LDAP
succeeds, resolve the session profile from the directory identity as
before and clear failed attempts on that profile.
AI-assisted-by: deepseek-v4.1-flash
* ⬆️ Update devenv dependencies
Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to Java 27
Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MinIO with RustFS in devenv
Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.
Keep the old MinIO volume untouched and use a new RustFS volume.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MailCatcher with persistent Mailpit
Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.
Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update Node.js to 24.21.0
Align the host NVM version with the Node.js version used by devenv.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to PostgreSQL 18.6
Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.
Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.
AI-assisted-by: gpt-5.6-sol
* 🐳 Expose RustFS ports in devenv
Publish the RustFS S3 API and management console on localhost port 9000
and 9001.
Keep both bindings on loopback so object storage is not exposed to the local
network.
AI-assisted-by: gpt-5.6-sol
* 🐳 Install standalone pnpm in devenv
Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.
Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.
AI-assisted-by: gpt-5.6-sol
* 🔥 Remove corepack, use system pnpm everywhere
Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.
Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix exporter watch missing render-wasm build step
The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.
AI-assisted-by: muse-spark-1.3-contributor
* 🔧 Add opencode V2 support and adapt plugins
Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.
Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.
AI-assisted-by: muse-spark-1.3-contributor
* ⬆️ Update pnpm and opencode
* ✨ Restrict optional RPC ids to user-provided UUIDs
Add ::sm/user-provided-uuid, backed by a version and variant
aware regex that only accepts v4, v7 and v8 instances. Use it
for the optional :id of the creation RPC commands so reserved
versions such as v3 are rejected at validation time. Reads
such as get-team keep the lax ::sm/uuid. Cover the predicate
and the schema on both JVM and JS runtimes.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Cover id version restriction at the RPC boundary
Add backend regression tests proving the seven creation commands
reject reserved-version ids (v3) with :params-validation and
accept v4 ids (plus v7/v8 on create-team) through the real
decode and validate path. Also drop two duplicated assertions
and document the version and variant of every fixture UUID in
user-provided-test.
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>
* 🐛 Reflow auto grid cells on flow direction change
Remap only single-span auto cells to the new
:layout-grid-dir traversal order, keeping source
order, manual and area placements untouched.
Update both grid direction controls to use the
new change-grid-direction event and refresh the
stale active button on persisted direction.
Add a RED-to-GREEN model regression covering a
2x2 row-to-column transition and source-order.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Keep source order on grid flow change with areas
Skip the generic grid cell pass for the
direction event, since reflowing already
places every eligible auto item and a blind
reorder rewrites shapes around pinned areas.
Pin area/span grids with a regression test
covering direction change and source order.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Scope grid skip to direction changes only
Replace the translation flag with a narrow
skip-grid-reassignment option so component
sync and reflow metadata stay intact while
the generic grid cell pass is skipped.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Clear leftover auto cells on grid flow change
Write remapped shapes to every target auto cell and
empty leftover cells so sparse grids cannot duplicate
a child across target cells. Manual, area and
spanned cells stay untouched; source order is kept.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Pin grid flow invariants with span and manual regressions
Keep the direction-change design unchanged and lock the
claimed invariants with tests: a real 2x1 manual span
cell and an occupied manual cell stay byte-identical,
row->column->row round-trips to the original cells,
and a mixed auto/manual/span/area grid shows no shape
loss or duplication. Also drop the unused page-objects
binding from the direction-change watcher.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Unoverlap mixed grid fixture and assert movement
Move auto C to (1,3) so it no longer overlaps the 2x1
manual span at (1,2). Row auto order A,C,B,E becomes
column order A,B,E,C; assert the exact placement
while keeping pinned, source-order and no-loss
checks. Test-only change.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Unify plugin dir setter and normalize missing direction
Route GridLayoutProxy.dir through change-grid-direction so the
plugin API shares the UI direction-change path with its reflow
and source-order guarantees. Normalize a missing
:layout-grid-dir to :row at the change-grid-direction entry
point and cover it with a missing-direction regression plus a
plugin setter routing regression.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Fix grid plugin dir setter syntax
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
AI-assisted-by: opencode-go/muse-spark-1.3-contributor
* 🐛 Fix comments and tests
---------
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* ✨ Add expires-in option to create-demo-profile
Allow passing an optional expires-in duration when creating a demo profile so its purge is scheduled sooner than the global deletion delay. Values below 5 minutes or above the global delay are rejected with an invalid-expires-in validation error, resolved before any profile is created.
Closes#11573
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Make duration schema decoding total instead of throwing
parse-duration returned by the duration schema decoder threw DateTimeParseException on invalid strings, escaping params validation as a raw error. It now returns the input unchanged so invalid values fail the duration predicate with a clean params-validation error. Closes#11573 AI-assisted-by: muse-spark-1.3-contributor
* 📎 Fix doc version for expires-in change entry
The expires-in change entry was documented under 2.20 but the current version is 2.18.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix nested board drop target ignoring ancestor clip bounds
Frame hit-testing (get-frame-by-position, get-frames-by-position and
top-nested-frame) only checked a candidate board's own rectangle,
without accounting for an ancestor board with clip content enabled.
A nested board wider/taller than its clipping ancestor could still be
picked as the drop target in its invisible, clipped-away area, so a
dragged shape would get reparented there and disappear from view.
Add clipped-by-ancestor? to reject a point when it falls outside the
bounds of any ancestor board that has clip content enabled, so the
lookup now stops at the correct visible ancestor instead of
descending into the hidden region.
* 🐛 Fix Ctrl+click deep-select reaching into clipped board area
The clip-aware quadtree query (query-index) filters candidate shapes
by whether they overlap every clip-parent ancestor, but the whole
filter was skipped whenever clip-children? was false. That flag is
turned off while a modifier key (Ctrl/Cmd) is held for deep/penetrate
selection, which was meant to let it reach past boolean/mask clip
boundaries, but it also disabled enforcement for board "Clip content"
ancestors, letting a modifier-held click select a shape sitting in a
board's invisible, clipped-away region.
overlaps-parent? now only relaxes the check for non-frame clip-parents
(bool shapes / mask children) when clip-children? is false; board clip
ancestors are always enforced regardless of the modifier key.
* 🎉 Implement independent image bounds resizing
Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.
AI-assisted-by: gemini-2.5-pro
* ♻️ Address reviewer feedback from elenatorro
- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer
* 🔧 Fix clippy needless borrow warnings in wasm image fills
---------
Co-authored-by: Andrey Antukh <niwi@niwi.nz>