diff --git a/CHANGELOG.md b/CHANGELOG.md index b46c86650..a4e51ef10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Add Figma-style page management in the Pages panel, including rename/delete actions and drag-and-drop page reordering. - Add DOM/CSS import and authoring support so HTML, CSS, Tailwind, and JSX can be converted into editable OpenPencil documents from the app, CLI, and SDK. - Add Tailwind class serialization for DOM/CSS HTML export in the SDK and CLI. -- Add standalone browser-openable HTML export with precompiled CSS, font links, and optional external asset bundles. +- Add standalone browser-openable HTML export with inline CSS or Tailwind browser runtime previews. - Add richer Design JSX authoring for components, variables, structured fills, gradients, shadows, and blur effects. - Add overlap analysis for finding layout collisions and overflowing children from the CLI, AI tools, and MCP. - Add saved per-node export settings for repeat exports. diff --git a/README.md b/README.md index ddb3be5ab..d3a9f6f0f 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ openpencil export design.fig -f jpg -s 2 -q 90 # JPG at 2x, quality 90 openpencil export design.fig -f fig --page "Page 1" # Export a page as .fig openpencil export design.fig -f jsx --style tailwind # Tailwind JSX openpencil export design.fig -f html --css tailwind # Tailwind HTML fragment -openpencil export design.fig -f html --html standalone --bundle external # HTML + assets openpencil convert design.pen output.fig # Convert between document formats openpencil import page.html --css styles.css -o page.fig # HTML/CSS → editable .fig ``` diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 63c0ff136..389d53a24 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -1,14 +1,14 @@ -import { mkdir, writeFile } from 'node:fs/promises' -import { basename, dirname, extname, join, resolve } from 'node:path' +import { writeFile } from 'node:fs/promises' +import { basename, extname, resolve } from 'node:path' import { defineCommand } from 'citty' import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io' import type { RasterExportFormat } from '@open-pencil/core/io' import { - bundleHTML, sceneGraphToDesignDocument, - type HTMLBundleOptions + serializeHTML, + type SerializeHTMLOptions } from '@open-pencil/dom-css' import { isAppMode, requireFile, rpc } from '#cli/app-client' @@ -22,8 +22,6 @@ const ALL_FORMATS = new Set([...RASTER_FORMATS, 'SVG', 'PDF', 'JSX', 'FIG', 'HTM const JSX_STYLES = new Set(['openpencil', 'tailwind']) const HTML_STYLES = new Set(['inline', 'tailwind']) const HTML_MODES = new Set(['fragment', 'standalone']) -const HTML_BUNDLES = new Set(['inline', 'external']) -const HTML_FONTS = new Set(['link', 'none']) interface ExportArgs { file?: string @@ -36,8 +34,6 @@ interface ExportArgs { style: string html: string css: string - bundle: string - fonts: string thumbnail?: boolean width: string height: string @@ -109,24 +105,6 @@ function targetLabel(pageName?: string, nodeId?: string): string { type FileExportTarget = { scope: 'node'; nodeId: string } | { scope: 'page'; pageId: string } -async function writeBundle(output: string, bundle: Awaited>) { - const entrypoint = bundle.files.find((file) => file.path === bundle.entrypoint) - if (!entrypoint) { - printError(`HTML bundle did not include ${bundle.entrypoint}.`) - process.exit(1) - } - - await writeAndLog(output, entrypoint.content) - const outputDir = dirname(output) - const assetFiles = bundle.files.filter((file) => file.path !== bundle.entrypoint) - for (const file of assetFiles) { - const assetPath = join(outputDir, file.path) - await mkdir(dirname(assetPath), { recursive: true }) - await writeFile(assetPath, file.content) - } - if (assetFiles.length > 0) console.log(ok(`Assets: ${assetFiles.length} files`)) -} - async function exportHTMLFromFile( args: ExportArgs, graph: Awaited>, @@ -136,16 +114,12 @@ async function exportHTMLFromFile( const document = sceneGraphToDesignDocument(graph, { rootId: target.scope === 'page' ? target.pageId : target.nodeId }) - const output = resolve(args.output ?? exportFileName(defaultName, 'html')) - const assetBasePath = `${basename(output, extname(output))}.assets` - const bundle = await bundleHTML(document, { - html: args.html as HTMLBundleOptions['html'], - style: args.css as HTMLBundleOptions['style'], - bundle: args.bundle as HTMLBundleOptions['bundle'], - fonts: args.fonts as HTMLBundleOptions['fonts'], - assetBasePath + const html = serializeHTML(document, { + html: args.html as SerializeHTMLOptions['html'], + style: args.css as SerializeHTMLOptions['style'] }) - await writeBundle(output, bundle) + const output = resolve(args.output ?? exportFileName(defaultName, 'html')) + await writeAndLog(output, html) console.log(ok(`Target: ${targetLabel(args.page, args.node)}`)) } @@ -267,16 +241,6 @@ export default defineCommand({ description: 'HTML CSS output: inline or tailwind (default: inline)', default: 'inline' }, - bundle: { - type: 'string', - description: 'HTML bundle strategy: inline or external (default: inline)', - default: 'inline' - }, - fonts: { - type: 'string', - description: 'HTML font loading: link or none (default: link)', - default: 'link' - }, thumbnail: { type: 'boolean', description: 'Export page thumbnail instead of full render' }, width: { type: 'string', description: 'Thumbnail width (default: 1920)', default: '1920' }, height: { type: 'string', description: 'Thumbnail height (default: 1080)', default: '1080' }, @@ -306,16 +270,6 @@ export default defineCommand({ process.exit(1) } - if (format === 'HTML' && !HTML_BUNDLES.has(args.bundle)) { - printError(`Invalid HTML bundle strategy "${args.bundle}". Use inline or external.`) - process.exit(1) - } - - if (format === 'HTML' && !HTML_FONTS.has(args.fonts)) { - printError(`Invalid HTML font loading "${args.fonts}". Use link or none.`) - process.exit(1) - } - if (isAppMode(args.file)) { await exportViaApp(format, args) } else { diff --git a/packages/docs/programmable/cli/exporting.md b/packages/docs/programmable/cli/exporting.md index 529fc5490..e0f7b3443 100644 --- a/packages/docs/programmable/cli/exporting.md +++ b/packages/docs/programmable/cli/exporting.md @@ -61,10 +61,9 @@ Use `--html standalone` for a browser-openable HTML document with reset styles a ```sh openpencil export design.fig -f html --html standalone --css inline openpencil export design.fig -f html --html standalone --css tailwind -openpencil export design.fig -f html --html standalone --css tailwind --bundle external ``` -Standalone Tailwind output is precompiled during export; it does not depend on the Tailwind browser runtime. Use `--bundle external` to write CSS and extracted image assets next to the HTML file. Font loading defaults to Google Fonts links for detected web fonts; use `--fonts none` to disable that. +Standalone Tailwind output includes Tailwind's v4 browser runtime from jsDelivr, so it needs network access when opened and is best for previews/prototypes. Use `--css inline` for a self-contained offline file. HTML export is available in file mode. diff --git a/packages/docs/reference/cli.md b/packages/docs/reference/cli.md index 7d87c2d3d..ed48254e8 100644 --- a/packages/docs/reference/cli.md +++ b/packages/docs/reference/cli.md @@ -107,8 +107,6 @@ openpencil export [file] [options] | `--style` | | JSX style: `openpencil` (default), `tailwind` | | `--html` | | HTML mode: `fragment` (default), `standalone` | | `--css` | | HTML CSS output: `inline` (default), `tailwind` | -| `--bundle` | | Standalone HTML bundle strategy: `inline` (default), `external` | -| `--fonts` | | Standalone HTML font loading: `link` (default), `none` | | `--thumbnail` | | Export page thumbnail instead of full render | | `--width` | | Thumbnail width (default: 1920) | | `--height` | | Thumbnail height (default: 1080) | diff --git a/packages/dom-css/src/bundle.ts b/packages/dom-css/src/bundle.ts deleted file mode 100644 index 91f7c6809..000000000 --- a/packages/dom-css/src/bundle.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { mergeClassNames, serializeHTML } from './serialize' -import type { DesignDocument, DesignElement, DesignNode, DesignStyleDeclaration } from './types' - -export interface HTMLBundleOptions { - html?: 'fragment' | 'standalone' - style?: 'inline' | 'tailwind' - bundle?: 'inline' | 'external' - fonts?: 'link' | 'none' - assetBasePath?: string -} - -export interface HTMLBundleFile { - path: string - content: string | Uint8Array -} - -export interface HTMLBundle { - entrypoint: string - files: HTMLBundleFile[] -} - -interface StandaloneBounds { - minX: number - minY: number - width: number - height: number -} - -const GENERIC_FONT_FAMILIES = new Set([ - 'serif', - 'sans-serif', - 'monospace', - 'cursive', - 'fantasy', - 'system-ui', - 'ui-serif', - 'ui-sans-serif', - 'ui-monospace', - 'ui-rounded', - 'emoji', - 'math', - 'fangsong' -]) - -const RESET_CSS = - '*,*::before,*::after{box-sizing:border-box}html,body{margin:0;padding:0}body{font-family:system-ui,sans-serif;background:#fff}' - -function styleToCSS(style: DesignStyleDeclaration): string { - return Object.entries(style) - .filter(([, value]) => value !== '') - .map(([property, value]) => `${property}: ${value}`) - .join('; ') -} - -function cloneNode(node: DesignNode): DesignNode { - if (node.type === 'text') return { ...node } - return { - ...node, - attrs: { ...node.attrs }, - inlineStyle: node.inlineStyle ? { ...node.inlineStyle } : undefined, - children: node.children.map(cloneNode) - } -} - -function standaloneStyleForNode( - node: DesignElement, - parent: DesignElement | undefined, - origin: StandaloneBounds -): DesignStyleDeclaration { - const style = { ...node.inlineStyle } - const source = node.sourceSceneNode - if (!source) return style - - style.position = 'absolute' - style.left = `${source.x - (parent ? 0 : origin.minX)}px` - style.top = `${source.y - (parent ? 0 : origin.minY)}px` - return style -} - -function standaloneNode( - node: DesignNode, - origin: StandaloneBounds, - parent?: DesignElement -): DesignNode { - if (node.type === 'text') return cloneNode(node) - const standalone: DesignElement = { - ...node, - attrs: { ...node.attrs }, - inlineStyle: standaloneStyleForNode(node, parent, origin), - children: [] - } - standalone.children = node.children.map((child) => standaloneNode(child, origin, node)) - return standalone -} - -function nodeBounds(node: DesignNode): StandaloneBounds | undefined { - if (node.type === 'text' || !node.sourceSceneNode) return undefined - return { - minX: node.sourceSceneNode.x, - minY: node.sourceSceneNode.y, - width: node.sourceSceneNode.width, - height: node.sourceSceneNode.height - } -} - -function standaloneSize(document: DesignDocument): StandaloneBounds { - const bounds = document.children - .map(nodeBounds) - .filter((value): value is NonNullable => value !== undefined) - const minX = bounds.length > 0 ? Math.min(...bounds.map((bound) => bound.minX)) : 0 - const minY = bounds.length > 0 ? Math.min(...bounds.map((bound) => bound.minY)) : 0 - const maxX = bounds.length > 0 ? Math.max(...bounds.map((bound) => bound.minX + bound.width)) : 1 - const maxY = bounds.length > 0 ? Math.max(...bounds.map((bound) => bound.minY + bound.height)) : 1 - return { minX, minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) } -} - -function standaloneDocument(document: DesignDocument, size: StandaloneBounds): DesignDocument { - return { - ...document, - children: document.children.map((node) => standaloneNode(node, size)) - } -} - -function normalizeFontFamily(value: string | undefined): string | undefined { - const family = value - ?.split(',')[0] - ?.trim() - .replace(/^['"]|['"]$/g, '') - if (!family || GENERIC_FONT_FAMILIES.has(family.toLowerCase())) return undefined - return family -} - -function normalizeFontWeight(value: string | undefined): string { - if (!value || value === 'normal') return '400' - if (value === 'bold') return '700' - return /^\d+$/.test(value) ? value : '400' -} - -function collectFontFamilies(node: DesignNode, fonts: Map>): void { - if (node.type === 'text') return - const family = normalizeFontFamily(node.inlineStyle?.['font-family']) - if (family) { - const weights = fonts.get(family) ?? new Set() - weights.add(normalizeFontWeight(node.inlineStyle?.['font-weight'])) - fonts.set(family, weights) - } - for (const child of node.children) collectFontFamilies(child, fonts) -} - -function googleFontsURL(fonts: Map>): string | undefined { - if (fonts.size === 0) return undefined - const families = [...fonts] - .sort(([left], [right]) => left.localeCompare(right)) - .map(([family, weights]) => { - const encodedFamily = encodeURIComponent(family).replaceAll('%20', '+') - const encodedWeights = [...weights] - .sort((left, right) => Number(left) - Number(right)) - .join(';') - return `family=${encodedFamily}:wght@${encodedWeights}` - }) - return `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap` -} - -function fontLinks(document: DesignDocument, fonts: HTMLBundleOptions['fonts']): string { - if (fonts === 'none') return '' - const fontMap = new Map>() - for (const child of document.children) collectFontFamilies(child, fontMap) - const href = googleFontsURL(fontMap) - if (!href) return '' - return `` -} - -function cssClassName(index: number): string { - return `op-${index.toString(36)}` -} - -function extractInlineStyles( - node: DesignNode, - rules: string[], - nextIndex: { value: number } -): DesignNode { - if (node.type === 'text') return node - const children = node.children.map((child) => extractInlineStyles(child, rules, nextIndex)) - if (!node.inlineStyle || Object.keys(node.inlineStyle).length === 0) return { ...node, children } - - const className = cssClassName(nextIndex.value) - nextIndex.value += 1 - rules.push(`.${className}{${styleToCSS(node.inlineStyle)}}`) - return { - ...node, - attrs: { ...node.attrs, class: mergeClassNames(node.attrs.class, className) ?? className }, - inlineStyle: undefined, - children - } -} - -function classNamesFromHTML(html: string): string[] { - const classes = new Set() - for (const match of html.matchAll(/\sclass="([^"]*)"/g)) { - for (const className of match[1]?.split(/\s+/) ?? []) { - if (className) classes.add(className) - } - } - return [...classes] -} - -async function compileTailwindClasses(classNames: string[]): Promise { - if (classNames.length === 0) return '' - const [{ compile }, { readFile }] = await Promise.all([ - import('tailwindcss'), - import('node:fs/promises') - ]) - const [themeCSS, utilitiesCSS] = await Promise.all([ - readFile(new URL(import.meta.resolve('tailwindcss/theme.css')), 'utf8'), - readFile(new URL(import.meta.resolve('tailwindcss/utilities.css')), 'utf8') - ]) - const compiler = await compile(`${themeCSS}\n${utilitiesCSS}`) - return compiler.build(classNames) -} - -function bytesFromBase64(value: string): Uint8Array { - return Uint8Array.from(atob(value), (char) => char.charCodeAt(0)) -} - -function extensionForMime(mime: string): string { - if (mime === 'image/jpeg') return 'jpg' - if (mime === 'image/webp') return 'webp' - if (mime === 'image/gif') return 'gif' - if (mime === 'image/svg+xml') return 'svg' - return 'png' -} - -function extractImages( - html: string, - assetBasePath: string -): { html: string; files: HTMLBundleFile[] } { - const files: HTMLBundleFile[] = [] - const sources = new Map() - const nextHTML = html.replaceAll( - /src="data:(image\/[a-zA-Z0-9.+-]+);base64,([^"]+)"/g, - (source, mime: string, base64: string) => { - const existing = sources.get(source) - if (existing) return `src="${existing}"` - const path = `${assetBasePath}/images/image-${sources.size + 1}.${extensionForMime(mime)}` - sources.set(source, path) - files.push({ path, content: bytesFromBase64(base64) }) - return `src="${path}"` - } - ) - return { html: nextHTML, files } -} - -function stylesheetLink(path: string): string { - return `` -} - -function styleTag(css: string): string { - return css ? `` : '' -} - -async function bundleStandaloneHTML( - document: DesignDocument, - options: Required -): Promise { - const size = standaloneSize(document) - const stageCSS = `.op-stage{position:relative;width:${size.width}px;height:${size.height}px;overflow:hidden;background:transparent}` - const doc = standaloneDocument(document, size) - const files: HTMLBundleFile[] = [] - let css = `${RESET_CSS}${stageCSS}` - let body = '' - - if (options.style === 'tailwind') { - body = serializeHTML(doc, { style: 'tailwind' }) - css += await compileTailwindClasses(classNamesFromHTML(body)) - } else { - const rules: string[] = [] - const styledDoc: DesignDocument = { - ...doc, - children: doc.children.map((node) => - extractInlineStyles(node, rules, { value: rules.length }) - ) - } - body = serializeHTML(styledDoc) - css += rules.join('') - } - - if (options.bundle === 'external') { - const extracted = extractImages(body, options.assetBasePath) - body = extracted.html - files.push(...extracted.files) - const cssPath = `${options.assetBasePath}/openpencil.css` - files.push({ path: cssPath, content: css }) - const html = `${fontLinks(document, options.fonts)}${stylesheetLink(cssPath)}
${body}
` - return { entrypoint: 'index.html', files: [{ path: 'index.html', content: html }, ...files] } - } - - const html = `${fontLinks(document, options.fonts)}${styleTag(css)}
${body}
` - return { entrypoint: 'index.html', files: [{ path: 'index.html', content: html }] } -} - -export async function bundleHTML( - document: DesignDocument, - options: HTMLBundleOptions = {} -): Promise { - const resolvedOptions: Required = { - html: options.html ?? 'fragment', - style: options.style ?? 'inline', - bundle: options.bundle ?? 'inline', - fonts: options.fonts ?? 'link', - assetBasePath: options.assetBasePath ?? 'assets' - } - - if (resolvedOptions.html === 'standalone') return bundleStandaloneHTML(document, resolvedOptions) - - return { - entrypoint: 'index.html', - files: [ - { path: 'index.html', content: serializeHTML(document, { style: resolvedOptions.style }) } - ] - } -} diff --git a/packages/dom-css/src/index.ts b/packages/dom-css/src/index.ts index 4d289d5e9..f64fd5a9e 100644 --- a/packages/dom-css/src/index.ts +++ b/packages/dom-css/src/index.ts @@ -1,6 +1,5 @@ import type * as DesignTypes from './types' -export { bundleHTML } from './bundle' export { serializeHTML, serializeNode } from './serialize' export { createBrowserCSSRuntime, createCSSRuntime, createHeadlessCSSRuntime } from './runtime' export { @@ -62,7 +61,6 @@ export type { BrowserToSceneGraphOptions } from './browser' export type { CompileTailwindCSSOptions } from './tailwind' -export type { HTMLBundle, HTMLBundleFile, HTMLBundleOptions } from './bundle' export type { SerializeHTMLOptions } from './serialize' export type { ToSceneGraphOptions } from './to-scene-graph' export type CSSComputeOptions = DesignTypes.CSSComputeOptions diff --git a/packages/dom-css/src/serialize.ts b/packages/dom-css/src/serialize.ts index f6f5adf03..5455003ef 100644 --- a/packages/dom-css/src/serialize.ts +++ b/packages/dom-css/src/serialize.ts @@ -1,11 +1,20 @@ import { twirl } from 'twirlwind' -import type { DesignDocument, DesignElement, DesignNode, DesignText } from './types' +import type { + DesignDocument, + DesignElement, + DesignNode, + DesignStyleDeclaration, + DesignText +} from './types' export interface SerializeHTMLOptions { style?: 'inline' | 'tailwind' + html?: 'fragment' | 'standalone' } +const TAILWIND_BROWSER_CDN = 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4' + const VOID_ELEMENTS = new Set([ 'area', 'base', @@ -50,7 +59,7 @@ function serializeTailwindClasses(node: DesignElement): string | undefined { return className.length > 0 ? className : undefined } -export function mergeClassNames(...values: Array): string | undefined { +function mergeClassNames(...values: Array): string | undefined { const className = values .flatMap((value) => value?.split(/\s+/) ?? []) .map((value) => value.trim()) @@ -85,6 +94,77 @@ function serializeElement(node: DesignElement, options: SerializeHTMLOptions): s return `<${tagName}${attrs}>${node.children.map((child) => serializeNode(child, options)).join('')}` } +interface StandaloneBounds { + minX: number + minY: number + width: number + height: number +} + +function standaloneStyleForNode( + node: DesignElement, + parent: DesignElement | undefined, + origin: StandaloneBounds +): DesignStyleDeclaration { + const style = { ...node.inlineStyle } + const source = node.sourceSceneNode + if (!source) return style + + style.position = 'absolute' + style.left = `${source.x - (parent ? 0 : origin.minX)}px` + style.top = `${source.y - (parent ? 0 : origin.minY)}px` + return style +} + +function standaloneNode( + node: DesignNode, + origin: StandaloneBounds, + parent?: DesignElement +): DesignNode { + if (node.type === 'text') return node + const standalone: DesignElement = { + ...node, + inlineStyle: standaloneStyleForNode(node, parent, origin), + children: [] + } + standalone.children = node.children.map((child) => standaloneNode(child, origin, node)) + return standalone +} + +function nodeBounds(node: DesignNode): StandaloneBounds | undefined { + if (node.type === 'text' || !node.sourceSceneNode) return undefined + return { + minX: node.sourceSceneNode.x, + minY: node.sourceSceneNode.y, + width: node.sourceSceneNode.width, + height: node.sourceSceneNode.height + } +} + +function standaloneSize(document: DesignDocument): StandaloneBounds { + const bounds = document.children + .map(nodeBounds) + .filter((value): value is NonNullable => value !== undefined) + const minX = bounds.length > 0 ? Math.min(...bounds.map((bound) => bound.minX)) : 0 + const minY = bounds.length > 0 ? Math.min(...bounds.map((bound) => bound.minY)) : 0 + const maxX = bounds.length > 0 ? Math.max(...bounds.map((bound) => bound.minX + bound.width)) : 1 + const maxY = bounds.length > 0 ? Math.max(...bounds.map((bound) => bound.minY + bound.height)) : 1 + return { minX, minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) } +} + +function serializeStandaloneHTML(document: DesignDocument, options: SerializeHTMLOptions): string { + const size = standaloneSize(document) + const body = document.children + .map((node) => serializeNode(standaloneNode(node, size), options)) + .join('') + const stageStyle = `position: relative; width: ${size.width}px; height: ${size.height}px; overflow: hidden; background: transparent` + const reset = + '*,*::before,*::after{box-sizing:border-box}html,body{margin:0;padding:0}body{font-family:system-ui,sans-serif;background:#fff}' + const tailwindBrowser = + options.style === 'tailwind' ? `` : '' + return `${tailwindBrowser}
${body}
` +} + export function serializeNode(node: DesignNode, options: SerializeHTMLOptions = {}): string { return node.type === 'text' ? serializeText(node) : serializeElement(node, options) } @@ -93,5 +173,6 @@ export function serializeHTML( document: DesignDocument, options: SerializeHTMLOptions = {} ): string { + if (options.html === 'standalone') return serializeStandaloneHTML(document, options) return document.children.map((node) => serializeNode(node, options)).join('') } diff --git a/packages/dom-css/tests/runtime.test.ts b/packages/dom-css/tests/runtime.test.ts index 776cf63eb..3e767dbe3 100644 --- a/packages/dom-css/tests/runtime.test.ts +++ b/packages/dom-css/tests/runtime.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test' -import { bundleHTML, createCSSRuntime, createHeadlessCSSRuntime, serializeHTML } from '../src/index' +import { createCSSRuntime, createHeadlessCSSRuntime, serializeHTML } from '../src/index' import { cardDocument, TEST_COLORS } from './helpers' describe('@open-pencil/dom-css runtime', () => { @@ -34,9 +34,8 @@ describe('@open-pencil/dom-css runtime', () => { expect(html).toBe('
OpenPencil
') }) - it('bundles standalone HTML documents when requested', async () => { - const bundle = await bundleHTML(cardDocument, { html: 'standalone' }) - const html = String(bundle.files[0]?.content) + it('serializes standalone HTML documents when requested', () => { + const html = serializeHTML(cardDocument, { html: 'standalone' }) expect(html).toContain('') expect(html).toContain('data-open-pencil-html="standalone"') @@ -44,34 +43,12 @@ describe('@open-pencil/dom-css runtime', () => { expect(html).not.toContain('@tailwindcss/browser@4') }) - it('precompiles Tailwind CSS for standalone Tailwind HTML', async () => { - const bundle = await bundleHTML(cardDocument, { html: 'standalone', style: 'tailwind' }) - const html = String(bundle.files[0]?.content) + it('loads the Tailwind browser runtime for standalone Tailwind HTML', () => { + const html = serializeHTML(cardDocument, { html: 'standalone', style: 'tailwind' }) - expect(html).toContain('