Lucentive Labs
Lucentive Labs Docs
Packages / API

@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 theme

mount(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 parts

renderToString 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:

FunctionRenders
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.