@lucentive-labs/loupe-dom
The canonical vanilla browser renderer — mount, renderToString, applyTheme, the render functions, and styles.css.
The canonical renderer. It mounts the full interactive decision-lock into a DOM element, wires every interaction through the core store, and re-renders deterministically on change. The React adapter mirrors its output exactly, and the generator bundles it into the portable artifact — so this package defines what Loupe looks like.
import {
mount,
applyTheme,
renderToString,
renderApp,
TILE_AR,
} from "@lucentive-labs/loupe-dom";
import "@lucentive-labs/loupe-dom/styles.css"; // the part styles + default thememount(el, config, opts?): LoupeInstance
Render the full interactive UI into el, returning the live store and a destroy().
interface MountOptions {
initial?: Selections; // initial picks (overrides storage + recommended)
storageKey?: string; // localStorage key; omit for ephemeral
store?: LoupeStore; // bring your own store (e.g. shared)
applyTheme?: boolean; // apply config.theme as inline vars (default: true)
}
interface LoupeInstance {
store: LoupeStore;
destroy(): void; // unsubscribe, remove listeners, clear DOM
}const instance = mount(document.getElementById("app")!, config, {
storageKey: "brand-lock",
});
// ...later
instance.destroy();mount uses event delegation on the host element and preserves keyboard focus across re-renders by re-addressing the active tile — so arrow-key navigation stays smooth even though the DOM is re-rendered on every change.
renderToString(config, state?, opts?): string
Deterministic SSR / static markup — no Date, no random, config order preserved. When state is omitted, the recommended selections are used (matching the store's server snapshot), so SSR and client hydration agree without a flash.
const html = renderToString(config); // app markup
const standalone = renderToString(config, undefined, { includeTheme: true });
// ^ prepends a <style> with the theme tokens; still import styles.css for partsrenderToString produces the app markup. For a fully self-contained file (JS + CSS + assets inlined), use loupe-generator instead.
applyTheme(el, tokens?): void
Set the --loupe-* CSS variables for a theme onto an element's inline style. mount calls it for you (with config.theme) unless applyTheme: false; call it directly to override after mount. See Theming.
applyTheme(el, { "color-primary": "#54deec" });Render functions
The renderer is built from small, exported, pure string-returning functions. You rarely need them directly, but they are public for embedding fragments or building a custom shell:
| Function | Renders |
|---|---|
renderApp(config, sel) | the whole UI (header + groups + stack + brief) |
renderGroup(config, group, sel, index) | one decision group (a radiogroup) |
renderTile(config, group, option, sel) | a single option tile |
renderSpecimen(config, spec, alt) | one specimen's visual |
renderComposedPreview(config, sel) | the composed preview panel |
renderExportBrief(config, sel) | the export-brief section |
renderStack(config, sel) | the sticky stack (preview + thumbs + controls) |
All of them escape author text and allowlist URLs via core, so the output is safe regardless of config source.
TILE_AR
The tile media aspect ratio, 4 / 3. The React adapter exports the same constant so both renderers frame crops identically.
styles.css
The canonical stylesheet: every part style (.loupe-tile, .loupe-pv__band, …) plus the default theme. Import it once, app-wide. Because the React adapter emits the same class names and data-loupe-part attributes, this one stylesheet styles both renderers.
import "@lucentive-labs/loupe-dom/styles.css";The DOM contract — class names, data-loupe-part / data-group / data-option, and ARIA — is shared with loupe-react. That is what lets one stylesheet, and one set of Playwright assertions, cover both.