diff --git a/examples/albums-example/package.json b/examples/albums-example/package.json index bc9ff81..704355f 100644 --- a/examples/albums-example/package.json +++ b/examples/albums-example/package.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", - "sunpeak": "^0.18.2", + "sunpeak": "^0.18.4", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, diff --git a/examples/albums-example/tests/e2e/albums.spec.ts b/examples/albums-example/tests/e2e/albums.spec.ts index aef10a0..066545d 100644 --- a/examples/albums-example/tests/e2e/albums.spec.ts +++ b/examples/albums-example/tests/e2e/albums.spec.ts @@ -222,21 +222,25 @@ for (const host of hosts) { }); }); - test('should render content after switching from inline to pip', async ({ page }) => { - // Start in inline mode - await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host })); - - const iframe = page.frameLocator('iframe').frameLocator('iframe'); - await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible(); - - // Switch to PiP via sidebar - await page.locator('button:has-text("PiP")').click(); - - // Content should still be visible after the mode transition - await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({ - timeout: 5000, - }); - }); + // Claude doesn't support PiP — only run this test for hosts that have the button. + (host === 'claude' ? test.skip : test)( + 'should render content after switching from inline to pip', + async ({ page }) => { + // Start in inline mode + await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host })); + + const iframe = page.frameLocator('iframe').frameLocator('iframe'); + await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible(); + + // Switch to PiP via sidebar + await page.locator('button:has-text("PiP")').click(); + + // Content should still be visible after the mode transition + await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({ + timeout: 5000, + }); + } + ); }); }); } diff --git a/examples/albums-example/tests/live/albums.spec.ts b/examples/albums-example/tests/live/albums.spec.ts index 4a337fb..9fe7164 100644 --- a/examples/albums-example/tests/live/albums.spec.ts +++ b/examples/albums-example/tests/live/albums.spec.ts @@ -25,6 +25,16 @@ test('albums tool renders photo grid with correct styles', async ({ live }) => { }); expect(containerStyles.overflow).toBe('hidden'); + // Background: the app's root should have a resolved background color + // (from --color-background-primary or the CSS Canvas system color), + // not transparent. A transparent root would show the host container + // rather than the app's own styled background. + const rootBg = await app + .locator(':root') + .evaluate((el) => window.getComputedStyle(el).backgroundColor); + expect(rootBg).not.toBe('rgba(0, 0, 0, 0)'); + expect(rootBg).toMatch(/^rgb/); + // Theme: text color has appropriate luminance in light mode const textColor = await albumCard.evaluate((el) => window.getComputedStyle(el).color); assertTextContrast(textColor); diff --git a/examples/carousel-example/package.json b/examples/carousel-example/package.json index 1fb1e24..7839acb 100644 --- a/examples/carousel-example/package.json +++ b/examples/carousel-example/package.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", - "sunpeak": "^0.18.2", + "sunpeak": "^0.18.4", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, diff --git a/examples/map-example/package.json b/examples/map-example/package.json index 4f7aaee..42705bf 100644 --- a/examples/map-example/package.json +++ b/examples/map-example/package.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "mapbox-gl": "^3.20.0", - "sunpeak": "^0.18.2", + "sunpeak": "^0.18.4", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, diff --git a/examples/review-example/package.json b/examples/review-example/package.json index 080ef73..933eafc 100644 --- a/examples/review-example/package.json +++ b/examples/review-example/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "clsx": "^2.1.1", - "sunpeak": "^0.18.2", + "sunpeak": "^0.18.4", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, diff --git a/packages/sunpeak/bin/lib/sandbox-server.mjs b/packages/sunpeak/bin/lib/sandbox-server.mjs index c7fd5ef..306f32f 100644 --- a/packages/sunpeak/bin/lib/sandbox-server.mjs +++ b/packages/sunpeak/bin/lib/sandbox-server.mjs @@ -97,7 +97,7 @@ function generateProxyHtml(theme, platform) { @@ -168,6 +168,16 @@ iframe { border: none; width: 100%; height: 100%; display: block; } return; } + // Sync color-scheme on the inner iframe element when theme changes. + // This ensures prefers-color-scheme resolves correctly inside the app. + // Important: do NOT set color-scheme on the proxy's own document — + // changing it from the initial 'dark' causes Chrome to re-evaluate + // the CSS Canvas as opaque white, blocking the host's conversation + // background from showing through the transparent proxy. + if (data.method === 'ui/notifications/host-context-changed' && data.params && data.params.theme) { + if (innerFrame) innerFrame.style.colorScheme = data.params.theme; + } + // Forward all other messages to the inner iframe if (innerWindow) { try { innerWindow.postMessage(data, '*'); } catch(e) { /* detached */ } diff --git a/packages/sunpeak/src/claude/claude-conversation.test.tsx b/packages/sunpeak/src/claude/claude-conversation.test.tsx index 5a9ca76..25d51e7 100644 --- a/packages/sunpeak/src/claude/claude-conversation.test.tsx +++ b/packages/sunpeak/src/claude/claude-conversation.test.tsx @@ -42,7 +42,7 @@ describe('ClaudeConversation', () => { expect(screen.getByTestId('fullscreen-content')).toBeInTheDocument(); expect(container.querySelector('footer')).toBeInTheDocument(); - expect(screen.getAllByPlaceholderText('Reply to sunpeak...').length).toBeGreaterThan(0); + expect(screen.getAllByText('Reply to sunpeak...').length).toBeGreaterThan(0); expect(screen.getByLabelText('Back')).toBeInTheDocument(); }); diff --git a/packages/sunpeak/src/claude/claude-conversation.tsx b/packages/sunpeak/src/claude/claude-conversation.tsx index 018e6c1..4656db1 100644 --- a/packages/sunpeak/src/claude/claude-conversation.tsx +++ b/packages/sunpeak/src/claude/claude-conversation.tsx @@ -105,6 +105,8 @@ export function ClaudeConversation({ transform: 'translate(0)', backgroundColor: 'var(--sim-bg-conversation, var(--color-background-primary))', color: 'var(--color-text-primary)', + fontFamily: 'var(--font-sans)', + WebkitFontSmoothing: 'antialiased', }} > {/* ─── Fullscreen chrome overlay ─── */} @@ -139,18 +141,24 @@ export function ClaudeConversation({ }} >
- + > +
+ Reply to sunpeak... +
+
@@ -182,8 +190,12 @@ export function ClaudeConversation({
-
- +
+ > + Reply to sunpeak... +
diff --git a/packages/sunpeak/src/claude/claude-host.ts b/packages/sunpeak/src/claude/claude-host.ts index 27f6098..ad3527c 100644 --- a/packages/sunpeak/src/claude/claude-host.ts +++ b/packages/sunpeak/src/claude/claude-host.ts @@ -3,18 +3,30 @@ import { registerHostShell } from '../inspector/hosts'; import { DEFAULT_STYLE_VARIABLES } from '../inspector/host-styles'; import { ClaudeConversation } from './claude-conversation'; +/** + * Claude host version info — matches what Claude reports via the MCP protocol. + * Verified against production Claude on 2026-03-25. + */ const CLAUDE_HOST_INFO = { name: 'Claude', version: '1.0.0', }; +/** + * Claude host capabilities — matches what Claude reports via the MCP protocol. + * Verified against production Claude on 2026-03-25. + * + * Notable: Claude supports downloadFile, updateModelContext.image, and + * message.text. serverTools and serverResources both report listChanged. + * No sandbox.permissions (no microphone etc.). No PiP display mode. + */ const CLAUDE_HOST_CAPABILITIES: McpUiHostCapabilities = { openLinks: {}, - serverTools: {}, - serverResources: {}, downloadFile: {}, + serverTools: { listChanged: true }, + serverResources: { listChanged: true }, logging: {}, - updateModelContext: { text: {} }, + updateModelContext: { text: {}, image: {} }, message: { text: {} }, sandbox: {}, }; @@ -29,25 +41,90 @@ function applyClaudeTheme(theme: 'light' | 'dark'): void { } /** - * Claude-specific style variable overrides (warm beige/cream palette). - * Inherits defaults from DEFAULT_STYLE_VARIABLES, overriding only colors. + * Claude style variable overrides — warm beige/cream palette with Anthropic Sans. + * Verified against production Claude on 2026-03-25. + * + * Only overrides values that differ from DEFAULT_STYLE_VARIABLES. + * Claude sends all variables via styles.variables using light-dark(rgba()) format. */ const CLAUDE_STYLE_VARIABLES = { ...DEFAULT_STYLE_VARIABLES, - // Background colors — warm beige/cream palette - '--color-background-primary': 'light-dark(#faf9f5, #262624)', - '--color-background-secondary': 'light-dark(#ffffff, #3a3935)', - '--color-background-tertiary': 'light-dark(#e8e4dc, #4a4843)', - '--color-background-inverse': 'light-dark(#2b2a27, #f3f0e8)', - // Text colors - '--color-text-primary': 'light-dark(#2d2b27, #e8e4dc)', - '--color-text-secondary': 'light-dark(#6b6560, #9b9690)', - '--color-text-tertiary': 'light-dark(#9b9690, #6b6560)', - '--color-text-inverse': 'light-dark(#e8e4dc, #2d2b27)', - // Border colors - '--color-border-primary': 'light-dark(#e0ddd5, #4a4843)', - '--color-border-secondary': 'light-dark(#d5d1c8, #5a5753)', - '--color-border-tertiary': 'light-dark(#f0ede5, #3a3935)', + + // ── Background colors ── + '--color-background-primary': 'light-dark(rgba(255, 255, 255, 1), rgba(48, 48, 46, 1))', + '--color-background-secondary': 'light-dark(rgba(245, 244, 237, 1), rgba(38, 38, 36, 1))', + '--color-background-tertiary': 'light-dark(rgba(250, 249, 245, 1), rgba(20, 20, 19, 1))', + '--color-background-inverse': 'light-dark(rgba(20, 20, 19, 1), rgba(250, 249, 245, 1))', + '--color-background-ghost': 'light-dark(rgba(255, 255, 255, 0), rgba(48, 48, 46, 0))', + '--color-background-info': 'light-dark(rgba(214, 228, 246, 1), rgba(37, 62, 95, 1))', + '--color-background-danger': 'light-dark(rgba(247, 236, 236, 1), rgba(96, 42, 40, 1))', + '--color-background-success': 'light-dark(rgba(233, 241, 220, 1), rgba(27, 70, 20, 1))', + '--color-background-warning': 'light-dark(rgba(246, 238, 223, 1), rgba(72, 58, 15, 1))', + '--color-background-disabled': 'light-dark(rgba(255, 255, 255, 0.5), rgba(48, 48, 46, 0.5))', + + // ── Text colors ── + '--color-text-primary': 'light-dark(rgba(20, 20, 19, 1), rgba(250, 249, 245, 1))', + '--color-text-secondary': 'light-dark(rgba(61, 61, 58, 1), rgba(194, 192, 182, 1))', + '--color-text-tertiary': 'light-dark(rgba(115, 114, 108, 1), rgba(156, 154, 146, 1))', + '--color-text-inverse': 'light-dark(rgba(255, 255, 255, 1), rgba(20, 20, 19, 1))', + '--color-text-ghost': 'light-dark(rgba(115, 114, 108, 0.5), rgba(156, 154, 146, 0.5))', + '--color-text-info': 'light-dark(rgba(50, 102, 173, 1), rgba(128, 170, 221, 1))', + '--color-text-danger': 'light-dark(rgba(127, 44, 40, 1), rgba(238, 136, 132, 1))', + '--color-text-success': 'light-dark(rgba(38, 91, 25, 1), rgba(122, 185, 72, 1))', + '--color-text-warning': 'light-dark(rgba(90, 72, 21, 1), rgba(209, 160, 65, 1))', + '--color-text-disabled': 'light-dark(rgba(20, 20, 19, 0.5), rgba(250, 249, 245, 0.5))', + + // ── Border colors ── + '--color-border-primary': 'light-dark(rgba(31, 30, 29, 0.4), rgba(222, 220, 209, 0.4))', + '--color-border-secondary': 'light-dark(rgba(31, 30, 29, 0.3), rgba(222, 220, 209, 0.3))', + '--color-border-tertiary': 'light-dark(rgba(31, 30, 29, 0.15), rgba(222, 220, 209, 0.15))', + '--color-border-inverse': 'light-dark(rgba(255, 255, 255, 0.3), rgba(20, 20, 19, 0.15))', + '--color-border-ghost': 'light-dark(rgba(31, 30, 29, 0), rgba(222, 220, 209, 0))', + '--color-border-info': 'light-dark(rgba(70, 130, 213, 1), rgba(70, 130, 213, 1))', + '--color-border-danger': 'light-dark(rgba(167, 61, 57, 1), rgba(205, 92, 88, 1))', + '--color-border-success': 'light-dark(rgba(67, 116, 38, 1), rgba(89, 145, 48, 1))', + '--color-border-warning': 'light-dark(rgba(128, 92, 31, 1), rgba(168, 120, 41, 1))', + '--color-border-disabled': 'light-dark(rgba(31, 30, 29, 0.1), rgba(222, 220, 209, 0.1))', + + // ── Ring colors ── + '--color-ring-primary': 'light-dark(rgba(20, 20, 19, 0.7), rgba(250, 249, 245, 0.7))', + '--color-ring-secondary': 'light-dark(rgba(61, 61, 58, 0.7), rgba(194, 192, 182, 0.7))', + '--color-ring-inverse': 'light-dark(rgba(255, 255, 255, 0.7), rgba(20, 20, 19, 0.7))', + '--color-ring-info': 'light-dark(rgba(50, 102, 173, 0.5), rgba(128, 170, 221, 0.5))', + '--color-ring-danger': 'light-dark(rgba(167, 61, 57, 0.5), rgba(205, 92, 88, 0.5))', + '--color-ring-success': 'light-dark(rgba(67, 116, 38, 0.5), rgba(89, 145, 48, 0.5))', + '--color-ring-warning': 'light-dark(rgba(128, 92, 31, 0.5), rgba(168, 120, 41, 0.5))', + + // ── Typography ── + '--font-sans': '"Anthropic Sans", system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + '--font-mono': 'ui-monospace, monospace', + // Sizes (px, not rem — Claude uses fixed px values) + '--font-text-xs-size': '12px', + '--font-text-sm-size': '14px', + '--font-text-md-size': '16px', + '--font-text-lg-size': '20px', + '--font-heading-xs-size': '12px', + '--font-heading-sm-size': '14px', + '--font-heading-md-size': '16px', + '--font-heading-lg-size': '20px', + '--font-heading-xl-size': '24px', + '--font-heading-2xl-size': '28px', + '--font-heading-3xl-size': '36px', + // Line heights + '--font-text-md-line-height': '1.4', + '--font-text-lg-line-height': '1.25', + '--font-heading-lg-line-height': '1.25', + '--font-heading-2xl-line-height': '1.1', + '--font-heading-3xl-line-height': '1', + + // ── Border radius (Claude uses slightly larger values) ── + '--border-radius-xs': '4px', + '--border-radius-sm': '6px', + '--border-radius-md': '8px', + '--border-radius-lg': '10px', + + // ── Border width ── + '--border-width-regular': '0.5px', }; registerHostShell({ @@ -57,12 +134,28 @@ registerHostShell({ applyTheme: applyClaudeTheme, hostInfo: CLAUDE_HOST_INFO, hostCapabilities: CLAUDE_HOST_CAPABILITIES, - userAgent: 'claude', styleVariables: CLAUDE_STYLE_VARIABLES, pageStyles: { - '--sim-bg-sidebar': 'light-dark(#f9f8f3, #252523)', - '--sim-bg-conversation': 'light-dark(#faf9f5, #262624)', - '--sim-bg-user-bubble': 'light-dark(#f1eee6, #141413)', - '--sim-bg-reply-input': 'light-dark(#ffffff, #30302e)', + '--sim-bg-sidebar': 'light-dark(rgb(250, 249, 245), rgb(38, 38, 36))', + '--sim-bg-conversation': 'light-dark(rgb(250, 249, 245), rgb(38, 38, 36))', + '--sim-bg-user-bubble': 'light-dark(rgb(240, 238, 230), rgb(20, 20, 19))', + '--sim-bg-reply-input': 'light-dark(rgb(255, 255, 255), rgb(48, 48, 46))', }, + availableDisplayModes: ['inline', 'fullscreen'], + fontCss: `@font-face { + font-family: "Anthropic Sans"; + src: url("https://assets-proxy.anthropic.com/claude-ai/v2/assets/v1/cc27851ad-CFxw3nG7.woff2") format("woff2"); + font-weight: 300 800; + font-style: normal; + font-display: swap; + font-feature-settings: "dlig" 0; +} +@font-face { + font-family: "Anthropic Sans"; + src: url("https://assets-proxy.anthropic.com/claude-ai/v2/assets/v1/c9d3a3a49-BI1hrwN4.woff2") format("woff2"); + font-weight: 300 800; + font-style: italic; + font-display: swap; + font-feature-settings: "dlig" 0; +}`, }); diff --git a/packages/sunpeak/src/inspector/hosts.ts b/packages/sunpeak/src/inspector/hosts.ts index 8e7c2ec..4683064 100644 --- a/packages/sunpeak/src/inspector/hosts.ts +++ b/packages/sunpeak/src/inspector/hosts.ts @@ -80,6 +80,18 @@ export interface HostShell { * Values should use CSS light-dark() for automatic theme adaptation. */ pageStyles?: Record; + /** + * Display modes this host supports. + * The sidebar display mode picker only shows modes in this list. + * Defaults to ['inline', 'pip', 'fullscreen'] if not specified. + */ + availableDisplayModes?: McpUiDisplayMode[]; + /** + * CSS containing @font-face rules for the host's custom fonts. + * Injected into the inspector page when this host is active so the + * conversation chrome can use the same font as the real host. + */ + fontCss?: string; } // ── Host Shell Registry ────────────────────────────────────────── diff --git a/packages/sunpeak/src/inspector/inspector.tsx b/packages/sunpeak/src/inspector/inspector.tsx index 3d7a639..5ceb86d 100644 --- a/packages/sunpeak/src/inspector/inspector.tsx +++ b/packages/sunpeak/src/inspector/inspector.tsx @@ -340,7 +340,7 @@ export function Inspector({ const registeredHosts = getRegisteredHosts(); const ShellConversation = activeShell?.Conversation; - // Merge host style variables and userAgent into the hostContext. + // Merge host style variables, userAgent, and availableDisplayModes into hostContext. const hostContext = React.useMemo(() => { const styleVars = activeShell?.styleVariables; const userAgent = activeShell?.userAgent; @@ -351,11 +351,25 @@ export function Inspector({ if (userAgent) { (ctx as McpUiHostContext).userAgent = userAgent; } + if (activeShell?.availableDisplayModes) { + (ctx as McpUiHostContext).availableDisplayModes = activeShell.availableDisplayModes; + } return ctx as McpUiHostContext; }, [state.hostContext, activeShell]); - // Apply host style variables to the document root. + // Reset display mode to inline if the active host doesn't support it. + const { displayMode, setDisplayMode } = state; React.useEffect(() => { + const modes = activeShell?.availableDisplayModes; + if (modes && !modes.includes(displayMode)) { + setDisplayMode('inline'); + } + }, [activeShell, displayMode, setDisplayMode]); + + // Apply host style variables to the document root. + // Uses useLayoutEffect so variables are set BEFORE paint, preventing a flash + // of stale colors when switching hosts and then toggling theme. + React.useLayoutEffect(() => { const vars = activeShell?.styleVariables; if (!vars) return; const root = document.documentElement; @@ -365,8 +379,9 @@ export function Inspector({ }, [activeShell]); // Apply host page styles. Cleans up old properties when switching hosts. + // Uses useLayoutEffect to stay in sync with style variables above. const prevPageStyleKeysRef = React.useRef([]); - React.useEffect(() => { + React.useLayoutEffect(() => { const root = document.documentElement; for (const key of prevPageStyleKeysRef.current) { root.style.removeProperty(key); @@ -384,6 +399,26 @@ export function Inspector({ } }, [activeShell]); + // Inject host font CSS (@font-face rules) so the conversation chrome + // uses the same font as the real host (e.g., Anthropic Sans for Claude). + React.useLayoutEffect(() => { + const fontCss = activeShell?.fontCss; + const id = 'sunpeak-host-fonts'; + const existing = document.getElementById(id); + if (!fontCss) { + existing?.remove(); + return; + } + if (existing) { + existing.textContent = fontCss; + } else { + const style = document.createElement('style'); + style.id = id; + style.textContent = fontCss; + document.head.appendChild(style); + } + }, [activeShell]); + // Handle callServerTool from the iframe. // When a simulation is active: prefer serverTools mocks, fall back to MCP. // When "None": always use MCP (real handlers). @@ -838,7 +873,11 @@ export function Inspector({ { value: 'inline', label: 'Inline' }, { value: 'pip', label: 'PiP' }, { value: 'fullscreen', label: 'Full' }, - ]} + ].filter( + (opt) => + !activeShell?.availableDisplayModes || + activeShell.availableDisplayModes.includes(opt.value as McpUiDisplayMode) + )} /> diff --git a/packages/sunpeak/src/inspector/sandbox-proxy.ts b/packages/sunpeak/src/inspector/sandbox-proxy.ts index b00814f..c3a81b6 100644 --- a/packages/sunpeak/src/inspector/sandbox-proxy.ts +++ b/packages/sunpeak/src/inspector/sandbox-proxy.ts @@ -42,7 +42,7 @@ export function generateSandboxProxyHtml(platformScript?: string): string { @@ -105,6 +105,16 @@ iframe { border: none; width: 100%; height: 100%; display: block; } return; } + // Sync color-scheme on the inner iframe element when theme changes. + // This ensures prefers-color-scheme resolves correctly inside the app. + // Important: do NOT set color-scheme on the proxy's own document — + // changing it from the initial 'dark' causes Chrome to re-evaluate + // the CSS Canvas as opaque white, blocking the host's conversation + // background from showing through the transparent proxy. + if (data.method === 'ui/notifications/host-context-changed' && data.params && data.params.theme) { + if (innerFrame) innerFrame.style.colorScheme = data.params.theme; + } + // Forward all other messages to the inner iframe if (innerWindow) { try { innerWindow.postMessage(data, '*'); } catch(e) { /* detached */ } diff --git a/packages/sunpeak/template/tests/e2e/albums.spec.ts b/packages/sunpeak/template/tests/e2e/albums.spec.ts index aef10a0..066545d 100644 --- a/packages/sunpeak/template/tests/e2e/albums.spec.ts +++ b/packages/sunpeak/template/tests/e2e/albums.spec.ts @@ -222,21 +222,25 @@ for (const host of hosts) { }); }); - test('should render content after switching from inline to pip', async ({ page }) => { - // Start in inline mode - await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host })); - - const iframe = page.frameLocator('iframe').frameLocator('iframe'); - await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible(); - - // Switch to PiP via sidebar - await page.locator('button:has-text("PiP")').click(); - - // Content should still be visible after the mode transition - await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({ - timeout: 5000, - }); - }); + // Claude doesn't support PiP — only run this test for hosts that have the button. + (host === 'claude' ? test.skip : test)( + 'should render content after switching from inline to pip', + async ({ page }) => { + // Start in inline mode + await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host })); + + const iframe = page.frameLocator('iframe').frameLocator('iframe'); + await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible(); + + // Switch to PiP via sidebar + await page.locator('button:has-text("PiP")').click(); + + // Content should still be visible after the mode transition + await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({ + timeout: 5000, + }); + } + ); }); }); } diff --git a/packages/sunpeak/template/tests/live/albums.spec.ts b/packages/sunpeak/template/tests/live/albums.spec.ts index 4a337fb..9fe7164 100644 --- a/packages/sunpeak/template/tests/live/albums.spec.ts +++ b/packages/sunpeak/template/tests/live/albums.spec.ts @@ -25,6 +25,16 @@ test('albums tool renders photo grid with correct styles', async ({ live }) => { }); expect(containerStyles.overflow).toBe('hidden'); + // Background: the app's root should have a resolved background color + // (from --color-background-primary or the CSS Canvas system color), + // not transparent. A transparent root would show the host container + // rather than the app's own styled background. + const rootBg = await app + .locator(':root') + .evaluate((el) => window.getComputedStyle(el).backgroundColor); + expect(rootBg).not.toBe('rgba(0, 0, 0, 0)'); + expect(rootBg).toMatch(/^rgb/); + // Theme: text color has appropriate luminance in light mode const textColor = await albumCard.evaluate((el) => window.getComputedStyle(el).color); assertTextContrast(textColor);