A capture of a real page (search results, marketing sites) came in
with smeared paragraphs, invisible gradient headings, mistinted icons
and a mid-page navbar. Each had a distinct root cause in how the
extractor read the DOM or how the importer re-laid it out:
- Fold gates: Chrome computes the -webkit-line-clamp idiom as
flow-root, and undecorated single-line inline-block chips (search
result dates) are text flow — both blocked the inline fold and left
per-child union rects painting over each other.
- Partial folding: consecutive inline-flow children between
block-level siblings fold as runs, so a paragraph interrupted by a
list still folds the text around it. Script/style sources and
display:none subtrees are excluded from the folded segments.
- Per-line splitting: a wrapped bare text run that cannot fold is
emitted as one node per line box (binary search over character
offsets), each at its true rect — the union box anchored every line
at the block's left edge and overlapped whatever shared those lines.
- Line counting: vertical-band grouping with a mostly-overlapping
criterion, so nested-span fragments and bidi runs do not over-report
and tight display leading (line-height 1.05 under a CJK glyph box)
does not merge real lines into one.
- Single-line leading clamp: the capture measures glyph boxes, so the
page's half-leading is already in the captured y; re-applying a
vertically-centring line-height (40px footers) pushed runs a dozen
pixels below their neighbours.
- Gradient text: background-clip:text moves the box fill onto the
descendants' glyphs (first gradient stop) instead of painting a bar
over transparent text; -webkit-text-fill-color wins over color.
- Icon paint: each shape's computed fill/stroke is inlined into the
serialized svg clone — a standalone data URI loses the stylesheet
rule that actually painted the icon, and currentColor then leaked
the inherited theme accent.
- Multi-colour flat svg art becomes one path node per consecutive
same-fill group instead of an undecodable raster fallback.
- position:fixed subtrees capture in viewport coordinates; adding the
scroll offset dropped a scrolled page's navbar into mid-document.
- The element-picker overlay marks itself data-openpencil-ui and the
extractor skips it.
A block whose children are all inline (text plus <a>/<code>/<span>) was
captured as one node per inline child, and a wrapped run's rect was the
union of its line boxes anchored at the block's left edge — so
consecutive runs shared an origin and painted on top of each other
(the "Tehnegindearing paints..." smear in rich paragraphs and tables).
Fold an inline-formatting context into a single text node positioned
once at the inline content's own box, carrying per-run styling (link
colour/underline/href, code monospace, bold/italic) as segments with
CSS whitespace collapsing across inline boundaries. Single-<code>-only
cells are left unfolded so their pill background survives.
Captures from the current extension (no segments) still import as plain
text. Resolves the follow-up noted in da83157b7.
A multi-phase campaign to make importing real modern web pages
(Tailwind output, landing pages, component-library HTML) faithful.
Layout: bake CSS transforms (translate/scale/rotate incl. the
translate(-50%,-50%) centering idiom), emulate flex-wrap via row
chunking, honor grid span/line placement, apply position:relative
offsets through synthetic wrapper frames (jian has no offset-without-
reflow), aspect-ratio, per-child auto-margin alignment, and a
configurable viewport height (threaded through the CLI and MCP).
Content: list markers (incl. reversed/roman/alpha), basic table layout
(tr->row, colspan), <picture>/srcset candidate selection, @font-face
visibility warnings, background-size/position mapped to the image
fill's crop transform, and text-shadow.
Diagnostics: replace the untyped Vec<String> warnings with a typed
ImportWarning enum (stable per-variant codes, byte-identical Display so
CLI/MCP output is unchanged), localized into all 15 locales, surfaced
through a non-modal post-import diagnostics panel wired into both hosts.
The desktop path now forwards every warning, not just the first.
Snapshot import: fix inverted child paint order (the cause of whole
overlays vanishing under full-bleed backgrounds), stop clipping text to
the browser-measured box under font-metric drift, vectorize inline SVG
to editable paths, and capture per-corner radii, background paint,
position/z-index, video posters, page background, and open shadow DOM.
The extension's tokenless snapshot-ingress route lives here too
(op-host-services), scoped to a chrome-extension origin and the single
insert-only import tool.
Known follow-up: rich inline text runs (links/code spans + wrapping)
can still overlap in the snapshot path.
Completes the stringly-error conversion: 341 remaining
Result<_, String> signatures drop to two documented boundary sites
(a String-compat wrapper consumed across a crate boundary and a
test-only diagnostic helper). Eighty-plus enums follow the
established recipe — one enum per failure domain in a sibling
module, byte-identical Display text, From impls replacing map_err
adapters. mcp_live is fully typed (the screenshot channel included)
and all six temporary String bridges are deleted. The flaky
cli-model-discovery trio is made load-proof: exec'd sleeps so the
probe's kill actually closes the pipes, a serializing test lock,
and an escalating budget with a deadline-boundedness assertion.
New leaf crate op-util single-sources hex-color parsing (9 divergent
copies, one with a non-ASCII panic), JSON escaping (one copy was
lossy), and HTML/XML escaping (one copy missed the quote entity — an
attribute-injection gap). Desktop now delegates image generate/search,
settings payload serde, and the --mcp/--serve-web argv dispatch to
op-host-services / op-editor-host-core instead of carrying drifted
copies. Byte-identical widget_host twin files collapse into shared
op-editor-core host_ui_transitions. Auth routes, the MCP port, product
name, env-var names, service URLs, and status colors move to single
shared constants / theme tokens; the stale claude-sonnet-4-5 default
model id is corrected.
Add op-html project import beyond the single-document path: a directory
of HTML/asset files (project.rs) and packaged .zip archives
(project_zip.rs), each resolving relative resources against a virtual
project origin and picking an HTML entry point. Adds flate2 + zip deps
and re-exports the new import surface from lib.rs; the old 10MB
single-source truncation is dropped in favour of per-entry limits
(MAX_PROJECT_FILES / MAX_ZIP_ENTRIES).
Expand HTML/CSS import fidelity, preserve layered fills and accurate fonts across HTML, Figma, and OP files, and make missing-font resolution selectable and case-insensitive. Harden code generation recovery and preview rendering, make model selection immediate, and keep image decode/compositing consistent across native and web hosts. Add mobile dependency and widget-boundary guards so all supported targets retain the intended feature surface.
Rebasing onto the responsive-capable jian left two mechanical breaks.
Both sides had grown a `fill_rule` field on path nodes, so the textual
merge emitted it twice in nine initializers; and the html snapshot
importer predates the responsive fields, so its frame / text / image /
document literals lacked `breakpoint`, `limits`, and `responsive`.
No behavior change: duplicates removed, new fields default.
Vector plan Task 9 (approved design): corner expand toggle with a
TL/TR/BL/BR grid and Mixed state; Path-only nonzero/even-odd
segmented control; effects + menu offering shadow / layer blur /
background blur with slider + input + eye + remove rows; per-effect
visibility persists via the new schema field and scene build skips
hidden effects; native + web dispatch, Escape tiers, i18n across 15
locales. Includes jian pointer bump. no-verify: repo fmt gate trips
on unrelated op-html sources.
Merge Kayshen's feat/responsive-m1a (schema 1.2 + responsive opt-in,
breakpoint ranges, variant tables, transactional variant swap) plus
main's fill-rule/corner-radii/background-blur into the vendored pin
(merge/responsive-m1a-into-main @ 42772b6). Responsive stays double-
gated opt-in, so every existing document renders unchanged.
Adaptation is mostly new optional fields defaulted to None/false. The
one real behavior interaction: jian's node_rect now bakes a root's
authored origin into its returned rect (root_origins, added for the
responsive multi-root canvas), which doubled offsets at the three
places that added the origin themselves — the loader's layout harvest,
App Mode tap solving, and the preview's scene-to-runtime-space mapping
(the last now subtracts the root origin back out, gated on jian's own
is_origin_normalized so responsive roots stay untouched).