feat(studio): edit and style text in the preview - #3077
Open
miguel-heygen wants to merge 1 commit into
Open
Conversation
Double-press a text element in the canvas and the caret opens where you pressed, in the element itself rather than in a panel. Select characters and a small toolbar offers colour, bold, italic and underline, applied to exactly those characters. Styling text needed a capability Studio did not have. The patch vocabulary was inline-style, attribute, html-attribute and text-content, and text-content assigns textContent; the text-field model escapes markup on the way out and refuses a change in child structure. No route existed for a span to reach a composition file. Adds a rich-text operation that has one, guarded by a single sanitiser in packages/core called on both ends of the trip: in the browser so the preview shows what will be saved, and on the server because that is where the file is written. Tags and style properties are a small allowlist, and an unexpected tag loses its formatting rather than its words. Styling is applied by reading the element into a flat list of runs and rebuilding it, not by wrapping a DOM range. Replacing a colour, removing one, and styling across an existing run stop being special cases, and the markup cannot grow with repeated edits. In a flex or grid container the runs go inside one wrapper so a coloured word cannot reflow the element. Also fixes the keyboard: the shortcut guards matched contenteditable=true only, so playback shortcuts ate letters typed into the composition.
Fallow audit reportFound 17 findings. Duplication (15)
Health (2)
Generated by fallow. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Text in the canvas can now be edited and styled where it sits.
Double-press a text element and the caret opens at the point you pressed. Type, and it types. Select characters and a small toolbar appears over the selection with colour, bold, italic and underline, applied to exactly those characters. Enter commits, Escape cancels, clicking away keeps the work. Double click takes the word and triple click takes the lot, because nothing here claims either gesture.
Why
Changing a word previously meant leaving the canvas for the design panel, and styling part of a line was not possible at all.
Not possible for a concrete reason: Studio had no patch operation that could write markup. The vocabulary was
inline-style,attribute,html-attributeandtext-content, andtext-contentassignstextContent. The text-field model escapes markup withescapeHtmlTexton the way out, andbuildTextFieldChildOperationsreturnsnullwhen the child count changes, which throws. A<span>had no route to a composition file by any path.How
A
rich-textoperation, separate fromtext-content. Wideningtext-contentwould have turned the design panel and every other caller into a markup sink at once. The new operation is the only one that can carry markup, which keeps the dangerous one explicit and greppable.One sanitiser, in
packages/core, called on both ends. The client sanitises so the preview shows what will be saved; the server sanitises because it writes the file and a client is not a thing to trust. Two implementations would drift and the drift would be a security bug. The allowlist is small:span b strong i em u br,styleonly, and five paint-only properties. An unexpected tag is unwrapped, so a stray paste loses its formatting rather than the user's words.Styling rebuilds rather than wraps. Wrapping a DOM range is three lines and then every case is a special case: recolouring nests spans that shadow each other, removing a style cannot reach the ancestor that set it, styling across a run has to split it. Instead the element is read into a flat list of styled runs, the delta is applied to a span of characters, and the element is rebuilt. Replace, remove, split and merge stop being cases, and the markup cannot grow with repeated edits.
Layout is preserved in containers that box their children. In flex, grid or
-webkit-box, every child is an item to lay out, so text that was one anonymous item would become several the moment a word was styled, breaking centring and rewrapping the line. The runs go inside a single wrapper there.The selection chrome stands down while editing.
pointer-events: noneon the overlay does not disable a child that setsauto, and the selection box covers exactly the text being typed into. Left interactive it swallowed every press, so the caret could not be moved and characters could not be dragged over.Shortcut guards moved to a shared check. They matched
[contenteditable='true']and missed the editing surface, so playback shortcuts claimed letters typed into the composition:aseeked to the in-point instead of typing ana.Test plan
Suites on this branch: studio 3572 passing, studio-server 431, core utils 49. Around 120 tests are new.
Covered deliberately: the sanitiser against both parsers the codebase uses (jsdom and linkedom, which is what makes "one module, two runtimes" a claim rather than a hope); the server operation for hostile payloads, markup-shaped text, non-ASCII and idempotency; and the styling model for source newlines, emoji whose halves a selection boundary can fall between, a selection dragged outside the element, and flex or grid containers.
Verified by hand in Studio: styled a run, committed, read the span back out of the composition file on disk, reloaded and reopened the element for editing. Element measured 659x337 before and after styling a word inside it.
Known and deliberately left: a formatting span gets a fresh
data-hf-idon each restyle, which is diff noise on a line already being edited; preserving it means threading identity through a model that merges and splits runs. Deleting every character and committing empties the element, which then opens only through the design panel, unchanged from before.