Skip to content

feat: native @a2ui/angular rendering — custom catalog - #121

Open
jerelvelarde wants to merge 5 commits into
a2ui-project:mainfrom
jerelvelarde:jerel/pr3-native-catalog
Open

feat: native @a2ui/angular rendering — custom catalog#121
jerelvelarde wants to merge 5 commits into
a2ui-project:mainfrom
jerelvelarde:jerel/pr3-native-catalog

Conversation

@jerelvelarde

@jerelvelarde jerelvelarde commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Adds native (no-iframe) A2UI v0.9 rendering via @a2ui/angular, with an 11-component custom catalog as the first showcase. This is the foundation of a three-PR set — everything here is what the other two build on.

What

  • Adds @a2ui/angular + @a2ui/web_core for native A2UI rendering, alongside the existing iframe sandbox.
  • An 11-component custom A2UI v0.9 catalog (zod contract) with native Angular renderers — layout (Row / Column / DashboardCard), content (Title / Badge), data display (Metric / PieChart / BarChart / DataTable / FlightCard, with hand-rolled SVG charts), and an interactive Button.
  • An Assembled Components route (/custom-catalog): two example trees assembled from the one catalog, data-model editable with live re-bind.
  • New nav entry + route; renders natively (no startupGuard/iframe).

Why

Demonstrates the native rendering path and provides a reusable component catalog that the follow-up PRs build directly on.

Testing

ng build (lint + AOT) green on a clean build; custom-catalog + shell unit tests pass (25 specs).

Split note

Originally opened as one large PR spanning custom catalog + catalog reference + theater. Per review-size feedback it's now split into three stacked PRs, all based on main:

Because cross-repo PRs must target main, #129 and #130 include this PR's commits in their diffs until this one merges.


Contributed by CopilotKit.

Screenshots

Native @a2ui/angular rendering (no iframe) — 11-component catalog with SVG charts:

Light Dark

@google-cla

google-cla Bot commented Jul 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new features and architectural updates, including a native /custom-catalog route with assembled demo components, a /catalog-reference route for component documentation and source viewing, a /theater route for protocol playback, and a masonry /gallery showcase. The existing components gallery has been moved to /basic-catalog. Feedback focuses on improving code robustness and aligning with Angular best practices. Specifically, the path resolution in the gallery codegen script should be made more resilient to standard clone structures, and Angular effects used for propagating state changes and writing to signals in CustomCatalog and CatalogReference should be replaced with explicit state updates in event handlers and view-switching methods.

Comment on lines +40 to +44
const defaultSource = resolve(
scriptDir,
'../../../../private_a2ui_demo/apps/widget-builder/src/data/gallery/v09/generated.ts',
);
const sourceFile = process.argv[2] ? resolve(process.argv[2]) : defaultSource;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The hardcoded relative path ../../../../private_a2ui_demo assumes a specific nested worktree directory structure (e.g., composer-wt/ux-overhaul) and will break for other developers who clone the repository normally (where private_a2ui_demo is a sibling of the repository root).

We can make this robust for both standard clones and nested worktrees by checking if the standard sibling path exists first, and falling back to the worktree path if not.

const standardSource = resolve(
  shellDir,
  '../../private_a2ui_demo/apps/widget-builder/src/data/gallery/v09/generated.ts',
);
const worktreeSource = resolve(
  scriptDir,
  '../../../../private_a2ui_demo/apps/widget-builder/src/data/gallery/v09/generated.ts',
);
const defaultSource = existsSync(standardSource) ? standardSource : worktreeSource;
const sourceFile = process.argv[2] ? resolve(process.argv[2]) : defaultSource;

Comment on lines +110 to +123
constructor() {
// Re-render the surface whenever the selected example or data state changes,
// and reset the editor to that state's data model. Writes here don't feed
// the effect (untracked), so this runs once per selection, not per keystroke.
effect(() => {
const example = this.activeExample();
const index = this.activeStateIndex();
const data = example.states[index]?.data ?? {};
untracked(() => {
this.editorValue.set(JSON.stringify(data, null, 2));
this.applyData(example, data);
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Angular effect to propagate state changes and trigger imperative side effects (like writing to other signals or calling services) is generally discouraged. According to Angular best practices, side effects and state updates should be handled directly in event listeners/handlers whenever possible to prevent cascading updates and maintain clear data flow.

Since the active example and state index are only changed via user interaction (selectExample and selectState), we can initialize the state in the constructor and handle updates explicitly in those methods. This completely eliminates the need for the effect.

  constructor() {
    const example = this.activeExample();
    const data = example.states[0]?.data ?? {};
    this.editorValue.set(JSON.stringify(data, null, 2));
    this.applyData(example, data);
  }

Comment on lines +144 to +154
/** Selects a demo tree, resetting to its first data state. */
protected selectExample(id: string): void {
if (id === this.activeExampleId()) return;
this.activeStateIndex.set(0);
this.activeExampleId.set(id);
}

/** Selects one of the active demo's preset data states. */
protected selectState(index: number): void {
this.activeStateIndex.set(index);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In conjunction with removing the effect from the constructor, update selectExample and selectState to explicitly update the editor value and apply the new data model. This makes the data flow much more predictable and easier to debug.

  /** Selects a demo tree, resetting to its first data state. */
  protected selectExample(id: string): void {
    if (id === this.activeExampleId()) return;
    this.activeStateIndex.set(0);
    this.activeExampleId.set(id);
    const example = this.examples.find(e => e.id === id) ?? this.examples[0];
    const data = example.states[0]?.data ?? {};
    this.editorValue.set(JSON.stringify(data, null, 2));
    this.applyData(example, data);
  }

  /** Selects one of the active demo's preset data states. */
  protected selectState(index: number): void {
    this.activeStateIndex.set(index);
    const example = this.activeExample();
    const data = example.states[index]?.data ?? {};
    this.editorValue.set(JSON.stringify(data, null, 2));
    this.applyData(example, data);
  }

Comment on lines +147 to +158
// Default-select the first file whenever a source view is entered (or its
// file list changes) and nothing valid is selected.
effect(() => {
const files = this.sourceFiles();
if (this.viewMode() === 'components') return;
const hasSelection = untracked(() =>
files.some(file => file.path === this.selectedSourcePath()),
);
if (hasSelection) return;
const first = files[0];
if (first) untracked(() => this.selectedSourcePath.set(first.path));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Writing to signals inside an effect is generally discouraged in Angular as it can lead to cascading updates, infinite loops, and runtime errors (and is disallowed by default in Angular 17+ without allowSignalWrites: true).

Since CATALOG_SOURCE is static data, the file list (sourceFiles) only changes when viewMode changes. We can completely eliminate this effect by setting the default selected file directly in setViewMode when switching views.

Comment on lines +180 to +182
protected setViewMode(mode: ReferenceView): void {
this.viewMode.set(mode);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In conjunction with removing the signal-writing effect from the constructor, update setViewMode to automatically select the first file when switching to a source view.

  protected setViewMode(mode: ReferenceView): void {
    this.viewMode.set(mode);
    if (mode !== 'components') {
      const files = mode === 'renderers' ? CATALOG_SOURCE.renderers : CATALOG_SOURCE.definitions;
      if (files[0]) {
        this.selectedSourcePath.set(files[0].path);
      }
    }
  }

@jerelvelarde
jerelvelarde marked this pull request as ready for review July 23, 2026 12:23
@jerelvelarde
jerelvelarde force-pushed the jerel/pr3-native-catalog branch 2 times, most recently from ef6deb1 to 3f56de9 Compare July 24, 2026 17:47
@jerelvelarde jerelvelarde changed the title feat: native @a2ui/angular rendering — custom catalog, catalog reference, theater, gallery feat: native @a2ui/angular rendering — custom catalog Jul 24, 2026
Enables in-app (no-iframe) rendering of custom catalogs. Pin shell zod to
3.25.76 so app-authored catalog schemas share the exact zod instance the
@a2ui/web_core binder introspects (Angular's own zod 4 stays nested/isolated).
Validated end-to-end: a shell-authored custom Title resolves a {path} binding
via <a2ui-v09-surface>.
Port of the react-flight-catalog dashboard catalog to native @a2ui/angular
renderers (Row/Column/Title/Badge/Metric/DashboardCard/FlightCard/DataTable/
Button + hand-rolled SVG PieChart/BarChart), themed via --cpk-* tokens.
buildDashboardCatalog() registers all 11 into an AngularCatalog. Surface-render
tests green (children, chart shapes, path bindings, action dispatch).
Native /custom-catalog page: renders the Flight Card + Sales Dashboard trees
via <a2ui-v09-surface> (no iframe) from the 11-component dashboard catalog,
with an editable Monaco data-model panel. Example toggle + data-state tabs
(Morning/Afternoon, Q1-Q4); editing the JSON live-updates the surface via
updateDataModel. Surfaces are built once per example (createSurface+
updateComponents), so data changes push updateDataModel only. Nav item added
to Google's list; shell spec nav counts updated for the new route.
@jerelvelarde
jerelvelarde force-pushed the jerel/pr3-native-catalog branch 2 times, most recently from 2b5db7d to 41d0120 Compare July 27, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant