Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"record:catch": "tsx src/record-catch.ts"
},
"dependencies": {
"@aestheticfunction/dspack-emit": "^0.4.1",
"@aestheticfunction/dspack-emit": "^0.5.0",
"@aestheticfunction/dspack-export": "^0.5.0",
"@aestheticfunction/dspack-gen": "^0.2.0",
"@aestheticfunction/dspack-gen": "^0.2.1",
"@aestheticfunction/dspack-spec": "^0.4.2",
"@dspack-studio/agui-bridge": "workspace:*",
"@dspack-studio/composer-core": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions apps/composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"typecheck": "node scripts/demo-assets.mjs && tsc -p tsconfig.json"
},
"dependencies": {
"@aestheticfunction/dspack-gen": "^0.2.0",
"@aestheticfunction/dspack-gen": "^0.2.1",
"@aestheticfunction/dspack-spec": "^0.4.2",
"@dspack-studio/a2ui-ingest": "workspace:*",
"@dspack-studio/agui-bridge": "workspace:*",
Expand All @@ -23,7 +23,7 @@
"react-dom": "19.2.7"
},
"devDependencies": {
"@aestheticfunction/dspack-emit": "^0.4.1",
"@aestheticfunction/dspack-emit": "^0.5.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/design-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const DESIGN_SYSTEMS: Record<DesignSystemId, DesignSystemDef> = {
id: "shadcn",
label: "shadcn/ui",
registry: shadcnRegistry,
note: "11 of 12 catalog components render through vendored shadcn/ui visuals; Dialog shows the unimplemented placeholder (incremental adoption, stated plainly).",
note: "All 12 catalog components render through vendored shadcn/ui visuals.",
},
};

Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@aestheticfunction/dspack-gen": "^0.2.0",
"@aestheticfunction/dspack-gen": "^0.2.1",
"@astryxdesign/core": "0.1.4",
"@astryxdesign/theme-butter": "0.1.4",
"@astryxdesign/theme-chocolate": "0.1.4",
Expand Down
15 changes: 10 additions & 5 deletions docs/renderer-abstraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ The studio's rendering stack is layered so the design system is a plug-in,
not a foundation. This document states the boundaries as they exist in code
today (verified by `packages/a2ui-ingest/src/registry-abstraction.test.ts`).
Since the FM-10 groundwork the swap is real: `packages/shadcn-renderers`
supplies a second registry (11 of 12 catalog names; `Dialog` renders the
unimplemented placeholder by design), selected in the restyle view and
applied to every canvas. The e2e proof (`e2e/design-swap.spec.ts`) replays
one fixture under both design systems and asserts the receipt hash is
identical while the rendered DOM differs.
supplies a second registry covering all 12 catalog names (the `Dialog`
placeholder was the last, closed 2026-08-08 once measurement showed the
registry already rendered 100% of emitted instances — see
`packages/shadcn-renderers/src/dialog-render.test.tsx`). The registry is
selected in the restyle view and applied to every canvas. The
`unimplemented` fallback mechanism itself remains — a registry is still
free to leave a catalog name unrendered — and is exercised by the parity
suite rather than by a shipped gap. The e2e proof
(`e2e/design-swap.spec.ts`) replays one fixture under both design systems
and asserts the receipt hash is identical while the rendered DOM differs.

## The layers

Expand Down
4 changes: 2 additions & 2 deletions e2e/design-swap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test("honesty: the receipt hash is identical under both design systems; the pixe
// Swap the design system in the restyle view (shell-level state).
await page.getByTestId("view-canvas").click();
await page.getByTestId("design-system-shadcn").click();
await expect(page.getByTestId("design-system-note")).toContainText("11 of 12");
await expect(page.getByTestId("design-system-note")).toContainText("All 12 catalog components render");

// Back in replay, the same run renders through shadcn/ui.
await page.getByTestId("view-replay").click();
Expand Down Expand Up @@ -71,7 +71,7 @@ test("the restyle view swaps systems honestly: note, caption, and the theme dial
// shadcn: Astryx themes do not pretend to apply to another design system.
await expect(page.getByRole("button", { name: "butter" })).toHaveCount(0);
await expect(page.getByTestId("fm5-caption")).toContainText("Only the design system did");
await expect(page.getByTestId("design-system-note")).toContainText("unimplemented placeholder");
await expect(page.getByTestId("design-system-note")).toContainText("All 12 catalog components render");
await page.getByTestId("design-system-astryx").click();
await expect(page.getByRole("button", { name: "butter" })).toBeVisible();
await expect(page.getByTestId("fm5-caption")).toContainText("Only the design system's theme did");
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@aestheticfunction/dspack-emit": "0.4.1"
"@aestheticfunction/dspack-emit": "0.5.0"
},
"devDependencies": {
"@astryxdesign/cli": "0.1.4",
Expand Down
43 changes: 43 additions & 0 deletions packages/shadcn-renderers/src/components/DialogRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Catalog `Dialog` -> shadcn/ui Dialog content, rendered INLINE for the same
* reason AlertDialogRender is: on the studio canvas the governed content is
* visible evidence, not hidden behind a portal. The markup and classes are
* shadcn's dialog-content idiom; the portal/overlay behavior is deliberately
* not used, so no Radix dependency enters the bundle.
*
* This completes the shadcn registry to 12 of 12 catalog names — the
* incremental-adoption placeholder docs/renderer-abstraction.md named is now
* exercised as a real visual rather than the unimplemented fallback.
*
* `variant` (standard | fullscreen) widens the surface; `purpose`
* (required | form | info) is carried as a data attribute for styling hooks
* and assistive context without inventing chrome the catalog does not model.
*/
import { useId, type FC } from "react";
import { cn } from "../cn";

export const DialogRender: FC<any> = ({ props, buildChild }) => {
const titleId = useId();
const fullscreen = props.variant === "fullscreen";
return (
<div
role="dialog"
aria-modal="false"
aria-labelledby={props.title ? titleId : undefined}
data-purpose={props.purpose ?? undefined}
Comment on lines +26 to +27
className={cn(
"flex flex-col gap-4 rounded-lg border bg-background p-6 shadow-lg",
fullscreen ? "w-full" : "max-w-lg",
)}
>
{props.title && (
<div className="flex flex-col gap-1.5">
<h2 id={titleId} className="text-lg font-semibold leading-none tracking-tight">
{String(props.title)}
</h2>
</div>
)}
{props.child ? buildChild(props.child) : null}
</div>
);
};
47 changes: 47 additions & 0 deletions packages/shadcn-renderers/src/dialog-render.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* DialogRender — the visual that closed the last shadcn registry placeholder.
*
* Fail-first: before DialogRender existed, `shadcnRegistry.custom.Dialog` was
* undefined and a Dialog instance drew the a2ui-ingest `unimplemented`
* placeholder. `Dialog` is declared catalog vocabulary no worked example
* emits (measured 2026-08-08), so it has no corpus parity coverage — this
* focused test is its verification: given the catalog's Dialog props, the
* renderer draws the governed content (title + child) inline, no portal.
*/
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { shadcnRegistry } from "./registry";

const buildChild = (id: string) => createElement("div", { "data-child": id }, `body:${id}`);

const render = (props: Record<string, unknown>): string => {
const Visual = (shadcnRegistry.custom as Record<string, any>).Dialog;
return renderToStaticMarkup(createElement(Visual, { props, buildChild }));
};

describe("DialogRender", () => {
it("is registered — the last placeholder is now a real visual", () => {
expect((shadcnRegistry.custom as Record<string, unknown>).Dialog).toBeDefined();
});

it("draws the governed title and child inline (role=dialog, not modal)", () => {
const html = render({ title: "Invite people", child: "body_1", variant: "standard", purpose: "form" });
expect(html).toContain('role="dialog"');
expect(html).toContain('aria-modal="false"');
expect(html).toContain("Invite people");
expect(html).toContain("body:body_1");
expect(html).toContain('data-purpose="form"');
});

it("fullscreen widens the surface; standard is constrained", () => {
expect(render({ title: "T", child: "c", variant: "fullscreen" })).toContain("w-full");
expect(render({ title: "T", child: "c", variant: "standard" })).toContain("max-w-lg");
});

it("a titleless dialog omits the heading rather than drawing an empty one", () => {
const html = render({ child: "c" });
expect(html).not.toContain("<h2");
expect(html).toContain("body:c");
Comment on lines +44 to +45
});
});
21 changes: 15 additions & 6 deletions packages/shadcn-renderers/src/emitted-prop-parity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function render(componentName: string, props: Record<string, any>): string {

const visibleText = (html: string) => html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();

/** Instances this design system draws (Dialog is the deliberate placeholder). */
/** Instances this design system draws (now every catalog name, Dialog included). */
const renderable = (): EmittedInstance[] =>
emittedInstances().filter((i) => Boolean((shadcnRegistry.custom as Record<string, any>)[i.component.component]));

Expand Down Expand Up @@ -126,13 +126,22 @@ describe("emitted-prop vs consumed-prop parity", () => {
expect(ignored).toEqual([]);
});

it("covers a corpus that actually exercises the registry", () => {
it("renders every component the corpus actually emits (no unimplemented instance)", () => {
// A guard on the guard: if the corpus ever empties (a moved fixture
// directory, a skipped contracts build) every check above passes vacuously.
const instances = renderable();
expect(instances.length).toBeGreaterThan(100);
const covered = new Set(instances.map((i) => i.component.component));
for (const name of Object.keys(shadcnRegistry.custom)) expect([...covered]).toContain(name);
// The correctness direction is emitted -> rendered: every instance the
// corpus produces must have a visual. The reverse (every renderer must be
// exercised by THIS corpus) is not a correctness property — a registry may
// legitimately cover catalog vocabulary a given corpus doesn't use.
// Measured 2026-08-08: the Astryx corpus emits 11 of the 12 catalog names;
// `Dialog` is declared vocabulary no worked example, scenario, or replay
// fixture emits, yet its renderer exists so a Dialog-bearing surface draws
// rather than falling back to the placeholder.
const all = emittedInstances();
expect(all.length).toBeGreaterThan(100);
const emitted = new Set(all.map((i) => i.component.component));
const rendererNames = Object.keys(shadcnRegistry.custom);
for (const name of emitted) expect(rendererNames).toContain(name);
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/shadcn-renderers/src/registry-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const catalog = JSON.parse(
const names = Object.keys(catalog.components);

describe("shadcn registry parity (catalog owns vocabulary; registry owns pixels)", () => {
it("covers exactly the catalog names, with Dialog as the deliberate placeholder", () => {
it("covers exactly the catalog names, with none left unimplemented", () => {
const plan = planRegistry(names, shadcnRegistry);
expect([...plan.reuseBasic, ...plan.custom, ...plan.unimplemented].sort()).toEqual([...names].sort());
expect(plan.unimplemented).toEqual(["Dialog"]);
expect(plan.unimplemented).toEqual([]);
expect(plan.custom.sort()).toEqual(
["AlertDialog", "Badge", "Button", "Card", "Column", "List", "MetadataList", "SelectableCard", "Table", "Text", "TextField"].sort(),
["AlertDialog", "Badge", "Button", "Card", "Column", "Dialog", "List", "MetadataList", "SelectableCard", "Table", "Text", "TextField"].sort(),
);
});

Expand Down
13 changes: 7 additions & 6 deletions packages/shadcn-renderers/src/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* each wrapped with the same data-a2ui-id provenance tagging as the Astryx
* registry, so X-ray works identically under either design system.
*
* Coverage is deliberately 11 of the catalog's 12 names: `Dialog` has no
* visual here and renders the a2ui-ingest `unimplemented` placeholder —
* the incremental-adoption mechanism docs/renderer-abstraction.md names as
* the intended migration path, exercised in production rather than claimed.
* Coverage is now 12 of the catalog's 12 names. `Dialog` was the last
* placeholder — the incremental-adoption mechanism docs/renderer-abstraction.md
* names — and is now a real shadcn visual (DialogRender), closing the one
* measured production renderer gap. The placeholder mechanism itself stays
* exercised by the parity suite, which proves the complement is now empty.
*/
Comment on lines +9 to 11
import type { Registry } from "@dspack-studio/a2ui-ingest";
import { withProvenance } from "@dspack-studio/a2ui-ingest";
Expand All @@ -21,6 +22,7 @@ import { ColumnRender } from "./components/ColumnRender";
import { ListRender } from "./components/ListRender";
import { SelectableCardRender } from "./components/SelectableCardRender";
import { MetadataListRender } from "./components/MetadataListRender";
import { DialogRender } from "./components/DialogRender";

const renders = {
Text: TextRender,
Expand All @@ -34,8 +36,7 @@ const renders = {
List: ListRender,
SelectableCard: SelectableCardRender,
MetadataList: MetadataListRender,
// Dialog: deliberately absent — renders the visible unimplemented
// placeholder (legal vocabulary, missing pixels; the run is unaffected).
Dialog: DialogRender,
};

export const shadcnRegistry: Registry = {
Expand Down
38 changes: 19 additions & 19 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading