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
8 changes: 8 additions & 0 deletions assets/ignatius-logo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions assets/ignatius-logo-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions assets/noorm-logo.svg

This file was deleted.

8 changes: 4 additions & 4 deletions docs/design/branding.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The three output surfaces — interactive viewer, static dict HTML, static graph
- One config file at the models root drives branding for every surface.
- Logo + title + subtitle pinned upper-left, immune to pan/zoom.
- Footer at bottom-center, fixed, displaying user copyright + (by default) a "powered by Noorm" link that the user can disable.
- Sensible default: when the user supplies no branding, the page brands as Noorm Ignatius with the Noorm logo. Default case works offline.
- Sensible default: when the user supplies no branding, the page brands as Noorm Ignatius with the ignatius logo. Default case works offline.
- Static outputs are self-contained: logos inlined as base64 at generation time.
- Theme-aware: logo can have separate dark/light variants, with a string shorthand for the same-asset case.

Expand Down Expand Up @@ -52,7 +52,7 @@ Evidence: the parser already implements the "optional `_*.yaml` at models root"

# Logo: string shorthand OR explicit per-mode object.
# Each value can be a URL or a filepath relative to the models directory.
# Falls back to the embedded Noorm logo when absent.
# Falls back to the embedded ignatius logo when absent.
logo: "./assets/logo.svg" # shorthand: same asset for both modes
# OR
logo:
Expand All @@ -73,7 +73,7 @@ poweredBy: true # default true; set false to omit the Noorm
## Defaults (when the file is absent OR a field is unset)

```yaml
logo: <embedded Noorm SVG used for both dark and light>
logo: <embedded ignatius SVG, cream lettering for dark and slate for light>
title: "Noorm Ignatius"
subtitle: "Visualize your data model"
copyright:
Expand Down Expand Up @@ -136,7 +136,7 @@ Each value can be:
- Static outputs: read from disk, base64-inlined.

3. **Unset / null**:
- Falls back to the embedded Noorm SVG. No network access for this case.
- Falls back to the embedded ignatius SVG for the same mode. No network access for this case.

The Noorm SVG is committed to the repo and imported at compile time via Bun's `with { type: "file" }` attribute, the same mechanism that embeds the React bundle. It is NOT fetched at runtime.

Expand Down
33 changes: 21 additions & 12 deletions src/theme/branding-defaults.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isAbsolute, resolve as resolvePath } from 'path';

import noormLogoPath from '../../assets/noorm-logo.svg' with { type: 'file' };
import logoDarkPath from '../../assets/ignatius-logo-dark.svg' with { type: 'file' };
import logoLightPath from '../../assets/ignatius-logo-light.svg' with { type: 'file' };

export type LogoPair = {
dark: string;
Expand All @@ -20,14 +21,22 @@ export type Branding = {
poweredBy: boolean;
};

// Read the embedded SVG at module load and build a data URI.
// Read the embedded SVGs at module load and build data URIs.
// WHY: `with { type: "file" }` embeds the file into the compiled binary at
// $bunfs/ — Bun.file().arrayBuffer() works both in dev and in the binary.
const noormSvgBytes = new Uint8Array(await Bun.file(noormLogoPath).arrayBuffer());
const NOORM_DEFAULT_LOGO = `data:image/svg+xml;base64,${Buffer.from(noormSvgBytes).toString('base64')}`;
async function embedSvg(path: string): Promise<string> {
const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
return `data:image/svg+xml;base64,${Buffer.from(bytes).toString('base64')}`;
}

// The two files differ only in lettering colour: cream on dark, slate on light.
const DEFAULT_LOGO: LogoPair = {
dark: await embedSvg(logoDarkPath),
light: await embedSvg(logoLightPath),
};

export const defaultBranding: Branding = {
logo: { dark: NOORM_DEFAULT_LOGO, light: NOORM_DEFAULT_LOGO },
logo: DEFAULT_LOGO,
title: 'Noorm Ignatius',
subtitle: 'Visualize your data model',
get copyright(): CopyrightConfig {
Expand All @@ -51,8 +60,8 @@ function normalizeLogo(input: RawLogoInput): LogoPair {
if (typeof input === 'string') {
return { dark: input, light: input };
}
const dark = input.dark ?? input.light ?? NOORM_DEFAULT_LOGO;
const light = input.light ?? input.dark ?? NOORM_DEFAULT_LOGO;
const dark = input.dark ?? input.light ?? DEFAULT_LOGO.dark;
const light = input.light ?? input.dark ?? DEFAULT_LOGO.light;
return { dark, light };
}

Expand Down Expand Up @@ -96,19 +105,19 @@ const LOGO_MIME_BY_EXT: Record<string, string> = {
* an exported file is worse than the noorm mark, and parse has no findings
* channel at the point branding is merged.
*/
async function inlineLogo(value: string, modelRoot: string): Promise<string> {
async function inlineLogo(value: string, modelRoot: string, fallback: string): Promise<string> {
if (value.startsWith('data:') || value.startsWith('http://') || value.startsWith('https://')) {
return value;
}

const dot = value.lastIndexOf('.');
const mime = dot === -1 ? undefined : LOGO_MIME_BY_EXT[value.slice(dot).toLowerCase()];

if (mime === undefined) return NOORM_DEFAULT_LOGO;
if (mime === undefined) return fallback;

const file = Bun.file(isAbsolute(value) ? value : resolvePath(modelRoot, value));

if (!(await file.exists())) return NOORM_DEFAULT_LOGO;
if (!(await file.exists())) return fallback;

const bytes = new Uint8Array(await file.arrayBuffer());

Expand All @@ -126,8 +135,8 @@ async function inlineLogo(value: string, modelRoot: string): Promise<string> {
*/
export async function inlineBrandingLogos(branding: Branding, modelRoot: string): Promise<Branding> {
const [dark, light] = await Promise.all([
inlineLogo(branding.logo.dark, modelRoot),
inlineLogo(branding.logo.light, modelRoot),
inlineLogo(branding.logo.dark, modelRoot, DEFAULT_LOGO.dark),
inlineLogo(branding.logo.light, modelRoot, DEFAULT_LOGO.light),
]);

return { ...branding, logo: { dark, light } };
Expand Down
Loading