openpencil/packages/dom-css
Danila Poyarkov daef57d52d
build: update dependencies (#873)
* build: update dependencies

Update the AI SDK providers, Vue, Reka UI, Valibot, Zod, es-toolkit,
CodeMirror, Storybook, Playwright, Hono and other dependencies to their
current releases, consistently across workspaces.

The Tauri plugin packages must match their Rust crates, and the new plugin
crates require Tauri 2.12, so Cargo.lock, @tauri-apps/api and the Tauri CLI
move to 2.12 as well.

* build(harness): update the AI SDK harness packages

@ai-sdk/harness 1.0.74 pinned ai 7.0.67, so the workspace carried a second
copy of ai next to the root one; 1.0.138 depends on the same ai release.

The Pi adapter no longer takes a model: HarnessAgent does. The settings
were spread from untyped records, so the compiler could not reject the
stale key and the chosen model would have been dropped; they are plain
literals now.

PiAuthOptions is now PiAuthenticationMode, and auth accepts an environment
record. Pass the gateway key that way instead of writing it into the
process-wide environment while a session is created. Derive the thinking
level from the adapter's settings, which adds 'max'.

* build: hold vue-tsc at 3.3.11

vue-tsc 3.3.12 no longer sees a v-slot binding inside a component that
also has an event listener, so check:vue reports "Cannot find name
'control'" in MCPConnectionEditor and ProfileEditor. 3.3.11 checks them
cleanly.

* fix(ai): keep retryability for provider errors reported mid-stream

From ai 7.0.80 a provider error after the response stream starts is a StreamProviderError rather than an APICallError, so classifyAIChatError lost its isRetryable.

* feat(desktop): accept updates only when signed for their version

Tauri CLI 2.12 records the app version in each updater signature, and
updater 2.13 checks it against the version latest.json announces. With
requireSignedVersion it also rejects signatures that carry no version, so a
tampered manifest cannot pair a newer version number with an older, still
validly signed bundle.

Release assembly now fails when a signature does not name the version
being released, instead of shipping one that installed apps would reject.

* docs: note the dependency update's security fixes in the changelog

* feat(ai): recommend the latest models

The provider packages now know Claude Sonnet 5.5 and Opus 5.5 and the GPT-6
series. Make Sonnet 5.5 and GPT-6.1 Sol the defaults, list Opus 5.5, Fable
5.1, GPT-6 Astra and GPT-6 Luna, and replace the two free OpenRouter models
that OpenRouter no longer serves.

* build: align the fig package's valibot with the workspace
2026-10-04 12:48:24 +00:00
..
scripts feat(app): open DOM/CSS documents 2026-06-30 10:48:14 +03:00
src refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
tests feat: model variables as CSS design tokens (#852) 2026-10-04 10:50:46 +00:00
AGENTS.md refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
package.json build: update dependencies (#873) 2026-10-04 12:48:24 +00:00
README.md docs(dom-css): audit CSS parser boundaries 2026-06-30 10:51:05 +03:00
tsconfig.json refactor(dom-css): group sources into import, export, and runtime (#789) 2026-09-30 09:44:52 +04:00
tsconfig.test.json feat(dom-css): add JSX authoring bridge 2026-06-30 10:48:13 +03:00
tsdown.config.ts refactor(dom-css): group sources into import, export, and runtime (#789) 2026-09-30 09:44:52 +04:00

@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 APIs
  • bun run typecheck — type-checks src, tests, and package scripts
  • bun run build — builds the distributable dist entrypoint
  • bun run smoke:dist — imports the built dist bundle and exercises the public API
  • bun 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 / DesignElement types
  • Browser-backed runtime adapter for native HTML parsing, serialization, and computed-style extraction
  • Headless runtime adapter with parse5 HTML 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-css stable before splitting lower-level file-format packages such as future @open-pencil/kiwi and @open-pencil/fig