@@ -354,7 +408,7 @@ describe('SidebarLayout', () => {
it('toggles theme from the footer and shows settings only when catalog is provided', async () => {
const { rerender } = render(
-
+
@@ -382,7 +436,7 @@ describe('SidebarLayout', () => {
// Settings stays available with a locked agentName when catalog is present.
rerender(
-
+
@@ -404,7 +458,7 @@ describe('SidebarLayout', () => {
expect(await screen.findByRole('heading', { name: 'Settings' })).toBeInTheDocument();
rerender(
-
+
{
// No catalog → no Settings button.
rerender(
-
+
diff --git a/packages/trueforge-ui/test/publicUiExports.test.ts b/packages/trueforge-ui/test/publicUiExports.test.ts
index 249a0e2c7..aeb910515 100644
--- a/packages/trueforge-ui/test/publicUiExports.test.ts
+++ b/packages/trueforge-ui/test/publicUiExports.test.ts
@@ -110,6 +110,7 @@ const expectedRuntimeExports: Array = [
'libraryAgentId',
'mergeAgentSpec',
'preloadMarkdownOpenUI',
+ 'resolveBrandChrome',
'resolvePresetTokens',
'shellIsMutable',
'threadHasPendingMcpAuth',
diff --git a/packages/trueforge-ui/test/theme/ThemeProvider.test.tsx b/packages/trueforge-ui/test/theme/ThemeProvider.test.tsx
index 4ee9ed938..089a83cc5 100644
--- a/packages/trueforge-ui/test/theme/ThemeProvider.test.tsx
+++ b/packages/trueforge-ui/test/theme/ThemeProvider.test.tsx
@@ -138,7 +138,7 @@ describe('ThemeProvider', () => {
it('exposes brand config and labels BrandLogo with the brand name', () => {
render(
-
+
,
diff --git a/packages/trueforge-ui/test/theme/brand.test.tsx b/packages/trueforge-ui/test/theme/brand.test.tsx
index 8e6791f18..d00d800e7 100644
--- a/packages/trueforge-ui/test/theme/brand.test.tsx
+++ b/packages/trueforge-ui/test/theme/brand.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { WelcomeScreen } from '@/atoms/WelcomeScreen.js';
-import { BrandLogo, useBrandName } from '@/theme/brand.js';
+import { BrandLogo, resolveBrandChrome, useBrandName } from '@/theme/brand.js';
import { SlotsProvider } from '@/theme/SlotsProvider.js';
import { ThemeProvider } from '@/theme/ThemeProvider.js';
@@ -10,6 +10,53 @@ function BrandNameProbe() {
return ;
}
+describe('resolveBrandChrome', () => {
+ it('uses the default wordmark when brand.mode is omitted', () => {
+ expect(resolveBrandChrome(undefined)).toEqual({
+ expandedVariant: 'logo',
+ collapsedVariant: 'icon',
+ showTitle: false,
+ });
+ expect(resolveBrandChrome({})).toEqual({
+ expandedVariant: 'logo',
+ collapsedVariant: 'icon',
+ showTitle: false,
+ });
+ });
+
+ it('shows icon + title for mode icon-title', () => {
+ expect(resolveBrandChrome({ mode: 'icon-title', name: 'Acme', icon: '/icon.svg' })).toEqual({
+ expandedVariant: 'icon',
+ collapsedVariant: 'icon',
+ showTitle: true,
+ });
+ });
+
+ it('hides the text title for mode icon-only', () => {
+ expect(resolveBrandChrome({ mode: 'icon-only', name: 'Acme', icon: '/icon.svg' })).toEqual({
+ expandedVariant: 'icon',
+ collapsedVariant: 'icon',
+ showTitle: false,
+ });
+ });
+
+ it('uses the wide logo for mode logo', () => {
+ expect(resolveBrandChrome({ mode: 'logo', name: 'Acme', icon: '/icon.svg', logo: '/wordmark.svg' })).toEqual({
+ expandedVariant: 'logo',
+ collapsedVariant: 'icon',
+ showTitle: false,
+ });
+ });
+
+ it('pairs a host name with the default mark in icon-title without icon', () => {
+ expect(resolveBrandChrome({ mode: 'icon-title', name: 'Acme' })).toEqual({
+ expandedVariant: 'icon',
+ collapsedVariant: 'icon',
+ showTitle: true,
+ });
+ });
+});
+
describe('BrandLogo', () => {
it('renders the default mark without a provider', () => {
const { container } = render();
@@ -31,18 +78,30 @@ describe('BrandLogo', () => {
expect(icon).toHaveAttribute('viewBox', '0 0 120 120');
});
- it('labels the configured logo with the brand name', () => {
+ it('labels the configured icon with the brand name', () => {
render(
-
+
,
);
const logo = screen.getByRole('img', { name: 'Acme' });
- expect(logo).toHaveAttribute('src', '/acme-logo.svg');
+ expect(logo).toHaveAttribute('src', '/acme-icon.svg');
expect(logo).toHaveClass('logo-image');
});
+ it('keeps the brand name as alt for icon-only chrome', () => {
+ render(
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('brand-name')).toHaveTextContent('Acme');
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/acme-icon.svg');
+ });
+
it('renders the default mark and name when brand is omitted', () => {
const { container } = render(
@@ -56,9 +115,32 @@ describe('BrandLogo', () => {
expect(screen.getByTestId('brand-name')).toHaveTextContent('TrueForge');
});
- it('pairs a host name with the default mark when logo is omitted', () => {
+ it('renders the default wordmark for the logo variant', () => {
+ const { container, rerender } = render(
+
+
+ ,
+ );
+
+ const light = screen.getByRole('img', { name: 'TrueForge' });
+ expect(light).toHaveClass('host-logo', 'w-auto');
+ expect(light).toHaveAttribute('viewBox', '0 0 614 100');
+ // Sizing must come from the viewBox, not svgr's 1em width/height.
+ expect(light).not.toHaveAttribute('width');
+ expect(light).not.toHaveAttribute('height');
+ expect(container.querySelector('svg[aria-hidden="true"]')).toBeNull();
+
+ rerender(
+
+
+ ,
+ );
+ expect(screen.getByRole('img', { name: 'TrueForge' })).toHaveAttribute('viewBox', '0 0 737 120');
+ });
+
+ it('pairs a host name with the default mark when icon is omitted', () => {
const { container } = render(
-
+
,
@@ -69,50 +151,91 @@ describe('BrandLogo', () => {
expect(screen.getByTestId('brand-name')).toHaveTextContent('Acme');
});
- it('treats a bare string as an image source', () => {
+ it('treats a bare string as an icon source', () => {
render(
-
+
,
);
const img = screen.getByRole('img', { name: 'Acme' });
- expect(img).toHaveAttribute('src', '/acme-logo.svg');
+ expect(img).toHaveAttribute('src', '/acme-icon.svg');
expect(img).toHaveClass('logo-image');
});
it('picks the light source in light mode and the dark source in dark mode', () => {
- const logo = { light: '/logo/light.svg', dark: '/logo/dark.svg' };
+ const icon = { light: '/icon/light.svg', dark: '/icon/dark.svg' };
const { rerender } = render(
-
+
,
);
- expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/logo/light.svg');
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/icon/light.svg');
rerender(
-
+
,
);
- expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/logo/dark.svg');
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/icon/dark.svg');
});
it('falls back to the other mode when only one source is configured', () => {
render(
-
+
,
);
- expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/logo/light.svg');
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/icon/light.svg');
+ });
+
+ it('uses the wide logo only for the logo variant', () => {
+ const brand = {
+ mode: 'logo' as const,
+ name: 'Acme',
+ icon: '/acme-icon.svg',
+ logo: '/acme-wordmark.svg',
+ };
+ const { rerender } = render(
+
+
+ ,
+ );
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/acme-icon.svg');
+
+ rerender(
+
+
+ ,
+ );
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/acme-wordmark.svg');
+ });
+
+ it('falls back to the square icon when the wide logo is omitted from BrandLogo logo variant', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('img', { name: 'Acme' })).toHaveAttribute('src', '/acme-icon.svg');
});
it('wraps the logo in a same-tab link when href is set', () => {
render(
,
@@ -121,7 +244,7 @@ describe('BrandLogo', () => {
const link = screen.getByRole('link', { name: 'Acme' });
expect(link).toHaveAttribute('href', 'https://trueforge.dev');
expect(link).not.toHaveAttribute('target');
- expect(link.querySelector('img')).toHaveAttribute('src', '/logo/light.svg');
+ expect(link.querySelector('img')).toHaveAttribute('src', '/icon/light.svg');
});
it('lets a slot override replace the mark with a component', () => {
@@ -130,7 +253,10 @@ describe('BrandLogo', () => {
}
render(
-
+
,
);
@@ -139,9 +265,9 @@ describe('BrandLogo', () => {
expect(screen.queryByRole('img', { name: 'Acme' })).toBeNull();
});
- it('falls back to the default mark when a logo config resolves to no source', () => {
+ it('falls back to the default mark when an icon config resolves to no source', () => {
const { container } = render(
-
+
,
);