diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 80d2ca41..cb6f0d7c 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -3411,6 +3411,28 @@ } ] }, + { + "name": "ChatSidenavScrimComponent", + "kind": "class", + "description": "Backdrop scrim for chat-sidenav's drawer mode, rendered as a sibling of\n so its z-index sits cleanly between the page content\nand the drawer host (escapes the drawer host's stacking context).\n\nUsage:\n \n ", + "params": [], + "examples": [], + "properties": [ + { + "name": "close", + "type": "OutputEmitterRef", + "description": "Fires when the user clicks the backdrop.", + "optional": false + }, + { + "name": "open", + "type": "InputSignal", + "description": "When true, render the backdrop button covering the viewport.", + "optional": false + } + ], + "methods": [] + }, { "name": "ChatStreamingMdComponent", "kind": "class", diff --git a/docs/superpowers/plans/2026-05-19-mobile-sidenav-scrim.md b/docs/superpowers/plans/2026-05-19-mobile-sidenav-scrim.md new file mode 100644 index 00000000..bafe39b8 --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-mobile-sidenav-scrim.md @@ -0,0 +1,597 @@ +# Mobile Sidenav Scrim + Responsive Polish Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move the chat-sidenav drawer scrim into its own primitive (so it escapes the host's stacking context and stops intercepting drawer interaction), add a right-edge drawer shadow, move the demo's hamburger into the toolbar's flex row, and promote z-index hardcodes to documented CSS variables. + +**Architecture:** New `` primitive in `@ngaf/chat` is rendered as a sibling of `` by the consumer (demo wires it). `chat-sidenav` stops rendering its internal scrim. Three z-index layer tokens land in chat-tokens.ts and replace the hardcodes across the lib. + +**Tech Stack:** Angular 20+ standalone components, vitest, TypeScript-template-literal CSS, no new dependencies (no CDK). + +**Branch:** `claude/mobile-sidenav-scrim` (off `origin/main`; spec committed at `8177da3d`). + +**Spec:** `docs/superpowers/specs/2026-05-19-mobile-sidenav-scrim-design.md` + +--- + +## File map + +| File | Change | +|---|---| +| `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.ts` | **Create** — new component | +| `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts` | **Create** — unit tests | +| `libs/chat/src/public-api.ts` | Add export | +| `libs/chat/src/lib/styles/chat-tokens.ts` | Add `LAYER_TOKENS` block with 3 z-index vars; concat into `CHAT_HOST_TOKENS` | +| `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts` | Drop the scrim template block | +| `libs/chat/src/lib/styles/chat-sidenav.styles.ts` | Drop `.chat-sidenav__scrim` rule; add drawer box-shadow; use z-index token | +| `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts` | Update — no scrim assertion; new layer-token + shadow assertion | +| `libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts` | Replace `z-index: 30` with `var(--ngaf-chat-z-overlay-content, 30)` (2 places) | +| `libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts` | Replace `z-index: 30` with the same var | +| `examples/chat/angular/src/app/shell/demo-shell.component.ts` | Import `ChatSidenavScrimComponent` | +| `examples/chat/angular/src/app/shell/demo-shell.component.html` | Render `` sibling; move hamburger INTO `.demo-shell__toolbar` | +| `examples/chat/angular/src/app/shell/demo-shell.component.css` | Replace fixed-position hamburger styles with flex-child styles | +| `libs/{a2ui,ag-ui,chat,langgraph,licensing,render,telemetry}/package.json` | 0.0.43 → 0.0.44 | + +--- + +## Task 1: Add `LAYER_TOKENS` z-index CSS variables in chat-tokens.ts + +**Files:** +- Modify: `libs/chat/src/lib/styles/chat-tokens.ts` + +- [ ] **Step 1: Add a new LAYER_TOKENS block** + +In `libs/chat/src/lib/styles/chat-tokens.ts`, find the existing `SPACING_TOKENS` constant (around line 108). Add a new constant immediately after it, BEFORE `EDGE_CLAIM_TOKENS`: + +```ts +const LAYER_TOKENS = ` + /* Z-index layers — documented for consumers + future primitives. + * Default values listed; overridable per-app via :root or :host. */ + --ngaf-chat-z-overlay-content: 30; /* chat-sidebar panel, chat-popup window */ + --ngaf-chat-z-drawer-scrim: 1000; /* chat-sidenav-scrim backdrop */ + --ngaf-chat-z-drawer: 1001; /* chat-sidenav drawer mode host */ +`; +``` + +- [ ] **Step 2: Include LAYER_TOKENS in `ROOT_TOKEN_STYLES`** + +In the same file, find the `ROOT_TOKEN_STYLES` export (around line 319). The current `:root` block concatenates color/geometry/typography/spacing/edge-claim tokens: + +```ts +export const ROOT_TOKEN_STYLES = ` + ... + :root { + ${LIGHT_TOKENS} + ${GEOMETRY_TOKENS} + ${TYPOGRAPHY_TOKENS} + ${SPACING_TOKENS} + ${EDGE_CLAIM_TOKENS} + } +``` + +Add `${LAYER_TOKENS}` to that same `:root` block, placed alphabetically/logically between `${SPACING_TOKENS}` and `${EDGE_CLAIM_TOKENS}`: + +```ts + :root { + ${LIGHT_TOKENS} + ${GEOMETRY_TOKENS} + ${TYPOGRAPHY_TOKENS} + ${SPACING_TOKENS} + ${LAYER_TOKENS} + ${EDGE_CLAIM_TOKENS} + } +``` + +- [ ] **Step 3: Run the chat lib tests** + +Run: `pnpm nx test chat` +Expected: green (no behavior change yet). + +- [ ] **Step 4: Commit** + +```bash +git add libs/chat/src/lib/styles/chat-tokens.ts +git commit -m "feat(chat): add z-index layer tokens (overlay-content, drawer-scrim, drawer)" +``` + +--- + +## Task 2: Create the `chat-sidenav-scrim` primitive + +**Files:** +- Create: `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.ts` +- Create: `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts` + +- [ ] **Step 1: Write the failing tests first** + +Create `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts`: + +```ts +// SPDX-License-Identifier: MIT +import { describe, it, expect, beforeEach } from 'vitest'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChatSidenavScrimComponent } from './chat-sidenav-scrim.component'; + +describe('ChatSidenavScrimComponent', () => { + let fx: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ChatSidenavScrimComponent] }); + fx = TestBed.createComponent(ChatSidenavScrimComponent); + }); + + it('renders nothing when [open] is false (default)', () => { + fx.detectChanges(); + expect(fx.nativeElement.querySelector('button')).toBeNull(); + }); + + it('renders the scrim button when [open] is true', () => { + fx.componentRef.setInput('open', true); + fx.detectChanges(); + const btn = fx.nativeElement.querySelector('button.chat-sidenav-scrim__button') as HTMLButtonElement; + expect(btn).toBeTruthy(); + expect(btn.getAttribute('aria-label')).toBe('Close conversations'); + }); + + it('emits (close) on click', () => { + fx.componentRef.setInput('open', true); + fx.detectChanges(); + let closed = false; + fx.componentInstance.close.subscribe(() => { closed = true; }); + const btn = fx.nativeElement.querySelector('button.chat-sidenav-scrim__button') as HTMLButtonElement; + btn.click(); + expect(closed).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run the spec to verify it fails** + +Run: `pnpm nx test chat -- --runTestsByPath libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts` +Expected: FAIL — component doesn't exist. + +- [ ] **Step 3: Create the component** + +Create `libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.ts`: + +```ts +// libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.ts +// SPDX-License-Identifier: MIT +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +/** + * Backdrop scrim for chat-sidenav's drawer mode, rendered as a sibling of + * so its z-index sits cleanly between the page content + * and the drawer host (escapes the drawer host's stacking context). + * + * Usage: + * + * + */ +@Component({ + selector: 'chat-sidenav-scrim', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (open()) { + + } + `, + styles: [ + ` + :host { display: contents; } + .chat-sidenav-scrim__button { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: var(--ngaf-chat-z-drawer-scrim, 1000); + border: 0; + padding: 0; + cursor: pointer; + } + `, + ], +}) +export class ChatSidenavScrimComponent { + /** When true, render the backdrop button covering the viewport. */ + readonly open = input(false); + /** Fires when the user clicks the backdrop. */ + readonly close = output(); +} +``` + +- [ ] **Step 4: Run the spec to verify it passes** + +Run: `pnpm nx test chat -- --runTestsByPath libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts` +Expected: all 3 tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.ts libs/chat/src/lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component.spec.ts +git commit -m "feat(chat): add chat-sidenav-scrim primitive" +``` + +--- + +## Task 3: Export the new primitive from the public API + +**Files:** +- Modify: `libs/chat/src/public-api.ts` + +- [ ] **Step 1: Add the export** + +In `libs/chat/src/public-api.ts`, find the existing exports for other chat-sidenav-related symbols (e.g. `ChatSidenavComponent`). Add a sibling export: + +```ts +export { ChatSidenavScrimComponent } from './lib/primitives/chat-sidenav-scrim/chat-sidenav-scrim.component'; +``` + +Place it alphabetically near `ChatSidenavComponent`. + +- [ ] **Step 2: Run tests** + +Run: `pnpm nx test chat` +Expected: green. + +- [ ] **Step 3: Commit** + +```bash +git add libs/chat/src/public-api.ts +git commit -m "feat(chat): export ChatSidenavScrimComponent from public API" +``` + +--- + +## Task 4: Drop the internal scrim from `chat-sidenav` + add drawer shadow + use z-index token + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts` +- Modify: `libs/chat/src/lib/styles/chat-sidenav.styles.ts` +- Modify: `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts` + +- [ ] **Step 1: Write the failing spec assertions** + +In `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts`, find any existing test that asserts the `.chat-sidenav__scrim` element renders (search for `chat-sidenav__scrim`). REPLACE that test with one that asserts the scrim is GONE: + +```ts +it('does NOT render an internal .chat-sidenav__scrim — scrim is owned by chat-sidenav-scrim primitive', () => { + const fx = TestBed.createComponent(ChatSidenavComponent); + fx.componentRef.setInput('mode', 'drawer'); + fx.componentRef.setInput('open', true); + fx.detectChanges(); + expect(fx.nativeElement.querySelector('.chat-sidenav__scrim')).toBeNull(); +}); +``` + +Add a styles-string assertion to the spec (or to `chat-sidenav.styles.spec.ts` if that file exists in the project). Pattern from prior PRs: + +```ts +import { CHAT_SIDENAV_STYLES } from '../../styles/chat-sidenav.styles'; + +describe('CHAT_SIDENAV_STYLES — drawer elevation + z-index token', () => { + const normalized = CHAT_SIDENAV_STYLES.replace(/\s+/g, ' '); + it('uses the --ngaf-chat-z-drawer token for the drawer host z-index', () => { + expect(normalized).toMatch( + /:host\(\[data-mode="drawer"\]\)\s*\{[^}]*z-index:\s*var\(--ngaf-chat-z-drawer,\s*1001\)\s*;/, + ); + }); + it('applies a right-edge box-shadow when the drawer is open', () => { + expect(normalized).toMatch( + /:host\(\[data-mode="drawer"\]\[data-open="true"\]\)\s*\{[^}]*box-shadow:\s*8px\s+0\s+32px\s+rgba\(0,\s*0,\s*0,\s*0\.18\)\s*;/, + ); + }); + it('no longer declares the .chat-sidenav__scrim selector', () => { + expect(normalized).not.toMatch(/\.chat-sidenav__scrim\s*\{/); + }); +}); +``` + +(If `chat-sidenav.styles.spec.ts` doesn't exist, create it with the imports above + this describe block.) + +- [ ] **Step 2: Run specs to see the failures** + +Run: `pnpm nx test chat -- --runTestsByPath libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts` +Expected: FAIL on the "no internal scrim" test. + +Run: `pnpm nx test chat -- --runTestsByPath libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts` (if you created it) +Expected: FAIL on the three new assertions. + +- [ ] **Step 3: Update the chat-sidenav template — drop the scrim** + +In `libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts`, find the template block (around line 60). Locate: + +```html +@if (mode() === 'drawer' && open()) { + +} +