@leviosa-ai/konva
v0.8.0
Published
Shared carousel block→react-konva render core AND the canonical font bundle. Single source of truth for the editor (leviosa-frontend) and the headless renderer (leviosa-rendering-server) so the editor is pixel-identical to published output. Consumers MUST
Readme
leviosa-konva
Shared carousel block → react-konva render core. The single source of truth for how a carousel slide is drawn, imported by both:
- leviosa-frontend — the editor renders
<CarouselSlide>and layers a selection/ transform/drag overlay (keyed onblock.id) on top. - leviosa-rendering-server — the headless service renders
<CarouselSlide>and exports it to PNG.
Why this exists: the editor and the published renderer used to be two independent react-konva implementations, so the editor canvas drifted from the published image (text color, font size, spacing slightly off). Unifying on one renderer makes the editor pixel-identical to the output by construction.
Stack contract (must match both consumers): React 19.2.x, react-dom 19.2.x,
react-konva 19.2.x, konva 9.3.x. These are peer dependencies — consumers
provide them.
Status
Scaffold. The data contract (src/carousel-types.ts) is in place. The render core
is being extracted from leviosa-rendering-server — see below.
Extraction plan (from leviosa-rendering-server/src)
Pure render closure — ~1,030 LOC, browser-safe (no Node/FS deps, verified):
| Source file (rendering-server) | Action | Notes |
|---|---|---|
| carousel-types.ts | ✅ moved | data contract / SSOT |
| carousel-content-to-konva.ts | move | block.content → konva props |
| carousel-template-vars.ts | move | {{theme/asset}} + slide.number resolution |
| konva-render-helpers.ts | move | crop math, font-style normalize, emoji data-URI |
| font-coverage.ts | move | family resolution (names only, no FS) |
| lucide-icons.ts | move | icon → data-URI |
| carousel-renderer-entry.tsx (component slice) | split | extract <CarouselSlide> + per-block components; the headless harness (waitForFonts, PNG export, window.* global, ready-orchestration) STAYS in rendering-server |
SUPPORTED_FONT_FAMILIES + FONT_WEIGHT_OPTIONS are the SSOT here.
Fonts (consumer contract) — the manifest is the SSOT
This package ships fonts/font-manifest.json — per file, the version-pinned URL it was
frozen from and its sha256 — plus the woff2 bytes themselves at fonts/fonts/. This is
deliberate: text wrapping is canvas.measureText() against the loaded font, so identical
bytes are as load-bearing as identical wrap logic. When the two consumers each resolved their
own unpinned CDN URLs at their own build times they got different bytes → different metrics
→ different line counts.
Consumers MUST NOT decide which font bytes to fetch. At build time they run
leviosa-konva-fonts (scripts/gen-font-css.mjs, exposed as the package bin), which emits
@font-face CSS from the manifest. Two delivery modes differ only in the src URL:
# local (default) — copy the bytes, serve them yourself, zero network at render time
leviosa-konva-fonts --prefix=/render-fonts/fonts/ --out=public/render-fonts # editor
leviosa-konva-fonts --prefix=http://leviosa-renderer.local/fonts/ --out=dist # renderer
# cdn — immutable sources use pinned URLs; mutable sources still ship with the package
leviosa-konva-fonts --mode=cdn --prefix=/render-fonts/fonts/ --out=public/render-fontscdn only delegates a file whose upstream cannot be repointed at other bytes — gstatic
release paths and npm versions. Anything behind a mutable third-party URL (for example, a
gh/<user>/<repo>@<tag> that can be force-moved or a vendor URL without a versioned path)
still ships as frozen bytes.
The renderer still inlines the generated CSS + route-fulfills the bytes + blocks on
document.fonts.ready; the editor still loads the families before render then awaits
document.fonts.ready. Both measure against the same bytes either way — the manifest pins
them. npm run fonts:check-urls verifies every pinned URL is still live and unchanged.
See CLAUDE.md for which mode belongs where, the full contract, and the regen procedure
(fonts:freeze).
Assets (consumer contract)
The caller resolves brand_config + asset_map and puts them on
CarouselSlideRenderInput; the core only does {{...}} substitution. Byte fetching /
CDN proxying / caching stays in the consumer.
Model gaps being fixed here
MediaContent.image_preset— editor wrote it but block.content had no field (lost on save). Added; TODO mirror in db-schema + frontend types.- Numbering badges are identified by
block.name === "content_number"(NOTcontent.role/source). Detector must key onname. - segments lossless, rect
alpha/transparency polarity, labelcolor/backgroundswap, slide-field binding — reconcile during extraction.
Build
npm install
npm run build # tsup → dist/ (ESM + .d.ts)
npm run typecheckPublishing
The public @leviosa-ai/konva package is published to npm with Trusted Publishing (OIDC),
so GitHub stores no npm token. Configure npm once with organization Leviosa-AI, repository
leviosa-konva, workflow filename publish.yml, no environment, and the npm publish
permission. Then publish by pushing a version tag:
# bump package.json and LEVIOSA_KONVA_VERSION first, then:
VERSION=$(node -p "require('./package.json').version")
git tag "v$VERSION" && git push origin "v$VERSION"Consuming
Both consumers depend on a pinned version from the public npm registry:
{
"dependencies": {
"@leviosa-ai/konva": "0.1.0"
}
}