All notable changes to openfig-cli are documented here.
The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
0.6.0 - 2026-08-02
- Contrast lint compared every text node against the slide background instead of what is actually painted behind it. Light text on a dark card was measured against the white slide and flagged as low-contrast — up to 37 false warnings on a single fixture. The check now carries an effective background through the tree walk and resolves the nearest painted ancestor. Solid fills update the comparison colour; image and gradient fills suppress the check for their subtree, since there is no meaningful ratio against a non-uniform backdrop. (#7)
- Zero-opacity fills were treated as opaque black by the contrast lint.
FlexContainerframes default to{ opacity: 0, visible: true }fills, which overrode the real slide background for every descendant text node — the main source of the false warnings above. Paints withopacity === 0are now skipped so the parent background shows through. - A bold request could resolve onto a weight the family does not ship.
mapFontStyleand the measurement surface each encoded the same two-weight vocabulary independently and had already drifted: the clamp picked a weight the handoff then wrote as"Bold", naming a face that does not exist. Both now derive from a singleNAMEABLE_WEIGHTSconstant infont-normalize.mjs. (#8) - Curved vector paths rendered as straight-line polygons wherever a node's only geometry was its
vectorNetworkBlob. The rasterizer decided line-versus-curve from the wrong field, discarding curvature while keeping the endpoints. A segment is straight if and only if all four bezier tangent components are zero. This affected stroke-only vectors — those carrying neitherfillGeometrynorstrokeGeometry— in both.deckand.fig, and no fixture exposed it because our own output happened to satisfy the old rule.
- Vector geometry is now written in the form the format expects. Earlier releases reflected an incomplete understanding of the
vectorNetworkBloblayout: field order was off by one word and regions carried a spurious trailing value. Coordinates happened to land correctly, so files opened and rendered, but compound shapes were not described the way they should be. Corrected from our testing, with a conformance suite added so it stays correct. Rendering is unaffected — the rasterizer reference reports are pixel-identical. - Vector network and path-command encoding now come from
openfig-core. This package carried its ownvectorNetworkBlobencoder, its own decoder, and a byte-for-byte duplicate ofencodeCommandsBlob. That duplication is what allowed a wrong layout to live in one copy while the other was right — each looked self-consistent because it only ever round-tripped against itself.lib/slides/api-core.mjsnow contains no raw byte manipulation at all, anddecodeVnbis a thin adapter that scales coordinates and emits path commands. Verified equivalent before removal: emitted bytes identical across filled, multi-subpath and open stroked paths, and rendering unchanged at 0 of 2,073,600 pixels. decodeVnbreturnsnullwhen a blob will not parse. Previously it stopped mid-walk and emitted whatever it had read. Geometry that cannot be read in full now falls through to the caller's placeholder rather than being rendered from a partial parse.- Requires
openfig-core^0.4.1, up from ^0.3.8 — for the aligned encoder, the corrected sub-path grouping, and theemitRegionsoption that open stroked paths need.
- Conformance tests for the vector network format.
test/slides/vector-network-layout.test.mjsholds the deck emitter to the layout andtest/rasterizer/decode-vnb.test.mjspins curve classification, so neither can regress silently.
0.5.1 - 2026-07-27
- Reading a
.deckor.figcould parse unrelated memory instead of the file.canvas.figwas handed to the parser asnew Uint8Array(buf.buffer), which discards a Buffer'sbyteOffsetandbyteLength.readFileSyncroutinely returns a Buffer that is a view into a shared pool — in one observed case a 26,643-byte file sat at offset 960 of a 65,536-byte backing buffer — so the parser received the whole pool from offset 0 and read whatever happened to be adjacent in memory. Symptoms varied with pool contents:Unknown prelude: …,Offset is outside the bounds of the DataView, or silently wrong data. Because pooling depends on allocation history, failures moved between runs and between files, which made it look like unrelated flakiness in several commands (inspect,list-text,create-deck,deck-to-fig, and the rasterizer). It affected any file whose read happened to be pooled, not any particular size or format.
packageManageris pinned to npm. The repo trackspackage-lock.json, but straypnpm-lock.yaml/pnpm-workspace.yamlfiles had appeared alongside it, leaving the toolchain ambiguous and local installs resolving stale versions.
0.5.0 - 2026-07-27
convert-html: CSSconic-gradientis converted to vector geometry. Pie and donut charts in Claude Design decks are drawn as aconic-gradienton aborder-radius: 50%element — there is no SVG to extract. Previously the layer was skipped, and because such elements usually have a transparentbackground-color, nothing at all was emitted for them: the chart vanished silently. Cones are now emitted as wedge paths and routed through the SVG pipeline, so each slice becomes a selectable Figma VECTOR node with crisp edges at any zoom. Hard stops produce one exact wedge per segment; genuine colour ramps are subdivided. Handlesfrom <angle>,at <position>, and stop positions in%,deg,turn,rad, andgrad.convert-html: unsupportedbackground-imagefunctions now warn. Previously any layer that was notlinear-gradient,radial-gradient, orurl()was dropped without a word, so a missing visual looked like a clean conversion.
- SVG element and group transforms are applied instead of discarded. The shape parser never read
transformattributes and did not descend into<g>at all, so geometry under<g transform="translate(...)">or a per-pathmatrix(...)collapsed toward the SVG origin — icons rendered mangled, or disappeared. Enclosing group transforms are now recovered and composed with each element's own transform, then baked into the shape's coordinates. Affects every.deckand.figconversion that vectorizes SVG. convert-html: slides are measured at true 1920×1080 scale. A standalone whose viewer renders the slide inside a scaling stage (fitting it beside presenter chrome) yielded geometry at roughly 90% of true size — every coordinate uniformly wrong, with the error growing away from the origin. The converter now grows the measurement viewport until the slide reports its own CSS size 1:1.convert-html: fonts bundled in the export now load. Asset URLs were rewritten for<img>elements only, never for@font-facerules, so every bundled face failed to load and Chromium silently substituted. Text was measured in the wrong typeface, producing subtly wrong wrap points and box heights.
convert-htmlloads the standalone as authored rather than replaying its tweak state. 0.4.7 reproduced the export's runtime in Node — thequoteStyle/dividerBg/ icon-visibility mapping and the--t-icon-*variables. That mapping was written against one deck's markup and encoded its class names and section titles, so it did not generalise. The converter now runs the page's own runtime, which applies the same tweaks in the page's own vocabulary, and the hardcoded replay is gone. The saved tweak state is still honoured;__bundler/ext_resourcesand__bundler/page_orderare now handled too, as a side effect of letting the page rehydrate itself.
0.4.7 - 2026-05-26
convert-html: SVGcurrentColornow resolves to the element's computed color. Vectorized SVG paths usingfill/stroke="currentColor"(e.g. adaptive logos and icons) previously failed conversion withUnknown color: "currentColor". The extractor now captures each SVG's computed CSScolorand substitutes it before vectorizing, so an icon renders light on a dark slide and dark on a light one.convert-html: saved deck tweak state is replayed before extraction. The exported__bundler/templateis the DOM before the design tool applies the user's saved tweaks (TWEAK_DEFAULTS). These are now replayed —quoteStyle,dividerBg,.method-iconvisibility, and the--t-icon-size/--t-icon-scale/--t-kf-num-sizevars — so the rendered deck matches the design. Fixes a phantom box drawn around quote callouts whenquoteStyle: "top"was set.convert-html: hard-break labels that overflow their column now wrap. A text leaf containing a<br>was always markednoWrap; when a segment between breaks was wider than its (e.g. fixed-width) column it overflowed into adjacent content.noWrapis now kept only when each authored line fits — otherwise the text reflows within its measured width.
0.4.6 - 2026-05-22
openfig deck-to-figcommand — Convert a Figma Slides.deckfile into a standard Figma Design.figfile. Visual slides scaffolding is flattened, and components are baked/flattened with overrides into visual frames arranged as a canvas row/grid.- Unified ZIP/Binary auto-detection —
FigDeck.fromFile()unified loader automatically detects zipped.fig/.deckpackages versus raw Kiwi binaries by inspecting magic bytes.
- CLI Inspect/List Commands — Restored compatibility of
inspect,list-text, andlist-overridescommands when reading standard zipped.figfiles exported from Figma.
0.4.5 - 2026-05-21
convert-html: font alias map now derived from FreeDesktop's30-metric-aliases.conf(the canonical open-source list of metric-compatible font pairs that ships with fontconfig and underlies Linux/ChromeOS/LibreOffice font substitution). A new refresh script fetches the upstream config, parses the alias declarations, and keeps only the substitutes Figma actually serves. Surviving aliases are still Calibri→Carlito and Cambria→Caladea, but the provenance is now upstream rather than hand-typed, so new pairs land automatically the next time the refresh script runs.- Figma-availability set consolidated into a single JSON. The
previously-inline system-core list (Inter, Arial, Helvetica, …) is
now baked into
lib/slides/figma-available-fonts.jsonby the refresh script, making the JSON the single source of truth for "what Figma can resolve."
0.4.4 - 2026-05-21
-
convert-html: alias proprietary fonts and widen Figma availability check. Decks emitted from Claude Design HTML opened with a missing-font dialog when the source CSS used proprietary system fonts (Calibri, Cambria) that Figma cannot load. The font normalizer now walks the full CSS font stack and:- substitutes metric-compatible OFL clones —
Calibri → Carlito,Cambria → Caladea— so layout is preserved; - otherwise picks the first family Figma is known to have, checked
against the full Google Fonts catalog (~1900 families, vendored as
JSON, regenerated via
scripts/refresh-figma-available-fonts.mjs) plus the system core (Inter, Arial, Helvetica, Times, etc.); - falls back to the first portable token only when nothing in the stack is resolvable, letting Figma's font picker handle the rest.
Also eliminates the spurious "likely not available" warning that fired on common Google Fonts (e.g. EB Garamond) under the previous hand-curated 30-entry allowlist. HTML and SVG-text paths now share the same
lib/slides/font-normalize.mjsso the dispatcher can't reintroduce a raw CSS stack. - substitutes metric-compatible OFL clones —
0.4.3 - 2026-05-21
convert-html: bake CSSinvert(1)filters into raster image bytes. Logos that Claude Design ships as black assets and recolors viafilter: invert(1)(or compoundbrightness(0) invert(1)) no longer render as black on dark slides. Image bytes are now inverted via sharp before embedding, and a warning surfaces any other CSS filter we don't bake yet.convert-html: preserveimage/svg+xmlassets as native Figma vectors. SVG assets referenced through the runtime blob-URL manifest were previously routed through the raster<img>path and baked to pixels. They're now inlined asdata:URLs in the browser stage so the existing SVG-vector path picks them up and emits Figma VECTOR nodes — crisp at any zoom. The sameinvert(1)/brightness(0) invert(1)filters apply by rewriting fill/stroke colors directly in the SVG markup.
0.4.2 - 2026-04-28
- README: screenshots compressed and display widths constrained for faster page loads on npmjs.com.
0.4.1 - 2026-04-28
- README: Claude Design HTML export workflow (the standalone-HTML →
.deckflow added in 0.4.0).
0.4.0 - 2026-04-28
openfig convert-htmlcommand — convert a Claude Design standalone HTML export into a native.deckfile. Text, images, vectors, layouts, and speaker notes carry through as editable Figma Slides nodes.openfig_convert_htmlMCP tool — same conversion exposed to MCP clients (Claude Cowork etc.).- Zero-seed
.deckcreation —openfig create-deckandFigDeck.createEmpty()produce a fully programmatic deck without requiring a seed template. - Chromium-based layout extraction — the standalone-HTML converter drives Playwright/Chromium so CSS layout is browser-faithful, replacing the previous hand-rolled CSS engine.
- Broader SVG shape coverage in the handoff stage — polyline, polygon, rect, ellipse, gradient fills, and concatenated/relative path commands.
- CSS variable resolution —
var(--name)references resolve through the captured:roottoken values before handoff. ::before/::afterpseudo-elements rendered as text/shape nodes.- Inline rich-text flows coalesced into single richText elements so mid-sentence weight/style changes stay together.
- Text wrapping near slide right edge for large
noWraptext. - Font measurement: Playwright now uses Inter for metrics that match Figma's substitution behaviour; system-font stacks are forced onto Inter pre-measurement.
SHAPE_WITH_TEXTcontainers no longer absorb their inline SVG/IMG children as text leaves.- Empty inline elements with CSS-only geometry are imported as shapes rather than being dropped.
- Straight-line shapes emit as VECTOR paths so
strokeAlignis honoured (previously rasterised inconsistently). - Multi-line
noWrapcaptions:WIDTH_AND_HEIGHTsizing keeps the box tight to content. - CSS
vertical-alignhonoured on text elements. - SVG opacity attribute and direct text inside containers preserved.
- SVG subpath separation preserved for vector wordmarks (no more glyph merging).
- Converter warnings surface unsupported CSS so unknown constructs are visible at convert time instead of silently dropped.
0.3.31 - 2026-03-16
Pre-convert-html baseline. Earlier 0.3.x versions are not catalogued
here; see git log --tags='*0.3.*' for the full history.