From 6c23dba51bc2be2ebc697e4f26e2afab680759c2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 3 May 2026 17:30:31 -0400 Subject: [PATCH] feat(web/m7-363): branding helper + window.__MINSTREL__ consumer --- web/src/lib/branding.test.ts | 53 ++++++++++++++++++++++++++++++++++++ web/src/lib/branding.ts | 33 ++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 web/src/lib/branding.test.ts create mode 100644 web/src/lib/branding.ts diff --git a/web/src/lib/branding.test.ts b/web/src/lib/branding.test.ts new file mode 100644 index 00000000..f923db51 --- /dev/null +++ b/web/src/lib/branding.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { appName, description, pageTitle } from './branding'; + +const setGlobal = (g: { appName: string; description: string } | undefined) => { + if (g === undefined) { + delete (window as { __MINSTREL__?: unknown }).__MINSTREL__; + } else { + (window as { __MINSTREL__?: typeof g }).__MINSTREL__ = g; + } +}; + +describe('branding helpers', () => { + beforeEach(() => setGlobal(undefined)); + afterEach(() => setGlobal(undefined)); + + it('appName returns the global value when set', () => { + setGlobal({ appName: 'Family Jukebox', description: 'home' }); + expect(appName()).toBe('Family Jukebox'); + }); + + it('appName falls back to "Minstrel" when global is undefined', () => { + expect(appName()).toBe('Minstrel'); + }); + + it('appName falls back to "Minstrel" when value contains an unrendered Go token', () => { + setGlobal({ appName: '{{ .AppName }}', description: 'x' }); + expect(appName()).toBe('Minstrel'); + }); + + it('description returns the global value when set', () => { + setGlobal({ appName: 'Minstrel', description: 'Our home library.' }); + expect(description()).toBe('Our home library.'); + }); + + it('description falls back to a default tagline when unset', () => { + expect(description()).toContain('Self-hosted music server'); + }); + + it('pageTitle with section returns "{appName} · {section}"', () => { + setGlobal({ appName: 'Minstrel', description: 'x' }); + expect(pageTitle('Search')).toBe('Minstrel · Search'); + }); + + it('pageTitle without section returns just the appName', () => { + setGlobal({ appName: 'Minstrel', description: 'x' }); + expect(pageTitle()).toBe('Minstrel'); + }); + + it('pageTitle composes nested sections via the · separator', () => { + setGlobal({ appName: 'Minstrel', description: 'x' }); + expect(pageTitle('Library · Artists')).toBe('Minstrel · Library · Artists'); + }); +}); diff --git a/web/src/lib/branding.ts b/web/src/lib/branding.ts new file mode 100644 index 00000000..8857b12f --- /dev/null +++ b/web/src/lib/branding.ts @@ -0,0 +1,33 @@ +declare global { + interface Window { + __MINSTREL__?: { appName: string; description: string }; + } +} + +const DEFAULT_APP_NAME = 'Minstrel'; +const DEFAULT_DESCRIPTION = + 'Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration.'; + +const isRenderedToken = (v: string | undefined): v is string => + typeof v === 'string' && v.length > 0 && !v.includes('{{'); + +export const appName = (): string => { + if (typeof window === 'undefined') return DEFAULT_APP_NAME; + return isRenderedToken(window.__MINSTREL__?.appName) + ? window.__MINSTREL__!.appName + : DEFAULT_APP_NAME; +}; + +export const description = (): string => { + if (typeof window === 'undefined') return DEFAULT_DESCRIPTION; + return isRenderedToken(window.__MINSTREL__?.description) + ? window.__MINSTREL__!.description + : DEFAULT_DESCRIPTION; +}; + +export const pageTitle = (section?: string): string => + section ? `${appName()} · ${section}` : appName(); + +// Ensure this file is treated as a module (the global declaration above +// otherwise turns it into a global script). +export {};