diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md
index 9915fa400..059db43d4 100644
--- a/docs/guides/custom-components.md
+++ b/docs/guides/custom-components.md
@@ -1,83 +1,411 @@
+---
+render_macros: false
+---
+
# Custom Component Catalogs
-Extend A2UI by defining **custom catalogs** that include your own components alongside standard A2UI components.
+Extend A2UI with a catalog of your own components — video players, maps, charts, or anything your application needs.
## Why Custom Catalogs?
-The A2UI Standard Catalog provides common UI elements (buttons, text fields, etc.), but your application might need specialized components:
+The A2UI Basic Catalog covers common UI elements (text, buttons, inputs, layout), but your application might need specialized components:
+
+- **Media**: YouTube embeds, audio visualizers, 3D viewers
+- **Maps**: Google Maps, Mapbox, Leaflet
+- **Charts**: Chart.js, D3, Recharts
+- **Domain-specific**: Stock tickers, medical imaging, CAD viewers
+
+Custom catalogs let agents generate UI that includes **any** component your app supports — not just what's in the basic catalog.
+
+!!! tip "Already have a component library?"
+ If you're adding A2UI to an existing app with its own design system (Material, Ant Design, PrimeNG, etc.), start with the [Design System Integration](design-system-integration.md) guide first — it walks through wrapping your existing components as A2UI components.
+
+## How It Works
+
+```
+Agent ──generates──> A2UI JSON ──references──> "GoogleMap" component
+ │
+Client ──registers──> Catalog { GoogleMap: ... } ───┘
+ │
+Angular ──renders──> <───┘
+```
+
+1. You **implement** an Angular component that extends `DynamicComponent`
+2. You **register** it in a catalog
+3. The agent **references** it by name in `updateComponents` messages
+4. The A2UI renderer **instantiates** your component with the agent's properties
+
+## Adding a Custom Component: YouTube Example
+
+Let's add a YouTube video player as a custom A2UI component.
+
+### 1. Create the Component
+
+Custom components extend `DynamicComponent` from `@a2ui/angular`:
+
+```typescript
+// a2ui-catalog/youtube.ts
+import { DynamicComponent } from '@a2ui/angular';
+import * as Primitives from '@a2ui/web_core/types/primitives';
+import * as Types from '@a2ui/web_core/types/types';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+
+@Component({
+ selector: 'a2ui-youtube',
+ changeDetection: ChangeDetectionStrategy.Eager,
+ styles: `
+ :host { display: block; flex: var(--weight); padding: 8px; }
+ .video-container {
+ position: relative;
+ width: 100%;
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
+ border-radius: 8px;
+ overflow: hidden;
+ }
+ iframe {
+ position: absolute;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+ border: none;
+ }
+ `,
+ template: `
+ @if (resolvedVideoId()) {
+ @if (resolvedTitle()) {
+
{{ resolvedTitle() }}
+ }
+
+
+
+ }
+ `,
+})
+export class YouTube extends DynamicComponent {
+ private static readonly YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/;
+
+ readonly videoId = input.required();
+ protected readonly resolvedVideoId = computed(() =>
+ this.resolvePrimitive(this.videoId()),
+ );
+
+ readonly title = input();
+ protected readonly resolvedTitle = computed(() =>
+ this.resolvePrimitive(this.title() ?? null),
+ );
+
+ protected readonly safeUrl = computed(() => {
+ const id = this.resolvedVideoId();
+ if (!id) return null;
+
+ // Validate video ID format before constructing URL
+ if (!YouTube.YOUTUBE_ID_REGEX.test(id)) {
+ console.error('Invalid YouTube video ID received from agent:', id);
+ return null;
+ }
+
+ const url = `https://www.youtube.com/embed/${encodeURIComponent(id)}`;
+ return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+ });
+
+ constructor(private sanitizer: DomSanitizer) {
+ super();
+ }
+}
+```
+
+**Key patterns:**
+
+- Extend `DynamicComponent` for custom component types
+- Use `input()` for properties the agent will set
+- Use `resolvePrimitive()` to resolve values that may be literals or data-bound paths
+- Use `computed()` for reactive derivations
+- Validate agent-provided data before use (e.g., video ID format check)
+
+### 2. Register in a Catalog
+
+Add your component to a catalog:
+
+```typescript
+// a2ui-catalog/catalog.ts
+import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular';
+import { inputBinding } from '@angular/core';
+
+export const MY_CATALOG = {
+ ...DEFAULT_CATALOG, // Optionally include A2UI basic catalog components
+
+ YouTube: {
+ type: () => import('./youtube').then((r) => r.YouTube),
+ bindings: ({ properties }) => [
+ inputBinding('videoId', () =>
+ ('videoId' in properties && properties['videoId']) || undefined
+ ),
+ inputBinding('title', () =>
+ ('title' in properties && properties['title']) || undefined
+ ),
+ ],
+ },
+} as Catalog;
+```
+
+A2UI ships with a basic catalog to get you started quickly, but you do **not** need to use it. If your design system already provides the components you want to render, you can expose only your own components, or a mix of basic and custom components.
+
+**What's happening:**
+
+- `...DEFAULT_CATALOG` — optionally spread the basic catalog (Text, Button, etc.)
+- `type` — lazy-loaded import of your component class
+- `bindings` — maps properties from the A2UI JSON to Angular `@Input()` values
+
+### 3. Use the Custom Catalog
+
+Update your app config to use your catalog:
+
+```typescript
+// app.config.ts
+import { MY_CATALOG } from './a2ui-catalog/catalog';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ configureChatCanvasFeatures(
+ usingA2aService(MyA2aService),
+ usingA2uiRenderers(MY_CATALOG, theme),
+ ),
+ ],
+};
+```
+
+### 4. Agent-Side: Using the Custom Component
+
+The agent references your component by name in `updateComponents`:
+
+```json
+{
+ "type": "updateComponents",
+ "surfaceId": "main",
+ "components": {
+ "root": {
+ "component": "Column",
+ "properties": {},
+ "childIds": ["vid1"]
+ },
+ "vid1": {
+ "component": "YouTube",
+ "properties": {
+ "videoId": "dQw4w9WgXcQ",
+ "title": "Check out this video"
+ }
+ }
+ }
+}
+```
+
+The agent knows about your custom components through the catalog configuration in its prompt or system instructions.
+
+## Google Maps Example
+
+A map component that displays pins from the agent's data model:
+
+```typescript
+// a2ui-catalog/google-map.ts
+@Component({
+ selector: 'a2ui-map',
+ imports: [GoogleMapsModule],
+ template: `
+
+ @for (pin of resolvedPins(); track pin) {
+
+ }
+
+ `,
+})
+export class GoogleMap extends DynamicComponent {
+ readonly zoom = input();
+ readonly center = input<{ path: string } | null>();
+ readonly pins = input<{ path: string }>();
+
+ protected resolvedZoom = computed(() => this.resolvePrimitive(this.zoom()));
+ protected resolvedCenter = computed(() => this.resolveLatLng(this.center()));
+ protected resolvedPins = computed(() => this.resolveLocations(this.pins()));
+ // ... (resolve helpers iterate over data model paths)
+}
+```
+
+**Catalog entry:**
+
+```typescript
+GoogleMap: {
+ type: () => import('./google-map').then((r) => r.GoogleMap),
+ bindings: ({ properties }) => [
+ inputBinding('zoom', () => properties['zoom'] || 8),
+ inputBinding('center', () => properties['center'] || undefined),
+ inputBinding('pins', () => properties['pins'] || undefined),
+ inputBinding('title', () => properties['title'] || undefined),
+ ],
+},
+```
-- **Domain-specific widgets**: Stock tickers, medical charts, CAD viewers
-- **Third-party integrations**: Google Maps, payment forms, chat widgets
-- **Brand-specific components**: Custom date pickers, product cards, dashboards
+**Agent JSON:**
-**Custom catalogs** are collections of components that can include:
-- Standard A2UI components (Text, Button, TextField, etc.)
-- Your custom components (GoogleMap, StockTicker, etc.)
-- Third-party components
+```json
+{
+ "component": "GoogleMap",
+ "properties": {
+ "zoom": 12,
+ "center": { "path": "/mapCenter" },
+ "pins": { "path": "/restaurants" },
+ "title": "Nearby Restaurants"
+ }
+}
+```
-You register entire catalogs with your client application, not individual components. This allows agents and clients to agree on a shared, extended set of components while maintaining security and type safety.
+Maps uses **data binding** — the `center` and `pins` reference paths in the data model, so the agent can update locations dynamically via `updateDataModel`.
-## How Custom Catalogs Work
+## Chart Example
-1. **Client Defines Catalog**: You create a catalog definition that lists both standard and custom components.
-2. **Client Registers Catalog**: You register the catalog (and its component implementations) with your client app.
-3. **Client Announces Support**: The client informs the agent which catalogs it supports.
-4. **Agent Selects Catalog**: The agent chooses a catalog for a given UI surface.
-5. **Agent Generates UI**: The agent generates component messages (`surfaceUpdate` in v0.8, `updateComponents` in v0.9) using components from that catalog by name.
+A chart component using Chart.js:
-## Defining Custom Catalogs
+```typescript
+// a2ui-catalog/chart.ts
+@Component({
+ selector: 'a2ui-chart',
+ imports: [BaseChartDirective],
+ template: `
+
+
{{ resolvedTitle() }}
+
+
+ `,
+})
+export class Chart extends DynamicComponent {
+ readonly type = input.required();
+ readonly title = input();
+ readonly chartData = input.required();
-TODO: Add detailed guide for defining custom catalogs for each platform.
+ protected chartType = computed(() => this.type() as ChartType);
+ protected resolvedTitle = computed(() => this.resolvePrimitive(this.title() ?? null));
+ // ... (resolve chart data from data model paths)
+}
+```
-**Web (Lit / Angular):**
+**Catalog entry:**
-- How to define a catalog with both standard and custom components
-- How to register the catalog with the A2UI client
-- How to implement custom component classes
+```typescript
+Chart: {
+ type: () => import('./chart').then((r) => r.Chart),
+ bindings: ({ properties }) => [
+ inputBinding('type', () => properties['type'] || undefined),
+ inputBinding('title', () => properties['title'] || undefined),
+ inputBinding('chartData', () => properties['chartData'] || undefined),
+ ],
+},
+```
-**Flutter:**
+## Any Component
-- How to define custom catalogs using GenUI
-- How to register custom component renderers
+The same pattern works for **any Angular component**. If you can build it as an Angular component, you can make it an A2UI custom component:
-**See working examples:**
+- **Carousel**: Wrap your carousel library, bind slides via data model paths
+- **Code editor**: Monaco editor with syntax highlighting
+- **3D viewer**: Three.js scene driven by agent data
+- **Payment form**: Stripe Elements with A2UI event callbacks
+- **PDF viewer**: Display documents the agent references
-- [Lit samples](https://github.com/google/a2ui/tree/main/samples/client/lit)
-- [Angular samples](https://github.com/google/a2ui/tree/main/samples/client/angular)
-- [Flutter GenUI docs](https://docs.flutter.dev/ai/genui)
+The pattern is always:
-## Agent-Side: Using Components from a Custom Catalog
+1. Extend `DynamicComponent`
+2. Declare `input()` properties
+3. Use `resolvePrimitive()` for data binding
+4. Register in catalog with `inputBinding()` mappings
-Once a catalog is registered on the client, agents can use components from it in `surfaceUpdate` messages.
+## Data Binding with Custom Components
-The agent specifies which catalog to use via the `catalogId` in the `beginRendering` message.
+Custom components can use A2UI's data binding system. Instead of literal values, properties can reference paths in the data model:
-TODO: Add examples of:
+```json
+{
+ "component": "Chart",
+ "properties": {
+ "type": "pie",
+ "title": "Sales by Region",
+ "chartData": { "path": "/salesData" }
+ }
+}
+```
-- How agents select catalogs
-- How agents reference custom components from catalogs
-- How catalog versioning works
+The agent updates data separately via `updateDataModel`:
-## Data Binding and Actions
+```json
+{
+ "type": "updateDataModel",
+ "surfaceId": "main",
+ "data": {
+ "salesData": [
+ { "label": "North America", "value": 45 },
+ { "label": "Europe", "value": 30 },
+ { "label": "Asia", "value": 25 }
+ ]
+ }
+}
+```
-Custom components support the same data binding and action mechanisms as standard components:
+This separation means the agent can update chart data without re-sending the entire component tree.
-- **Data binding**: Custom components can bind properties to data model paths using JSON Pointer syntax
-- **Actions**: Custom components can emit actions that the agent receives and handles
+## Agent Configuration
-## Security Considerations
+For agents to use your custom components, include the component definitions in the agent's prompt or catalog configuration:
-When creating custom catalogs and components:
+```python
+# Agent-side catalog config
+catalog = CatalogConfig(
+ catalog_id="my-custom-catalog",
+ components={
+ "YouTube": {
+ "description": "Embedded YouTube video player",
+ "properties": {
+ "videoId": "YouTube video ID (e.g., 'dQw4w9WgXcQ')",
+ "title": "Optional title displayed above the video",
+ },
+ },
+ "GoogleMap": {
+ "description": "Interactive Google Map with pins",
+ "properties": {
+ "zoom": "Map zoom level (1-20)",
+ "center": "Center coordinates (data model path)",
+ "pins": "Array of pin locations (data model path)",
+ },
+ },
+ "Chart": {
+ "description": "Chart.js chart (pie, bar, line, doughnut)",
+ "properties": {
+ "type": "Chart type: pie, bar, line, doughnut",
+ "title": "Chart title",
+ "chartData": "Chart data (data model path)",
+ },
+ },
+ },
+)
+```
-1. **Allowlist components**: Only register components you trust in your catalogs
-2. **Validate properties**: Always validate component properties from agent messages
-3. **Sanitize user input**: If components accept user input, sanitize it before processing
-4. **Limit API access**: Don't expose sensitive APIs or credentials to custom components
+## Working Examples
-TODO: Add detailed security best practices and code examples.
+- [**rizzcharts**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/rizzcharts) — Chart.js + Google Maps + YouTube custom components
## Next Steps
-- **[Theming & Styling](theming.md)**: Customize the look and feel of components
-- **[Component Reference](../reference/components.md)**: See all standard components
-- **[Agent Development](agent-development.md)**: Build agents that use custom components
+- [Design System Integration](design-system-integration.md) — Wrap your existing design system components as A2UI components
+- [Theming Guide](theming.md) — Style custom components with your design system
+- [Agent Development](agent-development.md) — Build agents that use custom components
diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md
new file mode 100644
index 000000000..bfe8d0573
--- /dev/null
+++ b/docs/guides/design-system-integration.md
@@ -0,0 +1,184 @@
+---
+render_macros: false
+---
+
+# Integrating A2UI into an Existing Design System
+
+This guide walks through adding A2UI to an **existing** Angular application that already uses a component library (like Angular Material). Instead of using the A2UI basic catalog, you'll wrap your own Material components as A2UI components — so agents generate UI that matches your design system.
+
+> **Prerequisites**: An Angular 19+ application with a component library installed (this guide uses Angular Material). Familiarity with Angular components and dependency injection.
+
+## Overview
+
+Adding A2UI to an existing app involves four steps:
+
+1. **Install** the A2UI Angular renderer and web_core packages
+2. **Wrap** your existing components as A2UI custom components
+3. **Register** them in a custom catalog
+4. **Connect** to an A2A-compatible agent
+
+The key insight: A2UI doesn't replace your design system — it wraps it. Your existing components become the rendering targets for agent-generated UI. Agents compose your Material buttons, cards, and inputs — not generic A2UI ones.
+
+## Step 1: Install A2UI Packages
+
+```bash
+npm install @a2ui/angular @a2ui/web_core
+```
+
+The `@a2ui/angular` package provides:
+
+- `DynamicComponent` — base class for wrapping your components as A2UI-compatible
+- `Catalog` injection token — for providing your catalog to the renderer
+- `configureChatCanvasFeatures()` — helper for wiring everything together
+
+## Step 2: Wrap Your Components
+
+Create A2UI wrappers around your existing Material components. Each wrapper extends `DynamicComponent` and delegates rendering to your Material component:
+
+```typescript
+// a2ui-catalog/material-button.ts
+import { DynamicComponent } from '@a2ui/angular';
+import * as Types from '@a2ui/web_core/types/types';
+import { Component, computed, input } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+
+@Component({
+ selector: 'a2ui-mat-button',
+ imports: [MatButton],
+ template: `
+
+ `,
+})
+export class MaterialButton extends DynamicComponent {
+ readonly label = input.required();
+ readonly color = input();
+
+ protected resolvedLabel = computed(() => this.resolvePrimitive(this.label()));
+ protected resolvedColor = computed(() =>
+ this.resolvePrimitive(this.color() ?? null) || 'primary'
+ );
+}
+```
+
+The wrapper is thin — it just maps A2UI properties to your Material component's API.
+
+## Step 3: Register a Custom Catalog
+
+Build a catalog from your wrapped components. You do **not** need to include the A2UI basic catalog — your design system provides the components:
+
+```typescript
+// a2ui-catalog/catalog.ts
+import { Catalog } from '@a2ui/angular';
+import { inputBinding } from '@angular/core';
+
+// No DEFAULT_CATALOG spread — your Material components ARE the catalog
+export const MATERIAL_CATALOG = {
+ Button: {
+ type: () => import('./material-button').then((r) => r.MaterialButton),
+ bindings: ({ properties }) => [
+ inputBinding('label', () => properties['label'] || ''),
+ inputBinding('color', () => properties['color'] || undefined),
+ ],
+ },
+ Card: {
+ type: () => import('./material-card').then((r) => r.MaterialCard),
+ bindings: ({ properties }) => [
+ inputBinding('title', () => properties['title'] || undefined),
+ inputBinding('subtitle', () => properties['subtitle'] || undefined),
+ ],
+ },
+ // ... wrap more of your Material components
+} as Catalog;
+```
+
+You can also mix approaches — use some basic catalog components alongside your custom ones:
+
+```typescript
+import { DEFAULT_CATALOG } from '@a2ui/angular';
+
+export const MIXED_CATALOG = {
+ ...DEFAULT_CATALOG, // A2UI basic components as fallback
+ Button: /* your Material button overrides the basic one */,
+ Card: /* your Material card */,
+} as Catalog;
+```
+
+The basic components are entirely optional. If your design system already covers what you need, expose only your own components.
+
+## Step 4: Wire It Up
+
+```typescript
+// app.config.ts
+import {
+ configureChatCanvasFeatures,
+ usingA2aService,
+ usingA2uiRenderers,
+} from '@a2a_chat_canvas/config';
+import { MATERIAL_CATALOG } from './a2ui-catalog/catalog';
+import { theme } from './theme';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ... your existing providers (Material, Router, etc.)
+ configureChatCanvasFeatures(
+ usingA2aService(MyA2aService),
+ usingA2uiRenderers(MATERIAL_CATALOG, theme),
+ ),
+ ],
+};
+```
+
+## Step 5: Add the Chat Canvas
+
+The chat canvas is the container where A2UI surfaces are rendered. Add it alongside your existing layout:
+
+```html
+
+
+
+
+ ...
+
+
+
+
+
+
+
+
+```
+
+## What Changes, What Doesn't
+
+| Aspect | Before A2UI | After A2UI |
+|--------|------------|------------|
+| Your existing pages | Material components | Material components (unchanged) |
+| Agent-generated UI | Not possible | Rendered via your Material wrappers |
+| Component library | Angular Material | Angular Material (unchanged) |
+| Design consistency | Your theme | Your theme (agents use your components) |
+
+Your existing app is untouched. A2UI adds a rendering layer where agents compose **your** components.
+
+## Theming
+
+Because agents render your Material components, theming is automatic — your existing Material theme applies. You can optionally map tokens for any A2UI basic components you include:
+
+```typescript
+// theme.ts
+import { Theme } from '@a2ui/angular';
+
+export const theme: Theme = {
+ // Map your Material design tokens to A2UI
+ // See the Theming guide for full details
+};
+```
+
+See the [Theming Guide](theming.md) for complete theming documentation.
+
+## Next Steps
+
+- [Custom Components](custom-components.md) — Add specialized components to your catalog (Maps, Charts, YouTube, etc.)
+- [Theming Guide](theming.md) — Deep dive into theming
+- [Agent Development](agent-development.md) — Build agents that generate A2UI using your catalog
diff --git a/mkdocs.yaml b/mkdocs.yaml
index 3b076e6d7..6e886fd97 100644
--- a/mkdocs.yaml
+++ b/mkdocs.yaml
@@ -52,6 +52,7 @@ nav:
- Client Setup: guides/client-setup.md
- Agent Development: guides/agent-development.md
- Renderer Development: guides/renderer-development.md
+ - Design System Integration: guides/design-system-integration.md
- Custom Components: guides/custom-components.md
- Theming & Styling: guides/theming.md
- Reference:
diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts
index e7cc9f045..a4acf3234 100644
--- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts
+++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts
@@ -40,4 +40,12 @@ export const RIZZ_CHARTS_CATALOG = {
inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
],
},
+ YouTube: {
+ type: () => import('./youtube').then((r) => r.YouTube),
+ bindings: ({ properties }) => [
+ inputBinding('videoId', () => ('videoId' in properties && properties['videoId']) || undefined),
+ inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
+ inputBinding('autoplay', () => ('autoplay' in properties && properties['autoplay']) || undefined),
+ ],
+ },
} as Catalog;
diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts
new file mode 100644
index 000000000..914ea1858
--- /dev/null
+++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts
@@ -0,0 +1,131 @@
+/*
+ Copyright 2025 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
+
+ https://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.
+ */
+
+import { DynamicComponent } from '@a2ui/angular';
+import * as Primitives from '@a2ui/web_core/types/primitives';
+import * as Types from '@a2ui/web_core/types/types';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Component({
+ selector: 'a2ui-youtube',
+ changeDetection: ChangeDetectionStrategy.Eager,
+ styles: `
+ :host {
+ display: block;
+ flex: var(--weight);
+ padding: 8px;
+ }
+
+ .youtube-container {
+ background-color: var(--mat-sys-surface-container);
+ border-radius: 8px;
+ border: 1px solid var(--mat-sys-surface-container-high);
+ padding: 16px;
+ max-width: 800px;
+ }
+
+ .youtube-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ }
+
+ .youtube-header h3 {
+ margin: 0;
+ font-size: 18px;
+ color: var(--mat-sys-on-surface);
+ }
+
+ .video-wrapper {
+ position: relative;
+ width: 100%;
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+ }
+ `,
+ template: `
+ @if (resolvedVideoId()) {
+