chore: initialize sandbox and overwrite remote content
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# A2UI custom component integration guide
|
||||
|
||||
This guide details how to create, register, and use a custom component in the A2UI client.
|
||||
|
||||
## Create the component
|
||||
|
||||
Create a new Lit component file in `lib/src/0.8/ui/custom-components/`.
|
||||
Example: `my-component.ts`
|
||||
|
||||
```typescript
|
||||
import { html, css } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import { Root } from "../root.js";
|
||||
|
||||
export class MyComponent extends Root {
|
||||
@property() accessor myProp: string = "Default";
|
||||
|
||||
static styles = [
|
||||
...Root.styles, // Inherit base styles
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<h2>My Custom Component</h2>
|
||||
<p>Prop value: ${this.myProp}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the component
|
||||
|
||||
Update `lib/src/0.8/ui/custom-components/index.ts` to register your new component.
|
||||
You must pass the desired tag name as the third argument.
|
||||
|
||||
```typescript
|
||||
import { componentRegistry } from "../component-registry.js";
|
||||
import { MyComponent } from "./my-component.js"; // Import your component
|
||||
|
||||
export function registerCustomComponents() {
|
||||
// Register with explicit tag name
|
||||
componentRegistry.register("MyComponent", MyComponent, "my-component");
|
||||
}
|
||||
|
||||
export { MyComponent }; // Export for type usage if needed
|
||||
```
|
||||
|
||||
## Define the schema (server-side)
|
||||
|
||||
Create a JSON schema for your component properties. This will be used by the server to validate messages.
|
||||
Example: `lib/my_component_schema.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "object" },
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"myProp": {
|
||||
"type": "string",
|
||||
"description": "A sample property."
|
||||
}
|
||||
},
|
||||
"required": ["myProp"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
}
|
||||
```
|
||||
|
||||
## Use in client application
|
||||
|
||||
In your client application (e.g., `contact` sample), ensure you import and call the registration function.
|
||||
|
||||
```typescript
|
||||
import { registerCustomComponents } from "@a2ui/lit/ui";
|
||||
|
||||
// Call this once at startup
|
||||
registerCustomComponents();
|
||||
```
|
||||
|
||||
## Overriding standard components
|
||||
|
||||
You can replace standard A2UI components (like `TextField`, `Video`, `Button`) with your own custom implementations.
|
||||
|
||||
### Steps to override
|
||||
|
||||
1. **Create your component** extending `Root` (just like a custom component).
|
||||
|
||||
2. **Ensure it accepts the standard properties** for that component type (e.g., `label` and `text` for `TextField`).
|
||||
|
||||
3. **Register it** using the **standard type name** (e.g., `"TextField"`).
|
||||
|
||||
```typescript
|
||||
// 1. Define your override
|
||||
class MyPremiumTextField extends Root {
|
||||
@property() accessor label = "";
|
||||
@property() accessor text = "";
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
/* your premium styles */
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="premium-field">
|
||||
<label>${this.label}</label>
|
||||
<input .value="${this.text}" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register with the STANDARD type name
|
||||
import { componentRegistry } from "@a2ui/lit/ui";
|
||||
componentRegistry.register(
|
||||
"TextField",
|
||||
MyPremiumTextField,
|
||||
"my-premium-textfield"
|
||||
);
|
||||
```
|
||||
|
||||
**Result:**
|
||||
When the server sends a `TextField` component, the client will now render `<my-premium-textfield>` instead of the default `<a2ui-textfield>`.
|
||||
|
||||
## Verify
|
||||
|
||||
You can verify the component by creating a simple HTML test file or by sending a server message with the new component type.
|
||||
|
||||
**Server message example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceId": "main",
|
||||
"component": {
|
||||
"type": "MyComponent",
|
||||
"id": "comp-1",
|
||||
"properties": {
|
||||
"myProp": "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`NotSupportedError`**: If you see "constructor has already been used", ensure you **removed** the `@customElement` decorator from your component class.
|
||||
- **Component not rendering**: Check if `registerCustomComponents()` is actually called. Verify the tag name in the DOM matches what you registered (e.g., `<my-component>` vs `<a2ui-custom-mycomponent>`).
|
||||
- **Styles missing**: Ensure `static styles` includes `...Root.styles`.
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
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 { Root } from '@a2ui/lit/ui';
|
||||
import { v0_8 } from '@a2ui/lit';
|
||||
import { html, css, TemplateResult } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
|
||||
// Use aliases for convenience
|
||||
const StateEvent = v0_8.Events.StateEvent;
|
||||
type Action = v0_8.Types.Action;
|
||||
|
||||
export interface OrgChartNode {
|
||||
title: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement('org-chart')
|
||||
export class OrgChart extends Root {
|
||||
@property({ type: Array }) accessor chain: OrgChartNode[] = [];
|
||||
@property({ type: Object }) accessor action: Action | null = null;
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node:focus {
|
||||
outline: 2px solid #1a73e8;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.node.current {
|
||||
background: #e8f0fe;
|
||||
border-color: #1a73e8;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.85rem;
|
||||
color: #5f6368;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #9aa0a6;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
`];
|
||||
|
||||
render() {
|
||||
if (!this.chain || this.chain.length === 0) {
|
||||
return html`<div class="empty">No hierarchy data</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${map(this.chain, (node, index) => {
|
||||
const isLast = index === this.chain.length - 1;
|
||||
return html`
|
||||
<button
|
||||
class="node ${isLast ? 'current' : ''}"
|
||||
@click=${() => this.handleNodeClick(node)}
|
||||
aria-label="Select ${node.name} (${node.title})"
|
||||
>
|
||||
<span class="title">${node.title}</span>
|
||||
<span class="name">${node.name}</span>
|
||||
</button>
|
||||
${!isLast ? html`<div class="arrow">↓</div>` : ''}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleNodeClick(node: OrgChartNode) {
|
||||
if (!this.action) return;
|
||||
|
||||
// Create a new action with the node's context merged in
|
||||
const newContext = [
|
||||
...(this.action.context || []),
|
||||
{
|
||||
key: 'clickedNodeTitle',
|
||||
value: { literalString: node.title }
|
||||
},
|
||||
{
|
||||
key: 'clickedNodeName',
|
||||
value: { literalString: node.name }
|
||||
}
|
||||
];
|
||||
|
||||
const actionWithContext: Action = {
|
||||
...this.action,
|
||||
context: newContext as Action['context']
|
||||
};
|
||||
|
||||
const evt = new StateEvent<"a2ui.action">({
|
||||
eventType: "a2ui.action",
|
||||
action: actionWithContext,
|
||||
dataContextPath: this.dataContextPath,
|
||||
sourceComponentId: this.id,
|
||||
sourceComponent: this.component,
|
||||
});
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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 { Root } from '@a2ui/lit/ui';
|
||||
import { html, css } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class PremiumTextField extends Root {
|
||||
@property() accessor label = '';
|
||||
@property() accessor text = '';
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
:host(:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #6200ee;
|
||||
background: #fff;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.badge {
|
||||
background: #6200ee;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<label>${this.label}</label>
|
||||
<div class="input-container">
|
||||
<input type="text" .value="${this.text}" placeholder="Type here...">
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="badge">Custom</span>
|
||||
<span>This is a premium override of the standard TextField.</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 { componentRegistry } from "@a2ui/lit/ui";
|
||||
import { OrgChart } from "./org-chart.js";
|
||||
import { PremiumTextField } from "./premium-text-field.js";
|
||||
|
||||
export function registerContactComponents() {
|
||||
// Register OrgChart
|
||||
componentRegistry.register("OrgChart", OrgChart, "org-chart");
|
||||
|
||||
// Register PremiumTextField as an override for TextField
|
||||
componentRegistry.register(
|
||||
"TextField",
|
||||
PremiumTextField,
|
||||
"premium-text-field"
|
||||
);
|
||||
|
||||
console.log("Registered Contact App Custom Components");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Contact sample verification tests
|
||||
|
||||
This directory contains tests to verify custom component integration specifically within the `contact` sample application environment.
|
||||
|
||||
## How to run
|
||||
|
||||
These tests run via the Vite development server used by the contact sample.
|
||||
|
||||
### 1. Start the dev server
|
||||
From the `web/lit/samples/contact` directory, run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. Access the tests
|
||||
Open your browser and navigate to the local server (usually port 5173):
|
||||
|
||||
- **Component override test**:
|
||||
[http://localhost:5173/ui/custom-components/test/override-test.html](http://localhost:5173/ui/custom-components/test/override-test.html)
|
||||
*Verifies that a standard component (TextField) can be overridden by a custom implementation.*
|
||||
|
||||
- **Hierarchy graph integration test**:
|
||||
[http://localhost:5173/ui/custom-components/test/hierarchy-test.html](http://localhost:5173/ui/custom-components/test/hierarchy-test.html)
|
||||
*Verifies that the HierarchyGraph component renders correctly within the contact app's build setup.*
|
||||
|
||||
## Files
|
||||
|
||||
- `override-test.html` & `override-test.ts`: Implements and tests a custom `TextField` override.
|
||||
- `hierarchy-test.html`: Tests the `HierarchyGraph` component.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>A2UI Org Chart Test (Contact Sample)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
import { registerContactComponents } from '../register-components.js';
|
||||
|
||||
// Register custom components
|
||||
registerContactComponents();
|
||||
console.log('Custom components registered in Contact Sample');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>A2UI Org Chart Test (Contact Sample)</h1>
|
||||
|
||||
<div class="container">
|
||||
<org-chart id="graph"></org-chart>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
customElements.whenDefined('org-chart').then(() => {
|
||||
const graph = document.getElementById('graph');
|
||||
|
||||
graph.chain = [
|
||||
{ title: 'CEO', name: 'Alice Johnson' },
|
||||
{ title: 'SVP', name: 'Bob Smith' },
|
||||
{ title: 'VP', name: 'Charlie Brown' },
|
||||
{ title: 'Director', name: 'Diana Prince' },
|
||||
{ title: 'Software Engineer', name: 'Evan Wright' }
|
||||
];
|
||||
|
||||
graph.action = {
|
||||
name: 'showContactCard',
|
||||
context: []
|
||||
};
|
||||
|
||||
graph.addEventListener('a2uiaction', (e) => {
|
||||
const status = document.createElement('div');
|
||||
const clickedNodeTitle = e.detail.action.context.find(c => c.key === 'clickedNodeTitle')?.value.literalString;
|
||||
const clickedNodeName = e.detail.action.context.find(c => c.key === 'clickedNodeName')?.value.literalString;
|
||||
status.textContent = `Clicked: ${clickedNodeTitle} - ${clickedNodeName}`;
|
||||
status.style.marginTop = '20px';
|
||||
status.style.padding = '10px';
|
||||
status.style.background = '#e0f7fa';
|
||||
status.style.border = '1px solid #006064';
|
||||
status.id = 'action-status';
|
||||
document.body.appendChild(status);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>A2UI Component Override Test</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
border: 1px dashed #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./override-test.ts"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Component Override Test</h1>
|
||||
<div id="app" class="container"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 { componentRegistry, Root } from "@a2ui/lit/ui";
|
||||
import { html, css } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
// 1. Define the override
|
||||
import { PremiumTextField } from "../premium-text-field.js";
|
||||
|
||||
// 2. Register it as "TextField"
|
||||
componentRegistry.register("TextField", PremiumTextField, "premium-text-field");
|
||||
console.log("Registered PremiumTextField override");
|
||||
|
||||
// 3. Render a standard TextField component node
|
||||
const container = document.getElementById("app");
|
||||
if (container) {
|
||||
const root = document.createElement("a2ui-root") as Root;
|
||||
|
||||
const textFieldComponent = {
|
||||
type: "TextField",
|
||||
id: "tf-1",
|
||||
properties: {
|
||||
label: "Enter your name",
|
||||
text: "John Doe",
|
||||
},
|
||||
};
|
||||
|
||||
// Root renders its *children*, so we must pass the component as a child.
|
||||
root.childComponents = [textFieldComponent];
|
||||
|
||||
root.enableCustomElements = true; // Enable the feature
|
||||
container.appendChild(root);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
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 { LitElement, html, css, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { SnackbarActionEvent } from "../events/events";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
|
||||
const DEFAULT_TIMEOUT = 8000;
|
||||
|
||||
@customElement("ui-snackbar")
|
||||
export class Snackbar extends LitElement {
|
||||
@property({ reflect: true, type: Boolean })
|
||||
accessor active = false;
|
||||
|
||||
@property({ reflect: true, type: Boolean })
|
||||
accessor error = false;
|
||||
|
||||
@property()
|
||||
accessor timeout = DEFAULT_TIMEOUT;
|
||||
|
||||
#messages: SnackbarMessage[] = [];
|
||||
#timeout = 0;
|
||||
|
||||
static styles = [
|
||||
unsafeCSS(v0_8.Styles.structuralStyles),
|
||||
css`
|
||||
:host {
|
||||
--text-color: var(--n-0);
|
||||
--bb-body-medium: 16px;
|
||||
--bb-body-line-height-medium: 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: var(--bb-grid-size-7);
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border-radius: var(--bb-grid-size-2);
|
||||
background: var(--n-90);
|
||||
padding: var(--bb-grid-size-3) var(--bb-grid-size-6);
|
||||
width: 60svw;
|
||||
max-width: 720px;
|
||||
z-index: 1800;
|
||||
scrollbar-width: none;
|
||||
overflow-x: scroll;
|
||||
font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)
|
||||
var(--bb-font-family);
|
||||
}
|
||||
|
||||
:host([active]) {
|
||||
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host([error]) {
|
||||
background: var(--e-90);
|
||||
--text-color: var(--e-40);
|
||||
}
|
||||
|
||||
.g-icon {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-color);
|
||||
margin-right: var(--bb-grid-size-4);
|
||||
|
||||
&.rotate {
|
||||
animation: 1s linear 0s infinite normal forwards running rotate;
|
||||
}
|
||||
}
|
||||
|
||||
#messages {
|
||||
color: var(--text-color);
|
||||
flex: 1 1 auto;
|
||||
margin-right: var(--bb-grid-size-11);
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--bb-ui-600);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--bb-ui-500);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#actions {
|
||||
flex: 0 1 auto;
|
||||
width: fit-content;
|
||||
margin-right: var(--bb-grid-size-3);
|
||||
|
||||
& button {
|
||||
font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)
|
||||
var(--bb-font-family);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 0 var(--bb-grid-size-4);
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
color: var(--text-color);
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 0 0 0 var(--bb-grid-size-2);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
|
||||
|
||||
.g-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
to {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
show(message: SnackbarMessage, replaceAll = false) {
|
||||
const existingMessage = this.#messages.findIndex(
|
||||
(msg) => msg.id === message.id
|
||||
);
|
||||
if (existingMessage === -1) {
|
||||
if (replaceAll) {
|
||||
this.#messages.length = 0;
|
||||
}
|
||||
|
||||
this.#messages.push(message);
|
||||
} else {
|
||||
this.#messages[existingMessage] = message;
|
||||
}
|
||||
|
||||
window.clearTimeout(this.#timeout);
|
||||
if (!this.#messages.every((msg) => msg.persistent)) {
|
||||
this.#timeout = window.setTimeout(() => {
|
||||
this.hide();
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);
|
||||
this.active = true;
|
||||
this.requestUpdate();
|
||||
|
||||
return message.id;
|
||||
}
|
||||
|
||||
hide(id?: SnackbarUUID) {
|
||||
if (id) {
|
||||
const idx = this.#messages.findIndex((msg) => msg.id === id);
|
||||
if (idx !== -1) {
|
||||
this.#messages.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
this.#messages.length = 0;
|
||||
}
|
||||
|
||||
this.active = this.#messages.length !== 0;
|
||||
this.updateComplete.then((avoidedUpdate) => {
|
||||
if (!avoidedUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let rotate = false;
|
||||
let icon = "";
|
||||
for (let i = this.#messages.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
!this.#messages[i].type ||
|
||||
this.#messages[i].type === SnackType.NONE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
icon = this.#messages[i].type;
|
||||
if (this.#messages[i].type === SnackType.PENDING) {
|
||||
icon = "progress_activity";
|
||||
rotate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return html` ${icon
|
||||
? html`<span
|
||||
class=${classMap({
|
||||
"g-icon": true,
|
||||
round: true,
|
||||
filled: true,
|
||||
rotate,
|
||||
})}
|
||||
>${icon}</span
|
||||
>`
|
||||
: nothing}
|
||||
<div id="messages">
|
||||
${repeat(
|
||||
this.#messages,
|
||||
(message) => message.id,
|
||||
(message) => {
|
||||
return html`<div>${message.message}</div>`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div id="actions">
|
||||
${repeat(
|
||||
this.#messages,
|
||||
(message) => message.id,
|
||||
(message) => {
|
||||
if (!message.actions) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${repeat(
|
||||
message.actions,
|
||||
(action) => action.value,
|
||||
(action) => {
|
||||
return html`<button
|
||||
@click=${() => {
|
||||
this.hide();
|
||||
this.dispatchEvent(
|
||||
new SnackbarActionEvent(
|
||||
action.action,
|
||||
action.value,
|
||||
action.callback
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
${action.title}
|
||||
</button>`;
|
||||
}
|
||||
)}`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
id="close"
|
||||
@click=${() => {
|
||||
this.hide();
|
||||
this.dispatchEvent(new SnackbarActionEvent("dismiss"));
|
||||
}}
|
||||
>
|
||||
<span class="g-icon">close</span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export { Snackbar } from "./snackbar";
|
||||
Reference in New Issue
Block a user