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
24 changes: 24 additions & 0 deletions src/components/brand.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ describe("brand components", () => {
await expectNoAxeViolations(container);
});

it("overrides only the wordmark text with label, keeping the theme symbol", async () => {
const { container } = render(
<ThemeProvider theme={mind} forcedTheme="light" enableSystem={false}>
<Logo label="Drive" />
</ThemeProvider>,
);
// The product name renders in place of the default "Mind" wordmark...
expect(screen.getByText("Drive")).toBeInTheDocument();
expect(screen.queryByText("Mind")).not.toBeInTheDocument();
// ...in the same serif lockup, and the shared mark is still painted.
expect(screen.getByText("Drive")).toHaveClass("font-serif");
expect(container.querySelector("img")).toBeInTheDocument();
await expectNoAxeViolations(container);
});

it("defaults the wordmark to the theme label when no label is given", () => {
render(
<ThemeProvider theme={mind} forcedTheme="light" enableSystem={false}>
<Logo />
</ThemeProvider>,
);
expect(screen.getByText("Mind")).toBeInTheDocument();
});

it("drops the wordmark with symbolOnly", () => {
render(
<ThemeProvider theme={mind} forcedTheme="light" enableSystem={false}>
Expand Down
14 changes: 11 additions & 3 deletions src/components/brand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export type SymbolProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"
export type LogoProps = React.HTMLAttributes<HTMLDivElement> & {
/** Render only the symbol, dropping the wordmark text. */
symbolOnly?: boolean;
/**
* Wordmark text. Defaults to the active theme's `label` ("Mind"). Pass the
* product name (e.g. `"Drive"`) so an app's lockup reads as the shared mark +
* its own name, set in the same brand serif. Overrides only the text — the
* symbol/mark still comes from the active theme.
*/
label?: string;
};

/**
Expand All @@ -32,15 +39,16 @@ export type LogoProps = React.HTMLAttributes<HTMLDivElement> & {
* fully bespoke wordmark image via its `logo` asset; when present that image is
* used as-is instead.
*/
export function Logo({ className, symbolOnly, ...props }: LogoProps) {
export function Logo({ className, symbolOnly, label, ...props }: LogoProps) {
const { theme, resolvedMode } = useMindTheme();
const mode = pickMode(resolvedMode);
const wordmark = label ?? theme.label;
const custom = theme.logo?.[mode];
if (custom) {
return (
<div className={cn("flex items-center", className)} {...props}>
{/* eslint-disable-next-line @next/next/no-img-element -- data-URI brand asset */}
<img src={custom} alt={`${theme.label} logo`} className="h-8 w-auto" />
<img src={custom} alt={`${wordmark} logo`} className="h-8 w-auto" />
</div>
);
}
Expand All @@ -53,7 +61,7 @@ export function Logo({ className, symbolOnly, ...props }: LogoProps) {
) : null}
{symbolOnly ? null : (
<span className="font-serif font-semibold text-2xl leading-none tracking-tight">
{theme.label}
{wordmark}
</span>
)}
</div>
Expand Down
17 changes: 17 additions & 0 deletions stories/foundations/Logo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ import { Logo, Symbol } from "@mind-studio/ui";

<Canvas of={LogoStories.Lockups} />

## 🏷️ Per-app wordmark

Every Mind product **shares the one weave mark** but signs itself with **its own
name** — the mark + "Drive", "Slides", "Chat", and so on — set in the same Fraunces
serif, weight, and size as the master wordmark. Pass the product name via `label`;
it overrides **only the text**, never the artwork. With no `label`, the lockup falls
back to the active brand's name ("Mind"), so the host shell and landing stay
unchanged.

```tsx
<Logo label="Drive" /> {/* mark + "Drive" in the brand serif */}
<Logo label="Slides" /> {/* mark + "Slides" */}
<Logo /> {/* mark + "Mind" (default) */}
```

<Canvas of={LogoStories.ProductLockups} />

## 📐 Construction

The mark is drawn on a circular grid — every node shares one unit radius and a
Expand Down
30 changes: 28 additions & 2 deletions stories/foundations/Logo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ function Caption({ children }: { children: React.ReactNode }) {
* renders in the real typeface rather than a baked image. `ink` fixes the text
* colour for the panel it sits on.
*/
function Wordmark({ ink, size = 40 }: { ink: string; size?: number }) {
function Wordmark({ ink, size = 40, text = "Mind" }: { ink: string; size?: number; text?: string }) {
return (
<span className="inline-flex items-center" style={{ gap: size * 0.28 }}>
<img src={MARK} alt="" aria-hidden style={{ height: size, width: "auto" }} />
<span
className="font-serif font-semibold leading-none tracking-tight"
style={{ fontSize: size * 0.82, color: ink }}
>
Mind
{text}
</span>
</span>
);
Expand Down Expand Up @@ -108,6 +108,32 @@ export const Lockups: Story = {
),
};

/* ------------------------------------------------------- product wordmarks */

const PRODUCTS = ["Mind", "Drive", "Slides", "Chat", "Calendar", "Videos"] as const;

export const ProductLockups: Story = {
tags: ["!dev"],
render: () => (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Panel bg={LIGHT_BG} label="Per-app wordmark · light background">
<div className="flex flex-col items-start gap-4">
{PRODUCTS.map((p) => (
<Wordmark key={p} ink="#18181b" size={36} text={p} />
))}
</div>
</Panel>
<Panel bg={DARK_BG} dark label="Per-app wordmark · dark background">
<div className="flex flex-col items-start gap-4">
{PRODUCTS.map((p) => (
<Wordmark key={p} ink="#fafafa" size={36} text={p} />
))}
</div>
</Panel>
</div>
),
};

/* ------------------------------------------------------------ construction */

export const Construction: Story = {
Expand Down