feat(shared): make @shared the single home for brand logo assets (#6714)

## What

Makes `@shared` the single home for the Stirling brand logo assets.
Moves the editor's two logo sets — `classic-logo` + `modern-logo` (22
files: marks, wordmarks, favicons, login headers, PNGs) — out of
`editor/public/` into `shared/assets/brand/`, and adds a Storybook
**Brand/Logos** gallery.

## Why this shape (not a plain move)

The editor serves logos by **URL** from `public/` and switches
`classic`/`modern` by a **user preference** (`useLogoAssets`,
`manifest.json` / `manifest-classic.json`, `index.html` favicon links).
Rewiring all that to module imports would be a large, risky change to
the variant system.

Instead the editor keeps its variant system **unchanged** and just
sources the files from shared: `vite-plugin-static-copy` copies
`shared/assets/brand/{classic,modern}-logo/*` back to the served
`/{classic,modern}-logo` paths (the editor already uses this plugin for
pdfium/pdfjs assets). Single source of truth in shared, zero editor
code/manifest/markup changes.

## Verified

- **Build:** editor builds with both sets present at
`dist/{modern,classic}-logo/`; `manifest.json` + favicon refs resolve.
- **Dev:** the vite dev server serves the bridged paths —
`/modern-logo/logo512.png`,
`/modern-logo/StirlingPDFLogoNoTextDark.svg`,
`/classic-logo/favicon.ico` all return **HTTP 200** (the plugin's dev
middleware).
- Typecheck clean on core/proprietary/saas; prettier clean; `storybook
build` succeeds with the `Brand/Logos` gallery bundled.
- The portal's existing `@shared/assets` brand imports are untouched.

## Follow-ups (not in this PR)

- **Dedup:** `shared/assets/stirling-mark-*.svg` is byte-identical to
`brand/modern-logo/StirlingPDFLogoNoTextDark.svg`, and
`stirling-pdf-logo-*` is a near-twin of the modern wordmark. Reconciling
these (and re-pointing the portal) needs a designer eye on which
wordmark is canonical, so it's left out here to avoid changing the
portal's rendered logo.
- `editor/src/logo.svg` appears unused (no references) — candidate for
deletion separately.
This commit is contained in:
Reece Browne
2026-06-30 11:24:14 +00:00
committed by GitHub
parent 425b76e9a7
commit 0beff1a92b
36 changed files with 131 additions and 25 deletions
@@ -10,6 +10,11 @@ import type { LogoVariant } from "@app/services/preferencesService";
*/
describe("useLogoAssets - Logo Asset Files", () => {
const publicDir = path.resolve(__dirname, "../../../public");
// Brand logo assets live in the shared design system; the editor's vite
// config copies shared/assets/brand/<folder>/* into the served root at build
// time (see viteStaticCopy in editor/vite.config.ts), so useLogoAssets can
// keep referencing them by their public-URL path. Validate them at source.
const brandDir = path.resolve(__dirname, "../../../../shared/assets/brand");
// All asset files that useLogoAssets references
const requiredAssets = [
@@ -27,7 +32,7 @@ describe("useLogoAssets - Logo Asset Files", () => {
describe.each(logoVariants)("%s logo variant", (variant) => {
const folder = LOGO_FOLDER_BY_VARIANT[variant];
const folderPath = path.join(publicDir, folder);
const folderPath = path.join(brandDir, folder);
test(`folder "${folder}" should exist`, () => {
expect(fs.existsSync(folderPath)).toBe(true);
@@ -22,7 +22,7 @@ import {
import SpringLoginForm from "@shared/auth/ui/SpringLoginForm";
import { useSpringLogin } from "@shared/auth/ui/useSpringLogin";
import LoggedInState from "@app/routes/login/LoggedInState";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
export default function Login() {
const navigate = useNavigate();
@@ -16,7 +16,7 @@ import {
SignupFieldErrors,
} from "@app/routes/signup/SignupFormValidation";
import { useAuthService } from "@app/routes/signup/AuthService";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
export default function Signup() {
const navigate = useNavigate();
+1 -1
View File
@@ -19,7 +19,7 @@ import ErrorMessage from "@shared/auth/ui/ErrorMessage";
import EmailPasswordForm from "@app/routes/login/EmailPasswordForm";
import OAuthButtons from "@app/routes/login/OAuthButtons";
import LoggedInState from "@app/routes/login/LoggedInState";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
export default function Login() {
const navigate = useNavigate();
@@ -8,7 +8,7 @@ import "@shared/auth/ui/auth.css";
import "@app/routes/authShared/saas-auth.css";
import { withBasePath } from "@app/constants/app";
import ErrorMessage from "@shared/auth/ui/ErrorMessage";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
/**
* OAuth 2.1 consent screen for the Supabase OAuth server (used by MCP clients
+1 -1
View File
@@ -19,7 +19,7 @@ import {
SignupFieldErrors,
} from "@app/routes/signup/SignupFormValidation";
import { useAuthService } from "@app/routes/signup/AuthService";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
export default function Signup() {
const navigate = useNavigate();
+11
View File
@@ -271,6 +271,17 @@ export default defineConfig(async ({ mode }) => {
src: "../node_modules/pdfjs-dist/standard_fonts/*",
dest: "pdfjs/standard_fonts",
},
{
// Brand assets live in the shared design system; the editor serves
// them by URL per variant, so copy each set to the /{variant}-logo
// path its manifests, index.html and useLogoAssets resolve against.
src: "../shared/assets/brand/classic-logo/*",
dest: "classic-logo",
},
{
src: "../shared/assets/brand/modern-logo/*",
dest: "modern-logo",
},
],
}),
compressStaticCopyPlugin(),
@@ -7,7 +7,7 @@ import SpringLoginForm from "@shared/auth/ui/SpringLoginForm";
import { useSpringLogin } from "@shared/auth/ui/useSpringLogin";
import "@shared/auth/ui/auth-theme.css";
import "@shared/auth/ui/auth.css";
import loginHeader from "@shared/assets/login/LoginLightModeHeader.svg";
import loginHeader from "@shared/assets/brand/modern-logo/LoginLightModeHeader.svg";
/**
* Full-screen login shown by the portal's auth gate. Renders the same screen as
+2 -2
View File
@@ -8,8 +8,8 @@ import { useLink } from "@portal/contexts/LinkContext";
import { useAsync } from "@portal/hooks/useAsync";
import { fetchHomeKpis, type KpiEntry } from "@portal/api/home";
import { EDITOR_URL } from "@portal/auth/editorUrl";
import markLight from "@shared/assets/stirling-mark-light.svg";
import markDark from "@shared/assets/stirling-mark-dark.svg";
import markLight from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg";
import markDark from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg";
import {
HomeIcon,
UsersIcon,
+106
View File
@@ -0,0 +1,106 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
// Visual catalogue of the shared brand assets (shared/assets/brand) — the single
// source of truth for the Stirling logos used across the apps.
import modernMarkDark from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg";
import modernMarkLight from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg";
import modernBlack from "@shared/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg";
import modernWhite from "@shared/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg";
import modernGrey from "@shared/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg";
import classicMarkDark from "@shared/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg";
import classicMarkLight from "@shared/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg";
import classicBlack from "@shared/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg";
import classicWhite from "@shared/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg";
import classicGrey from "@shared/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg";
type Asset = { label: string; src: string; onDark?: boolean };
type VariantSet = { variant: string; mark: Asset[]; wordmark: Asset[] };
const SETS: VariantSet[] = [
{
variant: "modern",
mark: [
{ label: "NoTextDark", src: modernMarkDark },
{ label: "NoTextLight", src: modernMarkLight, onDark: true },
],
wordmark: [
{ label: "BlackText", src: modernBlack },
{ label: "GreyText", src: modernGrey },
{ label: "WhiteText", src: modernWhite, onDark: true },
],
},
{
variant: "classic",
mark: [
{ label: "NoTextDark", src: classicMarkDark },
{ label: "NoTextLight", src: classicMarkLight, onDark: true },
],
wordmark: [
{ label: "BlackText", src: classicBlack },
{ label: "GreyText", src: classicGrey },
{ label: "WhiteText", src: classicWhite, onDark: true },
],
},
];
function Swatch({ label, src, onDark, h }: Asset & { h: number }) {
return (
<figure
style={{ margin: 0, display: "grid", gap: 6, justifyItems: "center" }}
>
<div
style={{
display: "grid",
placeItems: "center",
padding: 16,
minWidth: 140,
borderRadius: 8,
border: "1px solid rgba(128,128,128,0.25)",
background: onDark ? "#1a1a1a" : "#ffffff",
}}
>
<img src={src} alt={label} style={{ height: h, maxWidth: 200 }} />
</div>
<figcaption style={{ fontSize: 12, color: "var(--text-muted, #71717a)" }}>
{label}
</figcaption>
</figure>
);
}
function Row({ title, items }: { title: string; items: Asset[] }) {
return (
<div style={{ display: "grid", gap: 8 }}>
<h4 style={{ margin: 0, textTransform: "capitalize" }}>{title}</h4>
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
{items.map((a) => (
<Swatch key={a.label} {...a} h={title === "mark" ? 48 : 28} />
))}
</div>
</div>
);
}
const meta: Meta = {
title: "Brand/Logos",
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj;
/** Every brand mark + wordmark, per variant, on the background each is built for. */
export const Logos: Story = {
render: () => (
<div style={{ display: "grid", gap: 32 }}>
{SETS.map((set) => (
<section key={set.variant} style={{ display: "grid", gap: 16 }}>
<h3 style={{ margin: 0, textTransform: "capitalize" }}>
{set.variant}
</h3>
<Row title="mark" items={set.mark} />
<Row title="wordmark" items={set.wordmark} />
</section>
))}
</div>
),
};

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before

Width:  |  Height:  |  Size: 253 B

After

Width:  |  Height:  |  Size: 253 B

Before

Width:  |  Height:  |  Size: 253 B

After

Width:  |  Height:  |  Size: 253 B

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -1,4 +0,0 @@
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#E6E6E6" fill-opacity="0.4"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#E6E6E6" fill-opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 253 B

@@ -1,4 +0,0 @@
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#ACACAC" fill-opacity="0.3"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#FC9999" fill-opacity="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 253 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB