Dev - #16
Merged
Merged
Dev#16
Conversation
…/useCommands Phase G of the v2.0 brief. No behaviour change beyond two deliberate fixes. - hooks/useDialogs.ts one reducer replaces 15 overlay useStates; open/close/ toggle and the per-name thunk lookups are identity-stable so effects and memos never re-run on a dialog toggle. - hooks/useAiActions.ts all AI assist behaviour (staged suggestions, prompt driven actions, ghost completion). - hooks/useDeepLinks.ts the ?template=/?skin= allowlist effect. - hooks/useCommands.ts builds the 40-entry command list from injected handlers. App.tsx: 1150 -> 832 lines. Fixed along the way: - the AI abort controller was never aborted on unmount, leaving a request in flight after navigation/lock; useAiActions now aborts on cleanup. - the editor activity rAF was never cancelled on unmount. - command-palette AI entries bypassed handleAiAction, so those invocations were missing from the 'AI Action' analytics event; every entry point now routes through the one dispatcher.
Phase A of the v2.0 brief. A hard split between the screen-only Stage and the
sacred Sheet: every ground, texture, light, paper treatment, ruler and motion
lives OUTSIDE `.scripto-doc`, which the export path still clones verbatim.
New module `src/preview/`:
- stage/stages.ts 21 stage descriptors as DATA, typed
Record<DocumentSkin, StageDescriptor> so tsc proves all 21
exist. Visuals are CSS keyed by [data-stage='x'].
- stage/stage.css token layer (depth/ground/edge/motion scales) + the 21
stages. Pure CSS gradients: 4.94 KB gzipped, 0 image bytes
(budget was 12 KB).
- motion/vocabulary the 10 shared primitives (rise/wipe/draw/type/snap/spring/
glow/cascade/bloom/stamp) plus a small per-skin signature
table, so stages share primitives without bespoke code.
transform + opacity only; never applied to `.scripto-doc`.
- motion/useStageTransition interruptible skin-to-skin transition, veil run and
the brief stage-name label.
- chrome/ PreviewToolbar (Flow/Pages/Focus, zoom, stage level),
SkinRail (hover to preview, click to commit, full keyboard
listbox), PageRuler, FocusOverlay, PreviewEmpty.
Preview.tsx is now a thin adapter; PreviewHandle.getDocElement() is unchanged.
The stage sizes itself with container queries rather than viewport units: the
preview pane is often a third of the window, and `vw` sizing left the sheet
absurdly narrow next to an open settings panel. The rail, the toolbar labels and
the zoom stepper all respond to the pane's own width.
Also fixes tests/visual/harness.ts, which produced no output at all on Chrome
>=132: that release removed the old headless mode, and the new one ignores
--virtual-time-budget, so --dump-dom snapshotted the page long before Paged.js
finished. The harness now drives Chrome over the DevTools Protocol and polls for
the probe. The suite went from 0/25 to 19/25 — and the 6 remaining failures
reproduce identically on bc6eba0, so they are pre-existing layout bugs the
harness simply could not surface before, not regressions from this work.
Phase B of the v2.0 brief. A UI layer on top of the directive system that
`remarkPageDirectives` already ships — no new pipeline.
- pdf/pageBreaks.ts pure, tested Markdown mutations: insert/remove a
`::page-break`, wrap a run in `:::keep-together` or
`:::landscape`, and map a body line to a document line
through the front-matter offset.
- preview/print/ usePageBlocks reads the paginated DOM back to source lines
via the `data-source-line` anchors scroll sync already
uses; PageBreakLayer draws the affordances.
- PrintPreview "Break here" between blocks, a removable chip on each
existing break, click / shift-click block selection with
Keep together + Landscape, an inline landscape prompt on
any block too wide for the page, and the fitToPage result
surfaced as a warning strip ("N scaled / N still clipped").
Every action rewrites the Markdown through the normal setter, so undo, the
editor and the export stay in step; the rendered DOM is never authored.
An insert snaps to the nearest block boundary rather than splitting a paragraph
in half — which also makes the edit exactly two lines, so insert then remove
restores the document byte for byte. Covered by 15 unit tests and verified live:
inserting took the sample from 5 to 6 pages, and removing it returned the
document to its original 3,373 characters.
Also fixes a bug found while verifying: `.page-break` rules are zero-height by
design, so the visibility guard rejected them before they could be recognised as
markers, and no break was ever removable.
Phase C of the v2.0 brief; the last feature RENDERING_AUDIT.md §8 listed as
unstarted.
- ExportPreset { id, name, config, createdAt } at `scripto:export-presets`.
- lib/presets.ts holds the one merge rule every preset path now shares, so the
built-in themes and user presets can never drift apart. A named margin preset
wins over the config's current margins, exactly as applyPreset always did.
- A preset captures the presentation half of the config and never the document
`meta`: a title and author belong to a document, not to a house style. Import
strips `meta` back out of anyone else's file too.
- PresetsDialog: save the current settings, apply, rename, delete, and move
presets between machines as .json — the closest thing to a shared house style
without a backend. Reachable from the settings panel, the theme gallery and
⌘K.
- Imported files are untrusted: every entry is narrowed individually and
anything malformed is dropped rather than poisoning the library.
12 unit tests cover the merge rule, capture and the import validation.
Phase D of the v2.0 brief. The hypothesis in PAGEDJS_RTL_DEBUG_PROMPT.md was wrong: the trigger has nothing to do with RTL. Paged.js 0.4.3 lays out two pages and then stops dead whenever an <img> has to be placed at a page boundary. It is a stall, not a runaway loop. Bisected from the `checklists` fixture down to a minimal repro; ruled out RTL, task lists, `break-inside: avoid`, `overflow-x`, undetermined image boxes, display mode, image preloading, trailing content and an explicitly sized wrapper. Replacing the `<img>` with any non-replaced element paginates cleanly every time — the chunker cannot cope with the replaced element itself. Shipped here: - tests/visual/fixtures/image-stall.md, the minimal repro. - An `it.fails` tripwire that passes while the bug exists and starts failing the moment it is fixed, capped at 25s so the suite stays fast. - `paginate()` takes a per-call timeout. - docs/PAGEDJS_IMAGE_STALL.md: every hypothesis tested, why no fix has shipped, and the exact next step. No workaround is shipped on purpose. The obvious one — swapping the img for a div with a background-image — needs `print-color-adjust: exact` to survive a real Save-as-PDF, and getting that wrong would make images silently disappear from exported PDFs. That is a worse bug than the one being fixed, and it needs verification in a real print dialog first. The 45s watchdog already turns the stall into a clean error, so users are not left with a frozen tab.
The 21 skins only ever differed in headings, blockquotes and table headers: callouts had no per-skin rule in 14 of them, code blocks in 14, rules in 13 and lists in 15-18. Every skin therefore drew the same rounded pastel callout, which is why 21 skins read as about three. Introduces a component token layer on `.scripto-doc` — radius, hairline width, accent edge, fill strength, shadow, rule weight, label font/spacing/case — that the shared components consume. A skin now redeclares a handful of values instead of restating the components, so Brutalist gets hard 3px borders and offset shadows, Swiss a single heavy bar, Blueprint drawn boxes with `// LABEL`, Terminal `[NOTE]` machine labels, Newsprint printed rules, Elegant hairlines and letterspacing, Playful stickers. +1.5 KB gzipped for all 21. Also fixes a real bug found while auditing: the Terminal and Dark skins declared a light-on-dark palette unconditionally, but the sheet they render on is white and print.css forces the paginated preview back to dark-on-white. The live preview showed nearly unreadable pale text for a PDF that was perfectly legible — a straight violation of "preview === PDF". Both now carry a light-sheet palette by default and their light-on-dark one under `.dark`, exactly as the base `.scripto-doc` rules already do. The dark atmosphere lives on the Stage, where it costs the export nothing. Exported PDFs change by design (this was the agreed scope). The visual layout suite reports the same 19 passed / 6 pre-existing failures as before, so nothing regressed in pagination.
…eaching
21 -> 29 skins. Each one exists because a real document needs it, not to pad
the count, and each carries its own stage, marketing page and Arabic label.
- invoice lining tabular figures, uppercase column heads, the last row ruled
off as a total, money aligned to the end edge.
- contract justified serif in numbered clauses, centred uppercase title,
nothing decorative near the terms.
- letter letterspaced masthead over a single accent rule, then generous
paragraphs and room for a signature.
- journal justified and hyphenated, small-caps section heads, and the opening
quote set as an abstract.
- thesis chapter openers over a rule, indented continuation paragraphs,
wider leading.
- changelog versions as accent chips over tight dashed lists.
- rfc monospaced status block, uppercase numbered sections, double rule.
- handout large type and one topic per printed page (`break-before: page` on
each section, in the print stylesheet only).
The build picked all eight up automatically: 29 prerendered skin pages, 112
pages total, 110 sitemap URLs.
Deliberately not shipped: a two-column journal body. Multi-column fragmentation
is exactly the kind of thing the paginator already struggles with (see
docs/PAGEDJS_IMAGE_STALL.md), and a trustworthy PDF matters more than a second
column. The journal skin earns its identity through typography instead.
Visual layout suite unchanged: 19 passed, 6 pre-existing failures, 1 expected.
…t real
Two problems. First, 37 of the 54 templates declared no skin at all — only the
newest 17 ever did — so opening most templates produced a generically-styled
document and left the user to go and find the right look themselves. Every
template except Blank now declares its skin, accent and, where it matters,
margins, numbering and a table of contents. With the eight new skins the mapping
is close to one-to-one: Invoice to `invoice`, NDA to `contract`, letters to
`letter`, academic to `journal`, RFC to `rfc`, changelog to `changelog`,
onboarding to `handout`, the Mermaid diagrams to `blueprint`.
Second, the content was placeholder soup — {Topic}, Task A, "…" — which is
exactly what a template should not be. A template's job is to show what a good
document of that kind looks like, so the reader can paste their own facts over
something already shaped correctly.
Rewritten with credible, specific content: meeting notes with real decisions and
owners, a status update with believable metrics, OKRs with actual start and
target numbers, a retrospective that carries an item forward from last time, an
ADR with a status block and a rollback plan, a bug report with a real root cause
and the offending snippet, an invoice with line items and VAT, and a press
release with a dateline, quotes and boilerplate.
…ents
Two bugs, both found from a real document a user pasted in.
**Double-numbered headings.** `startsWithManualNumber` only recognised an
author's own numbering when it was followed by `.` or `)`, or was
dotted-multipart. A document numbering its sections `# 1 · META`, `# 2 · TikTok`
was therefore not recognised, so auto-numbering prepended its own counter and
spent a counter on each — rendering "2. 1 · META", "3. 2 · TikTok", with every
following number one out. The detector now also accepts `·`, `—`, `–`, `-`, `:`
and `|` as separators.
It stays deliberately generous, because the failure modes are not symmetric:
treating a number as manual when it wasn't merely leaves that heading
unnumbered, while missing one produces visibly broken output. The same predicate
drives `bakeHeadingNumbers`, so the preview and the PDF agree either way.
**A partial document record could crash the app.** `importDocs` shallow-merged
untrusted JSON over the defaults, so a file carrying `meta: { title: 1 }` — or
no `meta` — reached the UI and threw on `config.meta.title.toLowerCase()`. The
same was true of any half-written localStorage record, and because the state
survived the reload, the error boundary's "Try again" could not recover: the
only way out was clearing site data.
Validation now happens at the boundary, in a pure `lib/documentRecord.ts`:
records without string content are dropped, ids and timestamps are replaced when
unusable, and configs are completed field by field including nested meta,
margins and custom size. `loadLibrary` also repoints `activeId` when it does not
match a surviving document. Imported records get fresh ids so they cannot
collide with existing ones.
Covered by 8 new tests for the numbering separators and 10 for the record
validation.
The rail's cards were a generic mini-sheet — three grey lines on the stage's ground — so with 29 skins they all read the same, which is the opposite of what a skin picker is for. Each card is now a real `.scripto-doc` in that skin, laid out at full size and scaled down. The thumbnail is the skin itself rather than a drawing of it: Modern's bold sans, Classic's serif, Résumé's letterspaced caps, Blueprint's mono `//`, Terminal's `>`, Corporate's accent — all visible at a glance. A new skin gets a correct thumbnail with no extra work. Also: - The catalogue gained a `group`, and the rail is now sectioned into Essentials, Business, Technical, Editorial, Academic and Expressive. 29 items in one flat strip was no longer scannable. - Each card carries its name, with a flyout on hover giving the full name and the stage it opens onto. - Options moved from `<button>` to `role="option"` elements: a button inside a listbox was the wrong role, and it could not legally contain the heading and paragraph the miniature needs. - Ends of the list fade so a scrollable rail looks scrollable, and the selected card is marked by both a ring and its label. The miniatures are safe next to the export path: every consumer of the live document reaches it through `PreviewHandle.getDocElement()`, and the only `.scripto-doc` query in app code is scoped to a paginated page.
The rail was a permanent 92px strip pinned beside the settings panel. With 29 skins it forced 9px labels and miniatures too small to read, it made a third column of chrome next to the editor and the settings panel, and it took that width away from the preview for the whole session in exchange for a picker people use occasionally. It is now a panel opened from the toolbar, which also shows the current skin's name. Inside: a search field, sticky group headings, and a two-up grid of cards large enough to read. Each card names the skin and the stage it opens onto, and is still a real `.scripto-doc` in that skin — the card is the skin itself rather than a drawing of it, so a new skin needs no artwork. The last line fades so the crop reads as intentional. Hover still previews without writing to config, click commits and closes, arrow keys walk the grid, Enter applies, Escape and click-away close and restore the committed skin. The preview keeps its full width when the palette is shut. Also adds roughjs, perfect-freehand and opentype.js for the handwriting engine. roughjs dedupes against the copy Mermaid already ships, so it costs no extra bundle; the other two are lazy-loaded only by the flows that need them.
Handwriting as its own axis rather than a skin: hand x ink x stationery x neatness x slant x variation x aging, composing with all 29 skins instead of needing 20 more of them. Data model - `HandConfig` on `PdfConfig`, with front-matter mapping for `hand`, `ink`, `stationery`, `neatness`, `aging` and `drawn`, validated against the unions the same way every other key is. - `hand: 'none'` is a total no-op, and there are tests that say so: no attribute, no custom property, no stylesheet match, no font request. Determinism - All jitter is a pure function of `(seed, wordIndex)` via mulberry32 — never Math.random. Fresh randomness would change word widths on every pagination, which means different page breaks each time Print Preview opens, a preview that no longer matches the PDF, and a document that shimmers as you type. - A word's bucket does not depend on how many words precede it, so editing the top of a document cannot reshuffle the bottom. Variation - `rehypeHandwriting` wraps words — never characters. Per-character spans destroy Latin kerning, and for Arabic they are catastrophic: it is a connected script, and splitting a word breaks shaping into unreadable isolated forms. - Jitter is 16 (or 32) CSS bucket classes, not an inline style per word: a 100-page document would otherwise carry ~50,000 style attributes into the DOM, the export clone and the paginator. Neatness, slant and aging are therefore single custom-property changes — instant, with no re-wrap and no re-pagination. - Code, maths and Mermaid keep their own typeface and are presented as artefacts taped into the page. Setting them in a handwriting font is the mistake every handwriting theme makes. - Mermaid draws its sketchy variant natively, seeded from the same document seed. Rule locking - Ruled paper locks the whole vertical rhythm to the rule pitch in absolute units, and snaps the top margin to a whole rule. A derived unitless leading was tried first and abandoned: the rounding error accumulates down the page until the text visibly drifts off the lines. Everything is written by hand - Headings, table headers and cells, callout titles and bodies, list items and their markers, definitions, blockquotes and footnotes all take the hand. Inheriting from the root was not enough — skins set `font-family` directly on those elements, and a direct declaration beats an inherited one. Arabic - Aref Ruqaa, Lateef and Mirza, filtered to RTL documents, with the slant control hidden because slant is meaningless for Arabic. Verified rendering RTL with connected letterforms across headings, lists, tables and callouts. Fonts load on demand, never from the global URL in index.html, and a hand is never applied before its face can be measured — the fallback flash and the metric jump that follows are the worst thing this feature can do, and if Paged.js measures during that window the PDF paginates against the wrong font. A hand arriving from front-matter or a template loads too, not just one picked in the UI. Failure is reported plainly rather than silently falling back. 51 new tests. roughjs dedupes against Mermaid's copy; perfect-freehand and opentype.js are installed for Release 2 and lazy-loaded only by the flows that need them.
… numbering Hand-drawn furniture - Rules, bullets, checkboxes, table borders, callout boxes, quote brackets, highlighter swipes and link underlines are inline SVG strokes, three variants each, chosen by an nth-child rotation. Deterministic without a line of JavaScript, so it survives the export clone and prints as vector. - Boxes use `border-image` rather than a stretched background: with `preserveAspectRatio: none` a wide, short element pulled the box's vertical strokes into black slabs that swallowed the table's first column. - Mermaid draws its sketchy variant natively, seeded from the document seed. Six handwriting-native skins, each with a stage (compile-enforced): handwritten, journal-hand, field-notes, chalkboard, letter-hand, worksheet. 29 -> 35 skins, 118 prerendered pages. Chalkboard sets `print-color-adjust: exact`, since light chalk ink on an unprinted slate would be invisible. Fonts - Seven more Arabic hands: Amiri, Scheherazade New, Katibeh, Noto Nastaliq Urdu, Gulzar, Harmattan and Rakkas — ten in total. - Sixteen more document faces across both scripts, grouped by script in the picker and fetched on selection, never from the global stylesheet in index.html. Right-to-left correctness - Arabic documents number in Arabic-Indic digits, headings and ordered lists alike, in the preview and in the baked PDF numbering. - Numbers are bidi-isolated: "1." was being reordered to ".1" in an RTL paragraph, because the full stop is a neutral character. - The paginator's progress messages were hardcoded English and bypassed i18n entirely, so an Arabic user watched "Laying out pages…" go by in English. Rule masking behind headings is now a toggle, off by default. Masking only the heading words — not the auto-number, not the rule running on past the text — read worse than not masking at all. The print preview now states the two dialog settings that decide whether the export matches it: margins none, scale 100%. Verified via Page.printToPDF that the geometry is correct at those settings; a dialog that overrides them makes the fixed-size page clip rather than scale, and nothing in the app could say so.
…abic **Arabic handwriting fell back to a default face when exported.** Google Fonts serves subsetted `@font-face` blocks keyed by `unicode-range`, and `document.fonts.load(font)` without sample text only resolves the subset covering its default probe string — which for an Arabic family is the Latin one. The face reported as loaded while none of its Arabic glyphs were, so the preview and the PDF quietly rendered in the fallback. Every load and check now passes text in the family's own script. Verified end to end: an exported PDF now comes out in Aref Ruqaa with connected letterforms. **The error boundary was hardcoded to English.** It already translated its heading, but both call sites passed a literal English `fallbackTitle` that overrode it — so an Arabic user saw "Scripto hit an unexpected error." The prop is now a translation key.
A prompt for a fresh session to verify this work commit by commit, written to be adversarial rather than confirmatory: it tells the reader to assume nothing in it is true, treats each commit message as a claim to be checked, and asks for evidence rather than impressions. Includes the baseline worktree setup so "did this regress?" can be answered properly, the expected gate counts, the six known pre-existing visual failures so nobody chases them, and the two harness traps that cost real time here — Chrome >=132 silently ignoring --virtual-time-budget, and the editor flushing its library over a seeded localStorage on pagehide.
Phase E of the v2 brief. Résumé and AI-output traffic is heavily mobile, and the app was giving that traffic a degraded desktop layout: view switching hidden in a 32px header control, the formatting toolbar pinned to the top of the screen where no thumb reaches, and the settings panel sliding in as a side drawer that left a sliver of document visible. - A bottom tab bar owns navigation on a phone: Write, Preview, Settings and a primary PDF action, all at least 48px, inset for the home indicator. The header's segmented control now hides below `lg` rather than duplicating it. - The formatting toolbar moves to the foot of the editor pane on a phone, above the tab bar, and scrolls horizontally as one row instead of stacking three. Its targets are 44px on touch and return to the compact sizes on desktop. - Settings became a bottom sheet with a grab handle, capped in `svh`. A percentage max-height does not resolve reliably against a flex-sized parent, and the sheet grew past its pane and covered the tab bar. Not verified: Save-as-PDF on real iOS Safari and Android Chrome. That is the riskiest part of the product on mobile and it needs actual devices; it is called out in docs/QA_VERIFICATION_PROMPT.md rather than assumed working.
Phase F of the v2 brief. The library — base64 images and all — was serialised into a single localStorage string on every debounce, against a quota of a few megabytes. One pasted screenshot could exhaust it, and the app could only apologise afterwards. - `lib/docStore.ts` wraps IndexedDB, degrading quietly to localStorage where it is unavailable (private windows, hardened profiles, site data disabled). Six tests cover that path, because a throw there would take the editor down instead of falling back. - Migration is non-destructive: the library is copied across, read back and verified before the migration is marked done, and the old key is then left in place — unwritten but readable — so a downgrade still finds the documents. Verified both ways: a fresh profile migrates cleanly, and an existing localStorage library is copied with the original key kept. - The first paint still reads localStorage synchronously, so opening the app is never blocked on a database handshake. - Version history: up to 20 past versions per document, captured at most every few minutes and only when the content actually changed, restorable from a dialog and ⌘K. It never leaves the device. **The vault now covers both stores.** It encrypted `scripto:*` localStorage entries only; leaving the library in IndexedDB would have meant the whole document set sat readable on disk while the UI said "locked". Snapshot, wipe and restore all handle both, and restore still reads vaults written in the old flat shape. Found by testing rather than by reading: writes resolved on the IndexedDB *request* succeeding rather than the *transaction* committing. A wipe therefore returned before it was durable — and `lockNow` reloads the page the instant it returns, which would abort the transaction and leave the documents in the clear. Writes now resolve on `oncomplete`. The wipe was verified against a live database before and after the fix: it genuinely did not clear before.
Phase H of the v2 brief. Actions were tracked but the funnel was not, so there was no way to tell where a first-time user was lost: they arrived, and either a PDF came out or it did not, with nothing measured in between. Added: Export Dialog Opened, Print Dialog Reached, Time To First Export, Paste Detected (rich / plain / image), Page Break Inserted, Stage Viewed, Skin Previewed and Mobile Export Attempted. Two of these answer questions the product has been guessing at. Paste Detected finally measures the paste-to-Markdown path, which has shipped for a while with nobody knowing whether it is used. Skin Previewed records whether a commit followed a hover, which is the only way to tell whether the palette's hover-preview earns its place. Every property stays flat and structural — a skin id, a page count, a paste kind, a duration in seconds. No document content, no PII. The targets are written down next to the events, because a funnel nobody has agreed a target for is just a dashboard: activation is a first export within the first session, target above 35%; the north star is weekly returning exporters.
Adds handwritten letter, journal entry, Cornell lecture notes, recipe card, handwriting worksheet, capture sheet and an Arabic ruqaa letter -- each declaring its hand, ink and stationery in front-matter, with EN and AR names. The menu template was entirely braced placeholders, so it read as a form to fill in rather than a document. Replaced with a real menu whose values happen to be editable.
Under the Arabic interface the whole app is <html dir="rtl">. In print, the sheet stack's overflow then resolves to the *left*, so the page was positioned off the left edge of the paper and Chrome shrank everything to compensate. The result was small, left-clipped output that read as a font problem -- Aref Ruqaa was embedded and drawing correctly the whole time. Forcing the print root to LTR fixes it. Each document keeps its own direction from the dir attribute documentStyle writes onto .scripto-doc, so Arabic still lays out RTL where it matters. Verified over CDP against the real export path (window.print stubbed so the app's own print setup stays applied): with the print root left RTL, text runs x=-17.4..62.7 on a 595pt page; with the fix, x=40.0..360.5, matching an English-UI export of the same document.
Two ways the Save-as-PDF dialog could hand back a different document from the one on screen: Background graphics ships unticked, which silently dropped every painted surface -- ruled and graph stationery, aged parchment, callout and code fills, table banding, skin accents. Every sheet now opts in with print-color-adjust: exact, so the checkbox has nothing left to change. Verified over CDP against the real export path: the page content stream is byte-identical with the option on and off (sha da89eac1, 49047 bytes). Browser headers/footers are likewise inert -- @page already carries margin: 0, so there is no margin box for them to occupy. break-after: page was set on every sheet including the last, so the browser emitted one extra blank page and a 14-page preview saved as a 15-page PDF. Breaking only between sheets fixes it; the dialog and the in-app preview now agree.
…issing Paged.js 0.4.3 stalls when a replaced element has to be placed at a page boundary. flattenImages swaps every <img> for an equivalent background-image box before pagination, sized from intrinsic dimensions so it survives the editor pane and the printed page having different widths. The image-stall fixture goes from an unbounded hang to 3 pages in under 3 seconds. The workaround was written up months ago and held back because Chrome ships 'Background graphics' off, which would have dropped images from PDFs silently. That objection is closed: the sheets and the flattened boxes both carry print-color-adjust: exact, verified by driving the real export with printBackground: false -- the PDF carries the picture as a 64x64 XObject and renders it. Two ordering bugs surfaced on the way, either of which alone would have defeated the swap: preloadImages ran after buildExportContent, so the DOM transforms saw images that had not decoded and reported no intrinsic size. The renderer marks images loading="lazy". Anything below the fold never loads, and an export clone is never scrolled, so those images had no intrinsic size at all. preloadImages now forces eager loading. Three of the six visual failures were the harness, not the product: unclosed fences run to end of document, an indented fence strips that indent from its content, and KaTeX's screen-reader MathML twin is hidden by clipping rather than by size, so it measured as forty overflows. Visual suite: 19/25 with one expected failure -> 26/26.
Release 2 of the handwriting brief, both paths, entirely client-side. Path A imports a font file: magic-byte validation before the bytes ever reach the font system, a 5 MB cap, FontFace registration, and IndexedDB storage. Each hand gets its own CSS family so several can coexist, and config carries which one a document means. Path B draws the alphabet in the browser. Pointer strokes are captured as vectors from the first event and outlined by perfect-freehand, so the drawn path *is* the glyph contour -- there is no raster-tracing step anywhere. opentype.js assembles them and writes a real .otf. Side bearings come from the drawn bounding box, so a narrow i advances less than a wide m; no kerning pairs, which the brief scopes out. Progress autosaves, so a half-finished alphabet survives a reload. Arabic is deliberately excluded from Path B and the UI says why: four positional forms per letter plus shaping is a different project, and shipping something broken would be worse than not shipping it. Verified the generated font round-trips through real Chrome, not just jsdom: 2088 bytes, OTTO magic, FontFace accepts it and document.fonts reports it usable.
The font builder only runs when someone draws an alphabet, but importing it eagerly put ~600 kB of opentype.js in front of every visitor. Lazy so the editor chunk drops from 1.5 MB to 1.3 MB and the builder loads as a 240 kB chunk on first open.
The affinity data existed on all 35 skins but was never surfaced. Badges appear only while the document is handwritten -- on a typeset document they would label a setting that is off -- and 'good' is left unbadged so the grid does not fill with a word that carries no information. Checked in a real browser: a handwritten template shows 25 badges across 35 cards with all three labels; a typeset one shows none.
…imports A HandConfig is small enough to live in a URL, so /app?hand=<recipe> lets someone post their exact setup and have a stranger land in the editor with it applied. Every field is checked against the same allowlists the pickers use, and the custom hand is never encoded -- it names a font only its author's browser has, so a recipe carrying it would arrive broken. The separator is ~ rather than the obvious . because a full stop is also the decimal point, which silently split 0.35 into two fields. Sliders travel as whole percentages so no value can contain a separator. Four hand-aware DOCUMENT_PRESETS: field notes, keepsake letter, Cornell lecture notes, and Arabic ruqaa. The hand axis has eight controls and a good combination is not obvious from the pickers alone, which is exactly what a preset is for. Preset files are shared, so an imported hand block is now narrowed to values we ship rather than cast and trusted -- the same treatment stored documents already got. Stored configs go through the same sanitiser.
…ten in it Audited all 35 skins per section 8.2. Most needed nothing -- a skin that decorates with type and spacing already sits happily under a hand. The ones that fight it now yield only when data-hand is present, so a typeset document keeps its skin exactly as designed: Filled and heavy heading furniture (corporate, memo, letter, poster, brutalist, newsprint) thins to a rule in the ink. A solid accent bar under handwriting reads as a printed heading someone wrote on top of, and a filled title block inverts the text, which no pen can do. Terminal, dark and chalkboard force a light ink whatever the picker says: a dark sheet with dark ink is an unreadable page. Ledger, invoice and contract keep their figures monospace and drop the jitter inside cells. Handwritten numerals in a totals column stop lining up and the reader can no longer scan it. Jitter amplitude already scaled inversely with type size in em units, so that part of 8.2 needed nothing. Two fixes found while auditing: HTML export inlined only document.css, so a handwritten document arrived typeset -- every hand, paper and drawn rule dropped on the way out, and no font link for the hand. The attribution footer is now set in the document's own hand. A handwritten page with a line of sans-serif under it reads as a stamp; in the same hand it reads as the writer signing their work.
Paste-to-Markdown has shipped since MarkdownEditor.tsx:228 and nobody knows it exists. This is the page for it: targets "chatgpt to pdf", "claude output to pdf" and "ai answer formatting", and explains the one thing that actually differs -- pasting a chat answer into Word imports rendered HTML as styled text, while Scripto converts it back to Markdown so the structure survives a later change of skin or paper size. Registered in USE_CASES and USE_CASES_AR, so the prerender picks it up: 125 pages -> 127.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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.
Summary
Type of Change
feat— new featurefix— bug fixdocs— documentation onlyrefactor— code change that neither fixes a bug nor adds a featurechore— tooling / maintenancetest— adding or updating testsperf— performance improvementci— CI / build changesScreenshots
Checklist
npm run typecheckpassesnpm run lintpassesnpm run buildpassesany, immutable updates, logical CSS)console.log(uses@/lib/logger)