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.
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |