// ---------------------------------------------------------------------------
// The artifact gallery's preview slot.
//
// An artifact is a live component, so the grid needs something to show for it.
// Two things can be shown, and this component prefers the first:
//
// 1. a CAPTURED RENDER — sanitized markup of the artifact actually rendering.
// Written at create time from the smoke render, which produces its loading
// composition (cards, tables, chart frames, skeletons) with no data in it,
// and REPLACED once a viewer opens the artifact and it settles, at which
// point the same composition carries real numbers and rows.
// 2. a SCHEMATIC — the deterministic figure below, derived from the id.
//
// The fallback is total: a real render if there is one, otherwise the
// schematic. Nothing here can fail to draw something, which is what lets every
// producer upstream fail open.
//
// ## Why the markup is inserted directly, and not into an iframe
//
// An iframe with `srcdoc` is the reflex for "render HTML I did not write", and
// it is the wrong tool here on both axes:
//
// - It does not inherit the console's stylesheet. The markup's entire visual
// layer is Tailwind classes plus theme tokens, and a srcdoc document has
// neither — so every preview would need the compiled stylesheet and the
// Geist woff2 inlined into it, per card, or it would render as unstyled
// black-on-white text. Nine cards would each pay that.
// - The security it buys is against a threat this markup does not carry.
//
// What is actually being inserted is not arbitrary HTML. It is OUR OWN server
// render, produced by React's server renderer inside a QuickJS sandbox from
// code that already passed the create-time guards, then reduced by
// `sanitizeArtifactPreviewMarkup` to an ALLOWLIST: known-inert elements, known
// attributes, no `script`, no `style` element, no `img`, no `iframe`, no
// handler, no external reference of any kind, ids namespaced so a preview
// cannot collide with the console document. The sanitizer is the boundary, and
// it is written not to trust the renderer's output either — see its header.
//
// So direct insertion is the correct trade: the preview inherits the console's
// fonts, tokens and dark mode for free, costs one DOM subtree instead of one
// document, and is inert by allowlist rather than by container. The container
// still does its share — `pointer-events-none`, `aria-hidden`, `contain`, and a
// transform scale — so the preview cannot be clicked, focused, read by a screen
// reader, or paint outside its box.
//
// The schematic remains for everything with no preview yet: deliberately
// abstract hairline geometry in the grayscale ramp, the same vocabulary as the
// marketing site's graph-paper illustrations, because a placeholder that
// imitated a screenshot would be lying about what it knows. It is derived from
// `artifactId`, so a given artifact always draws the same figure and the eye
// can use the shape as a weak landmark.
// ---------------------------------------------------------------------------
import { useLayoutEffect, useRef, useState } from "react";
import type { ArtifactPreview as ArtifactPreviewValue } from "@executor-js/sdk/shared";
import { cn } from "../lib/utils";
/** FNV-1a over the id — a stable, well-mixed seed from an opaque string. */
const seedFrom = (value: string): number => {
let hash = 2_166_136_261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16_777_619);
}
return hash >>> 0;
};
/**
* mulberry32 — a tiny deterministic PRNG.
*
* `Math.random` would reshuffle every card on every render; the schematic is
* only useful as a landmark if it is a pure function of the artifact's id.
*/
const sequenceFrom = (seed: number): (() => number) => {
let state = seed;
return () => {
state = (state + 0x6d_2b_79_f5) >>> 0;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
};
};
/** Pick a value in `[min, max)` from the sequence, rounded to whole units. */
const between = (next: () => number, min: number, max: number): number =>
Math.round(min + next() * (max - min));
// The frame the schematics are drawn in. 16:10, matching the card's preview box.
const WIDTH = 320;
const HEIGHT = 200;
// Content inset. The chrome bar sits at the top; body layouts own everything
// below `BODY_TOP`.
const PAD = 20;
const BODY_TOP = 56;
/** Rounded blocks read as UI; every layout builds out of this one primitive. */
function Block(props: {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
readonly className?: string;
}) {
return (
);
}
/**
* The header every artifact has: a title bar and a couple of controls.
*
* Keeping it constant across layouts is what makes the four body figures read
* as variations of one interface rather than four unrelated drawings.
*/
function Chrome(props: { readonly next: () => number }) {
const titleWidth = between(props.next, 64, 120);
return (
<>
>
);
}
/** A bar chart: the shape of a metrics artifact. */
function BarsFigure(props: { readonly next: () => number }) {
const { next } = props;
const count = between(next, 6, 9);
const span = WIDTH - PAD * 2;
const gap = 6;
const barWidth = (span - gap * (count - 1)) / count;
const floor = HEIGHT - PAD - 14;
const maxHeight = floor - BODY_TOP - 4;
return (
<>
{Array.from({ length: count }, (_, index) => {
const height = between(next, Math.round(maxHeight * 0.25), maxHeight);
return (
);
})}
>
);
}
/** A table: the shape of a list or report artifact. */
function RowsFigure(props: { readonly next: () => number }) {
const { next } = props;
const rows = between(next, 4, 6);
const rowHeight = (HEIGHT - PAD - BODY_TOP) / rows;
return (
<>
{Array.from({ length: rows }, (_, index) => {
const y = BODY_TOP + index * rowHeight;
const leadWidth = between(next, 70, 130);
const tailWidth = between(next, 24, 44);
return (
{index < rows - 1 ? (
) : null}
);
})}
>
);
}
/** A tile grid: the shape of a stat or summary dashboard. */
function TilesFigure(props: { readonly next: () => number }) {
const { next } = props;
const columns = 2;
const rows = 2;
const gap = 12;
const tileWidth = (WIDTH - PAD * 2 - gap * (columns - 1)) / columns;
const tileHeight = (HEIGHT - PAD - BODY_TOP - gap * (rows - 1)) / rows;
return (
<>
{Array.from({ length: columns * rows }, (_, index) => {
const column = index % columns;
const row = Math.floor(index / columns);
const x = PAD + column * (tileWidth + gap);
const y = BODY_TOP + row * (tileHeight + gap);
const valueWidth = between(next, 28, 54);
return (
);
})}
>
);
}
/** A list beside a panel: the shape of a detail or explorer artifact. */
function SplitFigure(props: { readonly next: () => number }) {
const { next } = props;
const railWidth = 96;
const panelX = PAD + railWidth + 14;
const panelWidth = WIDTH - PAD - panelX;
const panelHeight = HEIGHT - PAD - BODY_TOP;
const points = Array.from({ length: 7 }, (_, index) => {
const x = panelX + 12 + (index * (panelWidth - 24)) / 6;
const y = BODY_TOP + panelHeight - 16 - between(next, 6, panelHeight - 40);
return `${x},${y}`;
}).join(" ");
return (
<>
{Array.from({ length: 4 }, (_, index) => (
))}
>
);
}
const FIGURES = [BarsFigure, RowsFigure, TilesFigure, SplitFigure] as const;
/** The figure names, in the order `FIGURES` holds them. */
export const ARTIFACT_PREVIEW_FIGURES = ["bars", "rows", "tiles", "split"] as const;
export type ArtifactPreviewFigure = (typeof ARTIFACT_PREVIEW_FIGURES)[number];
/**
* Which schematic an artifact draws.
*
* Exported so the choice is testable as a pure function: determinism is this
* component's contract, not an implementation detail.
*/
export const artifactPreviewFigure = (artifactId: string): ArtifactPreviewFigure =>
ARTIFACT_PREVIEW_FIGURES[seedFrom(artifactId) % ARTIFACT_PREVIEW_FIGURES.length] ?? "bars";
/**
* The width the stored markup is laid out at before being scaled to fit.
*
* The markup came from a server renderer with no viewport, so it has no
* intrinsic width. Laying it out at a desktop width and scaling the whole thing
* down is what makes the card read as a small screenshot of the artifact rather
* than a cramped phone rendering of it — a table stays a table, a four-column
* stat row stays four columns.
*
* 880 matches the width the shell actually frames an artifact at
* (`.artifact-root` is `max-width: 1100px` minus its padding), so the layout
* the card shows is the layout the artifact really has.
*/
const LAYOUT_WIDTH = 880;
/** The 16:10 slice of that width — one card's worth of the artifact. */
const LAYOUT_HEIGHT = Math.round((LAYOUT_WIDTH * 10) / 16);
/**
* The artifact's real loading-state markup, scaled into the card.
*
* `dangerouslySetInnerHTML` is the right call here and the sanitizer is why —
* see this file's header for the full argument. In short: the markup is our own
* sandboxed server render reduced to an allowlist of inert elements and
* attributes, with no script, style element, image, frame, handler or external
* reference able to survive. The wrapper adds containment on top: it cannot be
* clicked or focused (`pointer-events-none`, no tabbable descendants survive
* sanitizing), it is invisible to assistive tech (`aria-hidden`), and `contain`
* plus `overflow-hidden` stop it painting or laying out beyond its box.
*/
function LayoutPreview(props: { readonly markup: string }) {
const frameRef = useRef(null);
const [scale, setScale] = useState(null);
// The scale has to be MEASURED, not written as a constant.
//
// The card is fluid — one, two or three across depending on the viewport — so
// any fixed divisor is right at exactly one breakpoint and wrong at the rest.
// Container query units cannot do it either: `calc(100cqw / 880)` is a
// length, and `scale()` takes a unitless number, so that expression is
// invalid and the browser drops the whole transform — which renders the
// markup at 1:1 and shows nothing but its top-left corner.
useLayoutEffect(() => {
const frame = frameRef.current;
if (!frame) return;
const measure = () => {
const width = frame.clientWidth;
if (width > 0) setScale(width / LAYOUT_WIDTH);
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(frame);
return () => observer.disconnect();
}, []);
return (
);
}
/**
* What the gallery draws for an artifact.
*
* Strict fallback chain — image, else layout, else schematic — so there is
* always something to show and every producer upstream can fail open.
*
* Decorative by construction in all three cases, so it is hidden from assistive
* technology and from the accessibility tree the e2e locators read: the card's
* link already carries the artifact's name.
*/
export function ArtifactPreview(props: {
readonly artifactId: string;
readonly preview?: ArtifactPreviewValue | null;
readonly className?: string;
}) {
if (artifactPreviewKind(props.preview) === "layout" && props.preview) {
return ;
}
return ;
}
/**
* Which preview an artifact gets.
*
* Exported so the fallback is testable as a pure function rather than only
* through a rendered tree: "a real render beats the schematic, and an empty one
* is not a real render" is the contract, and it is the part that would silently
* regress — every producer upstream fails open, so this is what guarantees
* there is always something to draw.
*/
export const artifactPreviewKind = (
preview: ArtifactPreviewValue | null | undefined,
): "layout" | "schematic" => (preview && preview.markup !== "" ? "layout" : "schematic");
/**
* The fallback figure, for an artifact with no captured preview.
*
* Exported so the fallback is testable on its own terms, and so the gallery's
* empty-ish states can draw it deliberately.
*/
export function ArtifactSchematic(props: {
readonly artifactId: string;
readonly className?: string;
}) {
const seed = seedFrom(props.artifactId);
const next = sequenceFrom(seed);
const Figure = FIGURES[seed % FIGURES.length] ?? BarsFigure;
// Unique per artifact: two schematics on one page must not share a pattern id.
const gridId = `artifact-preview-grid-${seed.toString(36)}`;
return (
);
}