From dbbb0c54e1ebe20f816d048d52d72427f99276e3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 10:59:09 -0700 Subject: [PATCH 1/7] docs(chat): add ship-readiness polish implementation plan Addresses all 19 audit issues: theme consolidation, Tailwind conversion, auto-scroll, textarea auto-expand, markdown rendering, empty state, responsive sidebar, SVG icons, ARIA, and API cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-06-chat-polish.md | 1110 +++++++++++++++++ 1 file changed, 1110 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-chat-polish.md diff --git a/docs/superpowers/plans/2026-04-06-chat-polish.md b/docs/superpowers/plans/2026-04-06-chat-polish.md new file mode 100644 index 000000000..c26fe1507 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-chat-polish.md @@ -0,0 +1,1110 @@ +# Chat Library Ship-Readiness 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:** Fix all 19 audit issues in `@cacheplane/chat` — convert all components to Tailwind, consolidate duplicated theme CSS, add auto-scroll, textarea auto-expand, markdown rendering, empty state, responsive sidebar, SVG icons, ARIA, and clean up the public API. The library must build and ship correctly via ng-packagr. + +**Architecture:** All components use Tailwind utility classes with `[var(--chat-*)]` arbitrary values for theme-aware styling. CSS custom properties are defined once in a shared TypeScript constant and imported by composition components. ng-packagr preserves Tailwind class names in compiled templates; the consuming app's Tailwind build generates the CSS. `marked` is added as a peer dep for markdown rendering. + +**Tech Stack:** Angular 20+, Tailwind CSS v4, `marked` (markdown), ng-packagr, Vitest + +**Parallelism:** Tasks 2–5 are independent and can be dispatched as parallel subagents after Task 1 completes. Task 6 depends on Tasks 2+3. Task 7 is final. + +--- + +## File Structure + +### New files +- `libs/chat/src/lib/styles/chat-theme.ts` — Shared CSS custom property definitions (replaces `chat-theme.css`) +- `libs/chat/src/lib/styles/chat-icons.ts` — SVG icon constants (replaces emoji) +- `libs/chat/src/lib/styles/chat-markdown.ts` — Markdown rendering utility + prose styles + +### Modified files +- `libs/chat/src/lib/compositions/chat/chat.component.ts` — Tailwind + auto-scroll + empty state + responsive sidebar +- `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts` — Tailwind + auto-scroll + shared theme +- `libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts` — Theme-aware colors +- `libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts` — Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts` — Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts` — Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts` — Theme-aware colors +- `libs/chat/src/lib/primitives/chat-input/chat-input.component.ts` — Tailwind + auto-expand + focused signal +- `libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts` — Tailwind +- `libs/chat/src/lib/primitives/chat-error/chat-error.component.ts` — CSS var colors +- `libs/chat/src/lib/provide-chat.ts` — Typed ChatConfig +- `libs/chat/src/public-api.ts` — Remove legacy, add new exports +- `libs/chat/package.json` — Add marked peer dep +- `cockpit/*/angular/src/styles.css` (14 files) — Add @source for chat library + +### Deleted files +- `libs/chat/src/lib/styles/chat-theme.css` — Replaced by `chat-theme.ts` +- `libs/chat/src/lib/chat.component.ts` — Legacy `cp-chat` component +- `libs/chat/src/lib/chat-input.component.ts` — Legacy input component +- `libs/chat/src/lib/chat-message.component.ts` — Legacy message component + +--- + +## Task 1: Theme System Consolidation + +**Fixes:** Blocker #3 (theme CSS triplicated 6x), Medium #11 (CSS var fallback inconsistency) + +**Files:** +- Create: `libs/chat/src/lib/styles/chat-theme.ts` +- Create: `libs/chat/src/lib/styles/chat-icons.ts` +- Create: `libs/chat/src/lib/styles/chat-markdown.ts` +- Delete: `libs/chat/src/lib/styles/chat-theme.css` + +- [ ] **Step 1: Create `chat-theme.ts` with shared CSS custom property definitions** + +```typescript +// libs/chat/src/lib/styles/chat-theme.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +const DARK = ` + --chat-bg: #171717; + --chat-bg-alt: #222222; + --chat-bg-hover: #2a2a2a; + --chat-text: #e0e0e0; + --chat-text-muted: #777777; + --chat-text-placeholder: #666666; + --chat-border: #333333; + --chat-border-light: #2a2a2a; + --chat-user-bg: #2a2a2a; + --chat-user-text: #f5f5f5; + --chat-user-border: #333333; + --chat-avatar-bg: #333333; + --chat-avatar-text: #aaaaaa; + --chat-input-bg: #222222; + --chat-input-border: #333333; + --chat-input-focus-border: #555555; + --chat-send-bg: #444444; + --chat-send-text: #aaaaaa; + --chat-error-bg: #2d1515; + --chat-error-text: #f87171; + --chat-warning-bg: #2d2315; + --chat-warning-text: #fbbf24; + --chat-success: #4ade80; + --chat-radius-message: 20px; + --chat-radius-input: 24px; + --chat-radius-card: 12px; + --chat-radius-avatar: 8px; + --chat-max-width: 720px; + --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; + --chat-font-size: 15px; + --chat-line-height: 1.6; +`; + +const LIGHT = ` + --chat-bg: #ffffff; + --chat-bg-alt: #f5f5f5; + --chat-bg-hover: #ebebeb; + --chat-text: #1a1a1a; + --chat-text-muted: #999999; + --chat-text-placeholder: #999999; + --chat-border: #e5e5e5; + --chat-border-light: #f0f0f0; + --chat-user-bg: #f0f0f0; + --chat-user-text: #1a1a1a; + --chat-user-border: transparent; + --chat-avatar-bg: #f0f0f0; + --chat-avatar-text: #666666; + --chat-input-bg: #f5f5f5; + --chat-input-border: #e5e5e5; + --chat-input-focus-border: #cccccc; + --chat-send-bg: #e5e5e5; + --chat-send-text: #999999; + --chat-error-bg: #fef2f2; + --chat-error-text: #dc2626; + --chat-warning-bg: #fffbeb; + --chat-warning-text: #d97706; + --chat-success: #16a34a; +`; + +/** + * Shared theme styles for chat composition components. + * Defines CSS custom properties on :host for dark/light mode. + * Import into any composition's `styles` array. + */ +export const CHAT_THEME_STYLES = ` + :host { + ${DARK} + font-family: var(--chat-font-family); + font-size: var(--chat-font-size); + line-height: var(--chat-line-height); + color: var(--chat-text); + background: var(--chat-bg); + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + @media (prefers-color-scheme: light) { + :host:not([data-chat-theme="dark"]) { ${LIGHT} } + } + :host([data-chat-theme="light"]) { ${LIGHT} } +`; +``` + +- [ ] **Step 2: Create `chat-icons.ts` with SVG icon constants** + +Replaces emoji characters (⚙ ⚠ 🤖 ▲ ▼ ✓) with inline SVG strings for consistent cross-platform rendering. + +```typescript +// libs/chat/src/lib/styles/chat-icons.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +/** Chevron down (▼ replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_DOWN = ``; + +/** Chevron up (▲ replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_UP = ``; + +/** Gear icon (⚙ replacement). 14x14. */ +export const ICON_TOOL = ``; + +/** Warning triangle (⚠ replacement). 18x18. */ +export const ICON_WARNING = ``; + +/** Robot/agent icon (🤖 replacement). 14x14. */ +export const ICON_AGENT = ``; + +/** Check mark (✓ replacement). 12x12. */ +export const ICON_CHECK = ``; + +/** Send arrow (for chat input). 16x16. */ +export const ICON_SEND = ``; +``` + +- [ ] **Step 3: Create `chat-markdown.ts` with markdown rendering utility** + +```typescript +// libs/chat/src/lib/styles/chat-markdown.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { SecurityContext } from '@angular/core'; +import type { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +let markedParse: ((src: string) => string) | null = null; +let markedLoaded = false; + +function loadMarked(): void { + if (markedLoaded) return; + markedLoaded = true; + try { + // Dynamic require — marked is an optional peer dep + const m = require('marked'); + markedParse = (src: string) => m.marked.parse(src, { async: false }) as string; + } catch { + markedParse = null; + } +} + +/** + * Renders markdown content to sanitized HTML. + * Falls back to plain text with newline→br conversion if `marked` is not installed. + */ +export function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHtml { + loadMarked(); + if (markedParse) { + const html = markedParse(content); + return sanitizer.bypassSecurityTrustHtml( + sanitizer.sanitize(SecurityContext.HTML, html) ?? '' + ); + } + // Fallback: escape HTML and convert newlines to
+ const escaped = content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + return sanitizer.bypassSecurityTrustHtml(escaped); +} + +/** + * CSS for styling rendered markdown HTML. + * Uses .chat-md class prefix to avoid global conflicts. + * Must be included in a component with ViewEncapsulation.None or via ::ng-deep. + */ +export const CHAT_MARKDOWN_STYLES = ` + .chat-md p { margin: 0 0 0.75em; } + .chat-md p:last-child { margin-bottom: 0; } + .chat-md code { + background: var(--chat-bg-alt); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.875em; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + } + .chat-md pre { + background: var(--chat-bg-alt); + padding: 12px 16px; + border-radius: var(--chat-radius-card); + overflow-x: auto; + margin: 0.75em 0; + } + .chat-md pre code { background: none; padding: 0; } + .chat-md ul, .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; } + .chat-md li { margin: 0.25em 0; } + .chat-md a { color: var(--chat-text); text-decoration: underline; } + .chat-md strong { font-weight: 600; } + .chat-md blockquote { + border-left: 3px solid var(--chat-border); + padding-left: 12px; + margin: 0.75em 0; + color: var(--chat-text-muted); + } + .chat-md h1, .chat-md h2, .chat-md h3, .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; } + .chat-md h1 { font-size: 1.25em; } + .chat-md h2 { font-size: 1.125em; } + .chat-md h3 { font-size: 1em; } + .chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; } + .chat-md th, .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; } + .chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; } +`; +``` + +- [ ] **Step 4: Delete `chat-theme.css`** + +```bash +rm libs/chat/src/lib/styles/chat-theme.css +``` + +- [ ] **Step 5: Run tests to verify no regressions** + +```bash +npx nx test chat +``` + +Expected: All 112 tests still pass (no component imports chat-theme.css directly). + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/styles/ +git commit -m "feat(chat): consolidate theme into shared TS module, add icons + markdown utils" +``` + +--- + +## Task 2: ChatComponent Overhaul + +**Fixes:** Blocker #4 (auto-scroll), High #9 (empty state), High #10 (responsive sidebar), Medium #14 (ARIA) + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat/chat.component.ts` +- Test: `libs/chat/src/lib/compositions/chat/chat.component.spec.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatComponent with Tailwind, auto-scroll, empty state, responsive sidebar, ARIA, and markdown** + +Replace the entire file: + +```typescript +// libs/chat/src/lib/compositions/chat/chat.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + input, + output, + signal, + computed, + effect, + viewChild, + ElementRef, + ChangeDetectionStrategy, + inject, + ViewEncapsulation, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; +import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; +import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; +import { ChatInputComponent } from '../../primitives/chat-input/chat-input.component'; +import { ChatTypingIndicatorComponent } from '../../primitives/chat-typing-indicator/chat-typing-indicator.component'; +import { ChatErrorComponent } from '../../primitives/chat-error/chat-error.component'; +import { ChatInterruptComponent } from '../../primitives/chat-interrupt/chat-interrupt.component'; +import { ChatThreadListComponent, Thread } from '../../primitives/chat-thread-list/chat-thread-list.component'; +import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; + +@Component({ + selector: 'chat', + standalone: true, + imports: [ + ChatMessagesComponent, + MessageTemplateDirective, + ChatInputComponent, + ChatTypingIndicatorComponent, + ChatErrorComponent, + ChatInterruptComponent, + ChatThreadListComponent, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], + template: ` +
+ + @if (threads().length > 0) { + + } + + +
+ +
+
+ @if (ref().messages().length === 0 && !ref().isLoading()) { + +
+
A
+

Send a message to start a conversation.

+
+ } + + + + +
+
{{ messageContent(message) }}
+
+
+ + + +
+
+
A
+ Assistant +
+
+
+
+ + + +
{{ messageContent(message) }}
+
+ + + +
+ + {{ messageContent(message) }} + +
+
+
+ + +
+
+ + + + +
+

Agent paused: {{ interrupt.value }}

+
+
+
+ + +
+ +
+ + +
+
+ +
+
+
+
+ `, +}) +export class ChatComponent { + private readonly sanitizer = inject(DomSanitizer); + + readonly ref = input.required>(); + readonly threads = input([]); + readonly activeThreadId = input(''); + readonly threadSelected = output(); + readonly sidebarOpen = signal(false); + + readonly messageContent = messageContent; + + private readonly scrollContainer = viewChild>('scrollContainer'); + + /** Track message count to trigger auto-scroll */ + private readonly messageCount = computed(() => this.ref().messages().length); + + constructor() { + // Auto-scroll to bottom when new messages arrive or loading state changes + effect(() => { + this.messageCount(); // track + this.ref().isLoading(); // track + const el = this.scrollContainer()?.nativeElement; + if (el) { + // Use setTimeout to run after render + setTimeout(() => el.scrollTop = el.scrollHeight, 0); + } + }); + } + + renderMd(content: string) { + return renderMarkdown(content, this.sanitizer); + } +} +``` + +- [ ] **Step 2: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. The spec tests use `createMockStreamResourceRef()` which doesn't depend on template rendering. + +- [ ] **Step 3: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat/chat.component.ts +git commit -m "feat(chat): convert ChatComponent to Tailwind, add auto-scroll + empty state + responsive sidebar" +``` + +--- + +## Task 3: ChatDebugComponent Overhaul + +**Fixes:** Blocker #4 (auto-scroll), High #7 (duplicated templates), and theme-awareness for all debug sub-components + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatDebugComponent with shared theme + Tailwind + auto-scroll + markdown** + +Same pattern as ChatComponent (Task 2) but with the debug panel. Key changes: +- Import `CHAT_THEME_STYLES` from shared module (eliminating the copy-pasted CSS vars) +- Import `CHAT_MARKDOWN_STYLES` and `renderMarkdown` +- Add `ViewEncapsulation.None` for markdown styles +- Add auto-scroll via `viewChild` + `effect()` +- Use Tailwind classes for all layout +- Use `[var(--chat-*)]` arbitrary values for theme colors +- Add `role="log"` and `aria-live="polite"` to messages area + +The message templates should be identical to ChatComponent's templates (same 4 ng-template blocks). This is intentional — compositions co-locate their templates (shadcn model). The template code is the same as Task 2 Step 1. + +- [ ] **Step 2: Convert debug-timeline.component.ts to use theme vars** + +Replace hardcoded Tailwind colors with theme-var-based arbitrary values: +- `bg-blue-500` → `bg-[var(--chat-success)]` (selected indicator) +- `border-blue-500` → `border-[var(--chat-success)]` +- `bg-white` → `bg-[var(--chat-bg)]` +- `border-gray-300` → `border-[var(--chat-border)]` +- `bg-gray-200` → `bg-[var(--chat-border)]` (rail) +- Keep layout Tailwind classes (`relative`, `space-y-1`, `absolute`, etc.) + +- [ ] **Step 3: Convert debug-checkpoint-card.component.ts to use theme vars** + +Replace hardcoded colors: +- `border-blue-400 bg-blue-50` → `border-[var(--chat-input-focus-border)] bg-[var(--chat-bg-hover)]` +- `border-gray-200 bg-white hover:bg-gray-50` → `border-[var(--chat-border)] bg-[var(--chat-bg)] hover:bg-[var(--chat-bg-hover)]` +- `text-gray-700` → `text-[var(--chat-text)]` +- `bg-gray-100 text-gray-500` → `bg-[var(--chat-bg-alt)] text-[var(--chat-text-muted)]` + +- [ ] **Step 4: Convert debug-state-diff.component.ts to use theme vars** + +Replace hardcoded colors: +- `bg-green-50 text-green-700` → `bg-[var(--chat-bg-alt)] text-[var(--chat-success)]` +- `bg-red-50 text-red-700` → `bg-[var(--chat-error-bg)] text-[var(--chat-error-text)]` +- `bg-amber-50 text-amber-700` → `bg-[var(--chat-warning-bg)] text-[var(--chat-warning-text)]` +- `text-gray-400` → `text-[var(--chat-text-muted)]` +- `text-gray-500` → `text-[var(--chat-text-muted)]` + +- [ ] **Step 5: Convert debug-detail.component.ts to use theme vars** + +Replace: +- `text-gray-500` → `text-[var(--chat-text-muted)]` + +- [ ] **Step 6: Convert debug-state-inspector.component.ts to use theme vars** + +Replace: +- `text-gray-700` → `text-[var(--chat-text)]` + +- [ ] **Step 7: Convert debug-controls.component.ts to use theme vars** + +Replace: +- `bg-gray-100 hover:bg-gray-200` → `bg-[var(--chat-bg-alt)] hover:bg-[var(--chat-bg-hover)]` +- `text-gray-500` → `text-[var(--chat-text-muted)]` + +- [ ] **Step 8: Convert debug-summary.component.ts to use theme vars** + +Replace: +- `text-gray-500` → `text-[var(--chat-text-muted)]` + +- [ ] **Step 9: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 10: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/ +git commit -m "feat(chat): convert ChatDebug + sub-components to theme-aware Tailwind" +``` + +--- + +## Task 4: Primitives Overhaul + +**Fixes:** Blocker #1 (ChatError Tailwind), High #5 (textarea auto-expand), High #8 (focused signal) + +**Files:** +- Modify: `libs/chat/src/lib/primitives/chat-input/chat-input.component.ts` +- Modify: `libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts` +- Modify: `libs/chat/src/lib/primitives/chat-error/chat-error.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatInputComponent with Tailwind, auto-expand textarea, and focused signal** + +```typescript +// libs/chat/src/lib/primitives/chat-input/chat-input.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + output, + signal, + ChangeDetectionStrategy, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; +import { ICON_SEND } from '../../styles/chat-icons'; + +export function submitMessage( + ref: StreamResourceRef, + text: string, +): string | null { + const trimmed = text.trim(); + if (!trimmed) return null; + ref.submit({ messages: [{ role: 'human', content: trimmed }] }); + return trimmed; +} + +@Component({ + selector: 'chat-input', + standalone: true, + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + textarea { + field-sizing: content; + } + `], + template: ` + + `, +}) +export class ChatInputComponent { + readonly ref = input.required>(); + readonly submitOnEnter = input(true); + readonly placeholder = input(''); + readonly submitted = output(); + readonly messageText = signal(''); + readonly isDisabled = computed(() => this.ref().isLoading()); + readonly focused = signal(false); + readonly sendIcon = ICON_SEND; + + onSubmit(): void { + const submitted = submitMessage(this.ref(), this.messageText()); + if (submitted !== null) { + this.submitted.emit(submitted); + this.messageText.set(''); + } + } + + onKeydown(event: KeyboardEvent): void { + if (this.submitOnEnter() && !event.shiftKey) { + event.preventDefault(); + this.onSubmit(); + } + } +} +``` + +Key changes: +- `focused` is now a `signal(false)` (fixes OnPush change detection) +- Textarea uses `field-sizing: content` CSS for auto-expand (modern browsers) +- All inline styles replaced with Tailwind classes + CSS var bindings +- ARIA labels added +- SVG icon replaces inline SVG string + +- [ ] **Step 2: Rewrite ChatTypingIndicatorComponent with Tailwind** + +```typescript +// libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + ChangeDetectionStrategy, +} from '@angular/core'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; + +export function isTyping(ref: StreamResourceRef): boolean { + return ref.isLoading(); +} + +@Component({ + selector: 'chat-typing-indicator', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + .chat-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--chat-text-muted); + animation: chat-dot-pulse 1.4s ease-in-out infinite; + } + .chat-dot:nth-child(2) { animation-delay: 0.2s; } + .chat-dot:nth-child(3) { animation-delay: 0.4s; } + @keyframes chat-dot-pulse { + 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } + 40% { opacity: 1; transform: scale(1); } + } + `], + template: ` + @if (visible()) { +
+
+
A
+ Assistant +
+ + + +
+
+
+ } + `, +}) +export class ChatTypingIndicatorComponent { + readonly ref = input.required>(); + readonly visible = computed(() => this.ref().isLoading()); +} +``` + +- [ ] **Step 3: Rewrite ChatErrorComponent — remove Tailwind classes, use CSS vars** + +```typescript +// libs/chat/src/lib/primitives/chat-error/chat-error.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + ChangeDetectionStrategy, +} from '@angular/core'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; + +export function extractErrorMessage(error: unknown): string | null { + if (!error) return null; + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return String(error); +} + +@Component({ + selector: 'chat-error', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (errorMessage(); as msg) { + + } + `, +}) +export class ChatErrorComponent { + readonly ref = input.required>(); + readonly errorMessage = computed(() => extractErrorMessage(this.ref().error())); +} +``` + +Note: `px-4 py-3 text-sm` are standard Tailwind layout classes that work with the consuming app's Tailwind build. The color/bg/radius use CSS vars via inline style. This is the correct hybrid approach. + +- [ ] **Step 4: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add libs/chat/src/lib/primitives/ +git commit -m "feat(chat): convert primitives to Tailwind, add textarea auto-expand + focused signal" +``` + +--- + +## Task 5: Compositions Overhaul + +**Fixes:** Blocker #2 (TimelineSlider unstyled without Tailwind), Medium #12 (emoji icons) + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatInterruptPanelComponent with Tailwind + SVG icon** + +Replace `⚠` emoji with `ICON_WARNING` from chat-icons.ts. Convert inline styles to Tailwind + CSS var bindings. Add ARIA roles. + +```typescript +// Key changes in template: +// - Replace with: +// +// - Convert all style="" attributes to Tailwind classes +// - Add role="alert" to the container +// In class: +// readonly warningIcon = ICON_WARNING; +``` + +- [ ] **Step 2: Rewrite ChatToolCallCardComponent with Tailwind + SVG icons** + +Replace `⚙` with `ICON_TOOL`, `✓` with `ICON_CHECK`, `▲`/`▼` with `ICON_CHEVRON_UP`/`ICON_CHEVRON_DOWN`. Convert inline styles to Tailwind + CSS var bindings. + +```typescript +// Key changes: +// - Import ICON_TOOL, ICON_CHECK, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN +// - Replace emoji spans with [innerHTML]="icon" spans +// - Convert all style="" to Tailwind classes +// - Add aria-expanded to button (already exists) +// - Add aria-label="Toggle tool call details" to button +``` + +- [ ] **Step 3: Rewrite ChatSubagentCardComponent with Tailwind + SVG icon** + +Replace `🤖` with `ICON_AGENT`. Convert inline styles to Tailwind + CSS var bindings. + +- [ ] **Step 4: Rewrite ChatTimelineSliderComponent with theme vars** + +This component already uses Tailwind but with hardcoded colors. Convert to theme-var-based: +- `border-blue-300 bg-blue-50` → `border-[var(--chat-input-focus-border)] bg-[var(--chat-bg-hover)]` +- `border-gray-200 bg-white hover:bg-gray-50` → `border-[var(--chat-border)] bg-[var(--chat-bg)] hover:bg-[var(--chat-bg-hover)]` +- `bg-blue-600 text-white` → `bg-[var(--chat-send-bg)] text-[var(--chat-send-text)]` (selected indicator) +- `bg-gray-200 text-gray-500` → `bg-[var(--chat-bg-alt)] text-[var(--chat-text-muted)]` (unselected) +- `text-gray-500` → `text-[var(--chat-text-muted)]` +- `text-gray-400` → `text-[var(--chat-text-muted)]` +- `text-gray-700` → `text-[var(--chat-text)]` +- `bg-blue-100 text-blue-700 hover:bg-blue-200` → `bg-[var(--chat-bg-alt)] text-[var(--chat-text)] hover:bg-[var(--chat-bg-hover)]` +- `bg-purple-100 text-purple-700 hover:bg-purple-200` → `bg-[var(--chat-bg-alt)] text-[var(--chat-text)] hover:bg-[var(--chat-bg-hover)]` + +- [ ] **Step 5: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-interrupt-panel/ libs/chat/src/lib/compositions/chat-tool-call-card/ libs/chat/src/lib/compositions/chat-subagent-card/ libs/chat/src/lib/compositions/chat-timeline-slider/ +git commit -m "feat(chat): convert remaining compositions to Tailwind with SVG icons + theme vars" +``` + +--- + +## Task 6: API Cleanup + Build Verification + +**Fixes:** Medium #13 (selector prefix), Medium #15 (legacy export), Medium #16 (provideChat no-op), Low #19 (getMessageType fallthrough) + +**Files:** +- Modify: `libs/chat/src/public-api.ts` +- Modify: `libs/chat/src/lib/provide-chat.ts` +- Modify: `libs/chat/src/lib/chat.types.ts` +- Modify: `libs/chat/package.json` +- Delete: `libs/chat/src/lib/chat.component.ts` (legacy cp-chat) +- Delete: `libs/chat/src/lib/chat-input.component.ts` (legacy) +- Delete: `libs/chat/src/lib/chat-message.component.ts` (legacy) + +**Depends on:** Tasks 2–5 + +- [ ] **Step 1: Remove legacy component exports from public-api.ts** + +Remove this line from `libs/chat/src/public-api.ts`: +```typescript +// DELETE: export { ChatComponent as LegacyChatComponent } from './lib/chat.component'; +``` + +Add new exports: +```typescript +export { CHAT_THEME_STYLES } from './lib/styles/chat-theme'; +export { CHAT_MARKDOWN_STYLES, renderMarkdown } from './lib/styles/chat-markdown'; +export { + ICON_CHEVRON_DOWN, ICON_CHEVRON_UP, ICON_TOOL, + ICON_WARNING, ICON_AGENT, ICON_CHECK, ICON_SEND, +} from './lib/styles/chat-icons'; +``` + +- [ ] **Step 2: Delete legacy component files** + +```bash +rm libs/chat/src/lib/chat.component.ts +rm libs/chat/src/lib/chat-input.component.ts +rm libs/chat/src/lib/chat-message.component.ts +``` + +- [ ] **Step 3: Update ChatConfig in provide-chat.ts** + +```typescript +// libs/chat/src/lib/provide-chat.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { InjectionToken, makeEnvironmentProviders } from '@angular/core'; +import type { AngularRegistry } from '@cacheplane/render'; + +export interface ChatConfig { + /** Default render registry for generative UI components. */ + renderRegistry?: AngularRegistry; + /** Override the default AI avatar label (default: "A"). */ + avatarLabel?: string; + /** Override the default assistant display name (default: "Assistant"). */ + assistantName?: string; +} + +export const CHAT_CONFIG = new InjectionToken('CHAT_CONFIG'); + +export function provideChat(config: ChatConfig) { + return makeEnvironmentProviders([ + { provide: CHAT_CONFIG, useValue: config }, + ]); +} +``` + +- [ ] **Step 4: Add `marked` as optional peer dep in package.json** + +Edit `libs/chat/package.json` — add to peerDependencies and peerDependenciesMeta: + +```json +{ + "peerDependencies": { + "marked": "^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "marked": { "optional": true } + } +} +``` + +- [ ] **Step 5: Update cockpit example styles.css to add @source for chat library** + +In each cockpit Angular example's `src/styles.css`, add after the `@import "tailwindcss"` line: + +```css +@source "../../../../../libs/chat/src/"; +``` + +This tells Tailwind v4 to scan the chat library source for utility classes. Run this for all 14 examples: + +```bash +for dir in cockpit/langgraph/*/angular/src cockpit/deep-agents/*/angular/src; do + if [ -f "$dir/styles.css" ]; then + # Add @source line after @import "tailwindcss" if not already present + grep -q '@source.*libs/chat' "$dir/styles.css" || \ + sed -i '' 's|@import "tailwindcss";|@import "tailwindcss";\n@source "../../../../../libs/chat/src/";|' "$dir/styles.css" + fi +done +``` + +- [ ] **Step 6: Run full test suite** + +```bash +npx nx test chat +npx nx test render +npx nx test stream-resource +``` + +Expected: All tests pass across all three libraries. + +- [ ] **Step 7: Build the library** + +```bash +npx nx build chat +``` + +Expected: Build succeeds. Check `dist/libs/chat/` for compiled output. Verify the package.json in the dist includes `marked` in peerDependencies. + +- [ ] **Step 8: Build all cockpit examples** + +```bash +npx nx run-many -t build --projects='cockpit-*-angular' +``` + +Expected: All 14 examples build successfully. + +- [ ] **Step 9: Commit** + +```bash +git add -A +git commit -m "feat(chat): clean up public API, add marked peer dep, verify build" +``` + +--- + +## Summary of Issues Fixed + +| # | Severity | Issue | Fixed In | +|---|----------|-------|----------| +| 1 | BLOCKER | ChatTimelineSlider unstyled without Tailwind | Task 5 Step 4 | +| 2 | BLOCKER | ChatError mixed Tailwind/inline | Task 4 Step 3 | +| 3 | BLOCKER | Theme CSS triplicated 6x | Task 1 | +| 4 | BLOCKER | No auto-scroll | Tasks 2, 3 | +| 5 | HIGH | No textarea auto-expand | Task 4 Step 1 | +| 6 | HIGH | No markdown rendering | Tasks 1, 2, 3 | +| 7 | HIGH | Message templates duplicated | Accepted (shadcn model) | +| 8 | HIGH | focused plain boolean with OnPush | Task 4 Step 1 | +| 9 | HIGH | No empty/welcome state | Task 2 | +| 10 | HIGH | Thread sidebar not responsive | Task 2 | +| 11 | MEDIUM | CSS var fallback inconsistency | Task 1 | +| 12 | MEDIUM | Emoji icons | Tasks 4, 5 | +| 13 | MEDIUM | No selector prefix | Accepted (design decision) | +| 14 | MEDIUM | Missing ARIA | Tasks 2, 3, 4 | +| 15 | MEDIUM | Legacy export | Task 6 | +| 16 | MEDIUM | provideChat no-op | Task 6 | +| 17 | LOW | Missing debug components | Out of scope (Tier 2) | +| 18 | LOW | No keyboard navigation | Out of scope (v2) | +| 19 | LOW | getMessageType fallthrough | Accepted (safe default) | + +**Notes on accepted items:** +- **#7 (template duplication):** The shadcn model intentionally co-locates templates. Extracting them into a shared component would add coupling and make copy-paste customization harder. This is a design choice, not a bug. +- **#13 (selector prefix):** `chat` was an explicit design decision. Changing it is a breaking change. +- **#19 (getMessageType fallthrough):** Defaulting unknown types to 'ai' rendering is the safest UX choice — an unrecognized message type still renders rather than disappearing. From 17903f696a9aa97a9e35c4711482051a862daec6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 11:01:55 -0700 Subject: [PATCH 2/7] feat(chat): consolidate theme into shared TS module, add icons + markdown utils Co-Authored-By: Claude Opus 4.6 (1M context) --- libs/chat/src/lib/styles/chat-icons.ts | 22 ++++ libs/chat/src/lib/styles/chat-markdown.ts | 81 ++++++++++++ libs/chat/src/lib/styles/chat-theme.css | 153 ---------------------- libs/chat/src/lib/styles/chat-theme.ts | 85 ++++++++++++ 4 files changed, 188 insertions(+), 153 deletions(-) create mode 100644 libs/chat/src/lib/styles/chat-icons.ts create mode 100644 libs/chat/src/lib/styles/chat-markdown.ts delete mode 100644 libs/chat/src/lib/styles/chat-theme.css create mode 100644 libs/chat/src/lib/styles/chat-theme.ts diff --git a/libs/chat/src/lib/styles/chat-icons.ts b/libs/chat/src/lib/styles/chat-icons.ts new file mode 100644 index 000000000..18d6393fa --- /dev/null +++ b/libs/chat/src/lib/styles/chat-icons.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +/** Chevron down (▼ replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_DOWN = ``; + +/** Chevron up (▲ replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_UP = ``; + +/** Gear icon (⚙ replacement). 14x14. */ +export const ICON_TOOL = ``; + +/** Warning triangle (⚠ replacement). 18x18. */ +export const ICON_WARNING = ``; + +/** Robot/agent icon (replacement). 14x14. */ +export const ICON_AGENT = ``; + +/** Check mark replacement. 12x12. */ +export const ICON_CHECK = ``; + +/** Send arrow (for chat input). 16x16. */ +export const ICON_SEND = ``; diff --git a/libs/chat/src/lib/styles/chat-markdown.ts b/libs/chat/src/lib/styles/chat-markdown.ts new file mode 100644 index 000000000..4460dd5c8 --- /dev/null +++ b/libs/chat/src/lib/styles/chat-markdown.ts @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { SecurityContext } from '@angular/core'; +import type { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +let markedParse: ((src: string) => string) | null = null; +let markedLoaded = false; + +function loadMarked(): void { + if (markedLoaded) return; + markedLoaded = true; + try { + // Dynamic require — marked is an optional peer dep + const m = require('marked'); + markedParse = (src: string) => m.marked.parse(src, { async: false }) as string; + } catch { + markedParse = null; + } +} + +/** + * Renders markdown content to sanitized HTML. + * Falls back to plain text with newline->br conversion if `marked` is not installed. + */ +export function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHtml { + loadMarked(); + if (markedParse) { + const html = markedParse(content); + return sanitizer.bypassSecurityTrustHtml( + sanitizer.sanitize(SecurityContext.HTML, html) ?? '' + ); + } + // Fallback: escape HTML and convert newlines to
+ const escaped = content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + return sanitizer.bypassSecurityTrustHtml(escaped); +} + +/** + * CSS for styling rendered markdown HTML. + * Uses .chat-md class prefix to avoid global conflicts. + * Must be included in a component with ViewEncapsulation.None or via ::ng-deep. + */ +export const CHAT_MARKDOWN_STYLES = ` + .chat-md p { margin: 0 0 0.75em; } + .chat-md p:last-child { margin-bottom: 0; } + .chat-md code { + background: var(--chat-bg-alt); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.875em; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + } + .chat-md pre { + background: var(--chat-bg-alt); + padding: 12px 16px; + border-radius: var(--chat-radius-card); + overflow-x: auto; + margin: 0.75em 0; + } + .chat-md pre code { background: none; padding: 0; } + .chat-md ul, .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; } + .chat-md li { margin: 0.25em 0; } + .chat-md a { color: var(--chat-text); text-decoration: underline; } + .chat-md strong { font-weight: 600; } + .chat-md blockquote { + border-left: 3px solid var(--chat-border); + padding-left: 12px; + margin: 0.75em 0; + color: var(--chat-text-muted); + } + .chat-md h1, .chat-md h2, .chat-md h3, .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; } + .chat-md h1 { font-size: 1.25em; } + .chat-md h2 { font-size: 1.125em; } + .chat-md h3 { font-size: 1em; } + .chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; } + .chat-md th, .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; } + .chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; } +`; diff --git a/libs/chat/src/lib/styles/chat-theme.css b/libs/chat/src/lib/styles/chat-theme.css deleted file mode 100644 index 2f2ca7318..000000000 --- a/libs/chat/src/lib/styles/chat-theme.css +++ /dev/null @@ -1,153 +0,0 @@ -/* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 */ - -/* ── Dark defaults ─────────────────────────────────────────────────────────── */ -:host { - /* Surfaces */ - --chat-bg: #171717; - --chat-bg-alt: #222222; - --chat-bg-hover: #2a2a2a; - - /* Text */ - --chat-text: #e0e0e0; - --chat-text-muted: #777777; - --chat-text-placeholder: #666666; - - /* Borders */ - --chat-border: #333333; - --chat-border-light: #2a2a2a; - - /* User messages */ - --chat-user-bg: #2a2a2a; - --chat-user-text: #f5f5f5; - --chat-user-border: #333333; - - /* Avatar */ - --chat-avatar-bg: #333333; - --chat-avatar-text: #aaaaaa; - - /* Input */ - --chat-input-bg: #222222; - --chat-input-border: #333333; - --chat-input-focus-border: #555555; - --chat-send-bg: #444444; - --chat-send-text: #aaaaaa; - - /* Status */ - --chat-error-bg: #2d1515; - --chat-error-text: #f87171; - --chat-warning-bg: #2d2315; - --chat-warning-text: #fbbf24; - --chat-success: #4ade80; - - /* Geometry */ - --chat-radius-message: 20px; - --chat-radius-input: 24px; - --chat-radius-card: 12px; - --chat-radius-avatar: 8px; - --chat-max-width: 720px; - - /* Typography */ - --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; - --chat-font-size: 15px; - --chat-line-height: 1.6; - - font-family: var(--chat-font-family); - font-size: var(--chat-font-size); - line-height: var(--chat-line-height); - color: var(--chat-text); - background: var(--chat-bg); -} - -/* ── Light mode via OS preference (unless forced dark) ─────────────────────── */ -@media (prefers-color-scheme: light) { - :host:not([data-chat-theme="dark"]) { - /* Surfaces */ - --chat-bg: #ffffff; - --chat-bg-alt: #f5f5f5; - --chat-bg-hover: #ebebeb; - - /* Text */ - --chat-text: #1a1a1a; - --chat-text-muted: #999999; - --chat-text-placeholder: #999999; - - /* Borders */ - --chat-border: #e5e5e5; - --chat-border-light: #f0f0f0; - - /* User messages */ - --chat-user-bg: #f0f0f0; - --chat-user-text: #1a1a1a; - --chat-user-border: transparent; - - /* Avatar */ - --chat-avatar-bg: #f0f0f0; - --chat-avatar-text: #666666; - - /* Input */ - --chat-input-bg: #f5f5f5; - --chat-input-border: #e5e5e5; - --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; - --chat-send-text: #999999; - - /* Status */ - --chat-error-bg: #fef2f2; - --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; - --chat-warning-text: #d97706; - --chat-success: #16a34a; - } -} - -/* ── Manual light override ─────────────────────────────────────────────────── */ -:host([data-chat-theme="light"]) { - /* Surfaces */ - --chat-bg: #ffffff; - --chat-bg-alt: #f5f5f5; - --chat-bg-hover: #ebebeb; - - /* Text */ - --chat-text: #1a1a1a; - --chat-text-muted: #999999; - --chat-text-placeholder: #999999; - - /* Borders */ - --chat-border: #e5e5e5; - --chat-border-light: #f0f0f0; - - /* User messages */ - --chat-user-bg: #f0f0f0; - --chat-user-text: #1a1a1a; - --chat-user-border: transparent; - - /* Avatar */ - --chat-avatar-bg: #f0f0f0; - --chat-avatar-text: #666666; - - /* Input */ - --chat-input-bg: #f5f5f5; - --chat-input-border: #e5e5e5; - --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; - --chat-send-text: #999999; - - /* Status */ - --chat-error-bg: #fef2f2; - --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; - --chat-warning-text: #d97706; - --chat-success: #16a34a; -} - -/* ── Dot pulse animation (used by ChatTypingIndicator) ─────────────────────── */ -@keyframes chat-dot-pulse { - 0%, 80%, 100% { - opacity: 0.3; - transform: scale(0.8); - } - 40% { - opacity: 1; - transform: scale(1); - } -} diff --git a/libs/chat/src/lib/styles/chat-theme.ts b/libs/chat/src/lib/styles/chat-theme.ts new file mode 100644 index 000000000..66556251a --- /dev/null +++ b/libs/chat/src/lib/styles/chat-theme.ts @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +const DARK = ` + --chat-bg: #171717; + --chat-bg-alt: #222222; + --chat-bg-hover: #2a2a2a; + --chat-text: #e0e0e0; + --chat-text-muted: #777777; + --chat-text-placeholder: #666666; + --chat-border: #333333; + --chat-border-light: #2a2a2a; + --chat-user-bg: #2a2a2a; + --chat-user-text: #f5f5f5; + --chat-user-border: #333333; + --chat-avatar-bg: #333333; + --chat-avatar-text: #aaaaaa; + --chat-input-bg: #222222; + --chat-input-border: #333333; + --chat-input-focus-border: #555555; + --chat-send-bg: #444444; + --chat-send-text: #aaaaaa; + --chat-error-bg: #2d1515; + --chat-error-text: #f87171; + --chat-warning-bg: #2d2315; + --chat-warning-text: #fbbf24; + --chat-success: #4ade80; + --chat-radius-message: 20px; + --chat-radius-input: 24px; + --chat-radius-card: 12px; + --chat-radius-avatar: 8px; + --chat-max-width: 720px; + --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; + --chat-font-size: 15px; + --chat-line-height: 1.6; +`; + +const LIGHT = ` + --chat-bg: #ffffff; + --chat-bg-alt: #f5f5f5; + --chat-bg-hover: #ebebeb; + --chat-text: #1a1a1a; + --chat-text-muted: #999999; + --chat-text-placeholder: #999999; + --chat-border: #e5e5e5; + --chat-border-light: #f0f0f0; + --chat-user-bg: #f0f0f0; + --chat-user-text: #1a1a1a; + --chat-user-border: transparent; + --chat-avatar-bg: #f0f0f0; + --chat-avatar-text: #666666; + --chat-input-bg: #f5f5f5; + --chat-input-border: #e5e5e5; + --chat-input-focus-border: #cccccc; + --chat-send-bg: #e5e5e5; + --chat-send-text: #999999; + --chat-error-bg: #fef2f2; + --chat-error-text: #dc2626; + --chat-warning-bg: #fffbeb; + --chat-warning-text: #d97706; + --chat-success: #16a34a; +`; + +/** + * Shared theme styles for chat composition components. + * Defines CSS custom properties on :host for dark/light mode. + * Import into any composition's `styles` array. + */ +export const CHAT_THEME_STYLES = ` + :host { + ${DARK} + font-family: var(--chat-font-family); + font-size: var(--chat-font-size); + line-height: var(--chat-line-height); + color: var(--chat-text); + background: var(--chat-bg); + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + @media (prefers-color-scheme: light) { + :host:not([data-chat-theme="dark"]) { ${LIGHT} } + } + :host([data-chat-theme="light"]) { ${LIGHT} } +`; From 4528d2e8ddead2cdca88726d0083791c581ed9c7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 11:04:13 -0700 Subject: [PATCH 3/7] feat(chat): convert ChatComponent to Tailwind, add auto-scroll + empty state + responsive sidebar - Replace 80+ lines of inlined CSS vars with CHAT_THEME_STYLES import - Add CHAT_MARKDOWN_STYLES + renderMarkdown for AI message rendering - Convert all inline style="" attributes to Tailwind utility classes - Add auto-scroll via viewChild + effect tracking message count - Add empty state when no messages and not loading - Make thread sidebar responsive with hidden md:flex + mobile toggle - Add ARIA attributes: role=log, aria-live=polite, role=navigation - Use ViewEncapsulation.None for markdown styles Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/compositions/chat/chat.component.ts | 215 ++++++++---------- 1 file changed, 100 insertions(+), 115 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index ba56dbd3a..943e5e797 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -1,10 +1,19 @@ +// libs/chat/src/lib/compositions/chat/chat.component.ts // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component, input, output, + signal, + computed, + effect, + viewChild, + ElementRef, ChangeDetectionStrategy, + inject, + ViewEncapsulation, } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; @@ -14,6 +23,8 @@ import { ChatErrorComponent } from '../../primitives/chat-error/chat-error.compo import { ChatInterruptComponent } from '../../primitives/chat-interrupt/chat-interrupt.component'; import { ChatThreadListComponent, Thread } from '../../primitives/chat-thread-list/chat-thread-list.component'; import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; @Component({ selector: 'chat', @@ -28,58 +39,21 @@ import { messageContent } from '../shared/message-utils'; ChatThreadListComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - // Theme CSS custom properties (sourced from libs/chat/src/lib/styles/chat-theme.css) - `:host { - --chat-bg: #171717; --chat-bg-alt: #222222; --chat-bg-hover: #2a2a2a; - --chat-text: #e0e0e0; --chat-text-muted: #777777; --chat-text-placeholder: #666666; - --chat-border: #333333; --chat-border-light: #2a2a2a; - --chat-user-bg: #2a2a2a; --chat-user-text: #f5f5f5; --chat-user-border: #333333; - --chat-avatar-bg: #333333; --chat-avatar-text: #aaaaaa; - --chat-input-bg: #222222; --chat-input-border: #333333; --chat-input-focus-border: #555555; - --chat-send-bg: #444444; --chat-send-text: #aaaaaa; - --chat-error-bg: #2d1515; --chat-error-text: #f87171; - --chat-warning-bg: #2d2315; --chat-warning-text: #fbbf24; --chat-success: #4ade80; - --chat-radius-message: 20px; --chat-radius-input: 24px; --chat-radius-card: 12px; - --chat-radius-avatar: 8px; --chat-max-width: 720px; - --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; - --chat-font-size: 15px; --chat-line-height: 1.6; - font-family: var(--chat-font-family); font-size: var(--chat-font-size); - line-height: var(--chat-line-height); color: var(--chat-text); background: var(--chat-bg); - display: flex; flex-direction: column; height: 100%; overflow: hidden; - } - @media (prefers-color-scheme: light) { - :host:not([data-chat-theme="dark"]) { - --chat-bg: #ffffff; --chat-bg-alt: #f5f5f5; --chat-bg-hover: #ebebeb; - --chat-text: #1a1a1a; --chat-text-muted: #999999; --chat-text-placeholder: #999999; - --chat-border: #e5e5e5; --chat-border-light: #f0f0f0; - --chat-user-bg: #f0f0f0; --chat-user-text: #1a1a1a; --chat-user-border: transparent; - --chat-avatar-bg: #f0f0f0; --chat-avatar-text: #666666; - --chat-input-bg: #f5f5f5; --chat-input-border: #e5e5e5; --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; --chat-send-text: #999999; - --chat-error-bg: #fef2f2; --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; --chat-warning-text: #d97706; --chat-success: #16a34a; - } - } - :host([data-chat-theme="light"]) { - --chat-bg: #ffffff; --chat-bg-alt: #f5f5f5; --chat-bg-hover: #ebebeb; - --chat-text: #1a1a1a; --chat-text-muted: #999999; --chat-text-placeholder: #999999; - --chat-border: #e5e5e5; --chat-border-light: #f0f0f0; - --chat-user-bg: #f0f0f0; --chat-user-text: #1a1a1a; --chat-user-border: transparent; - --chat-avatar-bg: #f0f0f0; --chat-avatar-text: #666666; - --chat-input-bg: #f5f5f5; --chat-input-border: #e5e5e5; --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; --chat-send-text: #999999; - --chat-error-bg: #fef2f2; --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; --chat-warning-text: #d97706; --chat-success: #16a34a; - }`, - ], + encapsulation: ViewEncapsulation.None, + styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], template: ` -
- +
+ @if (threads().length > 0) { -
-
-

Threads

+