A table of prompt blocks against releases: one filled cell wherever that block changed in that release, blank where it did not, plus each version's total token count.
npx shadcn add https://design.helpmarq.com /r/prompt-version-grain.jsonregistry/core/prompt-version-grain/component.tsx"use client";
// ---------------------------------------------------------------------------
// PromptVersionGrain — which prompt blocks changed in which release.
//
// A table: columns are prompt blocks, rows are versions (newest first). A cell
// is filled when that block changed in that release and blank when it did not.
// No hover, no animation, no decoding.
// ---------------------------------------------------------------------------
export interface PromptGrainBlock {
/** Stable key used to look the block up in each version's `changed` map. */
id: string;
/** Column header, e.g. "rules". */
label: string;
}
export interface PromptGrainVersion {
id: string;
/** Short version label, e.g. "v37". */
label: string;
/** Total prompt tokens for this version. */
tokens: number;
/** blockId -> did this block change in this release. */
changed: Record<string, boolean>;
}
export interface PromptVersionGrainProps {
/** Prompt blocks, left to right. */
blocks?: PromptGrainBlock[];
/** Versions, NEWEST FIRST. */
versions?: PromptGrainVersion[];
ariaLabel?: string;
className?: string;
}
const DEFAULT_BLOCKS: PromptGrainBlock[] = [
{ id: "system", label: "system" },
{ id: "persona", label: "persona" },
{ id: "tools", label: "tools" },
{ id: "rules", label: "rules" },
{ id: "examples", label: "examples" },
{ id: "format", label: "format" },
];
function Swatch() {
return (
<span aria-hidden="true" className="inline-block h-2.5 w-2.5 bg-foreground" />
);
}
export function PromptVersionGrain({
blocks = DEFAULT_BLOCKS,
versions = [],
ariaLabel = "Prompt blocks changed per release",
className = "",
}: PromptVersionGrainProps) {
const head =
"font-mono text-[10px] font-normal leading-none tracking-tight text-muted";
return (
<div className={className}>
<table className="w-full border-collapse">
<caption className="sr-only">{ariaLabel}</caption>
<thead>
<tr>
<th scope="col" className={`${head} pb-3 pr-4 text-left`}>
version
</th>
{blocks.map((b) => (
<th key={b.id} scope="col" className={`${head} px-3 pb-3 text-center`}>
{b.label}
</th>
))}
<th scope="col" className={`${head} pb-3 pl-4 text-right`}>
tokens
</th>
</tr>
</thead>
<tbody>
{versions.map((v) => (
<tr key={v.id}>
<th
scope="row"
className="border-t border-border py-2 pr-4 text-left font-mono text-[11px] font-normal tabular-nums text-foreground"
>
{v.label}
</th>
{blocks.map((b) => (
<td key={b.id} className="border-t border-border px-3 py-2 text-center">
{v.changed[b.id] ? <Swatch /> : null}
<span className="sr-only">
{v.changed[b.id] ? "changed" : "unchanged"}
</span>
</td>
))}
<td className="border-t border-border py-2 pl-4 text-right font-mono text-[11px] tabular-nums text-muted">
{v.tokens}
</td>
</tr>
))}
</tbody>
</table>
<p className="mt-4 flex items-center gap-2 font-mono text-[10px] text-muted">
<Swatch />
changed in this release
</p>
</div>
);
}
export default PromptVersionGrain;
the panel that answers 'which parts of this prompt changed in which release' across a whole version history at once. Pick diff-unified-viewer instead when the user needs the actual line-level text change between two specific versions, approval-inline-diff when a proposed edit needs accepting or rejecting, and context-prompt-shims when the user is composing what goes into the prompt rather than auditing what already happened to it.
PromptVersionGrain renders a plain HTML table. Columns are prompt BLOCKS (`blocks: {id,label}[]` — system, persona, tools, rules, examples, format) as `<th scope="col">`, rows are VERSIONS newest at the top (`versions: {id,label,tokens,changed}[]`) with the version label as `<th scope="row">`. `changed` maps blockId to a boolean: true draws a 10px `--foreground` square in that cell, false leaves the cell blank. Every cell also carries a visually-hidden 'changed' / 'unchanged' word, so a screen reader gets the same facts as the swatch and nothing is conveyed by geometry alone. A trailing right-aligned column gives each version's total token count in Geist Mono tabular-nums. Rows are separated by a 1px `--border` hairline. A one-line legend under the table shows the same square next to the words 'changed in this release'. There is no hover state, no focusable element, no animation, no canvas, no SVG, no dependency and no hex literal — colours are only `--foreground`, `--muted` and `--border`, so both themes come for free and `prefers-reduced-motion` is moot. Props: `blocks`, `versions`, `ariaLabel` (becomes the table's visually-hidden `<caption>`), `className`. The component is fully controlled and holds no state. Demo: twelve releases of a support-triage system prompt where `tools` changed in eight releases, `rules` in six, `persona` in two, `examples` in one, and `system` and `format` were never touched.