From bb3ad7674c8f7b78866d7fda9a819f8ce4f55b25 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 10 May 2026 08:38:00 -0700 Subject: [PATCH] =?UTF-8?q?feat(chat):=20A2UI=20theme=20presets=20?= =?UTF-8?q?=E2=80=94=20default-light,=20material-dark,=20material-light=20?= =?UTF-8?q?(Pass=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 3 of the A2UI theming track. Ships four CSS preset files consumers @import to override the ~50 internal --a2ui-* tokens declared at :host (Pass 2b) at the :root level: - themes/default-dark.css — lib defaults, explicit (no-op import for symmetry/docs) - themes/default-light.css — neutral light palette with blue accent; off-white surface, softer elevation - themes/material-dark.css — Material Design 3 dark palette (purple primary, M3 surface tokens). Color values from m3.material.io. No @angular/material runtime dep — pure CSS custom properties. - themes/material-light.css — M3 light palette Each file declares only the tokens that differ from the lib's :host defaults — typically the 13-token color block, plus elevation softening on light themes and shape adjustments where they differ. Wired into the package via: - libs/chat/package.json `exports` map: ./themes/*.css per preset - libs/chat/ng-package.json `assets`: copies src/themes/*.css to dist/libs/chat/themes/ at build time The agent's beginRendering.styles.primaryColor continues to override --a2ui-primary per surface (host inline-style binding, highest specificity), so consumer-imported preset + agent-driven override compose cleanly. README.md gains an "A2UI surface theming" section documenting the agent-driven knobs (v1 wire format), built-in presets, and the full ~50-token vocabulary for custom themes. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/chat/README.md | 48 +++++++++++++++++++++++++ libs/chat/ng-package.json | 3 +- libs/chat/package.json | 6 +++- libs/chat/src/themes/default-dark.css | 32 +++++++++++++++++ libs/chat/src/themes/default-light.css | 36 +++++++++++++++++++ libs/chat/src/themes/material-dark.css | 38 ++++++++++++++++++++ libs/chat/src/themes/material-light.css | 45 +++++++++++++++++++++++ 7 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 libs/chat/src/themes/default-dark.css create mode 100644 libs/chat/src/themes/default-light.css create mode 100644 libs/chat/src/themes/material-dark.css create mode 100644 libs/chat/src/themes/material-light.css diff --git a/libs/chat/README.md b/libs/chat/README.md index e71f5f140..218a59c85 100644 --- a/libs/chat/README.md +++ b/libs/chat/README.md @@ -61,3 +61,51 @@ Each runtime adapter extracts citations into the `Message.citations` array: - **AG-UI** — reads from STATE_DELTA at JSON Pointer `/citations/{messageId}` The `CitationsResolverService` is provided to query citations in message-first or markdown-fallback order. + +## A2UI surface theming + +`` declares ~50 internal `--a2ui-*` tokens at `:host` with dark-theme defaults (spacing, typography, shape radius, focus ring, motion, elevation, color). Catalog components consume them via `var(--a2ui-*)`. Override at `:root` to retheme. + +### Agent-driven theming (v1 wire format) + +Agents control exactly two knobs per the canonical A2UI v1 spec, set via `beginRendering.styles`: + +- `font` — primary font family +- `primaryColor` — hex `#RRGGBB` + +Both flow through to the rendered surface as inline styles on `` (highest specificity), overriding any consumer `:root` defaults for the lifetime of that surface. + +### Built-in theme presets + +Four CSS files ship in the package, each declaring `:root` overrides for the relevant tokens: + +```css +/* In your global stylesheet */ +@import '@ngaf/chat/themes/default-dark.css'; /* lib defaults, explicit */ +@import '@ngaf/chat/themes/default-light.css'; /* neutral light, blue accent */ +@import '@ngaf/chat/themes/material-dark.css'; /* Material Design 3 dark */ +@import '@ngaf/chat/themes/material-light.css'; /* Material Design 3 light */ +``` + +Material presets map [Material Design 3 color tokens](https://m3.material.io/styles/color/the-color-system/tokens) to the `--a2ui-*` vocabulary — no `@angular/material` runtime dep, just CSS custom-property declarations. + +### Custom themes + +Override any subset of the ~50 tokens at `:root`: + +```css +:root { + --a2ui-primary: #FF6B35; /* brand orange */ + --a2ui-shape-medium: 4px; /* sharper corners */ + --a2ui-spacing-3: 16px; /* roomier layouts */ +} +``` + +The token surface: +- **Color** — `--a2ui-primary`, `--a2ui-on-primary`, `--a2ui-secondary`, `--a2ui-surface`, `--a2ui-on-surface`, `--a2ui-surface-variant`, `--a2ui-on-surface-variant`, `--a2ui-outline`, `--a2ui-outline-variant`, `--a2ui-error`, `--a2ui-on-error`, `--a2ui-scrim` +- **Spacing** — `--a2ui-spacing-1` (4px) through `--a2ui-spacing-7` (40px) +- **Typography** — `--a2ui-typography-{h1..h5,body,caption,label}-{size,weight,line-height}` +- **Shape** — `--a2ui-shape-{extra-small,small,medium,large,extra-large}` +- **Focus ring** — `--a2ui-focus-ring-color`, `--a2ui-focus-ring-width` +- **Motion** — `--a2ui-motion-duration-{short,medium,long}`, `--a2ui-motion-easing-{standard,emphasized}` +- **Elevation** — `--a2ui-elevation-{0,1,2,3,4,5}` (box-shadow tokens) diff --git a/libs/chat/ng-package.json b/libs/chat/ng-package.json index a34182b8d..8b492da8a 100644 --- a/libs/chat/ng-package.json +++ b/libs/chat/ng-package.json @@ -6,6 +6,7 @@ }, "allowedNonPeerDependencies": ["@cacheplane/partial-json", "@cacheplane/partial-markdown"], "assets": [ - { "input": "src/lib/styles", "glob": "chat.css", "output": "." } + { "input": "src/lib/styles", "glob": "chat.css", "output": "." }, + { "input": "src/themes", "glob": "*.css", "output": "themes" } ] } diff --git a/libs/chat/package.json b/libs/chat/package.json index aecf0b931..5f5b95694 100644 --- a/libs/chat/package.json +++ b/libs/chat/package.json @@ -10,7 +10,11 @@ "types": "./testing.d.ts", "default": "./fesm2022/ngaf-chat-testing.mjs" }, - "./chat.css": "./chat.css" + "./chat.css": "./chat.css", + "./themes/default-dark.css": "./themes/default-dark.css", + "./themes/default-light.css": "./themes/default-light.css", + "./themes/material-dark.css": "./themes/material-dark.css", + "./themes/material-light.css": "./themes/material-light.css" }, "dependencies": { "@cacheplane/partial-json": ">=0.1.1 <0.3.0", diff --git a/libs/chat/src/themes/default-dark.css b/libs/chat/src/themes/default-dark.css new file mode 100644 index 000000000..bde6c5670 --- /dev/null +++ b/libs/chat/src/themes/default-dark.css @@ -0,0 +1,32 @@ +/* + * @ngaf/chat — default dark theme + * SPDX-License-Identifier: MIT + * + * The lib's `:host` defaults on `` already declare these + * exact values — this preset is a no-op @import for symmetry/docs and + * for consumers who want their `:root` to make the choice explicit. + * + * Usage in your global stylesheet: + * @import '@ngaf/chat/themes/default-dark.css'; + * + * The agent's `beginRendering.styles.primaryColor` continues to override + * `--a2ui-primary` per surface (host inline-style binding, highest + * specificity). + */ +:root { + /* === Color (dark, blue accent) === */ + --a2ui-primary: #4f8df5; + --a2ui-on-primary: #ffffff; + --a2ui-primary-hover: #6699f7; + --a2ui-secondary: #8a92a3; + --a2ui-on-secondary: #ffffff; + --a2ui-surface: #1a1d23; + --a2ui-on-surface: #ffffff; + --a2ui-surface-variant: rgba(255, 255, 255, 0.05); + --a2ui-on-surface-variant: rgba(255, 255, 255, 0.7); + --a2ui-outline: rgba(255, 255, 255, 0.1); + --a2ui-outline-variant: rgba(255, 255, 255, 0.05); + --a2ui-error: #f5524f; + --a2ui-on-error: #ffffff; + --a2ui-scrim: rgba(0, 0, 0, 0.6); +} diff --git a/libs/chat/src/themes/default-light.css b/libs/chat/src/themes/default-light.css new file mode 100644 index 000000000..2056de9ad --- /dev/null +++ b/libs/chat/src/themes/default-light.css @@ -0,0 +1,36 @@ +/* + * @ngaf/chat — default light theme + * SPDX-License-Identifier: MIT + * + * Neutral light palette with blue accent. Off-white surface (#FAFAFA) + * for friendlier contrast vs. pure white. Same blue accent (#4f8df5) + * as default-dark — the only theming decision is foreground/background + * inversion + outline lightening + softer elevation. + * + * Usage in your global stylesheet: + * @import '@ngaf/chat/themes/default-light.css'; + */ +:root { + /* === Color (light, blue accent) === */ + --a2ui-primary: #4f8df5; + --a2ui-on-primary: #ffffff; + --a2ui-primary-hover: #3b7be3; + --a2ui-secondary: #5b6275; + --a2ui-on-secondary: #ffffff; + --a2ui-surface: #fafafa; + --a2ui-on-surface: #1a1d23; + --a2ui-surface-variant: rgba(0, 0, 0, 0.04); + --a2ui-on-surface-variant: rgba(0, 0, 0, 0.6); + --a2ui-outline: rgba(0, 0, 0, 0.12); + --a2ui-outline-variant: rgba(0, 0, 0, 0.06); + --a2ui-error: #d33533; + --a2ui-on-error: #ffffff; + --a2ui-scrim: rgba(0, 0, 0, 0.4); + + /* === Elevation (softer shadows on light backgrounds) === */ + --a2ui-elevation-1: 0 1px 2px rgba(0, 0, 0, 0.08); + --a2ui-elevation-2: 0 2px 4px rgba(0, 0, 0, 0.10); + --a2ui-elevation-3: 0 4px 8px rgba(0, 0, 0, 0.12); + --a2ui-elevation-4: 0 8px 16px rgba(0, 0, 0, 0.14); + --a2ui-elevation-5: 0 16px 32px rgba(0, 0, 0, 0.16); +} diff --git a/libs/chat/src/themes/material-dark.css b/libs/chat/src/themes/material-dark.css new file mode 100644 index 000000000..9304a962d --- /dev/null +++ b/libs/chat/src/themes/material-dark.css @@ -0,0 +1,38 @@ +/* + * @ngaf/chat — Material Design 3 dark theme + * SPDX-License-Identifier: MIT + * + * Maps Google's Material Design 3 dark color tokens (m3.material.io) + * into the @ngaf/chat token vocabulary. No `@angular/material` runtime + * dependency — pure CSS custom-property declarations. + * + * Color values from the Material Design 3 spec + * (https://m3.material.io/styles/color/the-color-system/tokens). + * + * Usage in your global stylesheet: + * @import '@ngaf/chat/themes/material-dark.css'; + */ +:root { + /* === Color (M3 dark) === */ + --a2ui-primary: #D0BCFF; + --a2ui-on-primary: #381E72; + --a2ui-primary-hover: #B69DF8; + --a2ui-secondary: #CCC2DC; + --a2ui-on-secondary: #332D41; + --a2ui-surface: #1C1B1F; + --a2ui-on-surface: #E6E1E5; + --a2ui-surface-variant: #49454F; + --a2ui-on-surface-variant: #CAC4D0; + --a2ui-outline: #938F99; + --a2ui-outline-variant: #49454F; + --a2ui-error: #F2B8B5; + --a2ui-on-error: #601410; + --a2ui-scrim: rgba(0, 0, 0, 0.6); + + /* === Shape (M3 favors slightly larger radii) === */ + --a2ui-shape-extra-small: 4px; + --a2ui-shape-small: 8px; + --a2ui-shape-medium: 12px; + --a2ui-shape-large: 16px; + --a2ui-shape-extra-large: 28px; +} diff --git a/libs/chat/src/themes/material-light.css b/libs/chat/src/themes/material-light.css new file mode 100644 index 000000000..4ec4e6a83 --- /dev/null +++ b/libs/chat/src/themes/material-light.css @@ -0,0 +1,45 @@ +/* + * @ngaf/chat — Material Design 3 light theme + * SPDX-License-Identifier: MIT + * + * Maps Google's Material Design 3 light color tokens (m3.material.io) + * into the @ngaf/chat token vocabulary. No `@angular/material` runtime + * dependency — pure CSS custom-property declarations. + * + * Color values from the Material Design 3 spec + * (https://m3.material.io/styles/color/the-color-system/tokens). + * + * Usage in your global stylesheet: + * @import '@ngaf/chat/themes/material-light.css'; + */ +:root { + /* === Color (M3 light) === */ + --a2ui-primary: #6750A4; + --a2ui-on-primary: #FFFFFF; + --a2ui-primary-hover: #533F8E; + --a2ui-secondary: #625B71; + --a2ui-on-secondary: #FFFFFF; + --a2ui-surface: #FFFBFE; + --a2ui-on-surface: #1C1B1F; + --a2ui-surface-variant: #E7E0EC; + --a2ui-on-surface-variant: #49454F; + --a2ui-outline: #79747E; + --a2ui-outline-variant: #CAC4D0; + --a2ui-error: #B3261E; + --a2ui-on-error: #FFFFFF; + --a2ui-scrim: rgba(0, 0, 0, 0.4); + + /* === Shape (M3 favors slightly larger radii) === */ + --a2ui-shape-extra-small: 4px; + --a2ui-shape-small: 8px; + --a2ui-shape-medium: 12px; + --a2ui-shape-large: 16px; + --a2ui-shape-extra-large: 28px; + + /* === Elevation (softer shadows on light backgrounds) === */ + --a2ui-elevation-1: 0 1px 2px rgba(0, 0, 0, 0.08); + --a2ui-elevation-2: 0 2px 4px rgba(0, 0, 0, 0.10); + --a2ui-elevation-3: 0 4px 8px rgba(0, 0, 0, 0.12); + --a2ui-elevation-4: 0 8px 16px rgba(0, 0, 0, 0.14); + --a2ui-elevation-5: 0 16px 32px rgba(0, 0, 0, 0.16); +}