The text options started with Japanese only. Chinese and Korean reused
its controls and helpers, which kept Japanese names, and some rules got
a copy per language.
- Rename text-japanese-layout to text-cjk-controls: the controls every
language panel shares. The Japanese panel and its own controls move to
text-language-layout, next to the Chinese and Korean ones.
- Share one option row, one writing mode row (languages add their radio
groups to it), and one emphasis and clearance select per language.
- Rename the selection, plugin and HTML style helpers from japanese to
cjk, `schema:jl-enum` to `schema:cjk-enum`, and the stored writing
mode helpers to `stored-writing-mode` and `stored-vertical?`, apart
from the rendered ones in writing-mode.
- Build the language label and dropdown from one table of languages,
and rename the profile-* translation keys to language-*.
- Decide the attrs each language renders from one table per level
(root, paragraph, span).
- Serialize korean-wrap through the CJK enum table and RawKoreanWrap.
- Compare the CJK option attrs in a loop in the text menu.
AI-assisted-by: claude-opus-5-5
The CJK text options were behind per-language "Enable layout" toggles in
the file menu and the settings, each with its own sidebar section. A text
now has one language, picked in a single Language panel, and the options
of that language show below it.
- Rename the `vertical-text-layout` flag and feature to `cjk-support`.
- Remove the per-file and per-profile layout toggles, the file options
that stored them and the `:set-text-layout` change.
- Add the Language panel: a grouped dropdown of Japanese, Korean and the
regional Chinese variants, a row to change the language and a button to
remove it, "Mixed" when the selected texts differ. Picking a language
writes it to every paragraph of the texts.
- Render the CJK attrs only under the language that uses them, so a text
without language is horizontal and plain. The stored content keeps
every attr.
- Remember the last picked language for the next new texts.
- Add File > Language: the language of the texts without one of their
own, applied at render time and laid out again when it changes.
- Add the profile text language in Settings > Options: new files start
with it as their file language.
- Extract the grouped dropdown of the board size presets into a shared
component used by the presets and the Language panel.
- Name the Japanese readings furigana, drop the redundant prefixes of
the furigana and pinyin options, move Distribute below the writing
mode row and show only the position sides of the writing mode.
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
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
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
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 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
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.
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
* 🐛 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
* 🎉 Copy v3 editor text with its styles in a payload
Copy and cut in the v3 text editor put the styled selection in a
hidden span of text/html. Add its codec and the clean-up for pasting
into another file or team.
Refs #12047
AI-assisted-by: claude-opus-5-5
* 🎉 Paste text copied in Penpot at the v3 editor caret
Text copied in Penpot keeps its styles when pasted at the caret. The
first paragraph keeps the target's attrs; later ones bring their own.
The context menu paste now shares the keyboard paste path.
Refs #12047
AI-assisted-by: claude-opus-5-5
* 🎉 Paste text copied in Penpot on the canvas with its styles
Refs #12047
AI-assisted-by: claude-opus-5-5
* 🎉 Paste copied text shapes at the v3 editor caret with styles
Copied shapes now record their team. Shapes that are not texts paste
nothing into the editor.
Refs #12047
AI-assisted-by: claude-opus-5-5
* ✨ Replace custom fonts in shapes pasted across teams
Refs #12047
AI-assisted-by: claude-opus-5-5
* ✨ Add rich HTML paste to the v3 text editor
Pasting HTML into the v3 text editor keeps bold, italic, decoration and
text-transform, and drops fonts, colors and links. Gated behind
text-editor-wasm/v1-html-paste.
Refs #10465
AI-assisted-by: claude-opus-5-5
* ✨ Paste HTML on the canvas as a styled text shape
With v3 HTML paste on, pasting HTML on the canvas creates a text shape
with the same emphasis. Clipboard options are now read at paste time.
Refs #10465
AI-assisted-by: claude-opus-5-5
* 🐛 Fix editor v3 quitting when changing typography options
* 🎉 Apply text styles to collapsed caret
* 🐛 Fix not persisting the new selrect
* 🐛 Fix selrect not being recomputed on caret style changes
* 🐛 Fix quitting the editor when changing typography on empty texts
* ✨ Add headless wasm render backend to the exporter
* ♻️ Move render-wasm bridge to common and split wasm builds
* 🔧 Upload builtin font variants in the wasm exporter
* ♻️ Move shared font and resources utils out of render_wasm
* ⚡ Fetch only the exported roots in the wasm exporter
* ⚡ Bound save_layer rects in the vector export path
* 🐛 Fix setting a font-related properties to nil
* ✨ Repair already-corrupted text nodes
* 🐛 Fix deleted fonts appearing in Recents and as the auto-selected font for new text shapes
* 🐛 Prevent WASM panics during WebGL context restore
Delay context-restored until reload finishes and no-op app
mutations while reloading so resize/modifiers cannot panic
mid-teardown.
* 🐛 Fall back to CLJS bool content when WASM is not ready
Returning nil from calculate-bool would persist empty path content
during context loss/reload; use path/calc-bool-content instead.
* 🐛 Fix dropdown shown Mixed Font Families for same family with different variant
* 🐛 Fix variants dropdown appearing blank on mixed variants but same family
* ✨ Add playwright test for mixed font families/variants