22 * D5 — chat-css script.
33 *
44 * Drives `/demos/chat-customization-css` through one user turn and
5- * verifies the demo's CSS theme is actually applied in the browser by
6- * reading computed styles on the user-message and assistant-message
5+ * verifies the demo's HALCYON theme is actually applied in the browser
6+ * by reading computed styles on the user-message and assistant-message
77 * bubbles. The demo (see
88 * `showcase/integrations/langgraph-python/src/app/demos/chat-customization-css/theme.css`)
9- * paints user bubbles with a hot-pink gradient (`#ff006e` → `#c2185b`)
10- * and assistant bubbles with amber (`#fde047`). A failure here means
11- * either the CSS import broke, the `chat-css-demo-scope` wrapper class
12- * was lost, or the upstream `.copilotKit*` class names drifted.
9+ * paints user bubbles with a parchment paper-elevated background and
10+ * an ember left-border accent (`#c44a1f` → rgb(196, 74, 31)) on the
11+ * inner `[class*="bg-muted"]` element, plus JetBrains Mono. Assistant
12+ * messages use Fraunces serif on a transparent background with an
13+ * ember `::before` rule. A failure here means either the CSS import
14+ * broke, the `chat-css-demo-scope` wrapper class was lost, or the
15+ * upstream `.copilotKit*` class names drifted.
1316 *
14- * Assertions look for the substantive theme color signature:
15- * - user bubble's background contains `255, 0, 110` (the pink/red rgb
16- * anchor) — gradient resolves to either `linear-gradient(...)` or to
17- * a flattened color depending on browser
18- * - assistant bubble's background resolves to `rgb(253, 224, 71)` (amber)
17+ * Three independent signals cover the theme — any one missing trips
18+ * the assertion with a specific error so operators don't have to
19+ * guess which layer broke:
20+ * 1. user bubble's inner element border-left-color contains
21+ * `196, 74, 31` (the halcyon-ember rgb anchor)
22+ * 2. user bubble's inner element font-family contains "JetBrains Mono"
23+ * 3. assistant bubble's font-family contains "Fraunces"
1924 *
2025 * One turn matches the recorded fixture (`chat-css.json`).
2126 */
@@ -36,26 +41,46 @@ export const USER_BUBBLE_SELECTOR = ".copilotKitMessage.copilotKitUserMessage";
3641/** Assistant-bubble selector. */
3742export const ASSISTANT_BUBBLE_SELECTOR =
3843 ".copilotKitMessage.copilotKitAssistantMessage" ;
44+ /** The user bubble's inner v2 element — paints the halcyon-paper-elevated
45+ * background plus the ember left border. The outer message wrapper is
46+ * transparent in the new theme; signals all live on this inner node. */
47+ export const USER_BUBBLE_INNER_SELECTOR = `${ USER_BUBBLE_SELECTOR } [class*="bg-muted"]` ;
3948
40- /** Hot-pink rgb anchor present in the user-bubble gradient (#ff006e). */
41- const USER_RGB_FRAGMENT = "255, 0, 110" ;
42- /** Amber rgb on the assistant bubble (#fde047). */
43- const ASSISTANT_RGB_FRAGMENT = "253, 224, 71" ;
49+ /** halcyon-ember rgb anchor (`#c44a1f`) on the user-bubble inner border-left. */
50+ const EMBER_RGB_FRAGMENT = "196, 74, 31" ;
51+ /** Editorial mono token (`var(--halcyon-mono)`) — JetBrains Mono with
52+ * a CSS fallback chain. The fallback covers Linux runners that lack
53+ * the webfont, so we accept either the real face or one of the
54+ * declared fallbacks. */
55+ const USER_MONO_FONT_FRAGMENTS = [
56+ "JetBrains Mono" ,
57+ // Fallback chain from `--halcyon-mono`.
58+ "ui-monospace" ,
59+ "SF Mono" ,
60+ "Menlo" ,
61+ "Consolas" ,
62+ ] ;
63+ /** Editorial serif token (`var(--halcyon-serif)`) — Fraunces with
64+ * fallbacks. Same rationale as the mono fragments. */
65+ const ASSISTANT_SERIF_FONT_FRAGMENTS = [
66+ "Fraunces" ,
67+ "Source Serif Pro" ,
68+ "ui-serif" ,
69+ ] ;
4470
4571const PROBE_TIMEOUT_MS = 5_000 ;
4672
4773/**
48- * Probe-result shape: per-bubble computed background string. `null` means
49- * the selector didn't match, which the assertion treats as
50- * "missing bubble" — distinct from "matched but wrong color".
74+ * Probe-result shape: per-bubble computed strings. `null` means the
75+ * selector didn't match, distinct from "matched but wrong style".
5176 */
5277export interface ChatCssProbeResult {
53- userBg : string | null ;
54- assistantBg : string | null ;
78+ userBorderLeft : string | null ;
79+ userFontFamily : string | null ;
80+ assistantFontFamily : string | null ;
5581}
5682
57- /** Read computed `background` / `background-color` for both bubbles
58- * inside the demo scope.
83+ /** Read computed styles on both bubbles inside the demo scope.
5984 *
6085 * Notes on the page.evaluate body shape:
6186 * - We do NOT declare local const-arrows or TS interfaces inside the
@@ -70,39 +95,57 @@ export async function probeChatCss(page: Page): Promise<ChatCssProbeResult> {
7095 const win = globalThis as unknown as {
7196 document : { querySelector ( sel : string ) : unknown } ;
7297 getComputedStyle ( el : unknown ) : {
73- background ?: string ;
74- backgroundColor ?: string ;
98+ borderLeftColor ?: string ;
99+ fontFamily ?: string ;
75100 } ;
76101 } ;
77- const userEl = win . document . querySelector (
102+ const userOuter = win . document . querySelector (
78103 ".copilotKitMessage.copilotKitUserMessage" ,
79104 ) ;
105+ // The inner node is the v2 framework's bubble that consumes the
106+ // `bg-muted` Tailwind utility — the demo's CSS hooks the substring
107+ // class-match because the upstream Tailwind class composition
108+ // includes `bg-muted` on the bubble div.
109+ const userInner = ( userOuter as { querySelector ?( sel : string ) : unknown } | null ) ?. querySelector ?.(
110+ '[class*="bg-muted"]' ,
111+ ) ;
80112 const assistantEl = win . document . querySelector (
81113 ".copilotKitMessage.copilotKitAssistantMessage" ,
82114 ) ;
83- const userBg = userEl
84- ? `${ win . getComputedStyle ( userEl ) . background ?? "" } ${ win . getComputedStyle ( userEl ) . backgroundColor ?? "" } ` . trim ( )
85- : null ;
86- const assistantBg = assistantEl
87- ? `${ win . getComputedStyle ( assistantEl ) . background ?? "" } ${ win . getComputedStyle ( assistantEl ) . backgroundColor ?? "" } ` . trim ( )
115+ const userStyle = userInner ? win . getComputedStyle ( userInner ) : null ;
116+ const assistantStyle = assistantEl
117+ ? win . getComputedStyle ( assistantEl )
88118 : null ;
89- return { userBg, assistantBg } ;
119+ return {
120+ userBorderLeft : userStyle ?. borderLeftColor ?? null ,
121+ userFontFamily : userStyle ?. fontFamily ?? null ,
122+ assistantFontFamily : assistantStyle ?. fontFamily ?? null ,
123+ } ;
90124 } ) ) as ChatCssProbeResult ;
91125}
92126
93127/** Validate the probe — returns null on pass, error string on fail. */
94128export function validateChatCss ( probe : ChatCssProbeResult ) : string | null {
95- if ( probe . userBg === null ) {
96- return `chat-css: user bubble (${ USER_BUBBLE_SELECTOR } ) not found in DOM` ;
129+ if ( probe . userBorderLeft === null || probe . userFontFamily === null ) {
130+ return `chat-css: user bubble inner (${ USER_BUBBLE_INNER_SELECTOR } ) not found in DOM` ;
97131 }
98- if ( probe . assistantBg === null ) {
132+ if ( probe . assistantFontFamily === null ) {
99133 return `chat-css: assistant bubble (${ ASSISTANT_BUBBLE_SELECTOR } ) not found in DOM` ;
100134 }
101- if ( ! probe . userBg . includes ( USER_RGB_FRAGMENT ) ) {
102- return `chat-css: user bubble background missing red/pink anchor (${ USER_RGB_FRAGMENT } ) — got "${ probe . userBg . slice ( 0 , 200 ) } "` ;
135+ if ( ! probe . userBorderLeft . includes ( EMBER_RGB_FRAGMENT ) ) {
136+ return `chat-css: user bubble missing halcyon-ember left border (${ EMBER_RGB_FRAGMENT } ) — got "${ probe . userBorderLeft . slice ( 0 , 200 ) } "` ;
137+ }
138+ if (
139+ ! USER_MONO_FONT_FRAGMENTS . some ( ( f ) => probe . userFontFamily ! . includes ( f ) )
140+ ) {
141+ return `chat-css: user bubble missing halcyon-mono font (one of ${ USER_MONO_FONT_FRAGMENTS . join ( ", " ) } ) — got "${ probe . userFontFamily . slice ( 0 , 200 ) } "` ;
103142 }
104- if ( ! probe . assistantBg . includes ( ASSISTANT_RGB_FRAGMENT ) ) {
105- return `chat-css: assistant bubble background missing yellow/amber anchor (${ ASSISTANT_RGB_FRAGMENT } ) — got "${ probe . assistantBg . slice ( 0 , 200 ) } "` ;
143+ if (
144+ ! ASSISTANT_SERIF_FONT_FRAGMENTS . some ( ( f ) =>
145+ probe . assistantFontFamily ! . includes ( f ) ,
146+ )
147+ ) {
148+ return `chat-css: assistant bubble missing halcyon-serif font (one of ${ ASSISTANT_SERIF_FONT_FRAGMENTS . join ( ", " ) } ) — got "${ probe . assistantFontFamily . slice ( 0 , 200 ) } "` ;
106149 }
107150 return null ;
108151}
0 commit comments