14576 Commits

Author SHA1 Message Date
alonso.torres
8d88966fdb 🐛 Fix the mixed and plain labels of the text layout selects
The language profile and Japanese layout selects labelled mixed values
with "labels.mixed-values", a key that no longer exists, so they showed
the raw key. Use "settings.multiple", as the other text options do.

The "No language profile" choice had a blank id, which the select shows
as "--". Give it the "plain" id and map it back to no language.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:49:00 +01:00
alonso.torres
3ddc5ea688 🐛 Undo text edits from the Edit menu while editing
Edit > Undo and Redo emit the same events as the shortcuts, but the menu
takes the focus. The blurred WASM editor then ignored the restore, so the
menu did nothing while the text edition stayed open.

Focus the editor again before restoring its history.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:49:00 +01:00
alonso.torres
be6ca88ab9 🐛 Relayout text whose font is fetched for another shape
Font fetches are shared per URL, so a text that asks for a face already
being fetched gets no fetch callback. process-pending only relaid out
text after its own callbacks, so such a text kept its fallback-font
layout. Auto-grow text recovered through the reflow resize, but fixed
text stayed wrong until another change.

Wait for the text's pending faces to be stored or to fail before the
relayout, whoever started their fetch.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:49:00 +01:00
alonso.torres
1635e8c3bb 🐛 Move distributed alignment to the language layout sections
The fifth alignment button made the first row of the text options 36px
wider than the sidebar grid: the options button fell outside it and the
panel scrolled sideways.

Distributed alignment is an East Asian convention (均等割り付け, 分散对齐,
배분 정렬), so it moves out of the alignment group into a switch of the
Japanese, Chinese and Korean layout sections, shown when the text's
language matches. Turning it off aligns the text to the start. The label
shortens to fit beside the switch.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:49:00 +01:00
alonso.torres
fd2c4b2a71 ✨ Add lang to text shapes and ranges in the Plugin API
The paragraph language selects the CJK typesetting rules (line breaking,
punctuation spacing, the default emphasis position and localized glyphs),
but plugins could not read or set it. `lang` holds it as a BCP 47 tag, as
the HTML `lang` attribute does; `null` or `''` removes it.

On a text shape it applies to every paragraph and reports 'mixed' when
they differ. On a text range it applies to the whole paragraphs the range
touches: `update-text-range-attrs` now writes paragraph attrs such as the
language there instead of copying them into spans. The tag pattern moves
to `schema:text-language`, shared by the content schema and the plugin
validator.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:49:00 +01:00
alonso.torres
38f27ad3fa 🐛 Join text spans left with the same style after range styling
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
2026-11-05 17:49:00 +01:00
alonso.torres
324e476926 ✨ Add distributed text alignment and preserve fractional line spacing
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
2026-11-05 17:49:00 +01:00
alonso.torres
3e8e5eecc9 🐛 Restore selected text when composition is cancelled
Restore the pre-composition content and UTF-16 selection when an IME ends
with an empty replacement. Keep styles, selection direction and redo intact
without recording cancellation as an edit. Share snapshot restoration with
editor undo and redo.

Add a regression for styled selections and cancellation after undo. Verify
commit, cancellation, deletion and history in four saved showcase examples,
including emoji and decomposed Korean Jamo in both writing directions.

AI-assisted-by: GPT-6
2026-11-05 17:49:00 +01:00
alonso.torres
4aec341e53 📎 Cover native Hanja conversion in editor history
Exercise the Hangul-to-Hanja composition sequence through the public editor
bindings. Keep previews out of saved content and history, then restore the
styled source selection with one undo and the converted content with redo.
Cover both writing directions and Korean wrapping modes.

Verify native Fcitx Hangul conversion in all four showcase examples and
confirm the saved examples survive reopening.

AI-assisted-by: GPT-6
2026-11-05 17:49:00 +01:00
alonso.torres
51da2812ce 🐛 Restore undo and redo inside the WASM text editor
Keep bounded content and selection history within each editing session.
Group committed compositions and styled insertions, omit IME previews,
and reset history when external content changes the editing baseline.

Route keyboard, workspace menu and browser history input to the editor.
Restore UTF-16 selections through a new WASM binding, preserving styles,
readings and paragraph settings without reaching earlier canvas edits.

Verify 1,587 frontend tests, 671 renderer tests and both WASM builds.
Add four editable showcase examples and verify them after reopening.

AI-assisted-by: GPT-6
2026-11-05 17:49:00 +01:00
alonso.torres
029c9fbd9f 🐛 Keep text editing open when an IME cancels composition
Remember when a key-down belongs to native composition. Consume its
Escape release after composition ends, preserving the active text editor.
Keep ordinary Escape behavior for leaving editing and closing menus.

Add regression coverage for native key ordering and composition markers.
Verify 1,583 frontend tests, lint and changed-file formatting. Check native
Pinyin, Zhuyin and Korean input with eight editable showcase examples.

AI-assisted-by: GPT-6
2026-11-05 17:48:59 +01:00
alonso.torres
5d63967cf8 ✨ Add Mandarin Zhuyin reading layout
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
2026-11-05 17:48:59 +01:00
alonso.torres
ff18d2bd34 ✨ Add Chinese emphasis and underline positions
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
2026-11-05 17:48:59 +01:00
alonso.torres
30fe8abf63 ✨ Add Chinese Pinyin reading controls
Support typed character and group readings through the shared ruby model.
Expose Chinese labels, presentation controls and selection guards without
changing Japanese controls or generating pronunciation.

Preserve complete source ranges for glyphs sharing a combining cluster.
Add pinned horizontal and vertical tone-mark fixtures and editor tests.

AI-assisted-by: GPT-6
2026-11-05 17:48:59 +01:00
alonso.torres
b5ff9ce59f ✨ Add Chinese regional and Korean word layout
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
2026-11-05 17:48:59 +01:00
alonso.torres
fe6c8b8373 ✨ Add language-specific CJK layout controls
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
2026-11-05 17:48:59 +01:00
alonso.torres
b60d095beb ✨ Add a CJK text layout proof of concept
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
2026-11-05 17:48:59 +01:00
alonso.torres
0e6e58f38a 🐛 Fix windows IME support 2026-11-05 17:48:59 +01:00
alonso.torres
19f425a3b1 ✨ Change iconography to reflect orientation 2026-11-05 17:48:59 +01:00
alonso.torres
d5fe85b776 🐛 Fix issues with auto-width/height 2026-11-05 17:48:59 +01:00
alonso.torres
75e9a2f6a6 ✨ Expose Japanese text layout in the Plugin API
Expose Japanese layout properties on Text and TextRange with validated
values and whole-shape writing mode behavior. Keep ruby readings tied
to their spans when applying styles through ranges.

Update public TypeScript declarations, the API changelog and runtime
and plugin test coverage, including active-page checks.
2026-11-05 17:48:59 +01:00
alonso.torres
54714403bd ✨ Add Japanese layout controls and opt-in settings
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.
2026-11-05 17:48:59 +01:00
alonso.torres
8fb9e4ff80 ✨ Edit Japanese text with vertical cursors and IME input
Apply Japanese attributes to text selections and whole shapes without
splitting ruby or warichu spans. Respect vertical writing when resizing
text and placing selection handles and pointer cursors.

Anchor native IME input to the WASM caret, keep composition previews
out of the store until commit, and add an optional geometry overlay.
Cover editor styles, serialization, fallback fonts and cursor behavior.
2026-11-05 17:48:59 +01:00
alonso.torres
f73cd3e824 ✨ Preserve Japanese text in browser rendering and export
Render Japanese annotations in HTML and SVG and expose their styles
in CSS code generation. Preserve vertical position data from WASM,
while keeping the legacy SVG renderer on its horizontal layout path.

Cover generated markup, styles and copied SVG output.
2026-11-05 17:48:59 +01:00
alonso.torres
390a0f7672 ✨ Render Japanese text layouts in WASM
Lay out horizontal and vertical Japanese text with kinsoku, ruby,
emphasis, warichu, tate-chu-yoko and proportional font features.
Carry the layout through glyph paths, SVG export and text selection.

Update the Rust and ClojureScript binary formats together, including
the editor bridge and headless exporter. Resolve CJK fallback fonts
and add renderer tests and font fixtures.
2026-11-05 17:48:59 +01:00
alonso.torres
7570a1a232 ✨ Define Japanese text layout attributes
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.
2026-11-05 17:48:59 +01:00
Elena Torró
02223a67fa
🐛 Fix unmoved layout child stretched during wasm gap drag (#12831)
Follow-up to 419513011c: only pure moves pass down to children in wasm.
Remove unused set-wasm-rotation-modifiers.
2026-11-05 17:33:07 +01:00
Eva Marco
eb6e976c01
♻️ Move the dashboard header styles to the DS (#12676)
* ♻️ Move the dashboard header styles to the DS

The page header of the dashboard and settings pages took its styles from
common/refactor/common-dashboard.scss. Move the rules still in use to
dashboard/common/header.scss, written with DS tokens:

- The page titles become heading* (title-large) with a class instead of
  a bare h1; the team section links get a class, and the active class
  moves from the li to the link.
- Drop the rules that matched nothing: .element-name, .context-menu,
  .icon and .btn-secondary inside the header, and the global .btn-primary
  and .btn-secondary (only the fonts page uses them; it keeps the old
  import until its open change lands).
- The sidebar and subscription styles imported the file without using
  it; drop the import.

The header looks the same (0 px in the new visual test). The title line
height goes from 1.15 to 1.4, which does not move the text.

AI-assisted-by: claude-opus-5-5

* ♻️ Move the dashboard libraries and search styles to the DS

Replace the deprecated spacing, size and border variables with DS
tokens and logical properties, and drop the refactor/ import.

- The search placeholder icon gets a class for its color instead of a
  .icon svg rule; its xl size already gives the 32px box.
- Delete the selectors no markup uses: .dashboard-projects in both
  files and .dashboard-shared in search.

No visual change (0 px in the new libraries and search visual test).

AI-assisted-by: claude-opus-5-5

* 🔥 Move the fonts page header to the DS and delete common-dashboard

The fonts page was the last user of common/refactor/common-dashboard.scss:
its header now uses the shared DS header styles and a DS heading, like
the other dashboard pages. Nothing else imports common-dashboard, so it
is deleted.

No visual change.

AI-assisted-by: claude-opus-5-5

* 📎 Remove a double blank line in the dashboard sidebar styles

Prettier rejects the two blank lines left after the imports, which made
the frontend lint fail in CI.

AI-assisted-by: claude-opus-5-5

* 📎 Remove a blank line between the subscription style imports

Stylelint (at-rule-empty-line-before) rejects the blank line left between
the @use rules after the common-dashboard import was dropped.

AI-assisted-by: claude-opus-5-5
2026-11-05 17:01:04 +01:00
Eva Marco
0f7e3fe92e
♻️ Use DS icon buttons in the path editing bar (#12682)
* ♻️ Use DS icon buttons in the path editing bar

The path editing bar now uses DS icon buttons (ghost, tooltip below) and
DS icons, like the top toolbar. The toggles (draw/move mode, handler
types, snap) use aria-pressed, and the handler type trigger uses the DS
flyout indicator, with no tooltip because its menu opens on hover.

This also shows the handler and snap icons again: since #12659 their
SVGs draw with currentColor, and the legacy %button-icon set color to
transparent, so they were invisible.

Add a visual test of the bar, its hover and the handler menu.

AI-assisted-by: claude-opus-5-5

* ♻️ Move the path editing bar styles to the DS

Drop the deprecated styles import, resolve --panel-border-color to
--color-background-quaternary and use logical properties, except top and
height of the bar, which its transition names.

Delete the selectors the DS buttons left unused: .handler-trigger and
.flyout-indicator (the DS icon button draws its own indicator).

No visual change.

AI-assisted-by: claude-opus-5-5

* 📎 Add the blank line stylelint expects in the path bar styles

Stylelint (scss/double-slash-comment-empty-line-before) rejects the
comment right after a declaration, which made the frontend lint fail in
CI.

AI-assisted-by: claude-opus-5-5
2026-11-05 16:28:56 +01:00
Belén Albeza
fa1b3d4708
⚡ Keep only the shape data the worker reads (#12840)
* ⚡ Keep only the shape data the worker reads

The worker kept a full copy of the current page for its selection and
snap indexes. It now keeps only the attributes the indexes and the
change replay read, and re-indexes only the shapes that changed.

On a 52k-shape page the worker heap goes from 698 MB to 286 MB.

Closes #12740

AI-assisted-by: claude-opus-5-5

* ♻️ Share the worker re-trim with its replay test

The worker and its replay test now share `retrim-objects`.

AI-assisted-by: claude-opus-5-5
2026-11-05 16:25:29 +01:00
Nimesh
9d3d6da0d0
♻️ Migrate inspect components to modern syntax (#12435)
Migrate the remaining legacy components in the inspect module to the
modern rumext syntax: typography-block, text-block, svg-attr, svg-block,
selection-rect, selection-feedback, layer-item and left-sidebar.

Rename the ?-suffixed props of layer-item (component-child? to
is-component-child, hide-toggle? to hide-toggle) and drop the unused
sortable? and filtered? props passed from left-sidebar.

Relates to #9260

AI-assisted-by: claude-opus-5-5

Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-05 15:47:59 +01:00
Belén Albeza
eae2c5bc95
🐛 Fix flaky plugin creation parent test (#12858)
The svg-with-images case asserted that the stubbed rp/cmd! saw only the
URL upload RPC. clear-thumbnail sends :delete-file-object-thumbnails after
a 200 ms timer, so on slow CI runners that call was recorded too and the
assertion failed.

Count only the :create-file-media-object-from-url calls and expect one,
so unrelated RPCs no longer break the test.

AI-assisted-by: claude-opus-5-5
2026-11-05 13:26:19 +01:00
Belén Albeza
c6a9e62a4a
🐛 Fix typography in Settings (#12841) 2026-11-05 11:39:43 +01:00
Eva Marco
9349c3b778
♻️ Make the workspace logo and project name real links (#12699)
* ♻️ Give the workspace logo a real link and the DS logo asset

The Penpot logo in the workspace header was an <a> without href, so the
keyboard could not reach it and it could not open in a new tab. It now
links to the dashboard Recent page; a plain click still runs the in-app
navigation (prevent default), and Ctrl/Cmd-click opens the link in a
new tab.

The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing), with its own 32px class. The legacy svg kept its 500px height
attribute, so it overflowed the header by 222px above and below. This
was the last legacy icon in left_header.cljs.

New visual test "Shows the Penpot logo link": 0 px of difference at rest
and on hover.

AI-assisted-by: claude-opus-5-5

* ♻️ Make the workspace project name a real link

The project name in the workspace header was a div with on-click that
opened the project files in a new tab, so the keyboard could not reach
it. It is now a link to the same page with target "_blank": the browser
opens the new tab, and Tab, Enter and "Open link" work as for any link.

The left header visual test gives 0 px of difference.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:32:08 +01:00
Eva Marco
1d8f2fc717
♻️ Give the viewer logo a real link and the DS logo asset (#12702)
The Penpot logo in the viewer header was an <a> without href. For team
members it now links to the dashboard Recent page of the file's team: a
plain click still runs the in-app navigation (prevent default), and
Ctrl/Cmd-click opens the link in a new tab. Without team permission it
stays as before: no href and no click.

The logo uses the DS raw-svg* asset instead of the legacy icon (same
drawing, same 28px class). This was the last legacy icon in
viewer/header.cljs.

The "Viewer header" visual test gives 0 px of difference.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:50 +01:00
Eva Marco
f827b7b4a6
🐛 Make the dashboard template cards keyboard buttons (#12710)
A template card was an <a> without href inside a focusable div: Tab
reached the div, and neither Enter nor Space imported the template, so
the cards could not be used from the keyboard. Importing is an action,
so the card is now a <button> with the same look; it takes the tab
index the div had, and the manual Enter handler goes away.

Keyboard focus looks like hover (accent border and "Add to your
project").

The templates visual test gives 0 px of difference at rest, on hover and
for the scroll and collapse buttons; a new shot covers the focus.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:33 +01:00
Eva Marco
686b47742d
🐛 Make the profile page modal links keyboard buttons (#12712)
"Change email" and "Want to remove your account?" were <a> without href
that open modals: the keyboard could not reach them. They are now
<button type="button"> that keep their text-link look (accent color,
underline on hover), so Tab reaches them and Enter or Space opens the
modal.

The profile visual test gives 0 px of difference, now also with a shot
of the hover on "Want to remove your account?".

AI-assisted-by: claude-opus-5-5
2026-11-05 11:31:15 +01:00
Eva Marco
8ab9624f0b
🐛 Make Download report.txt a keyboard button (#12723)
On the internal error page and the feedback page, "Download report.txt"
was an <a> without href, so Tab skipped it. It is an action, so it becomes
a <button type="button"> with the same look, underline on hover included.
The type keeps it from sending the feedback form.

A new visual test reaches both pages through a mocked server error and
checks 0 px of difference and the download with Enter.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:30:39 +01:00
Eva Marco
657ce8b054
🐛 Make the login dialog section links keyboard buttons (#12724)
"Forgot password?", "Create an account" and "Login here." switch the
section of the login dialog (viewer and MCP authorize) and of the dialog
on the not found page. They were <a> without href, so Tab skipped them.
They become <button type="button"> with the same look; a class replaces
the bare a selector in static.scss.

A new visual test opens both dialogs, uses the keyboard to switch
sections and checks 0 px of difference at rest and on hover.

AI-assisted-by: claude-opus-5-5
2026-11-05 11:30:23 +01:00
makesomethingshit
d42217a32b
🐛 Create Plugin API shapes at the page root (#12499)
* 🐛 Create Plugin API shapes at the page root

Create component instances and SVG shapes at the page root so a board at
the page origin cannot capture them. Continue using the existing forced
frame option for component instances and an explicit parent frame for
SVG imports. Interactive placement retains its current behavior.

Cover component instances, SVG imports and SVG imports with images on
ordinary, flex and grid origin boards, including undo, redo, position
changes and explicit reparenting.

Closes #12086

AI-assisted-by: deepseek-v4.1-flash

Signed-off-by: choi junsoo <junsoo1172@gmail.com>

* 🐛 Preserve interactive creation placement

Cover component and SVG placement inside and outside an origin board.
Exercise the sidebar move lifecycle and verify parent preservation.
Correct the Plugin API changelog category.

AI-assisted-by: gpt-6
Signed-off-by: choi junsoo <junsoo1172@gmail.com>

* 🔧 Cover page-root creation in the Plugin API test suite

Check that LibraryComponent.instance(), createShapeFromSvg() and
createShapeFromSvgWithImages() create shapes at the page root when a
board covers the page origin. Each test removes the shape it creates,
since it lives outside the scratch board.

AI-assisted-by: claude-opus-5-5

---------

Signed-off-by: choi junsoo <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-11-04 17:40:14 +01:00
Elena Torró
4be8df5877
⚡ Load the rasterizer iframe only when the SVG renderer needs it (#12835)
Create it once features say SVG, never in wasm mode. Also skip SVG frame
thumbnails in wasm mode.
2026-11-04 15:50:13 +01:00
Elena Torró
8d7e275b6b
🐛 Count id and type in the wasm shape proxy (#12793)
The proxy counted one entry fewer than it yields, so it was never equal to a
plain map with the same entries. Count both fields and test it.
2026-11-04 13:01:05 +01:00
Andrés Moya
177c3c6551
🔧 Add one more test case (#12827) 2026-11-04 12:05:38 +01:00
Florian Schrödl
4b2061f399
✨ Add unit tests for the token forms and token tree (#12781)
* ✨ Add unit tests for the token form and combobox helpers

Several helpers of the token forms and the token value combobox were
already pure but had no tests. Cover the reference under the cursor,
the order of the token tree, moving through the combobox options, the
shadow and typography form values and their validation, which value a
form tab validates, and that a token import needs a json file.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>

* ✨ Test the shadow and typography form rules

Add tests for the shadow and typography form schemas: a negative blur
is rejected, a negative spread and a blank blur or spread are accepted,
every shadow needs its offsets and color, a line height needs a font
size, and a token can't reference itself. A typography token submitted
without any field is rejected.

They replace the tests of the validator wrappers, the combobox focus
order and the import file check.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>

---------

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-11-04 12:04:52 +01:00
andrés gonzález
4ee2a812be
🔥 Remove Beta badge from MCP server in Integrations (#12813)
The Integrations settings page labelled the MCP server as Beta, while
the MCP docs give no maturity status. Drop the badge, its style and the
unused translation key so the UI no longer presents it as beta.

Closes #12800

AI-assisted-by: claude-opus-5-5
2026-11-04 11:30:53 +01:00
Luke Lau
9b672785af
🐛 Fix gap handle drag leaving a stuck preview in wasm (#12782)
AI-assisted-by: claude-sonnet-5-5
2026-11-04 11:29:39 +01:00
Florian Schrödl
e6153b61a5
♻️ Extract the token pill state and tooltip from its component (#12691)
The token pill worked out inside the component whether its token is
applied to the selection, whether it can apply to it at all and why
the token is invalid, so no test could reach that logic. Move it into
`pill-state`, and split the tooltip into `tooltip-info`, which decides
what it says, and `render-tooltip`, which renders it. Both are pure
and get unit tests for the applied, partial, disabled and error states
and each tooltip case.

The two chained `on-click` handlers become one. Nothing else changes
in behavior.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-11-04 10:56:50 +01:00
Luke Lau
a089f7a5a1
🐛 Fix library telemetry counters (#12787)
At some point the code around this changed, I believe this used to reduce over
file ids. However today it reduces over entire library maps, so it passes a map
instead of an id to `dm/get-in files`. This then hashes the library which
forces it to decode all of its shapes, and then it fails to lookup the data. So
the component/colors/typography counts were always returning 0.

Using reduce-kv fixes this, and we can just get the library data directly with
`(:data library)`.

On the backstage file in penpot-files this brings the time to the telemetry
snapshot down from 3.8s to 1.5s

AI-assisted-by: claude-5.5-sonnet

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-04 10:44:30 +01:00
Elena Torró
51cbae6ea5
⚡ Improve memory consumption on shape decoding (#12790)
* ⚡ Avoid decoding the opened page twice for its frame index

The frame index was built over a with-meta copy, so pages got decoded again.
Read it from the page objects themselves and fix the with-meta identity check.

* ⚡ Intern keywords when decoding transit on the frontend

Each shape is decoded on its own, so every shape had its own key keywords.
Cache them in the read handler so all decoded data shares one instance.

* ⚡ Use an array map as the wasm shape proxy delegate

A hash map with ~35 keys costs ~3 KB of nodes per shape on large pages.
An array map holds the same entries in one array, at a small lookup cost.
2026-11-03 17:41:36 +01:00
GennoBou
029d9c0af7
🐛 Fix Japanese autodetection fallback on "ja" locale (#12283)
Most browsers return "ja" rather than "ja-JP" in navigator.language.
Because parse-locale expands "ja-JP" to ["ja_jp" "ja"] but keeps "ja"
as ["ja"], using "ja_jp" causes autodetection to fall back to English.

Normalize the Japanese locale identifier to "ja" and map jpn_JP.po to
translation.ja.js, matching other single-region locales like Ukrainian.

AI-assisted-by: gemini-3.8-flash
2026-11-03 16:11:22 +01:00