diff --git a/a2a_agents/python/adk/samples/contact_lookup/__main__.py b/a2a_agents/python/adk/samples/contact_lookup/__main__.py index 2cd5841f1..d9e4ebed6 100644 --- a/a2a_agents/python/adk/samples/contact_lookup/__main__.py +++ b/a2a_agents/python/adk/samples/contact_lookup/__main__.py @@ -39,7 +39,7 @@ class MissingAPIKeyError(Exception): @click.command() @click.option("--host", default="localhost") -@click.option("--port", default=10002) +@click.option("--port", default=10003) def main(host, port): try: # Check for API key only if Vertex AI is not configured diff --git a/a2a_agents/python/adk/samples/contact_lookup/a2ui_examples.py b/a2a_agents/python/adk/samples/contact_lookup/a2ui_examples.py index f8188128a..89f2a0025 100644 --- a/a2a_agents/python/adk/samples/contact_lookup/a2ui_examples.py +++ b/a2a_agents/python/adk/samples/contact_lookup/a2ui_examples.py @@ -67,7 +67,7 @@ { "beginRendering": { "surfaceId":"contact-card","root":"main_card"} }, { "surfaceUpdate": { "surfaceId":"contact-card", "components":[ - { "id": "profile_image", "component": { "Image": { "url": { "path": "imageUrl"} } } } , + { "id": "profile_image", "component": { "Image": { "url": { "path": "imageUrl"}, "usageHint": "avatar", "fit": "cover" } } } , { "id": "user_heading", "weight": 1, "component": { "Text": { "text": { "path": "name"} , "usageHint": "h2"} } } , { "id": "description_text_1", "component": { "Text": { "text": { "path": "title"} } } } , { "id": "description_text_2", "component": { "Text": { "text": { "path": "team"} } } } , diff --git a/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py b/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py new file mode 100644 index 000000000..b77a685ea --- /dev/null +++ b/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py @@ -0,0 +1,186 @@ +# 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. + +RESTAURANT_UI_EXAMPLES = """ +---BEGIN SINGLE_COLUMN_LIST_EXAMPLE--- +[ + {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "surfaceUpdate": {{ + "surfaceId": "default", + "components": [ + {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }}, + {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }}, + {{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-card-template", "dataBinding": "/items" }} }} }} }} }}, + {{ "id": "item-card-template", "component": {{ "Card": {{ "child": "card-layout" }} }} }}, + {{ "id": "card-layout", "component": {{ "Row": {{ "children": {{ "explicitList": ["template-image", "card-details"] }} }} }} }}, + {{ "id": "template-image", weight: 1, "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, + {{ "id": "card-details", weight: 2, "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-book-button"] }} }} }} }}, + {{ "id": "template-name", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "name" }} }} }} }}, + {{ "id": "template-rating", "component": {{ "Text": {{ "text": {{ "path": "rating" }} }} }} }}, + {{ "id": "template-detail", "component": {{ "Text": {{ "text": {{ "path": "detail" }} }} }} }}, + {{ "id": "template-link", "component": {{ "Text": {{ "text": {{ "path": "infoLink" }} }} }} }}, + {{ "id": "template-book-button", "component": {{ "Button": {{ "child": "book-now-text", "primary": true, "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "address" }} }} ] }} }} }} }}, + {{ "id": "book-now-text", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} + ] + }} }}, + {{ "dataModelUpdate": {{ + "surfaceId": "default", + "path": "/", + "contents": [ + {{ "key": "items", "valueMap": [ + {{ "key": "item1", "valueMap": [ + {{ "key": "name", "valueString": "The Fancy Place" }}, + {{ "key": "rating", "valueNumber": 4.8 }}, + {{ "key": "detail", "valueString": "Fine dining experience" }}, + {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, + {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, + {{ "key": "address", "valueString": "123 Main St" }} + ] }}, + {{ "key": "item2", "valueMap": [ + {{ "key": "name", "valueString": "Quick Bites" }}, + {{ "key": "rating", "valueNumber": 4.2 }}, + {{ "key": "detail", "valueString": "Casual and fast" }}, + {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, + {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, + {{ "key": "address", "valueString": "456 Oak Ave" }} + ] }} + ] }} // Populate this with restaurant data + ] + }} }} +] +---END SINGLE_COLUMN_LIST_EXAMPLE--- + +---BEGIN TWO_COLUMN_LIST_EXAMPLE--- +[ + {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "surfaceUpdate": {{ + "surfaceId": "default", + "components": [ + {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "restaurant-row-1"] }} }} }} }}, + {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }}, + {{ "id": "restaurant-row-1", "component": {{ "Row": {{ "children": {{ "explicitList": ["item-card-1", "item-card-2"] }} }} }} }}, + {{ "id": "item-card-1", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-1" }} }} }}, + {{ "id": "card-layout-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-1", "card-details-1"] }} }} }} }}, + {{ "id": "template-image-1", "component": {{ "Image": {{ "url": {{ "path": "/items/0/imageUrl" }}, "width": "100%" }} }} }}, + {{ "id": "card-details-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-1", "template-rating-1", "template-detail-1", "template-link-1", "template-book-button-1"] }} }} }} }}, + {{ "id": "template-name-1", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/0/name" }} }} }} }}, + {{ "id": "template-rating-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/rating" }} }} }} }}, + {{ "id": "template-detail-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/detail" }} }} }} }}, + {{ "id": "template-link-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/infoLink" }} }} }} }}, + {{ "id": "template-book-button-1", "component": {{ "Button": {{ "child": "book-now-text-1", "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/0/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/0/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/0/address" }} }} ] }} }} }} }}, + {{ "id": "book-now-text-1", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }}, + {{ "id": "item-card-2", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-2" }} }} }}, + {{ "id": "card-layout-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-2", "card-details-2"] }} }} }} }}, + {{ "id": "template-image-2", "component": {{ "Image": {{ "url": {{ "path": "/items/1/imageUrl" }}, "width": "100%" }} }} }}, + {{ "id": "card-details-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-2", "template-rating-2", "template-detail-2", "template-link-2", "template-book-button-2"] }} }} }} }}, + {{ "id": "template-name-2", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/1/name" }} }} }} }}, + {{ "id": "template-rating-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/rating" }} }} }} }}, + {{ "id": "template-detail-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/detail" }} }} }} }}, + {{ "id": "template-link-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/infoLink" }} }} }} }}, + {{ "id": "template-book-button-2", "component": {{ "Button": {{ "child": "book-now-text-2", "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/1/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/1/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/1/address" }} }} ] }} }} }} }}, + {{ "id": "book-now-text-2", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} + ] + }} }}, + {{ "dataModelUpdate": {{ + "surfaceId": "default", + "path": "/", + "contents": [ + {{ "key": "title", "valueString": "Top Restaurants" }}, + {{ "key": "items", "valueMap": [ + {{ "key": "item1", "valueMap": [ + {{ "key": "name", "valueString": "The Fancy Place" }}, + {{ "key": "rating", "valueNumber": 4.8 }}, + {{ "key": "detail", "valueString": "Fine dining experience" }}, + {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, + {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, + {{ "key": "address", "valueString": "123 Main St" }} + ] }}, + {{ "key": "item2", "valueMap": [ + {{ "key": "name", "valueString": "Quick Bites" }}, + {{ "key": "rating", "valueNumber": 4.2 }}, + {{ "key": "detail", "valueString": "Casual and fast" }}, + {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, + {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, + {{ "key": "address", "valueString": "456 Oak Ave" }} + ] }} + ] }} // Populate this with restaurant data + ] + }} }} +] +---END TWO_COLUMN_LIST_EXAMPLE--- + +---BEGIN BOOKING_FORM_EXAMPLE--- +[ + {{ "beginRendering": {{ "surfaceId": "booking-form", "root": "booking-form-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "surfaceUpdate": {{ + "surfaceId": "booking-form", + "components": [ + {{ "id": "booking-form-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["booking-title", "restaurant-image", "restaurant-address", "party-size-field", "datetime-field", "dietary-field", "submit-button"] }} }} }} }}, + {{ "id": "booking-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "title" }} }} }} }}, + {{ "id": "restaurant-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, + {{ "id": "restaurant-address", "component": {{ "Text": {{ "text": {{ "path": "address" }} }} }} }}, + {{ "id": "party-size-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Party Size" }}, "text": {{ "path": "partySize" }}, "type": "number" }} }} }}, + {{ "id": "datetime-field", "component": {{ "DateTimeInput": {{ "label": {{ "literalString": "Date & Time" }}, "value": {{ "path": "reservationTime" }}, "enableDate": true, "enableTime": true }} }} }}, + {{ "id": "dietary-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Dietary Requirements" }}, "text": {{ "path": "dietary" }} }} }} }}, + {{ "id": "submit-button", "component": {{ "Button": {{ "child": "submit-reservation-text", "action": {{ "name": "submit_booking", "context": [ {{ "key": "restaurantName", "value": {{ "path": "restaurantName" }} }}, {{ "key": "partySize", "value": {{ "path": "partySize" }} }}, {{ "key": "reservationTime", "value": {{ "path": "reservationTime" }} }}, {{ "key": "dietary", "value": {{ "path": "dietary" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }} ] }} }} }} }}, + {{ "id": "submit-reservation-text", "component": {{ "Text": {{ "text": {{ "literalString": "Submit Reservation" }} }} }} }} + ] + }} }}, + {{ "dataModelUpdate": {{ + "surfaceId": "booking-form", + "path": "/", + "contents": [ + {{ "key": "title", "valueString": "Book a Table at [RestaurantName]" }}, + {{ "key": "address", "valueString": "[Restaurant Address]" }}, + {{ "key": "restaurantName", "valueString": "[RestaurantName]" }}, + {{ "key": "partySize", "valueString": "2" }}, + {{ "key": "reservationTime", "valueString": "" }}, + {{ "key": "dietary", "valueString": "" }}, + {{ "key": "imageUrl", "valueString": "" }} + ] + }} }} +] +---END BOOKING_FORM_EXAMPLE--- + +---BEGIN CONFIRMATION_EXAMPLE--- +[ + {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "surfaceUpdate": {{ + "surfaceId": "confirmation", + "components": [ + {{ "id": "confirmation-card", "component": {{ "Card": {{ "child": "confirmation-column" }} }} }}, + {{ "id": "confirmation-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["confirm-title", "confirm-image", "divider1", "confirm-details", "divider2", "confirm-dietary", "divider3", "confirm-text"] }} }} }} }}, + {{ "id": "confirm-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "title" }} }} }} }}, + {{ "id": "confirm-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, + {{ "id": "confirm-details", "component": {{ "Text": {{ "text": {{ "path": "bookingDetails" }} }} }} }}, + {{ "id": "confirm-dietary", "component": {{ "Text": {{ "text": {{ "path": "dietaryRequirements" }} }} }} }}, + {{ "id": "confirm-text", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "We look forward to seeing you!" }} }} }} }}, + {{ "id": "divider1", "component": {{ "Divider": {{}} }} }}, + {{ "id": "divider2", "component": {{ "Divider": {{}} }} }}, + {{ "id": "divider3", "component": {{ "Divider": {{}} }} }} + ] + }} }}, + {{ "dataModelUpdate": {{ + "surfaceId": "confirmation", + "path": "/", + "contents": [ + {{ "key": "title", "valueString": "Booking at [RestaurantName]" }}, + {{ "key": "bookingDetails", "valueString": "[PartySize] people at [Time]" }}, + {{ "key": "dietaryRequirements", "valueString": "Dietary Requirements: [Requirements]" }}, + {{ "key": "imageUrl", "valueString": "[ImageUrl]" }} + ] + }} }} +] +---END CONFIRMATION_EXAMPLE--- +""" diff --git a/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py b/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py index 9c2813dca..6eb2f7aa8 100644 --- a/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py +++ b/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py @@ -789,177 +789,7 @@ } ''' -RESTAURANT_UI_EXAMPLES = """ ----BEGIN SINGLE_COLUMN_LIST_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "default", - "components": [ - {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }}, - {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "literalString": "Top Restaurants" }} }} }} }}, - {{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-card-template", "dataBinding": "/items" }} }} }} }} }}, - {{ "id": "item-card-template", "component": {{ "Card": {{ "child": "card-layout" }} }} }}, - {{ "id": "card-layout", "component": {{ "Row": {{ "children": {{ "explicitList": ["template-image", "card-details"] }} }} }} }}, - {{ "id": "template-image", weight: 1, "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, - {{ "id": "card-details", weight: 2, "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-book-button"] }} }} }} }}, - {{ "id": "template-name", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "name" }} }} }} }}, - {{ "id": "template-rating", "component": {{ "Text": {{ "text": {{ "path": "rating" }} }} }} }}, - {{ "id": "template-detail", "component": {{ "Text": {{ "text": {{ "path": "detail" }} }} }} }}, - {{ "id": "template-link", "component": {{ "Text": {{ "text": {{ "path": "infoLink" }} }} }} }}, - {{ "id": "template-book-button", "component": {{ "Button": {{ "child": "book-now-text", "primary": true, "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "default", - "path": "/", - "contents": [ - {{ "key": "items", "valueMap": [ - {{ "key": "item1", "valueMap": [ - {{ "key": "name", "valueString": "The Fancy Place" }}, - {{ "key": "rating", "valueNumber": 4.8 }}, - {{ "key": "detail", "valueString": "Fine dining experience" }}, - {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, - {{ "key": "address", "valueString": "123 Main St" }} - ] }}, - {{ "key": "item2", "valueMap": [ - {{ "key": "name", "valueString": "Quick Bites" }}, - {{ "key": "rating", "valueNumber": 4.2 }}, - {{ "key": "detail", "valueString": "Casual and fast" }}, - {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, - {{ "key": "address", "valueString": "456 Oak Ave" }} - ] }} - ] }} // Populate this with restaurant data - ] - }} }} -] ----END SINGLE_COLUMN_LIST_EXAMPLE--- - ----BEGIN TWO_COLUMN_LIST_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "default", - "components": [ - {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "restaurant-row-1"] }} }} }} }}, - {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "literalString": "Top Restaurants" }} }} }} }}, - {{ "id": "restaurant-row-1", "component": {{ "Row": {{ "children": {{ "explicitList": ["item-card-1", "item-card-2"] }} }} }} }}, - {{ "id": "item-card-1", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-1" }} }} }}, - {{ "id": "card-layout-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-1", "card-details-1"] }} }} }} }}, - {{ "id": "template-image-1", "component": {{ "Image": {{ "url": {{ "path": "/items/0/imageUrl" }}, "width": "100%" }} }} }}, - {{ "id": "card-details-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-1", "template-rating-1", "template-detail-1", "template-link-1", "template-book-button-1"] }} }} }} }}, - {{ "id": "template-name-1", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/0/name" }} }} }} }}, - {{ "id": "template-rating-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/rating" }} }} }} }}, - {{ "id": "template-detail-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/detail" }} }} }} }}, - {{ "id": "template-link-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/infoLink" }} }} }} }}, - {{ "id": "template-book-button-1", "component": {{ "Button": {{ "child": "book-now-text-1", "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/0/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/0/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/0/address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text-1", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }}, - {{ "id": "item-card-2", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-2" }} }} }}, - {{ "id": "card-layout-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-2", "card-details-2"] }} }} }} }}, - {{ "id": "template-image-2", "component": {{ "Image": {{ "url": {{ "path": "/items/1/imageUrl" }}, "width": "100%" }} }} }}, - {{ "id": "card-details-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-2", "template-rating-2", "template-detail-2", "template-link-2", "template-book-button-2"] }} }} }} }}, - {{ "id": "template-name-2", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/1/name" }} }} }} }}, - {{ "id": "template-rating-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/rating" }} }} }} }}, - {{ "id": "template-detail-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/detail" }} }} }} }}, - {{ "id": "template-link-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/infoLink" }} }} }} }}, - {{ "id": "template-book-button-2", "component": {{ "Button": {{ "child": "book-now-text-2", "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/1/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/1/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/1/address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text-2", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "default", - "path": "/", - "contents": [ - {{ "key": "items", "valueMap": [ - {{ "key": "item1", "valueMap": [ - {{ "key": "name", "valueString": "The Fancy Place" }}, - {{ "key": "rating", "valueNumber": 4.8 }}, - {{ "key": "detail", "valueString": "Fine dining experience" }}, - {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, - {{ "key": "address", "valueString": "123 Main St" }} - ] }}, - {{ "key": "item2", "valueMap": [ - {{ "key": "name", "valueString": "Quick Bites" }}, - {{ "key": "rating", "valueNumber": 4.2 }}, - {{ "key": "detail", "valueString": "Casual and fast" }}, - {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, - {{ "key": "address", "valueString": "456 Oak Ave" }} - ] }} - ] }} // Populate this with restaurant data - ] - }} }} -] ----END TWO_COLUMN_LIST_EXAMPLE--- - ----BEGIN BOOKING_FORM_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "booking-form", "root": "booking-form-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "booking-form", - "components": [ - {{ "id": "booking-form-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["booking-title", "restaurant-image", "restaurant-address", "party-size-field", "datetime-field", "dietary-field", "submit-button"] }} }} }} }}, - {{ "id": "booking-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "title" }} }} }} }}, - {{ "id": "restaurant-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, - {{ "id": "restaurant-address", "component": {{ "Text": {{ "text": {{ "path": "address" }} }} }} }}, - {{ "id": "party-size-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Party Size" }}, "text": {{ "path": "partySize" }}, "type": "number" }} }} }}, - {{ "id": "datetime-field", "component": {{ "DateTimeInput": {{ "label": {{ "literalString": "Date & Time" }}, "value": {{ "path": "reservationTime" }}, "enableDate": true, "enableTime": true }} }} }}, - {{ "id": "dietary-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Dietary Requirements" }}, "text": {{ "path": "dietary" }} }} }} }}, - {{ "id": "submit-button", "component": {{ "Button": {{ "child": "submit-reservation-text", "action": {{ "name": "submit_booking", "context": [ {{ "key": "restaurantName", "value": {{ "path": "restaurantName" }} }}, {{ "key": "partySize", "value": {{ "path": "partySize" }} }}, {{ "key": "reservationTime", "value": {{ "path": "reservationTime" }} }}, {{ "key": "dietary", "value": {{ "path": "dietary" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }} ] }} }} }} }}, - {{ "id": "submit-reservation-text", "component": {{ "Text": {{ "text": {{ "literalString": "Submit Reservation" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "booking-form", - "path": "/", - "contents": [ - {{ "key": "title", "valueString": "Book a Table at [RestaurantName]" }}, - {{ "key": "address", "valueString": "[Restaurant Address]" }}, - {{ "key": "restaurantName", "valueString": "[RestaurantName]" }}, - {{ "key": "partySize", "valueString": "2" }}, - {{ "key": "reservationTime", "valueString": "" }}, - {{ "key": "dietary", "valueString": "" }}, - {{ "key": "imageUrl", "valueString": "" }} - ] - }} }} -] ----END BOOKING_FORM_EXAMPLE--- - ----BEGIN CONFIRMATION_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "confirmation", - "components": [ - {{ "id": "confirmation-card", "component": {{ "Card": {{ "child": "confirmation-column" }} }} }}, - {{ "id": "confirmation-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["confirm-title", "confirm-image", "divider1", "confirm-details", "divider2", "confirm-dietary", "divider3", "confirm-text"] }} }} }} }}, - {{ "id": "confirm-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "title" }} }} }} }}, - {{ "id": "confirm-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, - {{ "id": "confirm-details", "component": {{ "Text": {{ "text": {{ "path": "bookingDetails" }} }} }} }}, - {{ "id": "confirm-dietary", "component": {{ "Text": {{ "text": {{ "path": "dietaryRequirements" }} }} }} }}, - {{ "id": "confirm-text", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "We look forward to seeing you!" }} }} }} }}, - {{ "id": "divider1", "component": {{ "Divider": {{}} }} }}, - {{ "id": "divider2", "component": {{ "Divider": {{}} }} }}, - {{ "id": "divider3", "component": {{ "Divider": {{}} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "confirmation", - "path": "/", - "contents": [ - {{ "key": "title", "valueString": "Booking at [RestaurantName]" }}, - {{ "key": "bookingDetails", "valueString": "[PartySize] people at [Time]" }}, - {{ "key": "dietaryRequirements", "valueString": "Dietary Requirements: [Requirements]" }}, - {{ "key": "imageUrl", "valueString": "[ImageUrl]" }} - ] - }} }} -] ----END CONFIRMATION_EXAMPLE--- -""" +from a2ui_examples import RESTAURANT_UI_EXAMPLES def get_ui_prompt(base_url: str, examples: str) -> str: diff --git a/renderers/lit/src/0.8/styles/colors.ts b/renderers/lit/src/0.8/styles/colors.ts index 36dc3be78..240cfc219 100644 --- a/renderers/lit/src/0.8/styles/colors.ts +++ b/renderers/lit/src/0.8/styles/colors.ts @@ -21,24 +21,32 @@ const color = (src: PaletteKey) => ` ${src .map((key: string) => { - return `.color-bc-${key} { border-color: var(${toProp(key)}); }`; + const inverseKey = getInverseKey(key); + return `.color-bc-${key} { border-color: light-dark(var(${toProp( + key + )}), var(${toProp(inverseKey)})); }`; }) .join("\n")} ${src .map((key: string) => { + const inverseKey = getInverseKey(key); const vals = [ - `.color-bgc-${key} { background-color: var(${toProp(key)}); }`, - `.color-bbgc-${key}::backdrop { background-color: var(${toProp( + `.color-bgc-${key} { background-color: light-dark(var(${toProp( key - )}); }`, + )}), var(${toProp(inverseKey)})); }`, + `.color-bbgc-${key}::backdrop { background-color: light-dark(var(${toProp( + key + )}), var(${toProp(inverseKey)})); }`, ]; for (let o = 0.1; o < 1; o += 0.1) { vals.push(`.color-bbgc-${key}_${(o * 100).toFixed(0)}::backdrop { - background-color: oklch(from var(${toProp( + background-color: light-dark(oklch(from var(${toProp( key - )}) l c h / calc(alpha * ${o.toFixed(1)}) ); + )}) l c h / calc(alpha * ${o.toFixed(1)})), oklch(from var(${toProp( + inverseKey + )}) l c h / calc(alpha * ${o.toFixed(1)})) ); } `); } @@ -49,11 +57,26 @@ const color = (src: PaletteKey) => ${src .map((key: string) => { - return `.color-c-${key} { color: var(${toProp(key)}); }`; + const inverseKey = getInverseKey(key); + return `.color-c-${key} { color: light-dark(var(${toProp( + key + )}), var(${toProp(inverseKey)})); }`; }) .join("\n")} `; +const getInverseKey = (key: string): string => { + const match = key.match(/^([a-z]+)(\d+)$/); + if (!match) return key; + const [, prefix, shadeStr] = match; + const shade = parseInt(shadeStr, 10); + const target = 100 - shade; + const inverseShade = shades.reduce((prev, curr) => + Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev + ); + return `${prefix}${inverseShade}`; +}; + const keyFactory = (prefix: K) => { return shades.map((v) => `${prefix}${v}`) as PaletteKey; }; diff --git a/renderers/lit/src/0.8/types/components.ts b/renderers/lit/src/0.8/types/components.ts index 9e21d0de4..0e1765f5f 100644 --- a/renderers/lit/src/0.8/types/components.ts +++ b/renderers/lit/src/0.8/types/components.ts @@ -58,6 +58,7 @@ export interface Image { | "mediumFeature" | "largeFeature" | "header"; + fit?: "contain" | "cover" | "fill" | "none" | "scale-down"; } export interface Icon { diff --git a/renderers/lit/src/0.8/ui/image.ts b/renderers/lit/src/0.8/ui/image.ts index 24b0d2fbe..3ed12609b 100644 --- a/renderers/lit/src/0.8/ui/image.ts +++ b/renderers/lit/src/0.8/ui/image.ts @@ -33,6 +33,9 @@ export class Image extends Root { @property() accessor usageHint: ResolvedImage["usageHint"] | null = null; + @property() + accessor fit: "contain" | "cover" | "fill" | "none" | "scale-down" | null = null; + static styles = [ structuralStyles, css` @@ -51,6 +54,7 @@ export class Image extends Root { display: block; width: 100%; height: 100%; + object-fit: var(--object-fit, fill); } `, ]; @@ -103,9 +107,10 @@ export class Image extends Root { return html`
${this.#renderImage()}
`; diff --git a/renderers/lit/src/0.8/ui/root.ts b/renderers/lit/src/0.8/ui/root.ts index 261fa0731..854097acb 100644 --- a/renderers/lit/src/0.8/ui/root.ts +++ b/renderers/lit/src/0.8/ui/root.ts @@ -34,6 +34,7 @@ import { Theme, AnyComponentNode, SurfaceID } from "../types/types.js"; import { themeContext } from "./context/theme.js"; import { structuralStyles } from "./styles.js"; import { ComponentRegistry, REGISTRY } from './component-registry.js'; +import { ThemeManager } from "./theme/manager.js"; type NodeOfType = Extract< AnyComponentNode, @@ -93,6 +94,23 @@ export class Root extends SignalWatcher(LitElement) { */ #lightDomEffectDisposer: null | (() => void) = null; + #themeUnsubscribe: null | (() => void) = null; + + connectedCallback() { + super.connectedCallback(); + this.#themeUnsubscribe = ThemeManager.subscribe((sheets) => { + if (this.shadowRoot) { + const elementStyles = (this.constructor as typeof LitElement).elementStyles; + const baseStyles = elementStyles.map(s => { + if (s instanceof CSSStyleSheet) return s; + return s.styleSheet; + }).filter((s): s is CSSStyleSheet => !!s); + + this.shadowRoot.adoptedStyleSheets = [...baseStyles, ...sheets]; + } + }); + } + protected willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has("childComponents")) { if (this.#lightDomEffectDisposer) { @@ -122,6 +140,10 @@ export class Root extends SignalWatcher(LitElement) { if (this.#lightDomEffectDisposer) { this.#lightDomEffectDisposer(); } + + if (this.#themeUnsubscribe) { + this.#themeUnsubscribe(); + } } /** @@ -251,6 +273,7 @@ export class Root extends SignalWatcher(LitElement) { .url=${node.properties.url ?? null} .dataContextPath=${node.dataContextPath ?? ""} .usageHint=${node.properties.usageHint} + .fit=${node.properties.fit} .enableCustomElements=${this.enableCustomElements} >`; } diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/ui/text.ts index a33069a1d..bd62426ea 100644 --- a/renderers/lit/src/0.8/ui/text.ts +++ b/renderers/lit/src/0.8/ui/text.ts @@ -31,7 +31,7 @@ export class Text extends Root { @property() accessor text: StringValue | null = null; - @property() + @property({ reflect: true, attribute: "usage-hint" }) accessor usageHint: ResolvedText["usageHint"] | null = null; static styles = [ @@ -45,64 +45,62 @@ export class Text extends Root { ]; #renderText() { + let textValue: string | null | undefined = null; + if (this.text && typeof this.text === "object") { if ("literalString" in this.text && this.text.literalString) { - return html`${markdown( - this.text.literalString, - Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) - )}`; + textValue = this.text.literalString; } else if ("literal" in this.text && this.text.literal !== undefined) { - return html`${markdown( - this.text.literal, - Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) - )}`; + textValue = this.text.literal; } else if (this.text && "path" in this.text && this.text.path) { if (!this.processor || !this.component) { return html`(no model)`; } - const textValue = this.processor.getData( + const value = this.processor.getData( this.component, this.text.path, this.surfaceId ?? A2UIModelProcessor.DEFAULT_SURFACE_ID ); - if (textValue === null || textValue === undefined) { - return html`(empty)`; + if (value !== null && value !== undefined) { + textValue = value.toString(); } + } + } - let markdownText = textValue.toString(); - switch (this.usageHint) { - case "h1": - markdownText = `# ${markdownText}`; - break; - case "h2": - markdownText = `## ${markdownText}`; - break; - case "h3": - markdownText = `### ${markdownText}`; - break; - case "h4": - markdownText = `#### ${markdownText}`; - break; - case "h5": - markdownText = `##### ${markdownText}`; - break; - case "caption": - markdownText = `*${markdownText}*`; - break; - default: - break; // Body. - } + if (textValue === null || textValue === undefined) { + return html`(empty)`; + } - return html`${markdown( - markdownText, - Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) - )}`; - } + let markdownText = textValue; + switch (this.usageHint) { + case "h1": + markdownText = `# ${markdownText}`; + break; + case "h2": + markdownText = `## ${markdownText}`; + break; + case "h3": + markdownText = `### ${markdownText}`; + break; + case "h4": + markdownText = `#### ${markdownText}`; + break; + case "h5": + markdownText = `##### ${markdownText}`; + break; + case "caption": + markdownText = `*${markdownText}*`; + break; + default: + break; // Body. } - return html`(empty)`; + return html`${markdown( + markdownText, + Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) + )}`; } render() { diff --git a/renderers/lit/src/0.8/ui/theme/manager.ts b/renderers/lit/src/0.8/ui/theme/manager.ts new file mode 100644 index 000000000..8d3370d1f --- /dev/null +++ b/renderers/lit/src/0.8/ui/theme/manager.ts @@ -0,0 +1,42 @@ +/* + 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 class ThemeManager { + static #sheets: CSSStyleSheet[] = []; + static #listeners: Set<(sheets: CSSStyleSheet[]) => void> = new Set(); + + /** + * Registers a global CSS string to be applied to all A2UI components. + */ + static register(cssContent: string) { + const sheet = new CSSStyleSheet(); + sheet.replaceSync(cssContent); + this.#sheets.push(sheet); + this.#notify(); + } + + static subscribe(listener: (sheets: CSSStyleSheet[]) => void) { + this.#listeners.add(listener); + listener(this.#sheets); // Initial call + return () => this.#listeners.delete(listener); + } + + static #notify() { + for (const listener of this.#listeners) { + listener(this.#sheets); + } + } +} diff --git a/renderers/lit/src/0.8/ui/ui.ts b/renderers/lit/src/0.8/ui/ui.ts index af594e53c..db8a0cb48 100644 --- a/renderers/lit/src/0.8/ui/ui.ts +++ b/renderers/lit/src/0.8/ui/ui.ts @@ -47,6 +47,8 @@ export * as Context from "./context/theme.js"; export * as Utils from "./utils/utils.js"; export { ComponentRegistry, REGISTRY } from "./component-registry.js"; export { registerCustomComponents } from "./custom-components/index.js"; +export { ThemeManager } from "./theme/manager.js"; + export { Audio, diff --git a/samples/client/lit/restaurant/client.ts b/samples/client/lit/restaurant/client.ts deleted file mode 100644 index ba8588a8a..000000000 --- a/samples/client/lit/restaurant/client.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 { v0_8 } from "@a2ui/web-lib"; - -type A2TextPayload = { - kind: "text"; - text: string; -}; - -type A2DataPayload = { - kind: "data"; - data: v0_8.Types.ServerToClientMessage; -}; - -type A2AServerPayload = - | Array - | { error: string }; - -export class A2UIClient { - #ready: Promise = Promise.resolve(); - get ready() { - return this.#ready; - } - - async send( - message: v0_8.Types.A2UIClientEventMessage - ): Promise { - const response = await fetch("/a2a", { - body: JSON.stringify(message), - method: "POST", - }); - - if (response.ok) { - const data = (await response.json()) as A2AServerPayload; - const messages: v0_8.Types.ServerToClientMessage[] = []; - if ("error" in data) { - throw new Error(data.error); - } else { - for (const item of data) { - if (item.kind === "text") continue; - messages.push(item.data); - } - } - return messages; - } - - const error = (await response.json()) as { error: string }; - throw new Error(error.error); - } -} diff --git a/samples/client/lit/restaurant/index.html b/samples/client/lit/restaurant/index.html deleted file mode 100644 index 69722ee50..000000000 --- a/samples/client/lit/restaurant/index.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - Restaurant Booking Agent - - - - - - - - - - diff --git a/samples/client/lit/restaurant/restaurant.ts b/samples/client/lit/restaurant/restaurant.ts deleted file mode 100644 index 69010565a..000000000 --- a/samples/client/lit/restaurant/restaurant.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* - 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 { SignalWatcher } from "@lit-labs/signals"; -import { provide } from "@lit/context"; -import { - LitElement, - html, - css, - nothing, - HTMLTemplateResult, - unsafeCSS, -} from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { theme as uiTheme } from "./theme/theme.js"; -import { A2UIClient } from "./client.js"; -import { - SnackbarAction, - SnackbarMessage, - SnackbarUUID, - SnackType, -} from "./types/types.js"; -import { type Snackbar } from "./ui/snackbar.js"; -import { repeat } from "lit/directives/repeat.js"; -import { v0_8 } from "@a2ui/web-lib"; -import * as UI from "@a2ui/web-lib/ui"; - -// Restaurant demo elements. -import "./ui/ui.js"; - -@customElement("a2ui-restaurant") -export class A2UILayoutEditor extends SignalWatcher(LitElement) { - @provide({ context: UI.Context.themeContext }) - accessor theme: v0_8.Types.Theme = uiTheme; - - @state() - accessor #requesting = false; - - @state() - accessor #error: string | null = null; - - @state() - accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = []; - - static styles = [ - unsafeCSS(v0_8.Styles.structuralStyles), - css` - :host { - display: block; - max-width: 640px; - margin: 0 auto; - min-height: 100%; - } - - #surfaces { - width: 100%; - padding: var(--bb-grid-size-3); - animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards; - } - - form { - display: flex; - flex-direction: column; - flex: 1; - gap: 16px; - align-items: center; - padding: 16px 0; - animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards; - - & > div { - display: flex; - flex: 1; - gap: 16px; - align-items: center; - width: 100%; - - & > input { - display: block; - flex: 1; - border-radius: 32px; - padding: 16px 24px; - border: 1px solid var(--p-60); - font-size: 16px; - } - - & > button { - display: flex; - align-items: center; - background: var(--p-40); - color: var(--n-100); - border: none; - padding: 8px 16px; - border-radius: 32px; - opacity: 0.5; - - &:not([disabled]) { - cursor: pointer; - opacity: 1; - } - } - } - } - - .rotate { - animation: rotate 1s linear infinite; - } - - .pending { - width: 100%; - min-height: 200px; - display: flex; - align-items: center; - justify-content: center; - animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards; - - & .g-icon { - margin-right: 8px; - } - } - - .error { - color: var(--e-40); - background-color: var(--e-95); - border: 1px solid var(--e-80); - padding: 16px; - border-radius: 8px; - } - - @keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } - } - - @keyframes rotate { - from { - rotate: 0deg; - } - - to { - rotate: 360deg; - } - } - `, - ]; - - #processor = v0_8.Data.createSignalA2UIModelProcessor(); - #a2uiClient = new A2UIClient(); - #snackbar: Snackbar | undefined = undefined; - #pendingSnackbarMessages: Array<{ - message: SnackbarMessage; - replaceAll: boolean; - }> = []; - - render() { - return [ - this.#maybeRenderForm(), - this.#maybeRenderData(), - this.#maybeRenderError(), - ]; - } - - #maybeRenderError() { - if (!this.#error) return nothing; - - return html`
${this.#error}
`; - } - - #maybeRenderForm() { - if (this.#requesting) return nothing; - if (this.#lastMessages.length > 0) return nothing; - return html`
{ - evt.preventDefault(); - if (!(evt.target instanceof HTMLFormElement)) { - return; - } - const data = new FormData(evt.target); - const body = data.get("body") ?? null; - if (!body) { - return; - } - const message = body as v0_8.Types.A2UIClientEventMessage; - await this.#sendAndProcessMessage(message); - }} - > -

- Restaurant Finder -

-
- - -
-
`; - } - - #maybeRenderData() { - if (this.#requesting) { - return html`
- progress_activity - Awaiting an answer... -
`; - } - - const surfaces = this.#processor.getSurfaces(); - if (surfaces.size === 0) { - return nothing; - } - - return html`
- ${repeat( - this.#processor.getSurfaces(), - ([surfaceId]) => surfaceId, - ([surfaceId, surface]) => { - return html` - ) => { - const [target] = evt.composedPath(); - if (!(target instanceof HTMLElement)) { - return; - } - - const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] = - {}; - if (evt.detail.action.context) { - const srcContext = evt.detail.action.context; - for (const item of srcContext) { - if (item.value.literalBoolean) { - context[item.key] = item.value.literalBoolean; - } else if (item.value.literalNumber) { - context[item.key] = item.value.literalNumber; - } else if (item.value.literalString) { - context[item.key] = item.value.literalString; - } else if (item.value.path) { - const path = this.#processor.resolvePath( - item.value.path, - evt.detail.dataContextPath - ); - const value = this.#processor.getData( - evt.detail.sourceComponent, - path, - surfaceId - ); - context[item.key] = value; - } - } - } - - const message: v0_8.Types.A2UIClientEventMessage = { - userAction: { - name: evt.detail.action.name, - surfaceId, - sourceComponentId: target.id, - timestamp: new Date().toISOString(), - context, - }, - }; - - await this.#sendAndProcessMessage(message); - }} - .surfaceId=${surfaceId} - .surface=${surface} - .processor=${this.#processor} - >`; - } - )} -
`; - } - - async #sendAndProcessMessage(request) { - const messages = await this.#sendMessage(request); - - this.#lastMessages = messages; - this.#processor.clearSurfaces(); - this.#processor.processMessages(messages); - } - - async #sendMessage( - message: v0_8.Types.A2UIClientEventMessage - ): Promise { - try { - this.#requesting = true; - const response = this.#a2uiClient.send(message); - await response; - this.#requesting = false; - - return response; - } catch (err) { - this.snackbar(err as string, SnackType.ERROR); - } finally { - this.#requesting = false; - } - - return []; - } - - snackbar( - message: string | HTMLTemplateResult, - type: SnackType, - actions: SnackbarAction[] = [], - persistent = false, - id = globalThis.crypto.randomUUID(), - replaceAll = false - ) { - if (!this.#snackbar) { - this.#pendingSnackbarMessages.push({ - message: { - id, - message, - type, - persistent, - actions, - }, - replaceAll, - }); - return; - } - - return this.#snackbar.show( - { - id, - message, - type, - persistent, - actions, - }, - replaceAll - ); - } - - unsnackbar(id?: SnackbarUUID) { - if (!this.#snackbar) { - return; - } - - this.#snackbar.hide(id); - } -} diff --git a/samples/client/lit/restaurant/README.md b/samples/client/lit/shell/README.md similarity index 100% rename from samples/client/lit/restaurant/README.md rename to samples/client/lit/shell/README.md diff --git a/samples/client/lit/shell/THEMING.md b/samples/client/lit/shell/THEMING.md new file mode 100644 index 000000000..cdac80d68 --- /dev/null +++ b/samples/client/lit/shell/THEMING.md @@ -0,0 +1,167 @@ +# A2UI Theming & Configuration Guide + +This guide explains how the Universal App Shell handles theming and how to add new sample applications seamlessly. + +## Architecture Overview + +The styling system is built on three distinct layers: + +### 1. **Base Layer (`theme.ts`)** +* **Role**: Structural & Functional Styles. +* **What it does**: Maps A2UI components (like `Text`, `Card`, `Row`) to functional CSS utility classes (e.g., `layout-w-100`, `typography-f-sf`). +* **When to touch**: Rarely. Only if you need to change the fundamental layout behavior of a component. + +### 2. **Theme Layer (`styles.css`)** +* **Role**: Visual Design System. +* **What it does**: Defines the "look and feel" using standard CSS. It handles: + * Glassmorphism effects (`backdrop-filter`) + * Shadows and elevations + * Typography scales and weights (using **Outfit** font) + * Animations (hover states, transitions) +* **Key Mechanism**: It consumes **CSS Variables** (e.g., `var(--primary-color)`, `var(--bg-gradient)`) instead of hardcoding colors. +* **When to touch**: When you want to change the global design language (e.g., making buttons rounder, changing shadow depths). + +### 3. **Configuration Layer (`configs/*.ts`)** +* **Role**: App Identity & Brand Overrides. +* **What it does**: Defines the specific identity for an app (Restaurant, Contacts, etc.) and **injects** the CSS variable values that `styles.css` consumes. +* **Key Mechanism**: The `AppConfig` interface allows you to override any CSS variable defined in the theme. +* **When to touch**: Every time you add a new app or want to change an app's color scheme. + +--- + +## How to Add a New Sample App + +Follow these steps to add a new application (e.g., "Flight Booker") with its own unique theme. + +### Step 1: Create the Config +Create a new file `configs/flights.ts`: + +```typescript +import { AppConfig } from './types.js'; + +export const config: AppConfig = { + key: 'flights', + title: 'Flight Booker', + heroImage: '/hero-flights.png', + heroImageDark: '/hero-flights-dark.png', // Optional + placeholder: 'Where do you want to go?', + loadingText: ['Checking availability...', 'Finding best rates...'], + serverUrl: 'http://localhost:10004', // Your agent's URL + theme: { + // Sky Blue Theme Overrides + '--primary-color': 'light-dark(#0ea5e9, #38bdf8)', + '--primary-gradient': 'linear-gradient(135deg, #0ea5e9 0%, #3b82f6 100%)', + '--button-text': '#ffffff', + // Custom background gradient + '--bg-gradient': ` + radial-gradient(at 0% 0%, rgba(14, 165, 233, 0.2) 0px, transparent 50%), + linear-gradient(180deg, light-dark(#f0f9ff, #0c4a6e) 0%, light-dark(#e0f2fe, #075985) 100%) + `, + } +}; +``` + +### Step 2: Register the Config +Update `app.ts` to include your new config: + +```typescript +import { config as flightsConfig } from "./configs/flights.js"; + +const configs: Record = { + restaurant: restaurantConfig, + contacts: contactsConfig, + flights: flightsConfig, // Add this line +}; +``` + +### Step 3: Run It +Access your new app by adding the `app` query parameter: +`http://localhost:5173/?app=flights` + +The App Shell will automatically: +1. Load your `flights` config. +2. Inject your CSS variables into the document root. +3. `styles.css` will pick up these new colors and apply them to the UI. +4. Connect to your specified `serverUrl`. + +--- + +## Reference: Styling Levers + +This section lists the available styling "levers" (utility classes) you can use in your `theme.ts` file or directly in your components. These are defined in the core library (`renderers/lit/src/0.8/styles`). + +### 1. Layout (`layout-`) +**Source:** `styles/layout.ts` + +| Category | Prefix | Scale/Values | Examples | +| :--- | :--- | :--- | :--- | +| **Padding** | `layout-p-` | 0-24 (1 = 4px) | `layout-p-4` (16px), `layout-pt-2` (Top 8px), `layout-px-4` | +| **Margin** | `layout-m-` | 0-24 (1 = 4px) | `layout-m-0`, `layout-mb-4` (Bottom 16px), `layout-mx-auto` | +| **Gap** | `layout-g-` | 0-24 (1 = 4px) | `layout-g-2` (8px), `layout-g-4` (16px) | +| **Width** | `layout-w-` | 10-100 (Percentage) | `layout-w-100` (100%), `layout-w-50` (50%) | +| **Width (Px)** | `layout-wp-` | 0-15 (1 = 4px) | `layout-wp-10` (40px) | +| **Height** | `layout-h-` | 10-100 (Percentage) | `layout-h-100` (100%) | +| **Height (Px)** | `layout-hp-` | 0-15 (1 = 4px) | `layout-hp-10` (40px) | +| **Display** | `layout-dsp-` | `none`, `block`, `grid`, `flex`, `iflex` | `layout-dsp-flexhor` (Row), `layout-dsp-flexvert` (Col) | +| **Alignment** | `layout-al-` | `fs` (Start), `fe` (End), `c` (Center) | `layout-al-c` (Align Items Center) | +| **Justify** | `layout-sp-` | `c` (Center), `bt` (Between), `ev` (Evenly) | `layout-sp-bt` (Justify Content Space Between) | +| **Flex** | `layout-flx-` | `0` (None), `1` (Grow) | `layout-flx-1` (Flex Grow 1) | +| **Position** | `layout-pos-` | `a` (Absolute), `rel` (Relative) | `layout-pos-rel` | + +### 2. Colors (`color-`) +**Source:** `styles/colors.ts` + +| Category | Prefix | Scale/Values | Examples | +| :--- | :--- | :--- | :--- | +| **Text Color** | `color-c-` | Palette Key + Shade | `color-c-p50` (Primary), `color-c-n10` (Black), `color-c-e40` (Error) | +| **Background** | `color-bgc-` | Palette Key + Shade | `color-bgc-p100` (White/Lightest), `color-bgc-s30` (Secondary Dark) | +| **Border Color** | `color-bc-` | Palette Key + Shade | `color-bc-p60` (Primary Border) | + +**Palette Keys:** +* `p` = Primary (Brand) +* `s` = Secondary +* `t` = Tertiary +* `n` = Neutral (Grays) +* `nv` = Neutral Variant +* `e` = Error + +**Shades:** 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100 + +### 3. Typography (`typography-`) +**Source:** `styles/type.ts` + +| Category | Prefix | Scale/Values | Examples | +| :--- | :--- | :--- | :--- | +| **Font Family** | `typography-f-` | `sf` (Sans/Flex), `s` (Serif), `c` (Code) | `typography-f-sf` (System UI / Outfit) | +| **Weight** | `typography-w-` | 100-900 | `typography-w-400` (Regular), `typography-w-500` (Medium), `typography-w-700` (Bold) | +| **Size (Body)** | `typography-sz-` | `bs`, `bm`, `bl` | `typography-sz-bm` (Body Medium - 14px) | +| **Size (Title)** | `typography-sz-` | `ts`, `tm`, `tl` | `typography-sz-tl` (Title Large - 22px) | +| **Size (Headline)**| `typography-sz-` | `hs`, `hm`, `hl` | `typography-sz-hl` (Headline Large - 32px) | +| **Size (Display)** | `typography-sz-` | `ds`, `dm`, `dl` | `typography-sz-dl` (Display Large - 57px) | +| **Align** | `typography-ta-` | `s` (Start), `c` (Center) | `typography-ta-c` | + +### 4. Borders (`border-`) +**Source:** `styles/border.ts` + +| Category | Prefix | Scale/Values | Examples | +| :--- | :--- | :--- | :--- | +| **Radius** | `border-br-` | 0-24 (1 = 4px) | `border-br-4` (16px), `border-br-50pc` (50% / Circle) | +| **Width** | `border-bw-` | 0-24 (Pixels) | `border-bw-1` (1px), `border-bw-2` (2px) | +| **Style** | `border-bs-` | `s` (Solid) | `border-bs-s` | + +### 5. Behavior & Opacity +**Source:** `styles/behavior.ts`, `styles/opacity.ts` + +| Category | Prefix | Scale/Values | Examples | +| :--- | :--- | :--- | :--- | +| **Hover Opacity**| `behavior-ho-` | 0-100 (Step 5) | `behavior-ho-80` (Opacity 0.8 on hover) | +| **Opacity** | `opacity-el-` | 0-100 (Step 5) | `opacity-el-50` (Opacity 0.5) | +| **Overflow** | `behavior-o-` | `s` (Scroll), `a` (Auto), `h` (Hidden)| `behavior-o-h` | +| **Scrollbar** | `behavior-sw-` | `n` (None) | `behavior-sw-n` | + +### 6. Icons +**Source:** `styles/icons.ts` + +* Class: `.g-icon` +* Variants: `.filled`, `.filled-heavy` +* Usage: `icon_name` diff --git a/samples/client/lit/shell/app.ts b/samples/client/lit/shell/app.ts new file mode 100644 index 000000000..50d2d6108 --- /dev/null +++ b/samples/client/lit/shell/app.ts @@ -0,0 +1,745 @@ +/* + 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 { SignalWatcher } from "@lit-labs/signals"; +import { provide } from "@lit/context"; +import { + LitElement, + html, + css, + nothing, + HTMLTemplateResult, + unsafeCSS, +} from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { theme as uiTheme } from "./theme/theme.js"; +import { A2UIClient } from "./client.js"; +import { + SnackbarAction, + SnackbarMessage, + SnackbarUUID, + SnackType, +} from "./types/types.js"; +import { type Snackbar } from "./ui/snackbar.js"; +import { repeat } from "lit/directives/repeat.js"; +import { v0_8 } from "@a2ui/web-lib"; +import * as UI from "@a2ui/web-lib/ui"; + +// App elements. +import "./ui/ui.js"; +import { ThemeManager } from "@a2ui/web-lib/ui"; +// @ts-expect-error Inline import +import elegantTheme from "./theme/styles.css?inline"; + +// Configurations +import { AppConfig } from "./configs/types.js"; +import { config as restaurantConfig } from "./configs/restaurant.js"; +import { config as contactsConfig } from "./configs/contacts.js"; + +const configs: Record = { + restaurant: restaurantConfig, + contacts: contactsConfig, +}; + +ThemeManager.register(elegantTheme); + +@customElement("a2ui-shell") +export class A2UILayoutEditor extends SignalWatcher(LitElement) { + @provide({ context: UI.Context.themeContext }) + accessor theme: v0_8.Types.Theme = uiTheme; + + @state() + accessor #requesting = false; + + @state() + accessor #error: string | null = null; + + @state() + accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = []; + + @state() + accessor config: AppConfig = configs.restaurant; + + static styles = [ + unsafeCSS(v0_8.Styles.structuralStyles), + css` + :host { + display: block; + max-width: 640px; + margin: 0 auto; + min-height: 100%; + } + + #surfaces { + width: 100%; + padding: var(--bb-grid-size-3); + animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards; + } + + form { + display: flex; + flex-direction: column; + flex: 1; + gap: 16px; + align-items: center; + padding: 16px 0; + animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards; + + & > div { + display: flex; + flex: 1; + gap: 16px; + align-items: center; + width: 100%; + + & > input { + display: block; + flex: 1; + border-radius: 32px; + padding: 16px 24px; + border: 1px solid var(--p-60); + font-size: 16px; + } + + & > button { + display: flex; + align-items: center; + background: var(--p-40); + color: var(--n-100); + border: none; + padding: 8px 16px; + border-radius: 32px; + opacity: 0.5; + + &:not([disabled]) { + cursor: pointer; + opacity: 1; + } + } + } + } + + .rotate { + animation: rotate 1s linear infinite; + } + + .pending { + width: 100%; + min-height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards; + gap: 16px; + } + + .spinner { + width: 48px; + height: 48px; + border: 4px solid rgba(255, 255, 255, 0.1); + border-left-color: var(--p-60); + border-radius: 50%; + animation: spin 1s linear infinite; + } + + .loading-text { + font-size: 1.2rem; + font-weight: 600; + background: var(--primary-gradient); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: pulse 2s infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } + + @keyframes pulse { + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } + } + + .error { + color: var(--e-40); + background-color: var(--e-95); + border: 1px solid var(--e-80); + padding: 16px; + border-radius: 8px; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + @keyframes rotate { + from { + rotate: 0deg; + } + + to { + rotate: 360deg; + } + } + + .app-title { + font-size: 2.5rem; + font-weight: 800; + background: var(--primary-gradient); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin: 0 0 1rem 0; + text-align: center; + letter-spacing: -1px; + line-height: 1.2; + } + `, + ]; + + #processor = v0_8.Data.createSignalA2UIModelProcessor(); + #a2uiClient = new A2UIClient(); + #snackbar: Snackbar | undefined = undefined; + #pendingSnackbarMessages: Array<{ + message: SnackbarMessage; + replaceAll: boolean; + }> = []; + + #maybeRenderError() { + if (!this.#error) return nothing; + + return html`
${this.#error}
`; + } + + @state() + accessor #isDark = false; + + connectedCallback() { + super.connectedCallback(); + + // Load config from URL + const urlParams = new URLSearchParams(window.location.search); + const appKey = urlParams.get('app') || 'restaurant'; + this.config = configs[appKey] || configs.restaurant; + + // Initialize client with configured URL + this.#a2uiClient = new A2UIClient(this.config.serverUrl); + + this.#injectGlobalStyles(); + // Check system preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + this.#isDark = true; + document.documentElement.style.colorScheme = 'dark'; + } else { + document.documentElement.style.colorScheme = 'light'; + } + } + + #injectGlobalStyles() { + const styleId = 'a2ui-global-theme-vars'; + if (document.getElementById(styleId)) return; + + // Generate CSS variables from config + const themeOverrides = Object.entries(this.config.theme) + .map(([key, val]) => `${key}: ${val};`) + .join('\n'); + + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + :root { + color-scheme: light dark; + + /* Palette Variables */ + --n-100: #ffffff; + --n-99: #fcfcfc; + --n-98: #f9f9f9; + --n-95: #f1f1f1; + --n-90: #e2e2e2; + --n-80: #c6c6c6; + --n-70: #ababab; + --n-60: #919191; + --n-50: #777777; + --n-40: #5e5e5e; + --n-35: #525252; + --n-30: #474747; + --n-25: #3b3b3b; + --n-20: #303030; + --n-15: #262626; + --n-10: #1b1b1b; + --n-5: #111111; + --n-0: #000000; + + --p-100: #ffffff; + --p-99: #fffbff; + --p-98: #fcf8ff; + --p-95: #f2efff; + --p-90: #e1e0ff; + --p-80: #c0c1ff; + --p-70: #a0a3ff; + --p-60: #8487ea; + --p-50: #6a6dcd; + --p-40: #5154b3; + --p-35: #4447a6; + --p-30: #383b99; + --p-25: #2c2e8d; + --p-20: #202182; + --p-15: #131178; + --p-10: #06006c; + --p-5: #03004d; + --p-0: #000000; + + --s-100: #ffffff; + --s-99: #fffbff; + --s-98: #fcf8ff; + --s-95: #f2efff; + --s-90: #e2e0f9; + --s-80: #c6c4dd; + --s-70: #aaa9c1; + --s-60: #8f8fa5; + --s-50: #75758b; + --s-40: #5d5c72; + --s-35: #515165; + --s-30: #454559; + --s-25: #393a4d; + --s-20: #2e2f42; + --s-15: #242437; + --s-10: #191a2c; + --s-5: #0f0f21; + --s-0: #000000; + + --t-100: #ffffff; + --t-99: #fffbff; + --t-98: #fff8f9; + --t-95: #ffecf4; + --t-90: #ffd8ec; + --t-80: #e9b9d3; + --t-70: #cc9eb8; + --t-60: #af849d; + --t-50: #946b83; + --t-40: #79536a; + --t-35: #6c475d; + --t-30: #5f3c51; + --t-25: #523146; + --t-20: #46263a; + --t-15: #3a1b2f; + --t-10: #2e1125; + --t-5: #22071a; + --t-0: #000000; + + --nv-100: #ffffff; + --nv-99: #fffbff; + --nv-98: #fcf8ff; + --nv-95: #f2effa; + --nv-90: #e4e1ec; + --nv-80: #c8c5d0; + --nv-70: #acaab4; + --nv-60: #918f9a; + --nv-50: #777680; + --nv-40: #5e5d67; + --nv-35: #52515b; + --nv-30: #46464f; + --nv-25: #3b3b43; + --nv-20: #303038; + --nv-15: #25252d; + --nv-10: #1b1b23; + --nv-5: #101018; + --nv-0: #000000; + + --e-100: #ffffff; + --e-99: #fffbff; + --e-98: #fff8f7; + --e-95: #ffedea; + --e-90: #ffdad6; + --e-80: #ffb4ab; + --e-70: #ff897d; + --e-60: #ff5449; + --e-50: #de3730; + --e-40: #ba1a1a; + --e-35: #a80710; + --e-30: #93000a; + --e-25: #7e0007; + --e-20: #690005; + --e-15: #540003; + --e-10: #410002; + --e-5: #2d0001; + --e-0: #000000; + + /* Default Theme Variables using light-dark() */ + --bg-gradient: + radial-gradient(at 0% 0%, light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px, transparent 50%), + radial-gradient(at 100% 0%, light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px, transparent 50%), + radial-gradient(at 100% 100%, light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px, transparent 50%), + radial-gradient(at 0% 100%, light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px, transparent 50%), + linear-gradient(120deg, light-dark(#f0f4f8, #0f172a) 0%, light-dark(#e2e8f0, #1e293b) 100%); + + --card-bg: + radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), + radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), + linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8))); + + --card-border: light-dark( + rgba(255, 255, 255, 0.5), + rgba(148, 163, 184, 0.1) + ); + --card-shadow: light-dark( + 0 8px 32px 0 rgba(31, 38, 135, 0.1), + 0 8px 32px 0 rgba(0, 0, 0, 0.4) + ); + --text-primary: light-dark(#1e1b4b, #e0e7ff); + --text-secondary: light-dark(#4338ca, #a5b4fc); + --primary-color: light-dark(#667eea, #06b6d4); + --primary-gradient: linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%); + --input-bg: light-dark(rgba(255, 255, 255, 0.5), rgba(15, 23, 42, 0.5)); + --input-border: light-dark(rgba(0, 0, 0, 0.1), rgba(148, 163, 184, 0.2)); + --toggle-bg: light-dark(rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); + --toggle-icon: light-dark(#f1c40f, #fbbf24); + --button-text: light-dark(#ffffff, #cffafe); + + /* Config Overrides */ + ${themeOverrides} + } + + body { + background: var(--bg-gradient); + background-attachment: fixed; + background-repeat: no-repeat; + color: var(--text-primary); + margin: 0; + min-height: 100vh; + transition: background 0.5s ease, color 0.5s ease; + font-family: 'Outfit', sans-serif; + } + + /* Holiday Spirit: Snowfall Animation - Only in Dark Mode */ + @media (prefers-color-scheme: dark) { + @keyframes snow { + 0% { background-position: 0px 0px, 0px 0px, 0px 0px; } + 100% { background-position: 500px 1000px, 400px 400px, 300px 300px; } + } + + body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 1; + background-image: + radial-gradient(4px 4px at 100px 50px, rgba(255,255,255,0.8) 50%, transparent 50%), + radial-gradient(6px 6px at 200px 150px, rgba(255,255,255,0.6) 50%, transparent 50%), + radial-gradient(3px 3px at 300px 250px, rgba(255,255,255,0.9) 50%, transparent 50%), + radial-gradient(4px 4px at 400px 350px, rgba(255,255,255,0.7) 50%, transparent 50%), + radial-gradient(6px 6px at 500px 100px, rgba(255,255,255,0.5) 50%, transparent 50%), + radial-gradient(3px 3px at 50px 200px, rgba(255,255,255,0.8) 50%, transparent 50%), + radial-gradient(4px 4px at 150px 300px, rgba(255,255,255,0.6) 50%, transparent 50%), + radial-gradient(6px 6px at 250px 400px, rgba(255,255,255,0.9) 50%, transparent 50%), + radial-gradient(3px 3px at 350px 500px, rgba(255,255,255,0.7) 50%, transparent 50%); + background-size: 500px 500px, 400px 400px, 300px 300px; + animation: snow 10s linear infinite; + } + } + `; + document.head.appendChild(style); + } + + #toggleTheme() { + this.#isDark = !this.#isDark; + document.documentElement.style.colorScheme = this.#isDark ? 'dark' : 'light'; + } + + render() { + return [ + this.#renderThemeToggle(), + this.#maybeRenderForm(), + this.#maybeRenderData(), + this.#maybeRenderError(), + ]; + } + + #renderThemeToggle() { + return html` +
+ +
`; + } + + #maybeRenderForm() { + if (this.#requesting) return nothing; + if (this.#lastMessages.length > 0) return nothing; + + // Determine hero image based on theme + const heroSrc = (this.#isDark && this.config.heroImageDark) + ? this.config.heroImageDark + : this.config.heroImage; + + return html` +
{ + evt.preventDefault(); + if (!(evt.target instanceof HTMLFormElement)) { + return; + } + const data = new FormData(evt.target); + const body = data.get("body") ?? null; + if (!body) { + return; + } + const message = body as v0_8.Types.A2UIClientEventMessage; + await this.#sendAndProcessMessage(message); + }} + > + ${this.config.title} +

+ ${this.config.title} +

+
+ + +
+
`; + } + + @state() + accessor #loadingTextIndex = 0; + #loadingInterval: number | undefined; + + #startLoadingAnimation() { + if (Array.isArray(this.config.loadingText) && this.config.loadingText.length > 1) { + this.#loadingTextIndex = 0; + this.#loadingInterval = window.setInterval(() => { + this.#loadingTextIndex = (this.#loadingTextIndex + 1) % (this.config.loadingText as string[]).length; + }, 2000); + } + } + + #stopLoadingAnimation() { + if (this.#loadingInterval) { + clearInterval(this.#loadingInterval); + this.#loadingInterval = undefined; + } + } + + async #sendMessage( + message: v0_8.Types.A2UIClientEventMessage + ): Promise { + try { + this.#requesting = true; + this.#startLoadingAnimation(); + const response = this.#a2uiClient.send(message); + await response; + this.#requesting = false; + this.#stopLoadingAnimation(); + + return response; + } catch (err) { + this.snackbar(err as string, SnackType.ERROR); + } finally { + this.#requesting = false; + this.#stopLoadingAnimation(); + } + + return []; + } + + #maybeRenderData() { + if (this.#requesting) { + let text = 'Awaiting an answer...'; + if (this.config.loadingText) { + if (Array.isArray(this.config.loadingText)) { + text = this.config.loadingText[this.#loadingTextIndex]; + } else { + text = this.config.loadingText; + } + } + + return html`
+
+
${text}
+
`; + } + + const surfaces = this.#processor.getSurfaces(); + if (surfaces.size === 0) { + return nothing; + } + + return html`
+ ${repeat( + this.#processor.getSurfaces(), + ([surfaceId]) => surfaceId, + ([surfaceId, surface]) => { + return html` + ) => { + const [target] = evt.composedPath(); + if (!(target instanceof HTMLElement)) { + return; + } + + const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] = + {}; + if (evt.detail.action.context) { + const srcContext = evt.detail.action.context; + for (const item of srcContext) { + if (item.value.literalBoolean) { + context[item.key] = item.value.literalBoolean; + } else if (item.value.literalNumber) { + context[item.key] = item.value.literalNumber; + } else if (item.value.literalString) { + context[item.key] = item.value.literalString; + } else if (item.value.path) { + const path = this.#processor.resolvePath( + item.value.path, + evt.detail.dataContextPath + ); + const value = this.#processor.getData( + evt.detail.sourceComponent, + path, + surfaceId + ); + context[item.key] = value; + } + } + } + + const message: v0_8.Types.A2UIClientEventMessage = { + userAction: { + name: evt.detail.action.name, + surfaceId, + sourceComponentId: target.id, + timestamp: new Date().toISOString(), + context, + }, + }; + + await this.#sendAndProcessMessage(message); + }} + .surfaceId=${surfaceId} + .surface=${surface} + .processor=${this.#processor} + >`; + } + )} +
`; + } + + async #sendAndProcessMessage(request) { + const messages = await this.#sendMessage(request); + + this.#lastMessages = messages; + this.#processor.clearSurfaces(); + this.#processor.processMessages(messages); + } + + + + snackbar( + message: string | HTMLTemplateResult, + type: SnackType, + actions: SnackbarAction[] = [], + persistent = false, + id = globalThis.crypto.randomUUID(), + replaceAll = false + ) { + if (!this.#snackbar) { + this.#pendingSnackbarMessages.push({ + message: { + id, + message, + type, + persistent, + actions, + }, + replaceAll, + }); + return; + } + + return this.#snackbar.show( + { + id, + message, + type, + persistent, + actions, + }, + replaceAll + ); + } + + unsnackbar(id?: SnackbarUUID) { + if (!this.#snackbar) { + return; + } + + this.#snackbar.hide(id); + } +} diff --git a/samples/client/lit/shell/client.ts b/samples/client/lit/shell/client.ts new file mode 100644 index 000000000..d6b299796 --- /dev/null +++ b/samples/client/lit/shell/client.ts @@ -0,0 +1,112 @@ +/* + 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 { Part, SendMessageSuccessResponse, Task } from "@a2a-js/sdk"; +import { A2AClient } from "@a2a-js/sdk/client"; +import { v0_8 } from "@a2ui/web-lib"; + +const A2AUI_MIME_TYPE = "application/json+a2aui"; + +export class A2UIClient { + #serverUrl: string; + #client: A2AClient | null = null; + + constructor(serverUrl: string = "") { + this.#serverUrl = serverUrl; + } + + #ready: Promise = Promise.resolve(); + get ready() { + return this.#ready; + } + + async #getClient() { + if (!this.#client) { + // Default to localhost:10002 if no URL provided (fallback for restaurant app default) + const baseUrl = this.#serverUrl || "http://localhost:10002"; + + this.#client = await A2AClient.fromCardUrl( + `${baseUrl}/.well-known/agent-card.json`, + { + fetchImpl: async (url, init) => { + const headers = new Headers(init?.headers); + headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8"); + return fetch(url, { ...init, headers }); + } + } + ); + } + return this.#client; + } + + async send( + message: v0_8.Types.A2UIClientEventMessage | string + ): Promise { + const client = await this.#getClient(); + + let parts: Part[] = []; + + if (typeof message === 'string') { + // Try to parse as JSON first, just in case + try { + const parsed = JSON.parse(message); + if (typeof parsed === 'object' && parsed !== null) { + parts = [{ + kind: "data", + data: parsed as unknown as Record, + mimeType: A2AUI_MIME_TYPE, + } as Part]; + } else { + parts = [{ kind: "text", text: message }]; + } + } catch { + parts = [{ kind: "text", text: message }]; + } + } else { + parts = [{ + kind: "data", + data: message as unknown as Record, + mimeType: A2AUI_MIME_TYPE, + } as Part]; + } + + const response = await client.sendMessage({ + message: { + messageId: crypto.randomUUID(), + role: "user", + parts: parts, + kind: "message", + }, + }); + + if ("error" in response) { + throw new Error(response.error.message); + } + + const result = (response as SendMessageSuccessResponse).result as Task; + if (result.kind === "task" && result.status.message?.parts) { + const messages: v0_8.Types.ServerToClientMessage[] = []; + for (const part of result.status.message.parts) { + if (part.kind === 'data') { + messages.push(part.data as v0_8.Types.ServerToClientMessage); + } + } + return messages; + } + + return []; + } +} diff --git a/samples/client/lit/shell/configs/contacts.ts b/samples/client/lit/shell/configs/contacts.ts new file mode 100644 index 000000000..edcee85da --- /dev/null +++ b/samples/client/lit/shell/configs/contacts.ts @@ -0,0 +1,29 @@ +import { AppConfig } from './types.js'; + +export const config: AppConfig = { + key: 'contacts', + title: 'Contact Manager', + heroImage: '/hero.png', // We can use the same hero for now or a different one if available + placeholder: 'Alex Jordan', + loadingText: [ + 'Searching contacts...', + 'Looking up details...', + 'Verifying information...', + 'Just a moment...' + ], + serverUrl: 'http://localhost:10003', + theme: { + // Contacts Theme (Teal/Green/Emerald) + // Overriding the primary colors to give it a distinct look + '--primary-color': 'light-dark(#10b981, #34d399)', // Emerald 500/400 + '--primary-gradient': 'linear-gradient(135deg, light-dark(#10b981, #34d399) 0%, light-dark(#059669, #059669) 100%)', + '--button-text': '#ffffff', + '--bg-gradient': ` + radial-gradient(at 0% 0%, light-dark(rgba(45, 212, 191, 0.4), rgba(20, 184, 166, 0.2)) 0px, transparent 50%), + radial-gradient(at 100% 0%, light-dark(rgba(56, 189, 248, 0.4), rgba(14, 165, 233, 0.2)) 0px, transparent 50%), + radial-gradient(at 100% 100%, light-dark(rgba(163, 230, 53, 0.4), rgba(132, 204, 22, 0.2)) 0px, transparent 50%), + radial-gradient(at 0% 100%, light-dark(rgba(52, 211, 153, 0.4), rgba(16, 185, 129, 0.2)) 0px, transparent 50%), + linear-gradient(120deg, light-dark(#f0fdf4, #022c22) 0%, light-dark(#dcfce7, #064e3b) 100%) + `, + } +}; diff --git a/samples/client/lit/shell/configs/restaurant.ts b/samples/client/lit/shell/configs/restaurant.ts new file mode 100644 index 000000000..f300dade9 --- /dev/null +++ b/samples/client/lit/shell/configs/restaurant.ts @@ -0,0 +1,22 @@ +import { AppConfig } from './types.js'; + +export const config: AppConfig = { + key: 'restaurant', + title: 'Restaurant Finder', + heroImage: '/hero.png', + heroImageDark: '/hero-dark.png', + placeholder: 'Top 5 Chinese restaurants in New York.', + loadingText: [ + 'Finding the best spots for you...', + 'Checking reviews...', + 'Looking for open tables...', + 'Almost there...' + ], + serverUrl: 'http://localhost:10002', + theme: { + // Restaurant Theme (Blue/Purple/Cyan) + '--primary-hue': '230', // Example if we were using HSL, but we are using specific colors for now. + // We can override specific variables if needed, but the default "Elegant" theme is already the Restaurant theme. + // So we might not need many overrides here unless we want to be explicit. + } +}; diff --git a/samples/client/lit/shell/configs/types.ts b/samples/client/lit/shell/configs/types.ts new file mode 100644 index 000000000..34df7fdc8 --- /dev/null +++ b/samples/client/lit/shell/configs/types.ts @@ -0,0 +1,21 @@ +/** + * Configuration interface for the Universal App Shell. + */ +export interface AppConfig { + /** Unique key for the app (e.g., 'restaurant', 'contacts') */ + key: string; + /** Display title of the application */ + title: string; + /** Path to the hero image */ + heroImage: string; + /** Path to the dark mode hero image (optional) */ + heroImageDark?: string; + /** Placeholder text for the input field */ + placeholder: string; + /** Text to display while loading (optional). Can be a single string or an array of strings to rotate. */ + loadingText?: string | string[]; + /** Optional server URL for the agent (e.g., http://localhost:10003) */ + serverUrl?: string; + /** Theme overrides (CSS Variables) */ + theme: Record; +} diff --git a/samples/client/lit/restaurant/events/events.ts b/samples/client/lit/shell/events/events.ts similarity index 100% rename from samples/client/lit/restaurant/events/events.ts rename to samples/client/lit/shell/events/events.ts diff --git a/samples/client/lit/shell/index.html b/samples/client/lit/shell/index.html new file mode 100644 index 000000000..a580531c3 --- /dev/null +++ b/samples/client/lit/shell/index.html @@ -0,0 +1,194 @@ + + + + + + + + + Restaurant Finder + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/client/lit/restaurant/middleware/a2a.ts b/samples/client/lit/shell/middleware/a2a.ts similarity index 100% rename from samples/client/lit/restaurant/middleware/a2a.ts rename to samples/client/lit/shell/middleware/a2a.ts diff --git a/samples/client/lit/restaurant/middleware/index.ts b/samples/client/lit/shell/middleware/index.ts similarity index 100% rename from samples/client/lit/restaurant/middleware/index.ts rename to samples/client/lit/shell/middleware/index.ts diff --git a/samples/client/lit/restaurant/package.json b/samples/client/lit/shell/package.json similarity index 90% rename from samples/client/lit/restaurant/package.json rename to samples/client/lit/shell/package.json index f5d9d76b2..e74d2a1cd 100644 --- a/samples/client/lit/restaurant/package.json +++ b/samples/client/lit/shell/package.json @@ -1,10 +1,10 @@ { - "name": "@a2ui/restaurant", + "name": "@a2ui/shell", "private": true, "version": "0.1.0", - "description": "A2UI Restaurant Demo", - "main": "./dist/restaurant.js", - "types": "./dist/restaurant.d.ts", + "description": "A2UI Universal Shell", + "main": "./dist/shell.js", + "types": "./dist/shell.d.ts", "type": "module", "scripts": { "prepack": "npm run build", @@ -53,7 +53,7 @@ } }, "repository": { - "directory": "samples/client/lit/restaurant", + "directory": "samples/client/lit/shell", "type": "git", "url": "git+https://github.com/google/A2UI.git" }, @@ -83,4 +83,4 @@ "@types/node": "^24.7.1", "lit": "^3.3.1" } -} +} \ No newline at end of file diff --git a/samples/client/lit/shell/public/hero-dark.png b/samples/client/lit/shell/public/hero-dark.png new file mode 100644 index 000000000..b398a5a33 Binary files /dev/null and b/samples/client/lit/shell/public/hero-dark.png differ diff --git a/samples/client/lit/shell/public/hero.png b/samples/client/lit/shell/public/hero.png new file mode 100644 index 000000000..a8a4381cd Binary files /dev/null and b/samples/client/lit/shell/public/hero.png differ diff --git a/samples/client/lit/restaurant/public/sample/city_skyline.jpg b/samples/client/lit/shell/public/sample/city_skyline.jpg similarity index 100% rename from samples/client/lit/restaurant/public/sample/city_skyline.jpg rename to samples/client/lit/shell/public/sample/city_skyline.jpg diff --git a/samples/client/lit/restaurant/public/sample/forest_path.jpg b/samples/client/lit/shell/public/sample/forest_path.jpg similarity index 100% rename from samples/client/lit/restaurant/public/sample/forest_path.jpg rename to samples/client/lit/shell/public/sample/forest_path.jpg diff --git a/samples/client/lit/restaurant/public/sample/scenic_view.jpg b/samples/client/lit/shell/public/sample/scenic_view.jpg similarity index 100% rename from samples/client/lit/restaurant/public/sample/scenic_view.jpg rename to samples/client/lit/shell/public/sample/scenic_view.jpg diff --git a/samples/client/lit/shell/theme/styles.css b/samples/client/lit/shell/theme/styles.css new file mode 100644 index 000000000..2529df7ba --- /dev/null +++ b/samples/client/lit/shell/theme/styles.css @@ -0,0 +1,166 @@ +/* Elegant Theme for A2UI with Dark Mode Support */ + +/* + We use :host to style the component itself. + CSS Variables are expected to be defined in the document root or body. +*/ + +:host { + --font-family: 'Outfit', sans-serif; +} + +/* Global overrides */ +* { + font-family: 'Outfit', sans-serif !important; +} + +/* Card Styling */ +:host(a2ui-card) { + background: var(--card-bg) !important; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--card-border) !important; + box-shadow: var(--card-shadow) !important; + border-radius: 24px !important; + overflow: hidden; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; + padding: 1px; + /* Prevent content touching edges */ + color: var(--text-primary); +} + +:host(a2ui-card) section { + border-radius: 23px; +} + +:host(a2ui-card):hover { + transform: translateY(-5px); + box-shadow: 0 12px 40px 0 rgba(31, 38, 135, 0.15) !important; +} + +/* Avatar Styling */ +.is-avatar { + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + flex: 0 0 auto !important; + /* Prevent flexing */ + overflow: hidden; + margin: 0 auto; + /* Center it if needed */ +} + +/* Button Styling */ +:host(a2ui-button) { + --text-primary: var(--button-text); + overflow: visible !important; +} + +:host(a2ui-button) button { + background: var(--primary-gradient) !important; + color: var(--button-text) !important; + border-radius: 50px !important; + border: none !important; + padding: 12px 28px !important; + font-weight: 600 !important; + letter-spacing: 0.5px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: uppercase; + font-size: 0.9rem; + opacity: 1 !important; + /* Ensure no transparency */ + backdrop-filter: none !important; + /* Ensure no glass effect on button */ +} + +:host(a2ui-button) button:hover { + transform: scale(0.95); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); + filter: brightness(1.1); +} + +:host(a2ui-button) button:active { + transform: scale(0.98); +} + +/* Input Styling */ +:host(a2ui-textfield) input, +:host(a2ui-datetimeinput) input { + background: var(--input-bg) !important; + border: 1px solid var(--input-border) !important; + border-radius: 16px !important; + padding: 14px 20px !important; + transition: all 0.3s ease; + color: var(--text-primary) !important; + font-size: 1rem; +} + +:host(a2ui-textfield) input::placeholder, +:host(a2ui-datetimeinput) input::placeholder { + color: var(--text-secondary); + opacity: 0.7; +} + +:host(a2ui-textfield) input:focus, +:host(a2ui-datetimeinput) input:focus { + background: var(--card-bg) !important; + /* Slightly more opaque on focus */ + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15); + outline: none; +} + +/* Typography */ +:host(a2ui-text[usage-hint="h1"]) { + font-size: 4.5rem !important; + font-weight: 700 !important; + background: var(--primary-gradient); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 1.5rem !important; + display: block; + letter-spacing: -1px; + line-height: 1.2; +} + +:host(a2ui-text[usage-hint="h2"]), +:host(a2ui-text[usage-hint="h3"]) { + font-weight: 700 !important; + background: var(--primary-gradient); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + display: inline-block; + /* Required for background-clip on some elements */ +} + +:host(a2ui-text[usage-hint="h2"]) { + font-size: 2rem; + letter-spacing: -0.5px; +} + +:host(a2ui-text[usage-hint="h4"]), +:host(a2ui-text[usage-hint="h5"]) { + color: var(--text-primary) !important; +} + +:host(a2ui-text) { + color: var(--text-secondary) !important; + line-height: 1.7; + font-size: 1.05rem; +} + +:host(a2ui-text) a { + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + transition: all 0.2s ease; + border-bottom: 1px solid transparent; +} + +:host(a2ui-text) a:hover { + border-bottom-color: var(--primary-color); + opacity: 0.8; +} \ No newline at end of file diff --git a/samples/client/lit/restaurant/theme/theme.ts b/samples/client/lit/shell/theme/theme.ts similarity index 88% rename from samples/client/lit/restaurant/theme/theme.ts rename to samples/client/lit/shell/theme/theme.ts index dd5e8d30c..6a661650c 100644 --- a/samples/client/lit/restaurant/theme/theme.ts +++ b/samples/client/lit/shell/theme/theme.ts @@ -55,7 +55,6 @@ const button = { "border-c-n70": true, "border-bs-s": true, "color-bgc-s30": true, - "color-c-n100": true, "behavior-ho-80": true, }; @@ -120,6 +119,7 @@ const orderedList = { "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, + "color-c-n10": true, }; const unorderedList = { @@ -129,6 +129,7 @@ const unorderedList = { "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, + "color-c-n10": true, }; const listItem = { @@ -138,6 +139,7 @@ const listItem = { "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, + "color-c-n10": true, }; const pre = { @@ -159,25 +161,19 @@ const video = { "layout-el-cv": true, }; -const aLight = v0_8.Styles.merge(a, { "color-c-n5": true }); -const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true }); -const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true }); -const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true }); -const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true }); -const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true }); -const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true }); -const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true }); -const pLight = v0_8.Styles.merge(p, { "color-c-n35": true }); -const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true }); -const orderedListLight = v0_8.Styles.merge(orderedList, { - "color-c-n35": true, -}); -const unorderedListLight = v0_8.Styles.merge(unorderedList, { - "color-c-n35": true, -}); -const listItemLight = v0_8.Styles.merge(listItem, { - "color-c-n35": true, -}); +const aLight = v0_8.Styles.merge(a, {}); +const inputLight = v0_8.Styles.merge(input, {}); +const textareaLight = v0_8.Styles.merge(textarea, {}); +const buttonLight = v0_8.Styles.merge(button, {}); +const h1Light = v0_8.Styles.merge(h1, {}); +const h2Light = v0_8.Styles.merge(h2, {}); +const h3Light = v0_8.Styles.merge(h3, {}); +const bodyLight = v0_8.Styles.merge(body, {}); +const pLight = v0_8.Styles.merge(p, {}); +const preLight = v0_8.Styles.merge(pre, {}); +const orderedListLight = v0_8.Styles.merge(orderedList, {}); +const unorderedListLight = v0_8.Styles.merge(unorderedList, {}); +const listItemLight = v0_8.Styles.merge(listItem, {}); export const theme: v0_8.Types.Theme = { additionalStyles: { @@ -196,10 +192,9 @@ export const theme: v0_8.Types.Theme = { "border-bw-0": true, "border-bs-s": true, "color-bgc-p30": true, - "color-c-n100": true, "behavior-ho-70": true, }, - Card: { "border-br-9": true, "color-bgc-p100": true, "layout-p-4": true }, + Card: { "border-br-9": true, "layout-p-4": true }, CheckBox: { element: { "layout-m-0": true, @@ -254,7 +249,7 @@ export const theme: v0_8.Types.Theme = { "layout-w-100": true, "layout-h-100": true, }, - avatar: {}, + avatar: { "is-avatar": true }, header: {}, icon: {}, largeFeature: {}, @@ -299,7 +294,6 @@ export const theme: v0_8.Types.Theme = { all: { "layout-w-100": true, "layout-g-2": true, - "color-c-p30": true, }, h1: { "typography-f-sf": true, diff --git a/samples/client/lit/restaurant/tsconfig.json b/samples/client/lit/shell/tsconfig.json similarity index 100% rename from samples/client/lit/restaurant/tsconfig.json rename to samples/client/lit/shell/tsconfig.json diff --git a/samples/client/lit/restaurant/types/types.ts b/samples/client/lit/shell/types/types.ts similarity index 100% rename from samples/client/lit/restaurant/types/types.ts rename to samples/client/lit/shell/types/types.ts diff --git a/samples/client/lit/restaurant/ui/snackbar.ts b/samples/client/lit/shell/ui/snackbar.ts similarity index 100% rename from samples/client/lit/restaurant/ui/snackbar.ts rename to samples/client/lit/shell/ui/snackbar.ts diff --git a/samples/client/lit/restaurant/ui/ui.ts b/samples/client/lit/shell/ui/ui.ts similarity index 100% rename from samples/client/lit/restaurant/ui/ui.ts rename to samples/client/lit/shell/ui/ui.ts diff --git a/samples/client/lit/restaurant/vite.config.ts b/samples/client/lit/shell/vite.config.ts similarity index 95% rename from samples/client/lit/restaurant/vite.config.ts rename to samples/client/lit/shell/vite.config.ts index f5fd15404..4f0892ec2 100644 --- a/samples/client/lit/restaurant/vite.config.ts +++ b/samples/client/lit/shell/vite.config.ts @@ -26,7 +26,7 @@ export default async () => { config(); const entry: Record = { - restaurant: resolve(__dirname, "index.html"), + shell: resolve(__dirname, "index.html"), }; return {