Recipes
Reproduce a real brand lab, embed Loupe in a React app, and swap brands without touching the config.
Three concrete tasks, each end to end. Copy, adapt, ship.
Reproduce a brand lab
This is what the examples/ias-control-plane fixture does: lock a real product surface decision — not a marketing brand — as an asset-light config (palette / type / layoutMock / motion only, so the artifact is one tiny self-contained file). The decisions are the actual choices a control plane makes: the hero treatment, how a run is drawn, the type system, how color carries agent state.
The shape, with the IAS signal-cyan-on-ink theme baked in:
import type { Config } from "@lucentive-labs/loupe-schema";
const SIGNAL = "#54deec"; // the one accent
const INK = "#070c12"; // deep control-plane ink
export const config: Config = {
version: 1,
title: "IAS · Control-Plane Visual Lock",
assets: {}, // asset-light: no binary files
theme: {
"color-bg": INK,
"color-primary": SIGNAL,
"color-ring": SIGNAL,
"color-signal": SIGNAL,
"font-mono": "'JetBrains Mono', ui-monospace, monospace",
"radius-sm": "5px",
},
groups: [
{
id: "hero",
title: "Control-plane hero treatment",
prompt: "What does the first screen of the control plane show?",
options: [
{ id: "liveFleet", label: "Live fleet, no chrome", recommended: true, specimen: { kind: "layoutMock", plan: "belowfold" } },
{ id: "split", label: "Split: claim + live panel", specimen: { kind: "layoutMock", plan: "threshold" } },
],
},
{
id: "statusColor",
title: "Status & signal color usage",
prompt: "How does color carry agent state?",
options: [
{ id: "cyanPlusState", label: "Cyan brand + 3 state colors", recommended: true, specimen: { kind: "palette", colors: ["#54deec", "#3ad6a8", "#f5b34a", "#ef5d6b"] } },
{ id: "monoPlusCyan", label: "Monochrome + cyan signal", specimen: { kind: "palette", colors: ["#2a3744", "#5a6b7a", "#8499ab", "#54deec"] } },
],
},
],
banned: ["Generic SaaS gradient blobs, AI-purple hero glows.", "Cyan used as decoration until it stops meaning signal."],
workflow: ["Signal cyan #54deec is the single brand accent.", "Hand the exported brief to the IAS build pass as ground truth."],
};Generate, verify, and export exactly as in the agent method. Here is a faithful, asset-light version of that lab running live — note it wears its own theme (signal cyan on ink), not this site's Night Atlas skin:
Start from examples/ias-control-plane for an asset-light product lock, or examples/human-today when the decision is photographic art direction with imageCrop.
Embed Loupe in a React app
When a shipping app needs the picker live in its own UI, mount the React adapter and import the styles once.
Install
pnpm add @lucentive-labs/loupe-react @lucentive-labs/loupe-dom @lucentive-labs/loupe-schemaKeep the config a stable constant
The store is keyed on config identity, so define it as a module constant (or useMemo it) — never a fresh object per render.
import type { Config } from "@lucentive-labs/loupe-schema";
export const config: Config = {
version: 1,
title: "My direction",
groups: [ /* ... */ ],
};Mount it and import the stylesheet once
import { Loupe } from "@lucentive-labs/loupe-react";
import "@lucentive-labs/loupe-dom/styles.css"; // import once, app-wide
import { config } from "./loupe.config";
export default function DecisionPage() {
return (
<Loupe
config={config}
storageKey="my-decision" // persist picks across reloads
onLockChange={(sel) => console.log("locked:", sel)}
/>
);
}To keep a live brief beside the picker (like the tutorial), feed onLockChange selections into selectExportBrief:
import { useState } from "react";
import { Loupe } from "@lucentive-labs/loupe-react";
import type { Selections } from "@lucentive-labs/loupe-core";
import { recommendedSelections, selectExportBrief } from "@lucentive-labs/loupe-core";
import { config } from "./loupe.config";
export function DecisionWithBrief() {
const [sel, setSel] = useState<Selections | null>(null);
const brief = selectExportBrief(config, sel ?? recommendedSelections(config));
return (
<>
<Loupe config={config} onLockChange={setSel} />
<pre>{brief.markdown}</pre>
</>
);
}Swap brands without touching the config
One config, many skins. Because the brand is just the --loupe-* token contract, you re-skin at the render site — the decisions and tiles never change.
import { Loupe } from "@lucentive-labs/loupe-react";
import { config } from "./loupe.config";
const northwind = {
"color-bg": "#14110e",
"color-primary": "#f6a13c",
"color-ring": "#f6a13c",
"font-sans": "Manrope, ui-sans-serif, system-ui, sans-serif",
};
const ias = {
"color-bg": "#070c12",
"color-primary": "#54deec",
"color-ring": "#54deec",
"font-mono": "'JetBrains Mono', ui-monospace, monospace",
};
// The theme prop overrides config.theme; swap the object to swap brands.
<Loupe config={config} theme={northwind} />;
<Loupe config={config} theme={ias} />;For a portable artifact, do the same with config.theme (baked in) or applyTheme(el, tokens) after mount. To bridge an existing Tailwind/design-system, point the tokens at var(--your-token) — see Theming › the Tailwind bridge.
Precedence: DEFAULT_TOKENS → config.theme → the theme prop / applyTheme → your own CSS. They merge, so a partial override (just color-primary) is fine.