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
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
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
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
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
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
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
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
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
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
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
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.
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.
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.
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.
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.
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.
* ♻️ Use DS icons in the layer row buttons
The expand, visibility and lock buttons of the layer rows drew their
icons with the deprecated icon set (arrow, shown, hide, unlock, lock).
Use the DS icon instead. The buttons stay raw: they are segments of the
layer row, with backgrounds that follow the row state.
The icons get a class each, so their colors move from svg rules to
those classes, and they keep the old stroke weight. The DS icon draws
12px inside a 16px box:
- The expand button start padding goes from 8px to 6px, so the arrow
stays in place.
- The visibility and lock buttons leave 12px for the icon, so their
icons do not shrink (flex-shrink: 0) and stay centered, uncut.
The rows look the same as before, pixel for pixel. Add a visual test
with a selected, hidden and blocked layer and a hovered row.
AI-assisted-by: claude-opus-5-5
* ♻️ Move the layer name styles off the deprecated styles
The layer name and its rename input used the deprecated body-small
typography, mixins, spacing and token aliases. They now use the DS
body-small typography, sizes, borders and px2rem(), and the layer row
color aliases resolve to their DS tokens, in layer_item.scss too.
The DS body-small has a 1.3 line-height instead of 1.4, so the layer
names sit 1px higher. The name color is one local variable for every
state; the rename input sets it too, since it also has .element-name.
A visual test now covers the rename input.
AI-assisted-by: claude-opus-5-5
* ♻️ Use a DS button for previous in the onboarding questions
The "Previous" button of the onboarding questions becomes a DS
secondary button; it was a raw button with the deprecated modal cancel
styles. It is a bit narrower and its label uses the DS typography.
"Next" stays: it is the shared forms submit button. The "Other" option
icon stays too: it belongs to the forms image radio buttons, which move
with the radio buttons group. A visual test covers the step 2 buttons
at rest and with "Previous" hovered.
AI-assisted-by: claude-opus-5-5
* ♻️ Move onboarding questions styles closer to the DS
The onboarding questions stylesheet resolves its legacy modal color
aliases to DS tokens, uses logical properties and the DS typography
module without a namespace. refactor/ stays only for the modal
placeholders (%modal-*), which move with the modals refactor. No visual
change (checked on steps 1 and 2; only the shuffled options differ
between runs).
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons and icons in the dashboard team pages
The team pages (members, invitations, webhooks and settings) and their
modals move off the deprecated icons and raw buttons:
- The member and webhook row menus become ghost DS icon buttons named
"Options", with aria-expanded.
- The member and invitation role selectors become buttons with
aria-haspopup and aria-expanded, and use the DS arrow icon. They were
divs; one carried a role and an id repeated on every row.
- The four modal close buttons become ghost DS icon buttons named
"Close"; the class that places them goes on the tooltip wrapper.
- "Create webhook" becomes a DS primary button.
- The webhook status, team image and settings icons use the DS icons.
The invitation checkbox tick stays until the checkbox migration. A
visual test covers the role and row menus, the resend and webhook
modals and the team image overlay.
AI-assisted-by: claude-opus-5-5
* ♻️ Move dashboard team styles to logical properties and DS tokens
The remaining legacy color aliases resolve to their DS tokens, physical
offsets and margins become logical properties, and the DS typography
module is used without a namespace. Four selectors that team.cljs does
not use are commented out with a TODO.
refactor/ stays only for component-family placeholders: %input-checkbox
and %checkbox-icon (checkboxes) and %modal-* (modals). The shared
dashboard header styles (common-dashboard) are used by ten dashboard
modules and move in their own change. No visual change.
AI-assisted-by: claude-opus-5-5
* 🐛 Open the team row menus below their button
The member and webhook row menus were placed from the bottom of their
cell with a fixed offset, so their position depended on the menu
height and the button height. The two-item webhook menu covered its
button, the member menu covered the lower part of it, and the taller DS
icon button moved both 3px down. They now open 4px below the button.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the dashboard team pages
The SCSS refactor left four rules commented out with a TODO because
team.cljs does not use them. CSS modules scope the classes to this
module and no other source uses them, so they go: .dashboard-top-cta,
.error-notification, .empty-invitations-buttons and
.invitations-dropdown. No visual change.
AI-assisted-by: claude-opus-5-5
* 🐛 Find the member role selector as a button in the dashboard test
The "Bug 9443" test looked for the member role selector as a combobox.
The selector is now a button with aria-haspopup, so the test failed in
CI. It now looks for a button named "Admin", and checks that the owner
has no such button.
AI-assisted-by: claude-opus-5-5
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
* ♻️ Use DS icons in the copy button
The shared copy button (Inspect attribute rows and code blocks, share
link, integrations) draws its clipboard and tick icons with the DS
icons at the small size, colored through the color property. The
button itself stays: it wraps whole rows. The icons are the same
drawings, so nothing changes on screen.
A visual test covers an Inspect computed attribute row and the code
block copy button, at rest, on hover and after copying.
AI-assisted-by: claude-opus-5-5
* ♻️ Move copy button styles off the deprecated styles
The copy button stylesheet no longer imports refactor/: the
button-style, flex-center and copy-wrapper-base mixins are inlined with
DS values, the tertiary button and icon color aliases resolve to their
DS tokens, and physical properties become logical ones. No visual
change (whole viewer visual spec compared; the code tab syntax
highlighting loads late on a cold first run, with either stylesheet).
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons and icons in the assets tab
- "Add library" and "Manage library" become DS primary and secondary
buttons, full width with the label centered; they keep their test id.
- The filter button stays a raw segment of the search bar; its legacy
icon becomes the DS filter icon. Its opened state comes before the
focus and hover rules, as the legacy placeholder did, so the focus
border still shows.
- The create/rename group modal gets a ghost DS icon button named
"Close", placed on the tooltip wrapper, and DS cancel (secondary) and
create/rename (primary) buttons instead of raw inputs.
A visual test covers the add and manage library buttons, the filter at
rest, on hover and opened, and the create group modal.
AI-assisted-by: claude-opus-5-5
* ♻️ Move assets tab styles off the deprecated styles
assets.scss no longer imports refactor/: its mixins, sizes, spacing and
input / button icon color aliases resolve to DS values and tokens, with
logical properties. The unused .sections-container, .section-item and
.section-btn rules are deleted (the filter menu is the shared context
menu).
assets/groups.scss does the same and keeps refactor/ only for the modal
placeholders (%modal-*) and %input-with-label. Its legacy uppercase
title becomes the DS headline-small typography, which differs only in
its size (12px instead of 11px): the group modal grows 2px.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icons in the dashboard sidebar
The search, clear search, pinned projects placeholder and logout icons
of the dashboard sidebar use the DS icons, and they get their color
from the color property. The two search buttons had untranslated test
names as their labels ("dashboard-search"); they are now named "Search"
and "Clear search". The pin keeps its place: the DS small icon draws
12px inside a 16px box, so its margins shrink by 2px.
New key: dashboard.clear-search (en, es). The dashboard page object
looks the search button up by its new name. A visual test covers the
search box with and without a term, the pinned projects placeholder and
the profile menu.
AI-assisted-by: claude-opus-5-5
* ♻️ Move dashboard sidebar styles off the deprecated styles
Legacy color aliases resolve to their DS tokens, deprecated mixins,
sizes and font variables are inlined with DS values, physical
properties become logical ones and the DS typography module is used
without a namespace. An invalid max-width with two values, which the
browser ignored, is gone.
Two rules that match nothing are commented out with a TODO: the profile
menu arrow svg rule (the arrow is the DS icon svg itself) and the
upgrade plan section, which sidebar.cljs does not use.
refactor/ stays only for the profile menu placeholders (%menu-dropdown,
%menu-item-base) and the shared dashboard header styles
(common-dashboard). No visual change.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the dashboard sidebar
The SCSS refactor of the dashboard sidebar left rules commented out with
a TODO because the module does not use them, or because they target an
svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.profile-dropdown-item .open-arrow svg, .upgrade-plan-section,
.penpot-free, .no-limits, .power-up. No visual change.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons in the dashboard team modals
The close buttons of the create/rename team modal, the team permission
notice and the two "before you leave" modals become ghost DS icon
buttons named "Close"; the class that places them goes on the tooltip
wrapper. The cancel and "promote and leave" inputs of the leave modals
become DS buttons: secondary, and destructive once a member is picked
(primary and disabled before).
Two fixtures make the leave and reassign modal testable (the logged-in
user owns the second team, which has another member). A visual test
covers the create team modal and the leave and reassign modal.
AI-assisted-by: claude-opus-5-5
* ♻️ Move dashboard team modal styles closer to the DS
The change owner and team form stylesheets resolve their legacy color
aliases, spacing and typography mixins to DS tokens and use the DS
typography module without a namespace. The legacy uppercase title
becomes the DS headline-small typography, which differs only in its
size (12px instead of 11px): the "before you leave" title grows 1px and
the modal 2px. Unused selectors are commented out with a TODO.
refactor/ stays only for the modal placeholders (%modal-*), which move
with the modals refactor, and the input placeholders.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the dashboard team modals
The SCSS refactor of the dashboard team modals left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.input-wrapper, .cancel-button, &.danger. No visual change.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icons in the viewer flows and interactions menus
The flows and interactions triggers in the viewer header were divs with
a click handler, and their menus lived inside them. They are now
buttons with aria-expanded and aria-haspopup, and each menu sits next
to its button in a wrapper, since a button cannot hold the menu
entries. The play, arrow and tick icons use DS icons. The menu entries
stay as they are.
A visual test covers the interactions menu closed and open; the test
files have no flows, and the flows trigger shares its styles.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewer interactions styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The DS icons take their color from the
text and no longer shrink, so the old icon placeholders go away. The
DS body-small line-height is more compact, so the trigger labels sit
1px higher. The refactor/ import stays only for the shared menu
placeholders.
AI-assisted-by: claude-opus-5-5
* ♻️ Move dashboard subscription styles off the deprecated styles
The subscription stylesheet inlines the deprecated button-style and
flex-center mixins, resolves its legacy color aliases and sizes to DS
tokens, uses logical properties and the DS typography module without a
namespace. Three rules that match nothing (the arrow svg, .cta-link and
.open-arrow) are commented out with a TODO.
The subscription controls need no DS change: the plan card, the current
plan card and the manage subscription link have no DS equivalent, and
they use no legacy icons. refactor/ stays only for the power up menu
item placeholder and the shared dashboard header styles.
A visual test covers the sidebar plan card (closed and open), the team
settings plan and the members seats warnings. No visual change.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the dashboard subscription
The SCSS refactor of the dashboard subscription left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.icon-dropdown svg, .cta-link, .open-arrow. No visual change.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons in the delete shared libraries modal
The delete shared libraries modal (unpublish a library, delete a shared
file or project) gets a ghost DS icon button named "Close", placed on
the tooltip wrapper, and DS buttons for cancel (secondary) and accept
(destructive for the danger style, primary otherwise) instead of raw
inputs. The buttons are narrower and the accept button takes the DS
destructive red.
A new empty library references fixture and a visual test cover the
unpublish library modal from the workspace main menu.
AI-assisted-by: claude-opus-5-5
* ♻️ Move delete shared libraries modal styles closer to the DS
The delete shared libraries stylesheet resolves its legacy modal color
aliases to DS tokens, uses a logical property and the DS typography
module without a namespace. refactor/ stays only for the modal
placeholders (%modal-*), which move with the modals refactor. Two
unused selectors are commented out with a TODO. No visual change.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the delete shared libraries modal
The SCSS refactor of the delete shared libraries modal left rules
commented out with a TODO because the module does not use them, or
because they target an svg inside a DS icon that is the svg itself. CSS
modules scope the classes to this module and no other source uses them,
so they go: &.transparent, .list-wrapper. No visual change.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons and icons in the inspect code tab
"Copy all code" becomes a DS secondary button, and the expand panel
buttons ghost DS icon buttons labelled "Expand panel" (new key, en and
es). The collapse toggles of the CSS and markup blocks stay raw
buttons, since they sit in a 12px column; their arrow uses DS icons and
they get a name and aria-expanded. The copy buttons belong to the
shared copy button and move with it.
A visual test covers the code tab buttons, expanded and collapsed.
AI-assisted-by: claude-opus-5-5
* ♻️ Move inspect code tab styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The 11px uppercase titles become the
12px DS headline-small, the only difference. The code typography keeps
its values with a TODO: the DS code-font names "Roboto Mono", while
the app loads "robotomono". The full-width "Copy all code" DS button
centers its label explicitly. The refactor/ import stays only for the
copy buttons, styled here until the shared copy button moves to the DS.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons in the delete account modal
The close button of the delete account modal becomes a ghost DS icon
button named "Close"; the class that places it goes on the tooltip
wrapper. The cancel and delete buttons become DS secondary and
destructive buttons, so their labels no longer wrap and the delete
button takes the DS destructive red. The delete button keeps its test
id.
A visual test covers the modal at rest and with the close and delete
buttons hovered.
AI-assisted-by: claude-opus-5-5
* ♻️ Move delete account modal styles closer to the DS
The delete account stylesheet resolves its legacy sizes, spacing,
mixins and color aliases to DS tokens and uses logical properties and
the DS typography module without a namespace. The legacy uppercase
title becomes the DS headline-small typography, which differs only in
its size (12px instead of 11px), so the title and the content below sit
1px lower. The organization counts used a fixed dark-theme gray; they
now use the themed DS secondary foreground. Two unused selectors are
commented out with a TODO.
refactor/ stays only for the modal placeholders (%modal-*), which move
with the modals refactor.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the delete account modal
The SCSS refactor of the delete account modal left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.fields-row, .select-title. No visual change.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewport top bar styles off the deprecated styles
The viewport top bars (view only bar of the inspect tab, path edition
bar) already use DS buttons. Move top_bar.scss to DS sizes, borders,
spacing tokens and logical properties, resolve the panel and shadow
color aliases, and drop the refactor import. The unused done-btn class,
the last user of the deprecated button placeholders here, goes away.
The two z-index values have no DS token, so they stay with a TODO.
Add a visual test of the view only bar; it looks the same as before,
pixel for pixel.
AI-assisted-by: claude-opus-5-5
* ♻️ Use the body-small typography in the viewport top bar title
The title only set a 12px font size, which body-small already has.
Use the DS typography instead of the inline size and its TODO. The
view only bar looks the same, pixel for pixel.
AI-assisted-by: claude-opus-5-5
* ✨ Close search and shortcuts panels with Escape
The workspace search and shortcuts panels could only be dismissed with
the mouse, which interrupts keyboard-first workflows.
A new use-escape-to-close hook gives each panel an on-key-down handler
that closes it on Escape while the user is working in it: the latest
pointer press or focus change must have landed inside the panel, or the
panel must hold focus when it opens (a panel that autofocuses its search
box, such as Shortcuts, is closed by the first Escape). Focus left
behind in a panel after the user moved on to the canvas does not count,
so Escape keeps its usual meaning there.
The handler never stops the event. The global Escape shortcut still
runs and finishes in-progress operations such as path drawing; an
earlier version stopped propagation and left a half-drawn path that
failed shape validation on the next commit.
In the search panel an open filters menu takes the first Escape and
the panel closes on the second.
The history panel needs no handler: Escape already closes it through
the :interrupt it emits.
Closes#9540
AI-assisted-by: claude-opus-5-5
AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
* 📎 Add e2e test for closing panels with Escape
Cover the Escape handling added for the layers search and shortcuts
panels with four Playwright cases on the mocked workspace:
- Escape closes the layers search panel.
- With the search filters menu open, the first Escape closes only the
menu and the second closes the panel.
- Escape closes the shortcuts panel right after it opens with `?`. The
panel autofocuses its search box, so the first Escape must close it
instead of only blurring the box.
- A path drawn while focus sits in the shortcuts panel is finished by
Escape, which proves the panel does not swallow the global shortcut.
The first three cases fail on develop. The shortcuts case also fails
when the hook does not seed its tracker from the focused element.
AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
* 🐛 Close History with Escape when opened by shortcut or menu
Only the header button opened History through initialize-history, which
removes the :document-history flag on the next :interrupt. The
Ctrl/Cmd+Alt+H shortcut and the "Version history" menu item toggled the
flag directly, so the global Escape left the panel open. Both now emit
initialize-history, like the header, and keep their own event origin.
Two e2e cases cover the shortcut and the menu; both fail without this
change.
AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
* ♻️ Apply review suggestions to Escape-to-close
Use dom/child? instead of .contains in the Escape hook and drop the
redundant nil checks. Drive the path test through clickAt so its clicks
wait for the path tool. Add a negative Search case: Escape reaches the
panel after the last click landed on the viewport, and the panel stays
open. That case fails when the containment check is removed.
AI-assisted-by: claude-sonnet-5-5
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
---------
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
* ♻️ Use DS buttons and icons in the viewer header
The section buttons, full screen and edit become ghost DS icon buttons
(the last two were spans), with aria-pressed where they toggle. Share
and "Log in or sign up" become DS buttons. The zoom widget becomes a
button that toggles its menu, like the workspace one, with DS zoom
buttons inside. The breadcrumb and the current board name become
buttons with aria-expanded, and the page menu moves next to its
button. The arrow and tick icons use DS icons. The logo stays: it is
brand art.
A visual test covers the header in two sections and both menus.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewer header styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the button placeholders go away.
The section buttons column goes from a fixed 92px to auto, since the
three 32px DS buttons no longer fit; the 1fr side columns keep it
centered. The zoom widget follows the workspace header one. The 11px
project name becomes the 12px DS headline-small. The global .selected
rule also highlighted the current page in the page menu: that row
keeps its highlight through its own rule. The refactor/ import stays
only for the shared menu placeholders; the options z-index (10) has no
DS token and keeps a TODO.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icon buttons in the viewer board navigation
The previous, next and first board buttons of the viewer become
secondary DS icon buttons with the DS arrow and reload icons. They keep
their size and the viewer background at rest; on hover they take the DS
secondary look. The first board button had no accessible name; it is
now labelled. The position classes move to the tooltip wrappers, so the
fullscreen and comments sidebar offsets still apply.
New key: viewer.go-to-first-board (en, es). A visual test covers the
buttons at rest and on hover.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewer styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the file no longer imports
refactor/. The section scrollbar uses the DS custom-scrollbar mixin; the
text selection and placeholder styles that %new-scrollbar gave to the
comments layer stay, with DS tokens of the same value. An invalid
padding-right with four values, which the browser ignored, is gone.
The DS body-small typography differs from the old one only in a more
compact line-height, so the board counter sits 1px higher. The z-index
values (10, 2) have no DS token and keep a TODO. The .viewer-header,
.not-fixed, .fixed and .frame-children rules are not used by this module
and are commented out with a TODO.
AI-assisted-by: claude-opus-5-5
* 🔥 Remove unused styles from the viewer
The SCSS refactor of the viewer left rules commented out with a TODO
because the module does not use them, or because they target an svg
inside a DS icon that is the svg itself. CSS modules scope the classes
to this module and no other source uses them, so they go:
.viewer-header, .not-fixed, .fixed, .frame-children g. No visual change.
AI-assisted-by: claude-opus-5-5
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
* ♻️ Make the grid track direction icon a button
The direction icon at the start of each grid track value field selected
that column or row from a div with a click handler: no button role, no
name and no keyboard access. Make it a button labeled "Select column" or
"Select row", keeping its class and handler, and reset the browser
border so it looks the same. It stays a raw button: it is a segment of
the track field, not a DS button.
Add the workspace.layout-grid.editor.select-column and select-row keys
(en, es).
AI-assisted-by: claude-opus-5-5
* ♻️ Make the component swap panel groups buttons
The groups of the component swap panel opened from a div with a click
handler: no button role and no keyboard access. Make each group a
button, keeping its class, title and handler, and reset the browser
button styles so it looks the same. Its name is the visible group name.
Add a visual test with a fixture whose components sit in two groups, so
the swap panel lists them.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icons in the viewer comments filter
The "Comments" filter trigger in the viewer header was a div with a
click handler, and its menu lived inside it. It is now a button with
aria-expanded and aria-haspopup, and the menu sits next to it in a
wrapper, since a button cannot hold the menu entries. The arrow and the
ticks use DS icons. The menu entries stay as they are.
A visual test covers the filter closed and open.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewer comments styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties. The DS icons take their color from the
text and no longer shrink, so the old icon placeholders go away. The
DS body-small line-height is more compact, so "Comments" sits 1px
higher. The refactor/ import stays only for the shared menu
placeholders. The z-index values (1 and 10) have no DS token and keep a
TODO.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icon buttons in the viewer thumbnails panel
The expand, close and scroll buttons of the viewer thumbnails panel
become ghost DS icon buttons. They had no accessible name; they are now
labelled, and the expand button says whether the panel is expanded. The
rotated legacy arrow gives way to the DS arrow icons. The thumbnails
stay raw buttons, since they are list items.
New keys: labels.expand and viewer.thumbnails.scroll-left/right (en,
es). A visual test covers the panel collapsed, on hover and expanded.
AI-assisted-by: claude-opus-5-5
* ♻️ Move viewer thumbnails panel styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the file no longer imports
refactor/. The scroll buttons keep their 60px height through
--button-height and still show only on hover; their tooltip wrappers
take the grid cells. The DS body-small typography differs from the old
one only in a more compact line-height, so the counter and the board
names sit 1px higher. The panel z-index (10) has no DS token and keeps
a TODO.
AI-assisted-by: claude-opus-5-5
* ♻️ Use a DS icon in the grid editor track options button
The options button of each grid track on the canvas drew a deprecated
icon and had no accessible name. Use the DS menu icon and label the
button "Track options". The button stays raw: it lives on the canvas,
where the track editor keeps a fixed screen size at any zoom.
The DS icon has a fixed 16px size, so its class scales it down with the
zoom (transform: scale(1 / zoom)), as the old svg rule did with its
width. It looks the same, but for a few antialiasing pixels.
Add the workspace.layout-grid.editor.track-options key (en, es) and a
visual test of a track at two zoom levels.
AI-assisted-by: claude-opus-5-5
* ♻️ Move grid editor styles off the deprecated styles
Move grid_layout_editor.scss to DS sizes, borders, spacing tokens,
colors and logical properties, and drop the refactor import. The canvas
sizes keep their division by the zoom. The grid editor color aliases
resolve to their DS tokens, and the unused close-btn class, the last
user of the deprecated button placeholders here, goes away.
The track editor and the editing bar look the same as before.
AI-assisted-by: claude-opus-5-5
* ♻️ Name the grid track expand buttons
The menu button that expands the columns or rows of a grid layout had no
accessible name. Label it "Columns" or "Rows" and expose its state with
aria-expanded. The track title next to it also toggles on click; it
stays a div, as a larger pointer target for the same action, so the
keyboard does not stop twice on it.
Add the workspace.layout-grid.editor.columns and rows keys (en, es).
AI-assisted-by: claude-opus-5-5
* ♻️ Make the annotation title toggle a button
The annotation title of a component toggled from a div with a click
handler, and it holds the edit, delete, save and discard buttons, so it
cannot become a single button. Move the toggle to a button with the
arrow and the "Annotation" name, with aria-expanded, that takes the
whole free width of the title. While editing or creating, the title
stays text and no longer collapses the annotation on click, as its
cursor already suggested.
Extend the annotation visual test with the collapsed state; it looks the
same as before, pixel for pixel.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS buttons and icons in the export assets modal
The export modal close button becomes a ghost DS icon button and the
progress toast close button an action one, which fits its 24px grid
column; both are labelled "Close". Cancel and Export become DS buttons.
The checkbox ticks and the toast icons use DS icons. The selection rows
stay raw buttons, since they are list rows, and so does the link-styled
retry/cancel button of the toast.
The export frames e2e spec now finds the Export button by role, and a
visual test covers the modal with all, some and no rows selected.
AI-assisted-by: claude-opus-5-5
* ♻️ Make the export progress ring a button
The export progress ring in the header opens the export details, but it
was a div with a click handler. It is now a button named after the
export status, with aria-expanded, and keeps its look.
AI-assisted-by: claude-opus-5-5
* ♻️ Move export assets modal styles off the deprecated styles
Spacing, sizes, borders, typographies and color aliases move to DS
tokens and logical properties, and the button placeholders go away.
The classes left without use are removed: the old modal buttons, the
old close icon, and the file export entries, which live in files.scss.
The refactor/ import stays only for the shared modal and checkbox
placeholders, which move to the DS with the modals and the checkboxes.
The checkbox placeholder shrinks a ticked icon to 12px, which pushed
the DS tick off center: the tick keeps its 16px box now, and does not
shrink inside the 14px flex box either.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icons in the text palette and its menu
The scroll arrows of the text palette and the tick of the selected
palette in its menu used deprecated icons; use the DS arrow and tick
icons instead. The arrows are full-height scroll buttons with a fade, not
DS button sizes, so they stay raw buttons.
Add a visual test that opens a file with 16 local typographies, so the
arrows show, and captures the palette at rest, with the pointer over the
right arrow, after scrolling and with its menu open. Its fixture is the
library fixture with more typographies.
AI-assisted-by: claude-opus-5-5
* ♻️ Move text palette styles off the deprecated styles
Inline the button, flex and ellipsis mixins, move spacing, sizes and
borders to DS tokens and logical properties, and resolve the color
aliases to their DS tokens. The arrow, item and menu colors that change
on hover, disable or selection go through local variables, and the new
icons take them through currentColor instead of svg selectors. The three
typography lines share their ellipsis rule. The .disabled ::after rule
of the left arrow never matched and goes away; :disabled::after already
covers it.
The items, the empty message and the menu use the DS body-small
typography, whose line-height is 1.3 instead of 1.4. The arrows keep
their z-index, now commented, and the menu its legacy context menu one.
text_palette.scss and text_palette_ctx_menu.scss no longer import
refactor/.
AI-assisted-by: claude-opus-5-5
* 🐛 Show the text palette scroll arrows at rest
The enabled scroll arrows of the text palette had no color at rest, so
they only showed on hover, while the color palette arrows show in the
secondary color. Give them the same rest color.
AI-assisted-by: claude-opus-5-5
* ♻️ Use DS icons in the color palette and its menu
The scroll arrows of the color palette and the tick of the selected
palette in its menu used deprecated icons; use the DS arrow and tick
icons instead. The arrows are full-height scroll buttons with a fade, not
DS button sizes, so they stay raw buttons. The color cells are swatches
and stay as they are.
Add a visual test that opens a file with 24 local colors, so the arrows
show, and captures the palette before and after scrolling and with its
menu open. Its fixture is the library fixture with more colors.
AI-assisted-by: claude-opus-5-5
* ♻️ Move color palette styles off the deprecated styles
Inline the button, flex and ellipsis mixins, move spacing, sizes and
borders to DS tokens and logical properties, and resolve the color
aliases to their DS tokens. The arrow and menu item colors that change
on hover, disable or selection go through local variables, and the new
icons take them through currentColor instead of svg selectors.
The empty palette message and the menu items use the DS body-small
typography, whose line-height is 1.3 instead of 1.4. The arrows keep
their z-index, now commented: the left arrow comes before the
positioned color list, so without it the colors would cover its fade.
The menu keeps its legacy context menu z-index, also commented.
color_palette.scss and color_palette_ctx_menu.scss no longer import
refactor/.
AI-assisted-by: claude-opus-5-5