Image sizing, alignment, and full bleed; Paper's heading scale (#109) - #110
Merged
Merged
Conversation
Layout metadata rides in a Pandoc-style attribute block after the image, so width and alignment survive /:id.md, the EPUB, the print page, and an agent read rather than living only in the editor. align=left and align=right float the image and text wraps beside it; center and an absent align keep their own line. At the 65ch column a 50% float leaves about 32 characters beside it, which the doc records as the weakest part of the design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An image alone in a paragraph may now carry a Pandoc-style attribute
block: `{width=50% align=left}`.
Width is a percent from 1% to 100% or the token `full`; align is left,
center, or right. Both default to null, so a document without a block
serialises byte-identical and no existing round-trip changes.
Unrecognised keys and out-of-range values are dropped rather than
carried, so the node never holds what it cannot serialise back. Text
after the image that is not brace-shaped leaves the paragraph as it
was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The export parser is a separate MarkdownIt instance that knew nothing
about the attribute block, so `{width=50% align=left}` would have
printed as literal text in both the EPUB and the print page. It now
gets the same rule, applying the layout as inline CSS on the <img>.
Width, align, and the full-width rule move to app/shared/image-layout.ts
so both parsers share them without pulling ProseMirror into the Worker
bundle. READING_CSS clears floats at headings, rules, tables, code, and
blockquotes, so a float runs beside body text and stops at the next
structural boundary.
`full` is 100% of the text width in both exports: READING_CSS sets a
centred 42em column and there is no canvas outside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The node view puts data-width and data-align on the wrapper and sets the width inline, since an arbitrary percent cannot be a static rule. Full bleed drops max-width to escape the 65ch column, which costs nothing here: .tiptap is a left-aligned padded block rather than a centred column, and the comment rail is a flex sibling that keeps its space. Floats clear at headings, rules, tables, code, blockquotes, and non-floated attachments. Below 640px alignment drops and every image takes its own line, because a 50% float leaves too little text beside it to read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fourth bubble context, detected before the text gates because a node selection on an atom spans no text and the emptiness and textBetween checks would both reject it. Four width steps, full width, and three alignments, each a single setNodeMarkup and so a single Yjs update. Choosing an alignment on a full-width image steps the width down to 50%: nothing wraps beside a full-width image, so the alignment would be inert where it was just asked for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things running it proved wrong. The design said .tiptap is a left-aligned padded block and full bleed cost nothing but a dropped max-width. The editor is inside `mx-auto w-full max-w-3xl`, a centred 672px column, and a full-bleed image measured 630px, the same as a heading. It now breaks out with container units against .doc-frame, the flex sibling of the comment rail; the viewport would be the wrong unit because it includes the rail. At a 1400px viewport the frame is 1120, the text column 630, and full bleed 1078 centred with symmetric gaps, stopping before the rail. BubbleMenu renders its children into a portal and does not re-render them per transaction, so the pressed state read a stale editor.state and every button showed unpressed on a node carrying a width. It reads through useEditorState now. Material Symbols is loaded as a named subset, so the four new icons rendered as raw text until they were added to icon_names in root.tsx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#109) The filename was drawn under every image as a figcaption, which read as a caption the author never wrote. It is gone; `alt` still carries the filename. A floated image now stops at a third of the column, in the editor and in the EPUB and print stylesheets, so the text beside it keeps a readable measure. Dropbox Paper caps its own floats at the same third. Headings take Paper's extended-headings scale: 30px/36px at -0.4px, 24px/30px at -0.2px, 20px/26px, all at weight 600, with Paper's margins. The face stays the system sans. The editor sizes them in em rather than rem because the root here is 14px, which would shrink the whole scale by an eighth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rwise (#109) The bubble offered 25, 50, 75 and 100 percent. It now offers full bleed and the three alignments, nothing else. An image with no `width` in its markdown renders at its natural size, capped by the column; a left or right aligned one is capped at a third of it. Full bleed becomes a toggle, and aligning a full-bleed image clears the width rather than stepping it to 50%. A centred attachment shrinks to its image, or the auto margins have nothing to centre: the wrapper is a block and was filling the column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tall image could take the whole screen whatever its width said. The cap is in app.css and in READING_CSS, so the EPUB and print pages carry it too. It holds the aspect ratio rather than letterboxing, which means the height can be the binding constraint: measured at a 768px viewport, a 400x3000 image at width=100% renders 82x614 in a 630px column, ratio 0.133 either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trigger showed `comment`, which read as "leave a comment" for a button that opens the whole menu. It shows `menu` in plain Edit; Suggest and Markdown still replace it with their own glyph. `menu` joins the named Material Symbols subset in root.tsx and `comment` leaves it with its last use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
natural goes from 180-320ms between chunks to 60-107ms, and its sentence pause from 300-900ms to 100-300ms; fast goes from 20-40ms to 7-13ms. Chunk sizes are unchanged, so the granularity reads the same and only the rate moves: natural measures about 36 chars/s against 12 before. The doc comment no longer calls natural "a brisk human typist". 36 chars/s is roughly 430 words a minute, and the MCP `pace` description now says what the setting does rather than claiming it is human-paced. The tests were named for a 30-80ms natural and a 10-20ms fast that the code never ran at; their assertions were loose enough to pass either way. They now pin both ends of both ranges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both filters that hide resolved threads exempt the active one, and resolveThread left activeThreadId pointing at the thread it had just resolved, so the card sat there until something else was clicked. It now clears the selection on resolve, which deleteThread already did twenty-seven lines further down the same file. This is option 1 from #111: the rail's "Show resolved (n)" is the only undo path, and the mobile sheet still has no such control. Reopening is unchanged and leaves the selection alone, since the card is already in view under Show resolved. Closes #111 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes #109.
Images carry their layout in the markdown, as a Pandoc-style attribute block after the image:
widthis a percent from 1% to 100% or the tokenfull;alignisleft,center, orright. Both are optional and both round-trip. Pandoc readswidth=50%natively, which is why the brace block was chosen over the title slot, the Obsidian alt pipe, or class tokens; the cost is that GitHub prints the block as literal text after the image. The design, the alternatives, and the amendments made during review are indocs/plans/2026-09-13-image-sizing-design.md.What it does
widthrenders at its intrinsic size, capped by the column.align=left/align=rightfloat, and body text wraps beside the image until the next heading, rule, table, code block, or blockquote. Floats are capped at a third of the column so the text keeps a readable measure. Dropbox Paper caps its own floats at the same third.width=fullbreaks out of the centred column using container queries against.doc-frame, so it measures the space beside the comment rail rather than the viewport. There is an@supports not (width: 1cqw)fallback.width=100%renders 82px wide in a 630px column.fullcomes from the markdown.alt.em, notrem: the root font-size is 14px, so the old1.875remrendered an h1 at 26.25px against Paper's 30px.The EPUB and print pages get the same layout as inline styles, since neither has a per-image stylesheet hook. There,
fullmeans 100% of the text width: both are a centred 42em column with no canvas outside it.Shape
app/shared/image-layout.tsis pure and free of ProseMirror, so the editor schema and the Worker's export path share one parser, one serializer, and one set of rules.widthandaligndefault tonull, so every existing document serialises byte-identical.Verification
Measured against a local dev instance at a 630px column, 768px viewport:
align=left, 400x200align=left, 80x80align=center, 400x200{width=60% align=right}, 400x200width=full, 1600x300Headings measured in the editor: h1 30.19px/36.23px at weight 600 and -0.39px; h2 24.15px/30.19px, -0.19px; h3 20.13px/26.16px. Margins 30/12, 26/10, 24/8.
Also verified: a click on Align right reaching
/:id.mdas{width=50% align=right}; the print page carrying inline styles rather than literal braces; the bubble's buttons reading Full width, Align left, Align centre, Align right and nothing else.Not verified: no EPUB was opened in a reading system, so whether float and
vhbehave there is unknown.Tests
272 pass across
tests/unit/shared,tests/unit/components, and the image-command suite, including new files forimageLayoutStyleand for the bubble's layout row. Lint and typecheck clean.The three localStorage suites (
safe-storage,use-theme,anon-identity) fail on Node 25 withlocalStorage.clear is not a function. They fail identically onmain;CLAUDE.mdrecords this as an environment issue.Deployed
This branch is live on vapor.fyi as version
1d46aa0b-60ca-45f5-9bfb-5155e60ad778, deployed from the branch before merge at Nicholas's direction.🤖 Generated with Claude Code