Skip to content
Open
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ coverage
**/.angular/
**/test-results/
**/playwright-report/**
**/*.generated.ts
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default tseslint.config(
'**/node_modules/**',
'**/vite.config.ts',
'**/vitest.config.ts',
'**/*.generated.ts',
'shell/scripts/**',
],
},

Expand Down
6 changes: 5 additions & 1 deletion shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
"prettier": "prettier --ignore-path ../.prettierignore --write . | grep -v '(unchanged)'",
"prettier:check": "prettier --ignore-path ../.prettierignore --check .",
"clean": "rm -rf dist .angular node_modules/.vite",
"generate:catalog-source": "node scripts/generate-catalog-source.mjs",
"lint": "eslint ."
},
"dependencies": {
"@a2ui/angular": "0.10.4",
"@a2ui/web_core": "0.10.5",
"@angular/animations": "22.0.7",
"@angular/cdk": "22.0.5",
"@angular/common": "22.0.7",
Expand All @@ -34,7 +37,8 @@
"monaco-editor": "0.56.0",
"rxjs": "7.8.2",
"safevalues": "1.2.0",
"tslib": "2.8.1"
"tslib": "2.8.1",
"zod": "3.25.76"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "2.6.3",
Expand Down
102 changes: 102 additions & 0 deletions shell/scripts/generate-catalog-source.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Bundles the custom catalog's TypeScript source (definitions + renderers) into
* a committed data module the Catalog Reference route can display verbatim.
*
* Why codegen instead of `?raw` imports: the `?raw` suffix works under
* Angular's Vite dev-server but the esbuild production builder ignores it,
* which would break `ng build`. A plain generated `export const` is robust in
* both. Re-run after changing catalog source: yarn generate:catalog-source
*/
import {readFileSync, readdirSync, writeFileSync, statSync, mkdirSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
import {dirname, join, relative} from 'node:path';

const scriptDir = dirname(fileURLToPath(import.meta.url));
const shellDir = join(scriptDir, '..');
const repoRoot = join(shellDir, '..');
const catalogDir = join(shellDir, 'src/app/custom-catalog/catalog');
const componentsDir = join(catalogDir, 'components');
const outFile = join(shellDir, 'src/app/custom-catalog/reference/catalog-source.generated.ts');

/** Repo-relative POSIX path for stable display + labels. */
const repoPath = abs => relative(repoRoot, abs).split('\\').join('/');

/** 'bar-chart' -> 'BarChart' (matches the ComponentApi display names). */
const toPascal = kebab =>
kebab
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');

const readSourceFile = (abs, label) => ({label, path: repoPath(abs), code: readFileSync(abs, 'utf8')});

// Definitions: the zod contract + the Angular catalog assembly.
const definitions = [
readSourceFile(join(catalogDir, 'apis.ts'), 'apis.ts'),
readSourceFile(join(catalogDir, 'dashboard-catalog.ts'), 'dashboard-catalog.ts'),
];

// Renderers: one per component directory (components/<name>/<name>.ts),
// skipping specs and the shared test harness.
const renderers = readdirSync(componentsDir)
.filter(entry => statSync(join(componentsDir, entry)).isDirectory())
.sort()
.map(dir => readSourceFile(join(componentsDir, dir, `${dir}.ts`), toPascal(dir)));

const payload = {definitions, renderers};

const banner = `/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* eslint-disable */
// GENERATED by scripts/generate-catalog-source.mjs — do not edit by hand.
// Run \`yarn generate:catalog-source\` from shell/ after changing catalog source.

/** One bundled catalog source file, shown verbatim in the Catalog Reference. */
export interface CatalogSourceFile {
label: string;
path: string;
code: string;
}

export const CATALOG_SOURCE: {
definitions: CatalogSourceFile[];
renderers: CatalogSourceFile[];
} = `;

mkdirSync(dirname(outFile), {recursive: true});
writeFileSync(outFile, `${banner}${JSON.stringify(payload, null, 2)};\n`, 'utf8');

console.info(
`[generate-catalog-source] wrote ${repoPath(outFile)} — ` +
`${definitions.length} definitions, ${renderers.length} renderers.`,
);
14 changes: 14 additions & 0 deletions shell/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ export const routes: Routes = [
title: 'A2UI Components Gallery',
canActivate: [startupGuard],
},
{
path: 'custom-catalog',
loadComponent: () =>
import('./custom-catalog/assembled/custom-catalog').then(m => m.CustomCatalog),
title: 'A2UI Custom Catalog',
// No startupGuard: renders natively via @a2ui/angular, not the iframe renderer.
},
{
path: 'catalog-reference',
loadComponent: () =>
import('./custom-catalog/reference/catalog-reference').then(m => m.CatalogReference),
title: 'A2UI Catalog Reference',
// No startupGuard: renders natively via @a2ui/angular, not the iframe renderer.
},
{
path: 'settings',
loadComponent: () => import('./settings/settings-view/settings').then(m => m.Settings),
Expand Down
74 changes: 74 additions & 0 deletions shell/src/app/custom-catalog/assembled/custom-catalog.ng.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!--
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<div class="cc-page">
<header class="cc-header">
<div class="cc-intro">
<h1 class="cc-title">Custom Catalog</h1>
<p class="cc-subtitle">
Two example trees assembled from one custom A2UI v0.9 catalog (11 components), rendered
natively. Edit the data model on the right — the preview updates live.
</p>
</div>
<mat-button-toggle-group
class="cc-example-switch"
[value]="activeExampleId()"
(change)="selectExample($event.value)"
aria-label="Example tree"
hideSingleSelectionIndicator
>
@for (ex of examples; track ex.id) {
<mat-button-toggle [value]="ex.id">
<mat-icon aria-hidden="true">{{ ex.icon }}</mat-icon>
<span class="cc-example-label">{{ ex.label }}</span>
</mat-button-toggle>
}
</mat-button-toggle-group>
</header>

<div class="cc-body">
<section class="cc-preview">
<div class="cc-pane-label">Assembled Components</div>
<div class="cc-surface-scroll">
<a2ui-v09-surface [surfaceId]="activeExampleId()"></a2ui-v09-surface>
</div>
</section>

<section class="cc-data">
<div class="cc-pane-label">Component Data</div>
<div class="cc-data-tabs" role="tablist">
@for (state of activeStates(); track state.name; let i = $index) {
<button
type="button"
role="tab"
class="cc-data-tab"
[class.active]="i === activeStateIndex()"
[attr.aria-selected]="i === activeStateIndex()"
(click)="selectState(i)"
>
{{ state.name }}
</button>
}
</div>
<div class="cc-editor">
<a2ui-composer-monaco-editor
[value]="editorValue()"
(valueChange)="onEdit($event)"
></a2ui-composer-monaco-editor>
</div>
</section>
</div>
</div>
Loading