Lucentive Labs

Lucentive Labs · design-tooling

Loupe

Active

Lock a visual-design decision by clicking it, not describing it. Loupe turns a visual-design decision into clickable option tiles — real image crops, palettes, type, motion, layout mocks. Locking a tile recomposes a live preview and keeps a deterministic export brief in sync: the ground truth a human or an AI build pass consumes.

Live · running hereA real <Loupe /> from @lucentive-labs/loupe-react, themed Night Atlas. Lock a tile in each group; the preview composes below.
Loupe — live in labs-site
3 of 3 locked
01
Ground
Pick the canvas the system sits on.
02
Accent
Pick the load-bearing accent.
03
Display
Pick the display voice.

Build brief

The deterministic handoff for the next build pass. Stays in sync with your locked tiles.

The wedge

Config in. Live preview. Deterministic brief out.

A visual decision should end as something a build can reproduce, not a paragraph someone has to interpret. Loupe runs the decision through three steps and stops the drift.

01

Author the config

One typed file lists the decisions: groups of option tiles built from real image crops, palettes, type specimens, motion presets, and layout mocks. A Zod schema validates it.

02

Lock tiles in a live preview

Click a tile and a sticky preview recomposes from your picks — the same crop engine, theme, and type the real build will use. You are choosing against what you will ship, not a swatch in isolation.

03

Hand off a deterministic brief

Every lock keeps an export brief in sync: human-readable markdown and a machine-readable JSON of the same decisions. No timestamps, no absolute paths — the same picks always produce the same brief.

The change

From a decision log to a locked artifact

Before · text-first

A decision log nobody can rerun

  • Direction lives in a doc: prose notes, hex codes, and screenshots that drift from the build.
  • “Make it warmer” means something different to the person who wrote it and the agent that reads it.
  • Re-litigated next week, because nothing recorded which option won or why.
  • The build pass guesses at the crop, the weight, the spacing — and guesses differently each time.
After · click-to-lock

A brief the next pass reproduces

  • Direction is a locked artifact: each decision is a tile you can see, click, and compose against the others.
  • “Warmer” becomes a specific palette tile with exact colors, captured the moment it is chosen.
  • The brief is the record of what won — a human or an agent reads the same ground truth.
  • The exported crop, weight, and layout are exact values, so the next pass reproduces them.

What it is

Small surface, exact behavior

Five packages, one contract

Schema, headless core, vanilla DOM renderer, React adapter, and a static-HTML generator. Take only the layer you need; they all read the same config.

loupe-schema · core · dom · react · generator

Exact crop engine

Specimens carry a normalized crop rectangle (fractions of the intrinsic image). The core computes exact cover math from the rect plus intrinsic dimensions, so the tile, the preview, and the export all show the same pixels.

deterministic · zero-dependency core

Bring your own brand

A theme is a flat map of semantic tokens — background, surface, primary, ring, type. Pass it on the config or the component and the whole picker wears your skin. The three examples ship three different brands off the same machinery.

--loupe-* token contract

Agent-native contract

The config is a Zod schema, and one call emits its JSON Schema. An agent can author a valid decision lock the same way a person does, and a semantic validator catches missing assets, duplicate ids, and broken preview references before render.

toJsonSchema() · validateConfig()

Portable artifact or live React

Generate a single self-contained index.html that runs from a file path, or drop <Loupe/> into a React 19 app. Both render identical structure, classes, and ARIA from the same source — SSR- and hydration-safe.

static index.html · React 19 adapter

@lucentive-labs/loupe-schema

The Zod config contract a human or an agent fills, plus the JSON Schema emitter and a semantic validator.

@lucentive-labs/loupe-core

Zero-dependency headless core: SSR-safe store, deterministic preview and brief derivations, crop math, ARIA prop-getters.

@lucentive-labs/loupe-dom

The canonical vanilla browser renderer — mount(), renderToString(), the crop engine, and styles.css. No framework.

@lucentive-labs/loupe-react

The React 19 adapter: <Loupe/> and useLoupe(), a thin useSyncExternalStore view over the core store.

@lucentive-labs/loupe-generator

Node-only generate(): bundles a self-contained index.html with JS and CSS inlined and assets copied. Output is deterministic.

In code

Author it, render it, hand it off

The config is the contract. A person writes it by hand; an agent writes it from the JSON Schema. Either way, the same file drives the live React adapter and the portable artifact.

1 · Author a typed config. One file lists the groups, the option tiles, and your brand theme. Zod validates it.

loupe.config.tsts
// loupe.config.ts
import type { Config } from "@lucentive-labs/loupe-schema";

export const config: Config = {
  version: 1,
  title: "Northwind · Brand System",
  // No image assets in this lock — palette / type specimens only.
  assets: {},
  // Bring your own brand: semantic tokens, kebab keys, no --loupe- prefix.
  theme: {
    "color-bg": "#14110e",
    "color-primary": "#f6a13c",
    "color-ring": "#f6a13c",
    "font-sans": "Manrope, system-ui, sans-serif",
  },
  groups: [
    {
      id: "color",
      title: "Color system",
      prompt: "Which palette carries the brand?",
      options: [
        {
          id: "amberInk",
          label: "Amber on ink",
          caption: "Warm, premium, high-contrast",
          recommended: true,
          specimen: {
            kind: "palette",
            colors: ["#14110e", "#f6a13c", "#ffc879", "#f7efe3"],
          },
        },
        {
          id: "monoSignal",
          label: "Mono + one signal",
          specimen: {
            kind: "palette",
            colors: ["#101216", "#3a4048", "#9aa3ad", "#f6a13c"],
          },
        },
      ],
    },
  ],
  // What the build pass must never do — travels with the brief.
  banned: ["Generic SaaS gradient blobs.", "Cold corporate blue as primary."],
};

2 · Drop the React adapter into an app. <Loupe /> is a thin view over the headless core; import the shared stylesheet once.

app/decide/page.tsxtsx
// app/decide/page.tsx
"use client";

import { Loupe } from "@lucentive-labs/loupe-react";
import "@lucentive-labs/loupe-dom/styles.css";
import { config } from "./loupe.config";

export default function DecidePage() {
  return (
    <Loupe
      config={config}
      // Optional per-mount theme override.
      theme={{ "color-primary": "#2fd4c4" }}
      // Fires after every lock / clear / reset with the live selections.
      onLockChange={(selections) => console.log(selections)}
    />
  );
}

3 · Or generate a self-contained artifact. generate() validates, copies assets, and inlines everything into one index.html that runs anywhere.

generate.tsts
// generate.ts — Node, build-time
import { generate } from "@lucentive-labs/loupe-generator";
import { config } from "./loupe.config";

// Validates (structural + semantic), copies referenced assets, and inlines
// JS + CSS into ONE self-contained index.html. Throws on an invalid config.
const { htmlPath, assets } = await generate(config, {
  outDir: "out/decision-lock",
  assetsDir: "design/boards",
});

console.log(`Artifact: ${htmlPath}`);
console.log(`Copied ${assets.length} asset(s) — open the file anywhere.`);

And the agent path: hand a model the JSON Schema, parse what it returns, and run the semantic validator before render.

agent-authoring.tsts
// agent-authoring.ts — an agent fills the same contract
import {
  toJsonSchema,
  parseConfig,
  validateConfig,
} from "@lucentive-labs/loupe-schema";

// 1. Hand the JSON Schema to the model as the authoring contract.
const schema = toJsonSchema();

// 2. The model returns a config object; parse enforces structure.
const config = parseConfig(modelOutput);

// 3. Semantic checks beyond shape: missing assets, duplicate ids,
//    broken preview references. Empty array = ready to render.
const problems = validateConfig(config);
if (problems.length) throw new Error(problems.join("\n"));

Adoption

Two ways to run it

Ship a portable artifact

Run generate() in a build step and get a single self-contained index.html. It opens from a file path, drops onto any static host, and reviews in a browser with no install — the way design partners and agents pick up a decision lock.

Embed the live React adapter

Mount <Loupe/> inside an existing React 19 app for an always-current lab. It is a thin view over the headless core, so selections, the composed preview, and the export brief stay in sync with the same config the artifact uses. The demo above is this adapter, running here.

Used by

In production at Lucentive

IAS — Intuitive Agent System

The control-plane product surface is decided with a Loupe lock; the exported brief is the ground truth the IAS build pass consumes.

Lucentive agent harnesses

Build agents read the JSON-Schema contract to author and validate a config, then act on the deterministic brief instead of free-text direction.

Human Today

The “Soft Data Body” art direction is captured as a Loupe config (the human-today example fixture), ready to drive its next production pass.

Get started

Lock your next visual decision

Read the docs for the install and the config schema, or read the source. MIT-licensed and on GitHub.