Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brand-icon-wordmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@truefoundry/trueforge-ui": minor
"@truefoundry/trueforge": patch
---

Add `brand.mode` (`icon-title` | `icon-only` | `logo`) so hosts pick chrome look first; `name` always labels the mark, and `resolveBrandChrome` maps mode to layout chrome.
62 changes: 45 additions & 17 deletions docs/ui-sdk/guides/layouts-and-theme.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,58 @@ Built-in layouts are code-split, so only the layout you use is loaded into your

## Preset Themes

Choose from four built-in themes - Claude, ChatGPT, Gemini, and TrueFoundry, or create a [custom theme](../setup-custom-ui/custom-theme) to match your brand.
Choose from four built-in themes - Claude, ChatGPT, Gemini, and TrueForge, or create a [custom theme](../setup-custom-ui/custom-theme) to match your brand.

## The theme object

```ts
type BrandImage = string | { src?: string; light?: string; dark?: string };

type BrandMode = "icon-title" | "icon-only" | "logo";

type BrandConfig =
| { mode: "icon-title"; name: string; icon?: BrandImage; logo?: never; href?: string }
| { mode: "icon-only"; name: string; icon: BrandImage; logo?: never; href?: string }
| { mode: "logo"; name: string; icon: BrandImage; logo: BrandImage; href?: string };

type ThemeConfig = {
preset?: "trueforge" | "claude" | "chatgpt" | "gemini"; // default "trueforge"
mode?: "light" | "dark" | "system";
brand?: {
name: string; // required when `brand` is set
logo?: string | BrandLogoConfig;
},
brand?: BrandConfig;
...
};
```

Set `brand.mode`, then pass the fields that mode requires. `name` always labels the mark.

| Look | `mode` | Required | Expanded | Collapsed |
| --- | --- | --- | --- | --- |
| Default | omit `brand` | — | TrueForge wordmark | TrueForge square |
| Icon + title | `"icon-title"` | `name` (+ optional `icon`) | square + title | square |
| Icon only | `"icon-only"` | `name`, `icon` | square | square |
| Wide logo | `"logo"` | `name`, `icon`, `logo` | wide logo | square |

```tsx App.tsx
<TrueForgeUI
...
theme={{
preset: "trueforge",
mode: "system",
brand: {
name: "TrueForge",
logo: "/logo.svg",
mode: "logo",
name: "Acme",
icon: "/icon.svg",
logo: "/wordmark.svg",
}
}}
/>
```

`icon` is the square asset used in collapsed and compact surfaces. `logo` is the wider
asset used when `mode` is `"logo"` and requires `icon` as its compact fallback. When
`brand` is omitted, expanded chrome uses the built-in TrueForge wordmark and compact
surfaces use the built-in square mark.

The `chatgpt`, `claude`, and `gemini` themes are shown below:

<Columns cols={3}>
Expand Down Expand Up @@ -124,24 +146,30 @@ You can also provide separate logos for light and dark modes, allowing your bran
theme={{
...
brand: {
name: "TrueForge",
mode: "logo",
name: "Acme",
icon: {
light: "/icon/light.svg",
dark: "/icon/dark.svg",
},
logo: {
light: "/logo/light.svg",
dark: "/logo/dark.svg",
href: "https://trueforge.dev", // URL to redirect on logo click
light: "/wordmark/light.svg",
dark: "/wordmark/dark.svg",
},
href: "https://example.com",
}
}}
/>
```

The logo source is selected based on the resolved theme mode. When mode is omitted, the logo automatically follows the system theme.
The icon and logo sources are selected based on the resolved theme mode. When mode is omitted, they automatically follow the system theme.

* Provide both light and dark to use a different logo for each mode.
* Provide only one of light or dark to use the same logo in both modes.
* Use src for a mode-independent logo.
* `name` sets the brand name in the topbar right next to the logo.
* Set `href` to make the logo clickable.
* Provide both light and dark to use a different image for each mode.
* Provide only one of light or dark to use the same image in both modes.
* Use src for a mode-independent image.
* Set `brand.mode` first (`icon-title` | `icon-only` | `logo`); `name` is required and always labels images.
* Visible title text only appears for `mode: "icon-title"`.
* Set the brand-level `href` to make configured images clickable.

For more advanced use cases, such as rendering an inline SVG or an animated logo, override the BrandLogo slot instead of passing a React node through the theme object.

Expand Down
9 changes: 5 additions & 4 deletions docs/ui-sdk/reference/theme.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: "Theme provider, hooks, presets, brand, and slots."
| --- | --- |
| `ThemeProvider` | Owns light/dark/system state and injects token CSS variables on a wrapper `div.aui-theme-root`. |
| `SlotsProvider` | Slot override registry. Must sit outside `TrueFoundryChatProvider`. |
| `BrandLogo` | The product mark: renders `theme.brand.logo` for the active mode as an image labelled with `theme.brand.name`, linked when the config sets `href`. Falls back to the default mark. Overridable slot. Props: `{ className? }`. |
| `BrandLogo` | Product mark. `variant="icon"` renders the compact square asset; `variant="logo"` renders the wider asset and falls back to the icon. With no brand override, the logo variant uses the built-in TrueForge wordmark. Overridable slot. Props: `{ className?, variant?: "icon" \| "logo" }`. |
| `Icon` | Renders a built-in icon by name, or your replacement when `theme.icons` maps that name. Props: `{ name: string \| readonly string[] } & IconProps`. An array resolves to its **last** element, accommodating Font Awesome-style `["far", "clone"]` tuples; it is not a fallback chain. An unresolved name renders nothing. |

## Hooks
Expand All @@ -19,7 +19,8 @@ description: "Theme provider, hooks, presets, brand, and slots."
| `useTheme()` | `{ preset, mode, preference, isDark, setTheme }`. `setTheme` is a no-op when `theme.mode` is controlled. |
| `useThemeMode()` | Resolved `"light" \| "dark"`. Reads the mode published by `SlotsProvider`, so it returns `"light"` when no `SlotsProvider` is above it — even in dark mode. |
| `useBrand()` | The active `BrandConfig`. |
| `useBrandName()` | `theme.brand.name`, or `"TrueForge"` when unset. Safe outside a provider. |
| `useBrandName()` | The configured name; `undefined` for unnamed custom branding; or `"TrueForge"` when no custom image is configured. Safe outside a provider. |
| `resolveBrandChrome(brand)` | `{ expandedVariant, collapsedVariant, showTitle }` from `brand.mode`. Prefer this over re-deriving field combinations. |
| `useThemeIcons()` | The `IconMap` from `theme.icons`. |
| `useContentClassNames()` | `theme.classNames`; throws outside a provider. |
| `useOptionalContentClassNames()` | Same, but returns `{}` outside a provider instead of throwing — no null check needed. |
Expand All @@ -35,8 +36,8 @@ description: "Theme provider, hooks, presets, brand, and slots."

## Types

`ThemeConfig`, `ThemeMode`, `ThemePreset`, `SemanticTokens`, `BrandConfig`, `BrandLogoConfig`,
`ContentClassNames`, `IconMap`, `ThemeIconProps`, `IconProps`, `LayoutProp`, `AtomSlots`,
`ThemeConfig`, `ThemeMode`, `ThemePreset`, `SemanticTokens`, `BrandConfig`, `BrandMode`, `BrandImage`, `BrandLogoConfig`,
`BrandChrome`, `ContentClassNames`, `IconMap`, `ThemeIconProps`, `IconProps`, `LayoutProp`, `AtomSlots`,
`SlotOverrides`.

```ts
Expand Down
37 changes: 26 additions & 11 deletions docs/ui-sdk/reference/trueforge-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,29 @@ import { TrueForgeUI } from "@truefoundry/trueforge-ui";
</Expandable>
</ParamField>
<ParamField path="brand" type="BrandConfig">
Optional. Omit it to keep the default mark and the `"TrueForge"` name; setting it
requires a `name`.
Optional. Omit it to use the built-in TrueForge wordmark in expanded chrome and
square mark in compact surfaces. When set, choose `mode` first, then pass the
fields that mode requires.

<Expandable title="BrandConfig">
<ParamField path="name" type="string" required>
Shown beside the mark, and used as the logo's accessible label.
<ParamField path="mode" type='"icon-title" | "icon-only" | "logo"'>
Chrome look. `icon-title` shows `name` beside the square mark; `icon-only`
and `logo` keep `name` for alt only.
</ParamField>
<ParamField path="name" type="string">
Required. Accessible image label (`alt` / `aria-label`). Also shown as title
text beside the square icon when `mode` is `icon-title`.
</ParamField>
<ParamField path="icon" type="string | BrandLogoConfig">
Square image used in collapsed and compact surfaces. Optional for
`icon-title` (default mark); required for `icon-only` and `logo`.
</ParamField>
<ParamField path="logo" type="string | BrandLogoConfig">
Image URL, or per-mode sources. Omit to pair your name with the default mark.
Wider image used when `mode` is `logo`. Falls back to `icon` in
`BrandLogo` if needed. Required for `mode: "logo"`.
</ParamField>
<ParamField path="href" type="string">
Wraps configured brand images in a same-tab link.
</ParamField>
</Expandable>

Expand All @@ -154,14 +168,12 @@ import { TrueForgeUI } from "@truefoundry/trueforge-ui";
<ParamField path="src" type="string">
Mode-agnostic source, used when neither `light` nor `dark` is set.
</ParamField>
<ParamField path="href" type="string">
Wraps the logo in a same-tab link, labelled with `name`.
</ParamField>
</Expandable>

A single configured mode is used for both modes, so `{ light }` alone never renders a
missing image. To render a component rather than an image, override the `BrandLogo` slot
through `overrides`.
missing image. Use `BrandLogo` with `variant="icon"` or `variant="logo"` in custom
layouts. To render a component rather than an image, override its slot through
`overrides`.
</ParamField>
<ParamField path="icons" type="IconMap">
Replace registry icons by name. Values are Lucide components, React nodes, render
Expand Down Expand Up @@ -238,7 +250,10 @@ Persisted sessions remain available through the thread list.
server={{ type: "truefoundry", apiKey, controlPlaneURL }}
layout="sidebar"
agentConfig={{ mode: "AgentComposer", defaultAgentSpec: { model: { name: "openai-main/gpt-4.1" } } }}
theme={{ preset: "claude", brand: { name: "Acme", logo: "/logo.svg" } }}
theme={{
preset: "claude",
brand: { mode: "logo", name: "Acme", icon: "/icon.svg", logo: "/wordmark.svg" },
}}
overrides={{ ClearChatButton: MyClearButton }}
className="h-full min-h-0"
onError={(e) => reportError(e)}
Expand Down
19 changes: 19 additions & 0 deletions packages/trueforge-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Unreleased

### Minor Changes

- **`brand.mode`** — explicit chrome look. Set `mode`, then pass the fields it requires.
`name` always labels the mark (`alt` / `aria-label`):
- **`'icon-title'`** — `name` + optional `icon` (title shown in expanded chrome)
- **`'icon-only'`** — `name` + `icon` (alt kept, no title text)
- **`'logo'`** — `name` + `icon` + `logo` (wordmark in expanded chrome; `name` is alt only)
- **Default** — omit `brand` for the TrueForge wordmark / square mark
- **`resolveBrandChrome()`** — maps `brand.mode` to
`{ expandedVariant, collapsedVariant, showTitle }`. `SidebarLayout` uses it; custom
layouts should too.

### Changed

- **`BrandConfig`** — discriminated on `mode` (`BrandMode`). Removed `showTitle` from
config; visible title follows the mode.

## 0.2.4

### Patch Changes
Expand Down
52 changes: 40 additions & 12 deletions packages/trueforge-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export default function App() {
}}
theme={{
preset: 'claude',
brand: { name: 'Acme', logo: '/logo.svg' },
brand: { mode: 'icon-title', name: 'Acme', icon: '/icon.svg' },
}}
overrides={{/* slot overrides */}}
className="h-full"
Expand Down Expand Up @@ -319,38 +319,64 @@ package). Host CSS on `.aui-markdown` / `.aui-syntax-highlighter` / `.aui-openui

## Brand / logo

**Base layouts** — pass `theme.brand` and the SDK positions the mark + name in the default slots:
Set `brand.mode`, then pass the fields that mode requires. `name` always labels the
mark (`alt` / `aria-label`).

| Look | `mode` | Required | Expanded chrome | Collapsed / compact |
| ------------ | -------------- | -------------------------- | ------------------------- | ------------------- |
| Default | omit `brand` | — | TrueForge wordmark | TrueForge square |
| Icon + title | `'icon-title'` | `name` (+ optional `icon`) | square + title text | square |
| Icon only | `'icon-only'` | `name`, `icon` | square (no title text) | square |
| Wide logo | `'logo'` | `name`, `icon`, `logo` | wide logo (no title text) | square |

```tsx
<TrueForgeUI
server={server}
layout="sidebar"
theme={{
brand: {
mode: 'icon-title',
name: 'Acme',
logo: '/brand/logo.svg',
icon: '/brand/icon.svg',
},
}}
/>
```

**Light / dark marks** — pass `light` / `dark` sources instead and the SDK picks the one matching
the resolved mode. `href` wraps the logo in a same-tab link:
Icon-only chrome (`name` kept for alt):

```tsx
theme={{
brand: {
mode: 'icon-only',
name: 'Acme',
icon: '/brand/icon.svg',
},
}}
```

Wide logo for expanded chrome. A square `icon` is required because collapsed chrome,
the welcome screen, and the widget button continue to use the square asset:

```tsx
<TrueForgeUI
server={server}
layout="sidebar"
theme={{
brand: {
mode: 'logo',
name: 'Acme',
logo: { light: '/brand/logo-light.svg', dark: '/brand/logo-dark.svg', href: '/' },
icon: '/brand/icon.svg',
logo: '/brand/wordmark.svg',
href: '/',
},
}}
/>
```

Set only one mode and it is used for both. `name` labels the image, so no `alt` is needed.
Both `icon` and `logo` accept `{ src, light, dark }`. The SDK picks the source matching the
resolved theme mode; setting only one light/dark source uses it for both. `href` wraps
configured images in a same-tab link.

**Component marks** — `theme.brand` takes image URLs only. To render an inline SVG or a custom
component, override the `BrandLogo` slot, the same way you replace any other atom:
Expand All @@ -359,7 +385,10 @@ component, override the `BrandLogo` slot, the same way you replace any other ato
<TrueForgeUI server={server} layout="sidebar" overrides={{ BrandLogo: MyMark }} />
```

**Custom layouts** — import `BrandLogo` and place it anywhere; pair it with `useBrandName()` when you also want the name as text (see [Custom layouts](#custom-layouts)).
**Custom layouts** — import `BrandLogo` and use `variant="icon"` for compact surfaces or
`variant="logo"` for expanded chrome. Prefer `resolveBrandChrome(useBrand())` so expanded
vs collapsed choices match the base layouts. Pair with `useBrandName()` when chrome should
show the title text (see [Custom layouts](#custom-layouts)).

> _Screenshot: external brand mark rendered in the base layout header._

Expand Down Expand Up @@ -429,17 +458,15 @@ Built-in `layout` values:
For full control, pass a React component as `layout`. The SDK still wires server, shell mode, slots, and runtime behind it.

```tsx
import { Thread, ThreadListContainer, BrandLogo, useBrandName, useTheme } from '@truefoundry/trueforge-ui';
import { Thread, ThreadListContainer, BrandLogo, useTheme } from '@truefoundry/trueforge-ui';

function Layout({ className }: { className?: string }) {
const { mode, setTheme } = useTheme();
const brandName = useBrandName();

return (
<div className={className} style={{ display: 'flex', height: '100%' }}>
<aside style={{ width: 256 }}>
<BrandLogo className="size-6" />
<span>{brandName}</span>
<BrandLogo variant="logo" className="h-6 w-auto max-w-40" />
<ThreadListContainer />
</aside>
<main style={{ flex: 1, minWidth: 0 }}>
Expand Down Expand Up @@ -587,6 +614,7 @@ See [docs/server.md](./docs/server.md) for the full method list and BYO guidance
| `TrueForgeServerConfig` | Type | `server` prop: `truefoundry` / `trueforge` / `AgentUIServer` |
| `createTrueFoundryServer` | Function | Compose chat + builder into `AgentUIServer` |
| `Thread`, `ThreadListContainer`, `BrandLogo` | Components | Layout primitives for custom layouts |
| `resolveBrandChrome`, `useBrandName`, `useBrand` | Helpers | Brand chrome look + name for custom layouts |
| Composer / message / tool atoms | Components | Overridable, themeable building blocks |
| `SlotsProvider`, `useSlot`, `useTheme` | API | Overrides + theme mode |
| `AgentUIServer`, `AgentChatServer`, `AgentBuilderServer` | Types | Resolved server contract |
Expand Down
2 changes: 1 addition & 1 deletion packages/trueforge-ui/docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ over hacking third-party CSS:
preset: 'claude',
mode: 'dark',
tokens: { primary: '#e11d48' },
brand: { name: 'Acme', logo: '/brand/logo.svg' },
brand: { mode: 'logo', name: 'Acme', icon: '/brand/icon.svg', logo: '/brand/wordmark.svg' },
icons: { send: MySendSvg },
classNames: {
markdown: 'prose max-w-none',
Expand Down
2 changes: 2 additions & 0 deletions packages/trueforge-ui/docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,9 @@ export function App() {
theme={{
preset: 'chatgpt',
brand: {
mode: 'logo',
name: 'MyCo',
icon: { src: '/myco-icon.svg' },
logo: { src: '/myco-wordmark.svg' },
},
}}
Expand Down
Loading