Four fixes to how `digits` tate-chu-yoko composites lay out in vertical
text:
- A lone digit combines too, as CSS `digits <n>` combines runs of n or
fewer, so it stands upright instead of turning sideways. The renderer
(`split_digit_runs`) and the export path (`digit-combine-segments`)
share the rule.
- Composites centre on the OS/2 typographic metrics, like upright glyphs.
The hhea ascent of CJK faces exceeds the em, which set digits about
0.07em below the kanji beside them.
- Letter-spacing stays outside the composite, as in CSS. Counting the
trailing spacing in its width moved the digits left by half of it.
- The regional Chinese profiles no longer add the Han/Western gap around
a composite: it sets in one ideograph's cell, as JLREQ already does.
AI-assisted-by: claude-opus-5-5
Styling a range splits spans at its ends, and nothing joined them back, so
a text kept invisible boundaries between spans with equal styles. Two CJK
features depend on those boundaries: tate-chu-yoko `all` combines per
span, so "2" + "027" rendered as two upright blocks, and a ruby reading or
warichu note over a range that crossed a hidden boundary was rejected.
`merge-equal-spans` joins neighbouring spans of a paragraph whose styles
match, counting a missing attr as its default value. Spans with a reading
or a warichu note are never joined: each one is its own group. Both places
that split spans use it: `update-text-range-attrs` (plugin API) and
`apply-attrs-to-paragraph` (WASM editor styling).
AI-assisted-by: claude-opus-5-5
Add distributed alignment to text styles, sidebar controls, CSS export,
and the Plugin API. Spread characters across horizontal and vertical
lines, including the final line, while preserving graphemes and annotations.
Keep fractional line heights consistent across painting, hit testing,
selection, and text paths to prevent baseline drift between lines.
Add tests for distribution, nonbreaking text, and fractional line geometry.
AI-assisted-by: gpt-6
Add typed one-character Zhuyin readings with upright right-side symbols
and separate neutral and non-neutral tones in both writing modes.
Persist the mode through the editor, WASM bridge and Plugin API. Preserve
annotation clearance and canvas, PNG, PDF and SVG geometry.
Validate syntax and selection boundaries, and cover schema, bridge,
rendering, UI and API behavior. Native IME and reader review remain open.
AI-assisted-by: gpt-6
Carry optional span positions through the schema, WASM header, editor,
Plugin API and CSS output. Expose Chinese controls with language defaults
and independent overrides for horizontal and vertical text.
Reserve automatic clearance per side and stack underlines, ruby and
emphasis. Preserve reading groups during decoration edits and retain
canonical position values in DOM editor style data.
Add protocol, geometry, editing and API tests, including the prior Japanese
mixed-font gutter behavior. Verify both WASM targets and live exports.
AI-assisted-by: GPT-6
Add named Chinese punctuation presets and a Korean keep-words mode for
horizontal and vertical layout. Preserve the saved spaces-only mode and
keep source offsets stable through safe emergency wrapping.
Apply bounded CJK spacing and justification, expose the new choices in the
sidebar, and document preset bounds and remaining release checks.
Verify renderer, frontend, common and exporter tests, both WASM builds,
and sample save/reload plus PNG, PDF and SVG exports. Existing CI failures
remain documented in the layout notes.
AI-assisted-by: GPT-6
Add independent Japanese, Chinese and Korean file switches and all-files
preferences under the shared vertical-text-layout feature flag. Keep
control availability separate from saved paragraph profiles and rendering.
Expose explicit language profiles in separate sidebar sections. Preserve
mixed values, report the active paragraph profile at the caret, and keep
shared writing-mode controls consistent without radio group collisions.
Validation: common JVM/JS and frontend suites, backend profile tests,
exporter tests and lint pass. Existing runner formatting and Spanish
translation QA issues remain unchanged.
AI-assisted-by: GPT-6
Choose paragraph layout rules from an optional language tag. Pass language
to horizontal and vertical shaping, add basic Chinese punctuation rules,
and add experimental Korean spaces-only wrapping.
Keep text boundaries grapheme-safe and preserve paragraph profiles across
editor splits and merges. Add reproducible font fixtures for localized
glyphs and regression tests for the shared WASM protocol and layout paths.
Validation: renderer, common JVM/JS, frontend and exporter tests pass.
Both WASM targets build. Full CI still reports baseline lint and format
errors and one backend multipart-upload test failure.
AI-assisted-by: GPT-6
Enable Japanese layout per file through undoable changes, or for all
files through a saved profile preference. Show the controls when the
WASM renderer and either setting are active.
Add writing mode, orientation, ruby, emphasis, warichu and line fitting
controls with icons and English and Japanese translations. Cover the
file change, profile preference and control behavior.
Add schemas, defaults and helpers for vertical writing, ruby, emphasis
and warichu. Keep readings and notes tied to their own characters when
copying styles, and preserve base text when pasting HTML ruby.
Cover the model, property copying, clipboard and library file builder.
* 🐛 Fix legacy path migrations on content without coordinates
PRE still returned 500 on get-team-shared-files after #12357, now with
"expected rect instance" in legacy-6. When a path has no points, legacy-6
rebuilds its rect from the content, but content whose points are not
numbers (NaN or infinite coordinates, segments without params, only a
close path) gives no rect either, and rect->points rejects nil.
Fall back to the empty rect in legacy-6. A path with segments missing
coordinates then failed later, in 0003-convert-path-content-v2, since the
binary path data can not store them; drop those segments there.
Relates to #12343
AI-assisted-by: claude-opus-5-5
* 🐛 Skip shared files that fail to load in get-team-shared-files
get-team-shared-files loads and migrates every shared file of the team,
so one file whose migration fails made the whole request return 500 and
the "Add library" modal could not open.
Catch the error per file, log it with the file and team ids, and leave
that file out of the list. The other libraries still show, and the error
report names the broken file.
Closes#12343
AI-assisted-by: claude-opus-5-5
Noto Sans Symbols lacks most arrows and math operators. Fall back to
Noto Sans Math for them, and to Noto Sans for letterlike symbols.
Closes#12469
AI-assisted-by: claude-opus-5-5
get-initials used an ASCII-only [a-zA-Z] match, so names like Ábaco
and Órbita skipped the leading accented letter. Match Unicode letters
with \p{L} (CLJS RegExp + u/g flags) so org default avatars match the
expected first letter. Closes#12324.
AI-assisted-by: Cursor <cursoragent@cursor.com>
With the SVG renderer, resizing a shape with an image fill and the mod
key showed the picture scaled during the drag, as in a normal resize.
The crop only showed after releasing the mouse.
The SVG preview applies the resize matrix to the shape node and ignores
the fill transform change carried by the modifiers. Now the pictures of
the shape fill patterns get a matrix that takes them from the old fill
transform to the new one, which undoes that resize and keeps them in
place. This works for shapes and boards.
Closes#12349
AI-assisted-by: claude-opus-5-5
* 🐛 Fix token tree collapsing repeated path segments
Tokens whose path contains the same segment name twice in a row
(e.g. test.pointer.pointer) collapsed to a single level in the
token tree.
build-tree-node marked a node as a leaf when the current segment
name matched the first segment of every remaining path. Because
group-by-first-segment rewrites nested paths into the segment
name, a still-nested remaining path was indistinguishable from a
token ending at the current node, so test.pointer.pointer merged
into one leaf.
Mark rewritten segments with an internal ::remaining? marker and
strip it when the rest-path is fully consumed; is-leaf? only
accepts unmarked remaining segments as leaves.
Closes#11588
AI-assisted-by: glm-5.3-flash
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Preserve ancestor tokens in path trees
Keep a node's token and descendants independently when imported paths
contain both an ancestor and a child. Render both in the token tree and
include the ancestor token in existing path-based group operations.
Cover accepted same-set imports, leaf IDs, repeated segments, and internal
marker removal without changing import or collision validation.
AI-assisted-by: GPT-6
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* ♻️ Rework token tree path building
Carry the segments left to descend as a ::remaining-path vector on each
segment and advance it by subvec, instead of rewriting the segment :name
and re-splitting it later.
With that, build-tree-node finds the leaf token with a predicate over
::remaining-path and takes children from the segments that still have
one, so the pre-grouping children-first sort no longer changes the
result. Drop sort-children-first and its segment-has-children? helper,
and document inside-path?.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix and relocate the token tree tests
The ancestor-and-descendant test called a function that does not exist
in app.common.types.tokens-lib, so the namespace failed to load and the
whole common suite errored. Use make-tokens-status-from-lib and the
two-argument get-tokens-in-active-sets instead.
Move the build-tree-root tests to path-names-tree-test so
path-names-test stays a pure split/join unit test.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Refactor the token tree rendering and share path helpers
Extract the token pill into a leaf-node* component and treat a node as a
leaf only when it holds a token and has no children, so a node can carry
both a token and its children.
Make token-separator public and reuse it from the token tree and the
management panel instead of local copies. Both management path filters
inlined the same ancestor-or-descendant rule; extract it as
token-in-path? and cover it with a unit test.
AI-assisted-by: deepseek-v4.1-flash
---------
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: luisddm <luis.dedios@kaieldos.net>
* 🐛 Fix silent failure when uploading custom fonts
Custom font upload crashed with re-seq must match
against a string when opentype.js returned nil
subfamily names, leaving the user with no feedback.
Unknown signatures and unparseable files were also
silently dropped.
Make variant parsing nil-safe, harden entry
preparation for missing tables, report every
per-file failure with a bad-font notification, and
show a generic error if the stream itself fails.
Closes#12304
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename font mtype helper and mark helpers private
Avoid confusion with app.worker.import/parse-mtype,
which detects binfile signatures. The font helper now
lives as parse-font-mtype, and the upload helpers only
used internally are private, with tests reaching them
through var references.
AI-assisted-by: muse-spark-1.3-contributor
* 💄 Fix formatting in font upload tests
Realign map keys shifted by the var-quote prefix so
cljfmt check passes again. No behavior change.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix infer warnings in fonts metrics code
Add missing ^js type hints to the hhea and os2 metric lookups in
prepare-font so shadow-cljs can infer the interop targets and the
release build reports zero warnings.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Ensure normalized names in components and variants
We have a rule of that shape names that include / separator should have
it normalized, with one space before and one after. But this rule is
being enforced only in the UI and the plugins API. There may be other
ways of creating shapes that does not follow it.
For normal shapes this is not really a problem. But for main instances
it may cause that the name of the shape does not match the joined
path+name of the component, and this may generate some problems.
We have decided to add a migration to fix this in all main instances and
variant containers and enforce it in the future with integrity
validations (normal shapes may be left as is).
* 🐛 Fix bad variant-id in the component and the main instance
* 🔧 Fix some issues detected in PR review
* 🐛 Add nil guard
* ✨ Handle tokens libraries when exporting .penpot files
Exporting a file that takes its tokens from an external library left
the :tokens-source of the file pointing at a library that is not in the
export, and the tokens lib of that library was not exported.
With :detach-libraries, unset the :tokens-source and the tokens status
in detach-external-references. The file is exported without any link to
the library, and the shapes keep their applied tokens.
With :merge-libraries, add cfo/absorb-tokens-lib, which copies the tokens
lib of a library into a file and unsets its :tokens-source, so the file
becomes its own tokens source. The tokens status of the file is kept; if
it has none, the one of the library is copied. If the library has no
tokens lib, only the source is unset. embed-assets calls it for the
library that is the tokens source of the file, and ignores the other
linked libraries.
Add unit tests for absorb-tokens-lib, and binfile tests that export a
file with tokens from an external library with both modes, checking the
exported files, the tokens lib, the tokens source and the applied
tokens.
Also reformat the export-type docstring in binfile v3 and remove a
leftover debug prn in the export dialog.
AI-assisted-by: claude-sonnet-5-5
* 🐛 Preserve active token sets when importing linked libraries
Cache each decoded tokens library for the duration of an import so files
and their tokens source resolve active sets against the same generated IDs.
Add a round-trip regression test for exports that include linked libraries.
* 🐛 Preserve tokens when deleting or unpublishing libraries
Absorb the tokens library into files that use it as their tokens source.
Keep referencing file IDs in the deletion task before removing relations.
Embed tokens from deleted, unlinked sources during export, and clear stale
source and status data when the library no longer exists. Add regression
tests for deletion, unpublishing, and exports with unavailable libraries.
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
With the mod key, resizing a shape with an image fill crops it: the
shape is a window over a picture that keeps its size and position.
When the dragged edge crossed the opposite one, the crop mode turned
off and a normal resize with a negative scale flipped the shape. The
image transform is relative to the shape, so the picture was mirrored
and shrank a bit more on every crossing until it disappeared.
Keep the crop mode when the edge crosses: the window moves to the
other side of the fixed edge without flipping, the part outside the
picture shows empty, and moving the window back shows the picture
again. The crop math lives in app.common.geom.image-crop and replaces
the image transform code that was duplicated for fills and fill-image.
Closes#12349
AI-assisted-by: claude-opus-5-5
The legacy-6 migration read the rect of each path from its points and
checked whether the width or height was close to zero. A path with
missing or empty points has no rect, so the check got nil and threw a
NullPointerException. The migration then failed on every load of that
file, and get-team-shared-files returned 500 for the whole team, so the
"Add library" modal could not open.
Treat a missing rect like a degenerate one and rebuild the selrect and
points from the path content.
Closes#12343
AI-assisted-by: claude-opus-5-5
* 🐛 Stop the export-shapes modal event from dumping all exports
The `:export-shapes` modal event carried the full `:exports` vector,
each export embedding its shape (path data, geometry, text content).
One event reached 1.7 MB, which made the audit archive cron POST a
chunk past the proxy body limit and wedge the queue.
Build a compact body for that modal instead: total exports, distinct
shapes and one count per export type, plus the page name. Other modals
keep their props unchanged.
Move the modal `:origin` prop to `::ev/origin` so `make-proto-event`
records it on the event context as `:event-origin`; the UI still reads
the plain prop from the raw state.
Add tests for the summary and for the modal origin.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Bound exception messages in reports and audit events
A frontend crash whose message embeds a whole payload (a serialized
file) produced a 9.4 MB audit event: the message was repeated in the
`:hint` prop, in the report context and in the stack trace. A single
event that big wedged the audit archive queue.
Bound exception messages at the source: `get-hint` clamps to
`max-message-length` (1000 chars), and the CLJS `format-throwable`
clamps the message line of the stack trace and honors `:trace-length`
(parity with the CLJ version). `report-context` now prints `get-hint`
instead of the raw `:hint` from `ex-data`.
Fix `first-line` on the JVM: `cuerdas/index-of` returns nil when the
substring is missing and `(pos? nil)` throws there (it is false in
CLJS), so single-line messages crashed the backend path.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Detect and repair children missing from their parent's list
A shape could point to an existing parent that did not list it in its
:shapes. The validator missed it, since it only walks the tree from
the root plus the shapes whose parent does not exist. Operations that
walk :shapes missed it too: deleting a variant also deleted its
container, which looked empty, and left the other variants without a
parent, so the backend rejected the change and the file stopped
saving.
- get-orphan-shapes also returns shapes not listed by their parent,
so :child-not-in-parent is reported.
- The :child-not-in-parent repair adds every unlisted child of the
parent. All repairs share the same file data and each one sets the
whole :shapes, so with several children only the last one was kept.
- New migration 0032-fix-children-not-in-parent repairs the affected
files when they are opened.
Closes#11758
AI-assisted-by: claude-opus-5-5
* 🐛 List shapes moved to root by the parent-not-found repair
The :parent-not-found repair set the shape's parent to the root but
did not add it to the root's :shapes, so it left the shape in the
child-not-in-parent state that the validator now reports. It now
adds every shape without parent to the root at once, since all
repairs are built from the same file data and each one sets the
whole :shapes.
Also cover the missing parent case in the repair tests.
AI-assisted-by: claude-opus-5-5
* ♻️ Simplify unlisted children validation and repair
Address review comments on the validation and repair of shapes that
their parent does not list.
- get-orphan-shapes no longer rebuilds *children-sets*: both callers
bind it. The lookup moves to a listed-in-parent? helper shared with
check-parent-children, which scans :shapes when the var is unbound.
- The :child-not-in-parent and :parent-not-found repairs no longer
walk every shape of the page. Each one moves only its own shape with
pcb/change-parent. The :mov-objects change adds the shape to the
parent's :shapes when applied, so several repairs on one parent no
longer overwrite each other.
- The :child-not-in-parent repair test uses plain shapes instead of
variants.
AI-assisted-by: claude-opus-5-5
* ✨ Add stroke side-width helpers to common types
Add set-width-to-all-sides, set-width-to-single-side and side-width to
app.common.types.stroke, mirroring the border-radius helpers in
app.common.types.shape.radius. They back the per-side stroke width API
and are covered by unit tests.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Round-trip per-side stroke widths through the plugin API
Add strokeWidthTop/Right/Bottom/Left to format-stroke and parse-stroke so
reading shape.strokes exposes the per-side widths and writing a stroke
back preserves them instead of collapsing to the uniform width.
parse-stroke prefers a live stroke proxy's hidden raw state when present,
so a commit only rewrites the attributes the plugin touched.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add per-side stroke widths to the plugin API types
Add the four per-side members to the Stroke interface and to the
borderWidth and dimension token property lists, and refresh the generated
API surface used by the plugin test suite.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Expose per-side stroke width on the stroke proxy
Add strokeWidthTop/Right/Bottom/Left to StrokeProxy. Reads fall back to
the uniform strokeWidth; strokeWidth now writes all four sides, like
borderRadius writes all four corners; each side setter writes only its
own key and the top side also updates strokeWidth.
Reject per-side writes on shapes other than boards and rectangles with a
validation error naming the property, and expose the raw stroke map
through a hidden proxy member so a commit preserves untouched attributes.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Support stroke width tokens per side in the plugin API
Token attribute expansion now accepts the uniform strokeWidth property and
expands it to the four per-side attributes, mirroring the design tab.
shape.tokens exposes the per-side keys and reports strokeWidth when all
four sides share one token, and applyToken routes through the shared
expander.
AI-assisted-by: deepseek-v4.1-flash
* 📚 Document per-side stroke width in the plugins changelog
Note the new Stroke members, the restored strokeWidth token property and
the shape-type validation in plugins/CHANGELOG.md.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix some minor issues
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
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
Stroke width tokens used to be applied to the single `:stroke-width`
token attribute. Applying tokens per side replaced it with the four
`:stroke-width-*` attributes, but no migration moved the existing
references, so files written before 2.19 look unapplied: the token
pill stays off, the Design tab shows a plain number and propagation
finds no update action.
Add migration 0031, which copies the legacy reference to every side,
keeps any side reference already present and drops the old key. It
covers page and component shapes.
Closes#12016
AI-assisted-by: deepseek-v4.1-flash
Adding a flex or grid layout to a board with children derives the
paddings from the distance between the board and its children. A child
that overflows the board, or floating point noise after "Resize board
to fit content", gave negative paddings. The shape schema rejects them
since paddings must be non-negative, so update-file failed and autosave
stopped. Clamp the derived paddings to 0, as the derived gaps already
are.
Closes#12107
AI-assisted-by: claude-opus-5-5
* 🐛 Sanitize smuggled SVG on binfile import
Route imported SVG storage objects through the SVG sanitizer on
all three binfile paths (v3, v1, v2), closing the second bypass
of GHSA-ffhp-m958-qxvr. Integrity checks still run on the raw
bundle bytes first; only the persisted copy is the sanitized one.
Adds a shared sanitize-imported-svg helper with unit tests and a
v3 export-tamper-import test proving smuggled scripts no longer
survive the import.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Normalize SVG content-type on binfile import
Close the residual GHSA-ffhp-m958-qxvr bypass where a crafted
content-type spelling (uppercase, parameters) skipped the import
sanitizer. Detection now uses a shared case-insensitive predicate,
stored values are canonicalized, and the v3 re-read honors the
import size limit.
Extends the tamper tests to obfuscated spellings and adds
exhaustive v1 coverage, including the tempfile branch.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Gate binfile content-type on known storage types
Define the complete set of storage content-types in
app.common.media and enforce it on the binfile import schema, so
unknown types fail closed instead of passing through. Detection
keeps a single normalization at the boundary with an exact
predicate, and the string helpers use cuerdas.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Keep only produced types in storage-object-types
Drop apng, avif, penpot and plain text from the set: no flow
stores them, they only exist in the extension mapping table.
Every member must have a producing code path; unproduced types
stay out until some flow actually stores them.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Reject unknown content-type on v1 binfile import
Enforce the storage content-type allowlist on the v1 import
path, like v3 already does via schema. Bundles declaring types
outside the set, or none at all, fail closed with the same
media-type-not-allowed error as uploads. The sanitize branch is
explicit: SVG bytes are sanitized, anything else passes through.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Keep interactions of flex children when syncing components
The flex-child sync between main and copy children propagated every
attribute in swap-keep-attrs, which includes :interactions. Since
:interactions has no sync group, the touched check never protected it,
so any change that reflowed a flex component (like toggling a child's
visibility) overwrote the copy children's interactions with the main's.
Closes#11746
AI-assisted-by: claude-opus-5
Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
* 🐛 Add another test and simplify comments
---------
Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Kaocha finds every test namespace on the JVM, but the JS runner only
runs the ones listed in runner.cljc. Sixteen namespaces were missing
from both lists, and files.shapes-builder-test was required but not
listed, so 125 tests never ran in JS. Add them to both lists, sorted.
The only one that failed in JS was make-tokens-status-from-tokens-lib:
(.sets base-lib) is a method call in ClojureScript. On the JVM it read
the field, but (:sets ...) on the result gave nil, so the legacy lib
was built without sets or themes. Use (.-sets base-lib) and
(.-themes base-lib), as the first part of the same test does.
AI-assisted-by: claude-opus-5-5
* ✨ Add size limits to profile props and plugin registry
Bound the total serialized size of profile settings to 2 MiB
(:profile-props-max-size), checked on the merged result before
persisting, with a controlled :props-too-large error. Profiles
that already exceed the limit can still shrink but cannot grow.
Cap plugin registry entries in the shared schema (code 1 MiB, 50
plugins max, bounded name/host/description/icon) and restore rate
limiting on the plugin RPCs (profile-mutations bucket, one write
at a time per profile). The plugin manager now asks for
confirmation before removal and ignores repeated clicks while a
persist request is in flight.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Enforce plugin count cap, byte sizes and removal guard
Enforce the declared 50-plugin cap in add-profile-plugin with a
specific :too-many-plugins error (updates of existing entries
still pass); the cap lives in a shared max-plugins constant.
Measure profile props size in UTF-8 bytes instead of chars so
multibyte content cannot slip past the limit.
Cover install/remove persist logic with mocked-RPC frontend tests
(release semantics, in-flight dedupe, validation vs rollback
split) and add the missing boundary tests in common.
Expose the in-flight persist set from the plugin registry and
disable the remove button of entries being saved.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix rollback loops and restore paths in plugin registry
Restore the previous plugin version instead of dropping the entry
when a validation error rejects an update of an installed plugin.
Make compensating writes one-shot with terminal callbacks so a
persistent failure cannot ping-pong between install and remove.
Restores keep the original list position; the unused public
plugin-persisting? predicate is removed.
Pin count-before-size precedence with a dedicated test and fix
translation source refs to their canonical lines.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Guard notifications write and fix restore ordering
Route update-profile-notifications through check-props-size! so
oversized profiles cannot grow through that path; document the
exempt system writers. Remove the duplicated stale entries in
en.po, keeping the canonical translation refs.
Restore rejected plugin updates at their original list position
instead of leaving the optimistic move in place.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Skip no-op plugin removal and clarify size comments
Return early from remove-profile-plugin when the id is absent:
no wasted write, no size check, and no manufactured :plugins key
that could spuriously fail on oversized profiles.
Clarify that per-field string caps count chars while the byte
budget is enforced by profile-props-max-size.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 📎 Fix formatting in rlimit.edn for profile operations
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 📎 Fix formatting of import-binfile/global entry
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ♻️ Simplify props size check and tighten plugin entry caps
Measure props with transit bytes directly instead of the
PGobject string roundtrip.
Rename check-props-size! to check-props-size: single hard limit
on the merged props, no growth comparison, and return props so
writers thread the check into the update.
Move the 2 MiB default into default-props-max-size on the
profile namespace, still overridable with the optional
:profile-props-max-size config entry.
Tighten registry-entry :code and :icon to 500 chars: they hold
manifest paths, not content.
Closes#11592
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix compatibility problems
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* 🐛 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
* 🐛 Stop browser logging from crashing on empty levels
Stop level->int crashes from taking down the dashboard when a
nil or unknown level reaches the browser logger. Invalid levels
now warn and are ignored in enabled?, setup! and the console
handler, which renders unknown records with a neutral fallback.
Alias the schema-legal :fatal level to :error in the browser
mappings and validate the JS-exported debug.set_logging, which
previously threw on missing arguments and wrote unreachable
keyword keys into the loggers map.
Closes#11690
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Guard logger args and strengthen logging tests
Close the residual throw paths next to the empty-level crash:
guard non-string loggers in enabled? and setup!, coerce
set_logging arguments safely, and validate logger keys.
Strengthen the regression tests so the fatal alias cannot
regress silently: enabled-logger filtering, JVM fatal and bogus
cases, setup! skip proof, and invalid-logger cases.
Related to #11690
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Address low findings from logging review
Validate the logger before the level in console-log-handler,
share a public valid-logger? predicate with debug/set-logging,
and keep warn formatting consistent across boundaries.
Document the fail-soft-FE/strict-BE split on enabled? and the
valid-level? contract on set-level!. Cover safe fallbacks, bad
logger keys, handler logger skips, and loggers-map isolation in
common tests, and add a frontend test for debug/set-logging.
Related to #11690
AI-assisted-by: muse-spark-1.3-contributor
Individual stroke widths shipped with a `:stroke-per-side` boolean on every
stroke, telling the renderer and the CSS generator whether the four
per-side widths were meaningful. Comparing the sides answers that on its
own, so the attribute was later dropped from the closed stroke schema
and the toggle became ephemeral editor state.
Files written between those two changes still carry the attribute, and
the closed schema rejects the unknown key, so loading such a file fails
`check-file-data` with a `:malli.core/extra-key` error and surfaces as
an internal error in the editor.
Add migration `0030-remove-stroke-per-side-attr`, which drops the
attribute from every stroke of every page and component shape. The
per-side widths are the saved design data and are kept untouched, so a
file whose boolean was `true` renders exactly as before.
The migration is naturally idempotent and a no-op for files that never
carried the attribute, since `dissoc` on a map without the key returns
an equal map.
Cover it with tests for the schema rejection, the per-side and global
widths surviving, component shapes, idempotency, and the run through
`migrate-file`.
Closes#11943
AI-assisted-by: space-bunny-free
* ✨ 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