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/web/app/design-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ export const DESIGN_SYSTEMS: Record<DesignSystemId, DesignSystemDef> = {
id: "astryx",
label: "Astryx",
registry: astryxRegistry,
note: "All 9 catalog components render through @astryxdesign/core, with 8 runtime themes.",
note: "All 12 catalog components render through @astryxdesign/core, with 8 runtime themes.",
},
shadcn: {
id: "shadcn",
label: "shadcn/ui",
registry: shadcnRegistry,
note: "8 of 9 catalog components render through vendored shadcn/ui visuals; Dialog shows the unimplemented placeholder (incremental adoption, stated plainly).",
note: "11 of 12 catalog components render through vendored shadcn/ui visuals; Dialog shows the unimplemented placeholder (incremental adoption, stated plainly).",
},
};

Expand Down
2 changes: 1 addition & 1 deletion docs/renderer-abstraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 (8 of 9 catalog names; `Dialog` renders the
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
Expand Down
2 changes: 1 addition & 1 deletion 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("8 of 9");
await expect(page.getByTestId("design-system-note")).toContainText("11 of 12");

// Back in replay, the same run renders through shadcn/ui.
await page.getByTestId("view-replay").click();
Expand Down
14 changes: 14 additions & 0 deletions packages/astryx-renderers/src/components/ListRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Catalog `List` -> Astryx List. The contract's List takes arbitrary
* children (for a comparison, SelectableCards); density and dividers carry
* verbatim.
*/
import type { FC } from "react";
import { List } from "@astryxdesign/core/List";
import { childIds } from "@dspack-studio/a2ui-ingest";

export const ListRender: FC<any> = ({ props, buildChild }) => (
<List density={props.density} hasDividers={Boolean(props.hasDividers)}>
{childIds(props.children).map(buildChild)}
</List>
);
26 changes: 26 additions & 0 deletions packages/astryx-renderers/src/components/MetadataListRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Catalog `MetadataList` -> Astryx MetadataList. The catalog carries items
* as data-props (the Table idiom — sub-components carry no props under
* grammar-constrained generation); this renderer synthesizes the upstream
* MetadataListItem children from the array.
*/
import type { FC } from "react";
import { MetadataList, MetadataListItem } from "@astryxdesign/core/MetadataList";

interface Item {
label?: unknown;
value?: unknown;
}

export const MetadataListRender: FC<any> = ({ props }) => {
const items: Item[] = Array.isArray(props.items) ? props.items : [];
return (
<MetadataList columns={props.columns} orientation={props.orientation}>
{items.map((item, i) => (
<MetadataListItem key={i} label={String(item.label ?? "")}>
{String(item.value ?? "")}
</MetadataListItem>
))}
</MetadataList>
);
};
26 changes: 26 additions & 0 deletions packages/astryx-renderers/src/components/SelectableCardRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Catalog `SelectableCard` -> Astryx SelectableCard. Presentational in
* replay: `isSelected` is delivered state (selection lives in the shared
* data model), so onChange is a no-op until the interaction overlay grounds
* a select action.
*/
import type { FC } from "react";
import { SelectableCard } from "@astryxdesign/core/SelectableCard";
import { childIds } from "@dspack-studio/a2ui-ingest";

const VARIANTS = new Set([
"default", "transparent", "muted",
"blue", "cyan", "gray", "green", "orange", "pink", "purple", "red", "teal", "yellow",
]);

export const SelectableCardRender: FC<any> = ({ props, buildChild }) => (
<SelectableCard
label={String(props.label ?? "")}
isSelected={Boolean(props.isSelected)}
isDisabled={Boolean(props.isDisabled)}
variant={VARIANTS.has(props.variant as string) ? (props.variant as any) : "default"}
onChange={() => {}}
>
{childIds(props.children).map(buildChild)}
</SelectableCard>
);
6 changes: 6 additions & 0 deletions packages/astryx-renderers/src/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { TableRender } from "./components/TableRender";
import { AlertDialogRender } from "./components/AlertDialogRender";
import { DialogRender } from "./components/DialogRender";
import { ColumnRender } from "./components/ColumnRender";
import { ListRender } from "./components/ListRender";
import { SelectableCardRender } from "./components/SelectableCardRender";
import { MetadataListRender } from "./components/MetadataListRender";

const renders = {
Text: TextRender,
Expand All @@ -27,6 +30,9 @@ const renders = {
AlertDialog: AlertDialogRender,
Dialog: DialogRender,
Column: ColumnRender,
List: ListRender,
SelectableCard: SelectableCardRender,
MetadataList: MetadataListRender,
};

export const astryxRegistry: Registry = {
Expand Down
135 changes: 134 additions & 1 deletion packages/contracts/astryx.dspack.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,139 @@
"composition": {
"notes": "Takes arbitrary children (ReactNode); surfaces express them as child nodes."
}
},
"list": {
"name": "List",
"description": "A vertical collection of items with consistent spacing, dividers, and optional markers.",
"whenToUse": "Use it to display ordered or unordered groups of related content — including groups of selectable options. Lists imply a meaningful collection.",
"whenNotToUse": "Do not use a list for a single item or for laying out unrelated content. Records with uniform fields belong in a table.",
"props": {
"density": {
"type": "enum",
"values": [
"compact",
"balanced",
"spacious"
],
"description": "Item density.",
"default": "balanced"
},
"hasDividers": {
"type": "boolean",
"description": "Renders dividers between items."
}
},
"categories": [
"table-list",
"container"
],
"x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/List/List.doc.mjs",
"composition": {
"notes": "Takes arbitrary children (ReactNode); surfaces express items as child nodes — for a comparison, selectable-card children."
}
},
"selectable-card": {
"name": "SelectableCard",
"description": "A card that toggles between selected and unselected states with an accent border.",
"whenToUse": "Use for plan pickers, filter chips, or option grids — one named option among alternatives, with its selected state visible.",
"whenNotToUse": "Do not use for navigation; use ClickableCard for that (not in this contract).",
"props": {
"label": {
"type": "string",
"description": "The option's name. Every option is named.",
"required": true
},
"isSelected": {
"type": "boolean",
"description": "Whether this option is currently selected. Presentational: selection state lives in the shared data model and re-delivers this prop."
},
"isDisabled": {
"type": "boolean",
"description": "Whether the option can be chosen."
},
"variant": {
"type": "enum",
"values": [
"default",
"transparent",
"muted",
"blue",
"cyan",
"gray",
"green",
"orange",
"pink",
"purple",
"red",
"teal",
"yellow"
],
"description": "Card background variant, verbatim from SelectableCard's union; color variants categorize rather than signal status.",
"default": "default"
}
},
"categories": [
"container",
"data-input"
],
"x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/SelectableCard/SelectableCard.doc.mjs",
"composition": {
"notes": "Takes arbitrary children (ReactNode); an option's content — attributes, badges, actions — are child nodes."
}
},
"metadata-list": {
"name": "MetadataList",
"description": "Key-value pairs for object attributes, in a structured layout. DATA-DRIVEN: items are an array prop, the same Astryx data-props idiom as Table.",
"whenToUse": "MetadataList displays key-value pairs for object attributes like quality, condition, and status. Use it for detail panels, settings summaries, and record information — the attributes that make options comparable.",
"whenNotToUse": "Do not use for extensive form input (use a form layout) or for data without a clear key-value structure.",
"props": {
"items": {
"type": "array",
"description": "Attribute records, each { label, value } — e.g. { \"label\": \"Storage\", \"value\": \"1 TB\" }.",
"required": true,
"x-drift": "upstream MetadataList is children-based (MetadataListItem); the contract carries items as data-props because sub-components carry no props under grammar-constrained generation",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"label",
"value"
]
}
},
"columns": {
"type": "enum",
"values": [
"single",
"multi"
],
"description": "Column layout for the attribute grid.",
"default": "single"
},
"orientation": {
"type": "enum",
"values": [
"vertical",
"horizontal"
],
"description": "Label/value flow direction.",
"default": "vertical"
}
},
"categories": [
"content"
],
"x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/MetadataList/MetadataList.doc.mjs",
"composition": {
"notes": "Upstream MetadataList takes MetadataListItem children; the contract carries items as data-props because sub-components carry no props under grammar-constrained generation — the renderer synthesizes the item children."
}
}
},
"categories": {
Expand Down Expand Up @@ -575,7 +708,7 @@
"require": [
"table"
],
"rationale": "\"Table displays structured data in rows and columns with consistent dimensionality\"; \"Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead\". A record collection is uniform by definition — same fields on every record — so it renders as a table, not as prose or ad-hoc stacks. (Valid while table is this catalog's only collection structure; widen to a choice set if a list component lands.) — Astryx Table docs, v0.1.4",
"rationale": "\"Table displays structured data in rows and columns with consistent dimensionality\"; \"Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead\". A record collection is uniform by definition — same fields on every record — so it renders as a table, not as prose or ad-hoc stacks. (The catalog's list component serves heterogeneous and selectable content, per the same docs; uniform records still choose tables. A table-or-list choice set is inexpressible in current rule types — component-choice require is conjunctive — noted as candidate dspack-gen work if a scenario ever needs it.) — Astryx Table docs, v0.1.4",
"examples": [
"ex.support-ticket-triage"
],
Expand Down
113 changes: 113 additions & 0 deletions packages/contracts/src/astryx-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,119 @@ export const astryxProfile: Profile = {
required: [],
surfacePlan: { textProp: "text", childrenProp: "children" },
},

{
a2ui: "List",
dspackId: "list",
commons: ["ComponentCommon"],
structural: {
children: {
schema: { $ref: "#/$defs/ChildList" },
description: "Child component IDs rendered as the list's items, in order.",
synthNote:
"The contract's List takes arbitrary children (upstream List accepts ReactNode); " +
"projected as an ordered child list. For a comparison, the children are " +
"SelectableCards.",
},
},
propMap: {
density: {
a2ui: "density",
kind: "enum",
targetEnum: ["compact", "balanced", "spacious"],
default: "balanced",
description: "Item density, carried verbatim.",
},
hasDividers: {
a2ui: "hasDividers",
kind: "boolean",
description: "Renders dividers between items.",
},
},
required: ["children"],
surfacePlan: { childrenProp: "children" },
},

{
a2ui: "SelectableCard",
dspackId: "selectable-card",
commons: ["ComponentCommon"],
structural: {
children: {
schema: { $ref: "#/$defs/ChildList" },
description: "IDs of the option's content components: attributes, badges, actions.",
synthNote:
"The contract's SelectableCard takes arbitrary children (ReactNode); projected " +
"as a child list.",
},
},
propMap: {
label: {
a2ui: "label",
kind: "string",
description: "The option's name, carried verbatim.",
},
isSelected: {
a2ui: "isSelected",
kind: "boolean",
description:
"Selected state, carried verbatim. Presentational: selection lives in the shared " +
"data model and re-delivers this prop; the card itself carries no action " +
"(upstream onChange is the interaction overlay's territory, out of contract scope).",
},
isDisabled: {
a2ui: "isDisabled",
kind: "boolean",
description: "Whether the option can be chosen.",
},
variant: {
a2ui: "variant",
kind: "enum",
targetEnum: [
"default", "transparent", "muted",
"blue", "cyan", "gray", "green", "orange", "pink", "purple", "red", "teal", "yellow",
],
default: "default",
description: "Card background variant, carried verbatim from SelectableCard's union.",
},
},
required: ["label"],
surfacePlan: { childrenProp: "children" },
},

{
a2ui: "MetadataList",
dspackId: "metadata-list",
commons: ["ComponentCommon"],
structural: {
items: {
schema: { type: "array", items: { type: "object" } },
description: "Attribute records, each { label, value }.",
synthNote:
"The contract carries items as data-props (the Table idiom) because " +
"sub-components carry no props under grammar-constrained generation; the " +
"renderer synthesizes upstream MetadataListItem children from this array.",
},
},
propMap: {
columns: {
a2ui: "columns",
kind: "enum",
targetEnum: ["single", "multi"],
default: "single",
description: "Column layout for the attribute grid, carried verbatim.",
},
orientation: {
a2ui: "orientation",
kind: "enum",
targetEnum: ["vertical", "horizontal"],
default: "vertical",
description: "Label/value flow direction, carried verbatim.",
},
},
required: ["items"],
surfacePlan: { structuralPassthrough: ["items"] },
},
],

synthesized: [
Expand Down
Loading
Loading