540 Commits

Author SHA1 Message Date
Miguel de Benito Delgado
7b71aba0d7 🚧 WIP Step 12.4, Comments
AI-assisted-by: gpt6-astra
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
4b3e592971 🚧 WIP Step 12.3, Test cleanup + missing coverage
AI-assisted-by: muse-spark, deepseek-flash
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
b5a369ec8c 🚧 WIP Step 12.2, Fix browser bridge, lifecycle
AI-assisted-by: muse-spark, glm-5.3
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
6b5d50428b 🚧 WIP Step 12.1, Improve case validation
AI-assisted-by: muse-spark, glm-5.3
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
7adeb379fc 🚧 WIP Step 12, temporary pilot
- pilot runner for testing
- will be entirely removed later

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
fdaaa3f32d 🚧 WIP Step 11, browser bridge
- New schema:viewport
- Browser bridge (with some temp code)
- Move prepare-scene out measured regions
-
AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
0d77b38d5c 🚧 WIP Step 10, scene upload 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
a1839607f9 🚧 WIP Step 10, defcase body validation, docs 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
49f4d8661a 🚧 WIP Step 9, fix naming conventions 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
3246cd0873 🚧 WIP Step 8, simplify and clean up tests 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
5ddf1bf4fd 🚧 WIP Step 7, scene and case declarations 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
7edec67bbe 🚧 WIP Step 6, ellipses 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
da66471a6b 🚧 WIP Step 5, bools 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
ff51b82ca5 🚧 WIP Step 4, frames and groups 2026-10-30 20:08:27 +01:00
Miguel de Benito Delgado
3fff7a5679 🚧 WIP Step 3, fixture macros, prng, defaults, placeholder scene 2026-10-30 20:07:34 +01:00
Miguel de Benito Delgado
3541b95e82 🚧 WIP Step 2, common scene code, validation 2026-10-30 20:07:34 +01:00
Miguel de Benito Delgado
ee614ea309 🚧 WIP Step 1, scaffold cljs benchmark runner 2026-10-30 20:07:34 +01:00
Andrey Antukh
65a76a21cd Merge remote-tracking branch 'origin/staging' into develop 2026-10-29 16:31:26 +01:00
Pablo Alba
9dc222c642
🐛 Fix bad characters allowed on variants properties (#12688) 2026-10-29 10:13:33 +01:00
Eva Marco
1962860f04
🐛 Reduce thumbnail re-renders and report throttled update loops (#12610)
* 🐛 Stop thumbnail subscribers re-rendering on every store change

A thumbnail loaded from the server keeps a media id as :uri, and
workspace-thumbnail-by-id resolves it into a new map on each read. The
ref compared values with identical?, so every root frame, every
component in the assets panel and the interactions layer re-rendered
on every store change, mouse moves included. Compare by value instead.

This does not loop by itself, but it adds work to every commit and
brings other update loops closer to React's limit.

Relates to #12580

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

* ✨ Report update loops throttled by the update scheduler

When the scheduler throttles an update loop, it only logs a console
warning, so a loop that no longer crashes leaves no trace. Send an
error report too, with the events in flight and the last events, at
most once per report window.

Relates to #12580

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

* 🐛 Report throttled update loops as unhandled exceptions

The throttled update loop report used the handled-exception event, which
only lands in the audit log: error reports and alerts read
unhandled-exception and exception-page. A throttled loop is an
application defect (its cause has :type :internal), and the point of the
report is to find those loops, so it is now an unhandled exception.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:02:55 +01:00
Eva Marco
a3fe43c1ec
🐛 Report raw fetch failures as connectivity errors (#12624)
* 🐛 Report raw fetch failures as connectivity errors

A fetch that fails outside app.util.http (zip.js, plugins, schema
loads) rejects with a bare TypeError such as "Failed to fetch". It
reached the global handlers and was reported as an unhandled crash,
with no stack and no URL.

The global handlers now wrap the browser's network TypeErrors as
:network (or :offline when the browser says so), so they get the
connection toast and a compact handled report. Response body reads
in app.util.http also wrap a TypeError as :network with the request
uri, as fetch itself already did.

Relates to #12581

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

* 🐛 Treat only network TypeErrors on body read as connectivity errors

process-response-type turned every TypeError from a body read into a
:network error, so a bug such as "body stream already read" showed a
connection toast. It now uses network-failure?, which moves to
app.util.http so that both the http client and the global error
handlers share it. Add Firefox's "Error in body stream" message.

Tests now check :offline and :network separately by setting
navigator.onLine, and read-error builds on async/->promise.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:02:45 +01:00
Dr. Dominik Jain
e20a0d748e
✨ Add OAuth support for MCP server (#12480)
* 🎉 Add OAuth support for remote MCP connections

Add MCP-specific discovery, client registration, and approval endpoints.
Exchange single-use authorization codes with PKCE for the fixed MCP user
token, enabling MCP and creating the token on approval when needed.

Use self-contained client IDs without new database tables or stored client
records. Keep temporary authorization state in Redis/Valkey.

Accept bearer credentials while preserving query-parameter connections.
Enable OAuth through PENPOT_MCP_AUTH_URI in multi-user mode. Document the
environment variable in the README and implementation details in Serena.

Explain protocol and concurrency decisions in the Clojure code. Cover token
provisioning, OAuth state, client registration, HTTP flows, and the approval
page with tests.

AI-assisted-by: gpt-6

* 🐛 Wait for devenv initialization before starting builds

Docker reports a container as Running as soon as its main process starts,
while the entrypoint may still be preparing the development environment.
manage.sh used that status to launch tmux jobs, allowing frontend builds
to race with ownership changes to /opt/rustup and Cargo setup.

On a fresh container, rustup could fail with Permission denied before the
frontend watcher started. Nginx would later serve the existing JavaScript,
leaving source changes absent from the running app despite restarting
devenv.

Wait for nginx's local health endpoint before launching the jobs. Nginx
starts after entrypoint setup, so its response confirms that setup has
finished. Allow up to 120 seconds and report a timeout without starting
the jobs if initialization does not complete.

Validation: shell syntax and whitespace checks pass.

AI-assisted-by: gpt-6

* ♻️ Share the login modal with MCP authorization

Extract the viewer login modal and its styles into a shared auth
component, retaining the viewer wrapper and its close behavior.

Use the modal on the MCP approval page and preserve the pending request
through login. Closing it clears the saved return URL and shows a
cancellation message without further actions.

Cover return URL handling and cancellation with frontend tests.

AI-assisted-by: gpt-6

* ✨ Show client icons in MCP authorization

Preserve HTTPS logo URLs from client registration and published metadata
and show the icon beside the application name on the approval page.
Ignore unusable URLs and hide failed images without blocking approval.

Update the access description to refer to MCP connections established
from Penpot. Cover logo metadata and approval responses in backend tests.

AI-assisted-by: gpt-6

* 🐛 Follow the system theme when no preference is configured

Default to the system theme in the shared resolver used by the app and
plugins. Preserve explicit light and dark choices, including the legacy
default value that means dark.

Reuse the reactive theme hook in progress widgets and show the system
preference in settings for accounts without a configured theme.

Verify preference resolution with regression tests and browser checks.
All 1,474 frontend tests pass.

AI-assisted-by: gpt-6

* 🐛 Replace expired MCP tokens during OAuth approval

Settings can give an MCP token an expiry date. OAuth approval reused
the newest MCP token without checking it, so an expired token reached
the client and never matched a plugin connection.

Approval now treats an expired token as missing and creates a new one.
Token exchange rejects a code whose token expired after approval with
invalid_grant.

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

* 🐛 Log unexpected errors on MCP OAuth endpoints

The OAuth routes sit outside the shared HTTP middleware, so the shared
error handler never saw them, and their own handler turned unexpected
failures into a 503 without logging anything. Failures in the session
middleware of the approval endpoints had no handler at all.

Unexpected failures now also go through the shared error handler for
its logging, while clients still get an OAuth error body. The OAuth
error handler also wraps the session middleware of the approval
endpoints.

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

* 🔧 Add devenv script to run the MCP server with OAuth

start-mcp-devenv-oauth starts the MCP server in multi-user mode with
PENPOT_MCP_AUTH_URI derived from PENPOT_PUBLIC_URI, to test OAuth in
the devenv. Agentic tooling still uses the single-user
start-mcp-devenv.

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

* 🐛 Fix failing test case

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-28 18:40:04 +01:00
Alejandro Alonso
4dc684dd9c Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 11:50:56 +01:00
Pablo Alba
0532f15cee
🐛 Fix tab on variant name doesn't validate formula (#12655)
* 🐛 Fix tab on variant name doesn't validate formula

* ✨ Move helpers to common

* ✨ Mr proposed changes
2026-10-28 11:44:38 +01:00
Alejandro Alonso
a8e8a54a2b Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 10:35:31 +01:00
Pablo Alba
f1d15aee4b
✨ Keep toast for file reparation open until user closes it (#12662) 2026-10-27 19:47:10 +01:00
Alonso Torres
a70712fce4
🐛 Fix issue with compononent not sync on move (#11981) 2026-10-27 15:44:30 +01:00
Alejandro Alonso
deb5771248
✨ Use WASM for SVG markup in code inspect and copy (#12567) (#12608)
When render-wasm is active, generate SVG for the Code inspector and
Copy as SVG from the WASM export path instead of classic SVG codegen.
Add multi-root render_shapes_svg so multi-select stays one document.

Closes #12567
2026-10-27 12:05:30 +01:00
Eva Marco
86e01d56a2
🐛 Update canvas and keep local sets when switching tokens source (#12442)
Setting a library as tokens source did not update shapes that use tokens,
because set-tokens-source never ran the token propagation that every other
token operation runs. Run it after the commit.

Going back to the local tokens, or unlinking the library that was the
source, left no local set active: the file has a single tokens status, and
switching to a library replaced it with the library's one. Keep the status
of the file's own tokens lib aside in a new optional :own-tokens-status
attribute while an external library is the source, and restore it when the
file is its own source again. Exporting without libraries and absorbing a
deleted source library restore it as well.

Closes #12430

AI-assisted-by: claude-opus-5-5
2026-10-26 15:23:05 +01:00
Belén Albeza
577cbe43e4
🐛 Validate the text payload before pasting it (#12612)
Text copied in Penpot carries its content in a data attribute of the
clipboard HTML, which plugins and other apps can rewrite. The v3 editor
and canvas paste used it as is, so bad data could reach WASM and the
file and break the shape.

The payload is now dropped, and the paste falls back to plain text in
the caret style, unless its content passes the text shape schema, its
span geometry holds only safe numbers and its file and team ids are
uuids. Payloads in HTML over the paste size limit are not decoded.

Copied text shapes pasted into the editor get the same checks, plus a
numeric position; when they fail, nothing is pasted.

Closes #12167

AI-assisted-by: claude-opus-5-5
2026-10-26 12:36:13 +01:00
Alonso Torres
4fe73011ae
🐛 Reject restoring components of a linked library from plugins (#12611)
LibraryComponent.restore() did not check that the component belongs to
the current file's library. Called on a deleted component of a connected
shared library, it restored that component and saved the change to the
library file, while remove(), annotation and duplicate() reject such
handles with a validation error.

restore() now rejects them the same way: "Can only restore components
from the current file's library".

Closes #12607
2026-10-26 12:21:58 +01:00
Eva Marco
68ecfb1234
🐛 Fix default shortcuts that fail on some layouts and browsers (#12506)
Fixes the problems found after #12326 and listed in #12327.

Mousetrap now tries several names for each key press, in order, and
fires the first one with a binding: letters and digits by keyCode when
a modifier is held, punctuation by position (layout map, or a US table
when the browser has none), the typed glyph, and the typed glyph
without the Shift it needed. With AltGr the typed glyph comes before
the position. The shortcut recorder asks Mousetrap for the key it
stores, so a recorded shortcut always fires.

This makes "?" and "+" work on every layout, Shift and Ctrl+Shift with
punctuation work in Firefox and Safari, Option+letter work on macOS,
and AltGr+º hide the UI on Linux with a Spanish layout.

Chrome on Linux returns the map of the first configured layout, not
the active one. Mousetrap now drops the map when an unmodified
punctuation key types something else.

The layout twins are no longer needed and are removed: "shift+'" made
"?" zoom to the selection on Spanish layouts. The Add node shortcut is
removed too: on US layouts its key is the "+" zoom key, and on Firefox
with a Spanish layout it cannot fire. The Firefox and Safari warning
in the shortcuts settings is gone, since those shortcuts now work
there. The user guide and translations follow these changes.

AI-assisted-by: claude-opus-5-5
2026-10-26 11:56:09 +01:00
Pablo Alba
3cd073fc6a
✨ Add auto-repair for integrity error files (#12524)
* ✨ Add auto-repair for integrity error files

* ✨ MR changes
2026-10-26 11:04:06 +01:00
Alejandro Alonso
e764b17c31 Merge remote-tracking branch 'origin/staging' into develop 2026-10-26 09:53:59 +01:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +02:00
Luis de Dios
4bf70de73c
🐛 Restrict per-side stroke width tokens to boards and rects in plugin API (#12332)
* 🐛 Fix restrict per-side stroke width tokens to rectangles in plugins API

* ♻️ Reuse per-side stroke restriction check across plugin API

Move the per-side stroke width restriction into the public
`per-side-stroke-attrs-restricted?` helper, now taking `page-id`, and reuse
it from both `shape.applyToken` and the token application path. Keep the
error message in a single `per-side-stroke-widths-message` def used by all
three rejection sites.

Wait for token resolution in the uniform stroke width regression test
instead of relying on a bare `settle`, which could assert before the
applied token lands.

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

---------

Co-authored-by: Luis de Dios <luis.dedios@kaieldos.net>
2026-10-23 13:32:39 +02:00
Belén Albeza
2045fc4900
🐛 Keep text image fills in place when pasting (#12552)
A text image fill was always laid over the whole text box, so a pasted
image span showed another part of the picture in a box of another size.

Text image fills now honour the image transform, as shape fills do: the
image covers the transform rect instead of the whole box. Paste gives
each pasted image span a transform that keeps the image at the same
pixel offset from the span, like the gradients.

The SVG export emits one linked image per span transform, rather than
one per image id, and the style panel keeps the transform it reads back.

AI-assisted-by: claude-opus-5-5
2026-10-23 13:00:53 +02:00
Alejandro Alonso
e27c65c77a
✨ Run WASM asset exports locally in the browser (#12515)
When render-wasm is active, shape exports (single file, ZIP, and Export
boards multi-page PDF) run on the shared worker WASM module so the UI
stays responsive. A progressive PDF session (begin/add-page/finish)
updates progress per page; cancel settles on the client. The server
exporter path remains for the SVG renderer.
2026-10-23 11:02:23 +02:00
Florian Schrödl
9aa78a536b
✨ Resolve tokens with the tokenscript-interpreter package (#12348)
Replace the @penpot/tokenscript npm package (the TypeScript
interpreter) with penpot/tokenscript-interpreter, a Clojure and
ClojureScript interpreter that now lives in its own repository and
compiles into the frontend build.

The frontend adapter (app.main.data.tokenscript) resolves tokens with
it behind the existing :tokenscript flag and maps its results to the
values and errors the workspace uses:

- Token values are validated by the interpreter's token types, passed
  by their DTCG names: `1foo` is not a dimension, a number has no
  unit, an opacity is between 0 and 1. Penpot only adds its own
  limits (font weight names, text decoration keywords).
- Division by zero, incompatible units, unknown functions and
  function arguments out of range get messages of their own, and
  token pills show the message of their error.
- Font family names are quoted before resolving, as in the adapter
  this replaces (#11750): `Red Hat` stays one name.

The token form's value input passed the token name to the tokenscript
resolver in place of the typed value, and no name at all for a new
token. It now resolves the typed value under the form's token name, as
the StyleDictionary path does.

StyleDictionary stays the default until the interpreter is stable.

frontend/deps.edn pins the interpreter by tag and commit, v0.1.0.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-10-22 17:32:52 +02:00
Belén Albeza
0505eb3b1d
🐛 Fix pasted gradient fills in text editor v3 (#12516)
* 🐛 Keep text gradients in place when pasting at the caret

Text gradients are stored as fractions of the whole text box. Paste
copied those fractions unchanged, so in a box of another size the
handles and colours moved away from the pasted text.

Copy from the v3 editor now records where each span with a gradient or
image fill starts in its text box, and the box size, in the clipboard
payload. A new WASM export, text_editor_get_range_origin, gives the
start of any text range.

Paste at the caret finds where those spans start in the target and
rewrites their gradients so the handles keep the same pixel offset from
the span. Auto-sized texts use the box size they get when the editor
closes.

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

* 🐛 Rebuild text gradient shaders when the text box resizes

Text gradient and image shaders are built into the cached Skia
paragraphs against the text box size. A resize with no content change
reused those paragraphs, so the colours stayed matched to the old box
until a reload.

The text now remembers the box size its shaders were built for and lays
itself out again when that size changes and a span has a gradient or
image fill. Text with only solid fills keeps its cache.

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

* 🐛 Keep text gradients in place when pasting on the canvas

A text selection pasted on the canvas becomes a new text whose box
differs from the source, so its gradients moved like they did for caret
paste.

After adding the new text, lay it out, find where its gradient spans
start and move their gradients to keep the same pixel offset from the
span, in the same undo step. The range origin export now takes a shape
id, so it works on texts that are not being edited.

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

* 🐛 Keep text gradients in place when pasting copied texts at the caret

Text shapes copied whole, rather than as an editor selection, reached the
caret paste without any geometry, so their gradients still moved away from
the pasted text.

A shape copy now records where the gradient and image spans of each
selected text start, and the box size, by shape id. Paste joins them in
the order of the joined paragraphs, with no origin for texts copied
without one.

Copied texts each have their own box, so every span origin now carries
its own box size instead of one box for the whole payload.

AI-assisted-by: claude-opus-5-5
2026-10-22 13:39:26 +02:00
Eva Marco
ce5276065e
🐛 Skip unknown entries and reserve keys in shortcut settings (#12473)
* 🐛 Skip unknown entries when importing shortcuts

The shortcuts import rejected the whole file when it held a single entry
it did not know, so a profile exported from an older Penpot version, or
edited by hand, could not be imported at all. The spec in #9924 asks to
skip unknown keys, tell the user which ones, and apply the rest.

Importing now keeps the entries that map to a known shortcut with a valid
key combination and skips the rest: unknown contexts, unknown shortcut
keys, and commands that are not a key combination. The import modal
lists the skipped entries. The file is only rejected when it is not a
map or holds nothing that can be applied.

A context whose entries are all skipped is left out: an empty context
clears the user's shortcuts on import, so keeping it would wipe them.

valid-command? reads a command the way mousetrap does, so it accepts
every default shortcut and rejects strings mousetrap could never match.

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

* 📎 Sort requires in shortcuts settings test

cljfmt expects the namespace requires in alphabetical order.

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

* ✨ Reserve cancel, copy and paste shortcuts

Cancel, copy and paste are no longer customizable, in the workspace and
in the path editor, which shares their shortcut keys. Their key
combinations (Escape, Ctrl/Cmd+C, Ctrl/Cmd+V) are now reserved: the
shortcut recorder warns and blocks saving one of them for another
action, and the import skips entries that use them.

Reserved combinations come from the non-customizable workspace
shortcuts, so marking another one as non-customizable reserves its key
too. The path editor's cancel also binds V, the move tool key, so path
shortcuts are left out. Combinations compare the way mousetrap matches
them: aliases and modifier order do not matter.

apply-custom-overrides now ignores overrides of non-customizable
shortcuts. An override saved before this change would otherwise still
apply, with no way to reset it from the settings page.

AI-assisted-by: claude-opus-5-5
2026-10-22 12:24:12 +02:00
María Valderrama
338d009c1e
🐛 Fix sorting for accented teams and organizations (#12505)
* 🐛 Fix sorting for accented teams and organizations

* 📎 Code review
2026-10-21 15:12:41 +02:00
Andrés Moya
3863e92993
🐛 Fix loss of swap slot when copying to clipboard (#12434)
* 🐛 Fix loss of swap slot when copying to clipboard

* 🔧 Add test and make test helpers run same code as real functions

* 🔧 Changes after review
2026-10-21 10:35:40 +02:00
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Shreyash Agare
ecfa350470
✨ Auto-scroll sortable lists while dragging near their edges (#11595)
* ✨ Auto-scroll sortable lists while dragging near their edges

Native drag and drop swallows wheel events, so a long list could only be
scrolled mid-drag by hitting the browser's narrow auto-scroll strip at the
very edge of the container.

use-sortable now scrolls the item's nearest scrollable ancestor on every
animation frame while the pointer sits within 56px of its top or bottom
edge, ramping the speed up to 18px per frame as the pointer gets closer.
Every list built on the hook (layers, sitemap, option rows, gradients,
token sets) gets it without any change on their side.

* ✨ Cap auto-scroll zones and test the scroll loop

Each edge zone now takes at most a quarter of the container, so the
middle half of a short list, like the pages panel, never scrolls.

The scroll container is found once when the drag starts. The speed is
scaled by the time between frames, so 120 Hz screens no longer scroll
twice as fast. Starting a drag also stops any loop left over from a
drag whose end event was lost.

auto-scroll-speed is now private. New tests cover the running loop:
only one loop starts, it stops on drag end, when the speed goes back
to zero and after 500ms without dragover, and the frame time scaling.

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

---------

Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-10-20 11:27:31 +02:00
Andrey Antukh
4635eaa467
🐛 Show request access modal on viewer and dashboard urls (#12456)
The exception page compared the screen query param against literals
that never occur ("view", "dashboard"), so viewer and dashboard urls
of inaccessible files and teams fell through to the plain not-found
page instead of the request access modal. Classify the param with a
tested helper that recognizes "viewer" and every "dashboard-*" route.

Closes #11224

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 22:33:10 +02:00
Andrey Antukh
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +02:00
Andrey Antukh
fdd66978fd Merge branch 'main' into staging 2026-10-19 20:10:20 +02:00
Andrey Antukh
38a6961ff1
✨ Make keyboard shortcuts layout-independent (#12326)
* Revert "🐛 Fix shortcuts with modifiers on letters and digits (#12292)"

This reverts commit 21ecfb67b70ac2d6f4defb05a543f0fc18d692a2.

* 🐛 Migrate mousetrap to key with sane modifiers

Finish the half-done mousetrap migration from code/keyCode to
key: canonical lowercase tokens on both bind and event sides so
modifiers match again, US-only shift remap and keypress legacy
removed, and AltGr phantom ctrl filtered on Windows.

Record punctuation positionally through getLayoutMap (American
fallback) so Shift+Period stores shift+. on every layout, while
letters and digits stay glyph-based for AZERTY. Drop dead 219
aliases and revert the stray CamelCase escape command.

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Rewrite mousetrap as ESM class without behavior changes

Turn the 2012 constructor-plus-prototype shape into a class
with underscore-private methods, const tables and arrow
listeners. Drop dead _belongsTo, fold one-line prevent/stop
helpers into _fireCallback, and let handleKey absorb the old
_handleKey indirection. Public API and matching logic stay
identical; verified by existing tests plus a runtime smoke.

AI-assisted-by: muse-spark-1.3-contributor

* 🎉 Add node:test suite for mousetrap shortcuts

Cover canonical key normalization, modifiers with AltGr,
bind arrays plus aliases keyup reset overwrite, sequences
with fake timers, and the full stopCallback matrix, all
through isolated instances and synthetic events. Hook the
zero-dependency suite into frontend/scripts/test for CI.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Record shortcuts by glyph instead of position

Drop the getLayoutMap positional layer and the
American fallback: punctuation now records event.key
lowercased, the same token Mousetrap dispatches, so
Shift combos match with and without a layout map.
Keeps the Numpad numN branch both sides share.
Updates the pinned positional tests and adds the
Spanish Shift-plus regression.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Resolve shifted digits to the digit itself

Shift+DigitN now resolves to the digit on every layout, so
shift+0 fires on QWERTY ()) as well as AZERTY (0), like the
old keyCode behavior. Applies in the recorder and Mousetrap;
unshifted digits and numpad are untouched. Also stages the
layout-map suite left untracked.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Cover punctuation defaults with layout twins

Some defaults only fired where their glyph is unshifted (+ dead
on US layouts, shift++ dead outside ES/DE, \ dead where it
needs AltGr). Bind the twin token per layout family and freeze
the display with :show-command. No tooltip or translation
changes.

AI-assisted-by: muse-spark-1.3-contributor

* ✨ Warn when the keyboard layout API is missing

Shows a warning on the shortcuts settings page in browsers
without navigator.keyboard.getLayoutMap, where Shift with
punctuation may not fire. Uses a synchronous probe, English
and Spanish strings, and documents the layout limitations in
the user guide.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix hide-ui twins and qualify layout docs

The ctrl+alt+\ twin could never fire (the AltGr filter drops
Ctrl), so it goes away; alt+º covers Chrome on Spanish
layouts where AltGr+º resolves to º. The user guide now
states which claims need the layout map and which hold
everywhere.

AI-assisted-by: muse-spark-1.3-contributor

---------

Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2026-10-19 16:08:47 +02:00
makesomethingshit
91a5b3a9fb
🐛 Fix token tree path handling for repeated and nested segments (#11591)
* 🐛 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>
2026-10-19 09:44:32 +02:00