* feat(app): show atomic document loading progress - Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress - Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade - Prevent nested page setup and viewport cleanup from revealing partially prepared documents - Cover obsolete sessions, font-resolution ownership, and staged loader UI * refactor(app): scope editor preparation per tab - Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store - Keep Core page work progress-only and inject canvas suspension from the app boundary - Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles - Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal * fix(editor): commit prepared pages atomically - Prepare population, fonts, fallbacks, and layout without changing the visible page - Reject cancelled and stale prepared pages before committing viewport, selection, and page events - Keep the preparation overlay until the committed scene version is presented - Cover call order, cancellation, stale generations, and presentation acknowledgement * fix(app): stage imported documents before commit - Prepare imported graphs in an isolated Core editor before replacing the live document - Share font loading while keeping live selection, graph, renderers, and history untouched during staging - Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass * refactor(app): namespace preparation UI - Move canvas and tab preparation presentations into focused subfolders with concise component names - Share progress and phase presentation helpers across preparation surfaces - Show tab-local preparation status without covering the active canvas for background work * fix(app): cancel preparation work at source - Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes - Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads - Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations - Stop FIG manifest previews from replacing the live graph before atomic document commit * fix(app): cancel storage and DOM preparation - Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints - Reuse merged diagnostics and localized toasts for document, storage, and presentation failures - Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout - Guard stalled first presentation and fix the merged recovery dialog title bindings * fix(app): stage reload and font retry - Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure - Restore page and viewport state only after atomic graph commit with cancellable reload reads - Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement - Keep completed document pixels visible while Retry reports activity in the tab * fix(app): enforce exclusive preparation outcomes - Complete document, storage, recovery, and DOM preparations only after successful commit - Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes - Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes - Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity * feat(diagnostics): record preparation outcomes - Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder - Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket - Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces * chore(app): keep browser font tests with typography split - Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot * test(vue): assert injected render suspension - Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change * test(app): complete atomic preparation contracts - Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary * fix(app): preserve preparation cancellation - Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue * test(text): await fallback font request cancellation Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal. |
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| tests | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.test.json | ||
| tsdown.config.ts | ||
@open-pencil/dom-css
DOM and CSS projection utilities for OpenPencil.
This package is the compatibility layer between OpenPencil's scene graph and DOM-shaped design documents. It is intentionally separate from @open-pencil/core so browser/CSS parser integrations can evolve without adding DOM dependencies to the renderer and editor core.
Installation
bun add @open-pencil/dom-css @open-pencil/core
@open-pencil/core is a peer dependency because @open-pencil/dom-css projects to and from OpenPencil scene graphs. Consumers that only parse/serialize DesignDOM still need the peer installed for the package entrypoint.
Package-local checks
This package can be validated independently from the app shell:
cd packages/dom-css
bun run test
bun run typecheck
bun run check
Package scripts:
bun run test— package-local Bun tests for runtime, conversion, and Tailwind APIsbun run typecheck— type-checkssrc, tests, and package scriptsbun run build— builds the distributabledistentrypointbun run smoke:dist— imports the builtdistbundle and exercises the public APIbun run check— runs typecheck, tests, build, and dist smoke in sequence
The repository also keeps integration/oracle coverage under tests/engine/dom-css and tests/e2e/dom-css. The package-local suite focuses on the library's public API, while the repo-level E2E suite verifies browser getComputedStyle() parity through Playwright.
Runtime model
Use the browser runtime as the high-fidelity source of truth whenever a DOM is available. It uses native parsing and getComputedStyle() inside an isolated sandbox. Prefer sandbox: 'iframe' for production-style conversion because it isolates authored CSS from host-page styles:
import { createBrowserCSSRuntime } from '@open-pencil/dom-css'
const runtime = createBrowserCSSRuntime({ sandbox: 'iframe' })
const document = runtime.parseHTML('<article class="card">OpenPencil</article>')
const styled = await runtime.computeStyles(document, '.card { width: calc(10rem + 16px); }')
The headless runtime is useful for Bun/Node tests, CLI flows, and fast approximate conversion. It supports common selectors, inheritance, shorthands, CSSOM grouping rules, and simple variable/calc values, but it is not a browser replacement. Do not use it as an oracle for browser-only CSS behavior such as layout-dependent computed values, full custom-property fallback behavior, modern color serialization, or UA defaults. Do not expand headless CSS parsing with ad hoc regex/string parsers; add a maintained parser/runtime dependency or use the browser runtime instead.
import { createHeadlessCSSRuntime } from '@open-pencil/dom-css'
const runtime = createHeadlessCSSRuntime()
HTML/CSS to scene graph
The convenience helpers run the full pipeline:
import { htmlToSceneGraph } from '@open-pencil/dom-css'
const graph = await htmlToSceneGraph(
'<article class="card"><h1>OpenPencil</h1></article>',
{
cssText: '.card { display: flex; gap: 12px; width: 320px; padding: 24px; }'
}
)
For DesignDOM output without creating a scene graph, use htmlToDesignDocument().
JSX/DOM authoring
Use the package as a JSX import source when you want DOM-shaped authoring that still flows through DesignDOM, CSSOM, and SceneGraph conversion:
/** @jsxImportSource @open-pencil/dom-css */
import { createBrowserCSSRuntime, jsxToSceneGraph } from '@open-pencil/dom-css'
const graph = await jsxToSceneGraph(
<article class="card">
<h1>OpenPencil</h1>
</article>,
{
cssText: '.card { display: flex; width: 320px; padding: 24px; }',
runtime: createBrowserCSSRuntime({ sandbox: 'iframe' })
}
)
The JSX runtime preserves class, attributes, inline style, text, fragments, and simple function components as DesignDOM. Class semantics still come from generated or authored CSS passed to a CSS runtime; the JSX layer does not interpret Tailwind or CSS utility names directly.
When running in a browser, use the browser-first helpers so native getComputedStyle() is used automatically. Import them from @open-pencil/dom-css/browser so browser bundles do not load headless-only CSSOM dependencies:
/** @jsxImportSource @open-pencil/dom-css */
import { browserJSXToSceneGraph } from '@open-pencil/dom-css/browser'
const graph = await browserJSXToSceneGraph(
<article class="card">
<h1>OpenPencil</h1>
</article>,
{
cssText: '.card { display: flex; width: 320px; padding: 24px; }',
sandbox: 'iframe'
}
)
Use browserJSXToDesignDocument() / browserJSXToSceneGraph() for authored CSS. Use browserTailwindJSXToDesignDocument() / browserTailwindJSXToSceneGraph() only when Tailwind utilities should be compiled at runtime by the host app. Browser Tailwind compilation needs the host bundler to provide Tailwind source CSS or stylesheet loading; see the Tailwind recipes below.
Tailwind pipeline
Tailwind classes flow through Tailwind's own compiler, then through the CSS runtime. Prefer the browser helpers when a document is available so custom properties, calc(), modern colors, and browser-default behavior come from native getComputedStyle().
Browser recipe: precompiled Tailwind CSS
The most portable browser path is to compile Tailwind CSS in the host app, then pass the resulting CSS as normal cssText:
import { browserHTMLToSceneGraph } from '@open-pencil/dom-css/browser'
import tailwindCSS from './generated-tailwind.css?raw'
const graph = await browserHTMLToSceneGraph(
'<article class="flex w-80 rounded-xl bg-white p-6">OpenPencil</article>',
{
cssText: tailwindCSS,
sandbox: 'iframe'
}
)
This also works with JSX:
/** @jsxImportSource @open-pencil/dom-css */
import { browserJSXToSceneGraph } from '@open-pencil/dom-css/browser'
import tailwindCSS from './generated-tailwind.css?raw'
const graph = await browserJSXToSceneGraph(
<article class="flex w-80 rounded-xl bg-white p-6">OpenPencil</article>,
{
cssText: tailwindCSS,
sandbox: 'iframe'
}
)
Browser recipe: runtime Tailwind compilation with supplied CSS
If the app wants to compile utility candidates at runtime, provide Tailwind source CSS yourself. Do not rely on the default Node-oriented stylesheet loader in browser bundles:
import { browserTailwindHTMLToSceneGraph } from '@open-pencil/dom-css/browser'
const classes = ['flex', 'w-80', 'rounded-xl', 'bg-white', 'p-6']
const graph = await browserTailwindHTMLToSceneGraph(
`<article class="${classes.join(' ')}">OpenPencil</article>`,
classes,
{
css: await fetch('/tailwind-source.css').then((response) => response.text()),
sandbox: 'iframe'
}
)
Use loadStylesheet when the supplied Tailwind CSS contains imports and the host app owns import resolution:
import { browserTailwindHTMLToSceneGraph } from '@open-pencil/dom-css/browser'
const classes = ['flex', 'w-80', 'rounded-xl', 'bg-white', 'p-6']
const graph = await browserTailwindHTMLToSceneGraph(
`<article class="${classes.join(' ')}">OpenPencil</article>`,
classes,
{
css: '@import "tailwindcss";',
loadStylesheet: async (id) => {
const url = id === 'tailwindcss' ? '/tailwindcss/index.css' : `/tailwindcss/${id}`
return fetch(url).then((response) => response.text())
},
sandbox: 'iframe'
}
)
Bun/Node recipe
In Bun or Node, the package can load Tailwind's default stylesheet through filesystem-backed module resolution. Without a DOM, this uses the headless CSS runtime; pass a browser runtime only when the process has a real document available, such as Playwright or a browser extension page:
import { tailwindHTMLToSceneGraph } from '@open-pencil/dom-css'
const classes = ['flex', 'w-80', 'p-6', 'rounded-xl', 'bg-white']
const graph = await tailwindHTMLToSceneGraph(
`<article class="${classes.join(' ')}">OpenPencil</article>`,
classes
)
compileTailwindCSS() remains available when callers want to manage CSS compilation and runtime selection themselves.
Current scope
- DOM-shaped
DesignDocument/DesignElementtypes - Browser-backed runtime adapter for native HTML parsing, serialization, and computed-style extraction
- Headless runtime adapter with
parse5HTML parsing and CSSOM-backed style computation for basic selectors, nested CSSOM rules, cascade order, inheritance, common shorthands, and simple custom-property/calc values - SceneGraph ⇄ DesignDOM conversion for simple HTML/CSS-shaped layouts, including flex alignment/wrapping, self alignment, absolute positioning basics, logical padding, independent side borders, constraints, clipping, opacity, typography, and shadows
- JSX runtime helpers for DOM-shaped authoring into DesignDOM and SceneGraph
- Tailwind v4 compiler adapter
- Browser oracle fixtures for CSS variables,
calc(), sandboxed browser-runtime output, modern color output, JSX/Tailwind browser helpers, and Tailwind utility output
Roadmap
- Expand reusable fixtures: inputs, badges, nav/menu rows, dialog shells, and richer cards
- Map more computed CSS properties to scene graph fields through browser-native computed style or dependency-backed parsers: richer shadows, typography details, position constraints, borders, gradients, and grid once OpenPencil's grid support matures
- Improve SceneGraph → CSS export so generated HTML/CSS is useful for JSX, Tailwind, and web export
- Keep
@open-pencil/dom-cssstable before splitting lower-level file-format packages such as future@open-pencil/kiwiand@open-pencil/fig