feat(branding): brand kit architecture (Phase 1) #96
11 changed files with 74 additions and 32 deletions
feat(branding): drive PWA manifest from brand.json
vite-branding.ts now loads brand.json into a typed `brand` object and
exports a `brandManifestName()` helper. Schema:
{ name (required), shortName?, themeColor?, backgroundColor? }
Default brand.json drops themeColor/backgroundColor — they're optional
overrides; per-app accents (wallet yellow, chat green, …) keep working
via `?? '<existing>'` fallbacks in each standalone's vite config.
events: manifest.name/short_name driven by brand. VITE_APP_NAME env
override stays (Phase 2 server-deploy migration still in flight) and,
when set, overrides both name and short_name to preserve pre-#95
behavior. cfaun's VITE_APP_NAME=Bouge keeps working unchanged.
hub (vite.config.ts): brand.name flows into %VITE_APP_NAME% Hub title.
7 other standalones (wallet, chat, market, forum, tasks, restaurant,
libra): only theme_color/background_color get brand overrides. Their
manifest.name/short_name stay hardcoded so multi-PWA home-screen
labels remain differentiated ("Wallet", "Chat", …) rather than all
collapsing to the brand short_name.
Verified default build: events manifest name=AIO; wallet keeps
"Wallet — Lightning" + #eab308 accent.
Verified VITE_APP_NAME=Sortir override: events name+short_name=Sortir.
Part of aiolabs/webapp#95.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
ce5a1a6a56
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "AIO",
|
"name": "AIO",
|
||||||
"shortName": "AIO",
|
"shortName": "AIO"
|
||||||
"themeColor": "#ffffff",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9,6 +10,28 @@ import { resolve } from 'node:path'
|
||||||
*/
|
*/
|
||||||
export const BRAND_DIR = resolve(process.env.BRAND_DIR ?? './branding/default')
|
export const BRAND_DIR = resolve(process.env.BRAND_DIR ?? './branding/default')
|
||||||
|
|
||||||
|
/** Fields parsed from brand.json. All but `name` are optional. */
|
||||||
|
export interface Brand {
|
||||||
|
/** Brand label — drives PWA manifest name. */
|
||||||
|
name: string
|
||||||
|
/** PWA install/home-screen short label. Defaults to `name`. */
|
||||||
|
shortName?: string
|
||||||
|
/**
|
||||||
|
* Optional PWA chrome theme color (status bar / title bar when installed).
|
||||||
|
* When unset, each standalone's vite config keeps its hardcoded accent.
|
||||||
|
*/
|
||||||
|
themeColor?: string
|
||||||
|
/**
|
||||||
|
* Optional PWA splash background. When unset, each standalone's vite
|
||||||
|
* config keeps its hardcoded value.
|
||||||
|
*/
|
||||||
|
backgroundColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const brand: Brand = JSON.parse(
|
||||||
|
readFileSync(resolve(BRAND_DIR, 'brand.json'), 'utf-8'),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spread into a vite config's `resolve.alias` map. Lets components
|
* Spread into a vite config's `resolve.alias` map. Lets components
|
||||||
* import deployer-provided assets via `@brand/<file>` instead of
|
* import deployer-provided assets via `@brand/<file>` instead of
|
||||||
|
|
@ -17,3 +40,14 @@ export const BRAND_DIR = resolve(process.env.BRAND_DIR ?? './branding/default')
|
||||||
export const brandAlias = {
|
export const brandAlias = {
|
||||||
'@brand': BRAND_DIR,
|
'@brand': BRAND_DIR,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PWA manifest name for a standalone. Combines the brand name with the
|
||||||
|
* app's own label, or returns the bare brand when no label is given.
|
||||||
|
*
|
||||||
|
* Example: `brandManifestName('Wallet')` → "AIO Wallet" / "Cfaun Wallet".
|
||||||
|
* Example: `brandManifestName()` → "AIO" / "Sortir".
|
||||||
|
*/
|
||||||
|
export function brandManifestName(appLabel?: string): string {
|
||||||
|
return appLabel ? `${brand.name} ${appLabel}` : brand.name
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
function chatHtmlPlugin(): Plugin {
|
function chatHtmlPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
|
@ -72,8 +72,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Chat — Encrypted',
|
name: 'Chat — Encrypted',
|
||||||
short_name: 'Chat',
|
short_name: 'Chat',
|
||||||
description: 'End-to-end encrypted Nostr chat',
|
description: 'End-to-end encrypted Nostr chat',
|
||||||
theme_color: '#16a34a',
|
theme_color: brand.themeColor ?? '#16a34a',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
|
||||||
import Inspect from 'vite-plugin-inspect'
|
import Inspect from 'vite-plugin-inspect'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
//
|
//
|
||||||
|
|
@ -13,6 +13,12 @@ import { brandAlias } from './vite-branding'
|
||||||
// the entire origin and blocked Chrome from offering installs for the
|
// the entire origin and blocked Chrome from offering installs for the
|
||||||
// path-mounted standalones at /libra/, /market/, etc. The hub is a
|
// path-mounted standalones at /libra/, /market/, etc. The hub is a
|
||||||
// launcher page; users install the standalones they actually use.
|
// launcher page; users install the standalones they actually use.
|
||||||
|
|
||||||
|
// Brand name flows into index.html's `%VITE_APP_NAME% Hub` title.
|
||||||
|
// VITE_APP_NAME env override stays during the Phase 2 server-deploy
|
||||||
|
// migration (aiolabs/webapp#95); drop once all hosts move to BRAND_DIR.
|
||||||
|
process.env.VITE_APP_NAME = process.env.VITE_APP_NAME ?? brand.name
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
||||||
cacheDir: 'node_modules/.vite-hub',
|
cacheDir: 'node_modules/.vite-hub',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias, brandManifestName } from './vite-branding'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to rewrite dev server requests to events.html
|
* Plugin to rewrite dev server requests to events.html
|
||||||
|
|
@ -42,10 +42,12 @@ function eventsHtmlPlugin(): Plugin {
|
||||||
* VITE_BASE_PATH=/events/ → app.ariege.io/events/ (shared auth)
|
* VITE_BASE_PATH=/events/ → app.ariege.io/events/ (shared auth)
|
||||||
* (default: /) → bouge.ariege.io (standalone subdomain)
|
* (default: /) → bouge.ariege.io (standalone subdomain)
|
||||||
*
|
*
|
||||||
* Set VITE_APP_NAME to brand the standalone (PWA name, HTML title, console
|
* Brand name resolves from brand.json under $BRAND_DIR (see
|
||||||
* logs). cfaun overrides to "Bouge" via NixOS. Defaults to "Events".
|
* vite-branding.ts and aiolabs/webapp#95). VITE_APP_NAME remains an
|
||||||
|
* env override during the Phase 2 server-deploy migration; drop the
|
||||||
|
* env path once all hosts have moved to BRAND_DIR.
|
||||||
*/
|
*/
|
||||||
const APP_NAME = process.env.VITE_APP_NAME || 'Events'
|
const APP_NAME = process.env.VITE_APP_NAME ?? brandManifestName()
|
||||||
// Surface the resolved value back into env so Vite's HTML %VITE_APP_NAME%
|
// Surface the resolved value back into env so Vite's HTML %VITE_APP_NAME%
|
||||||
// substitution picks up the fallback when nothing was explicitly set.
|
// substitution picks up the fallback when nothing was explicitly set.
|
||||||
process.env.VITE_APP_NAME = APP_NAME
|
process.env.VITE_APP_NAME = APP_NAME
|
||||||
|
|
@ -86,10 +88,12 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: APP_NAME,
|
name: APP_NAME,
|
||||||
short_name: APP_NAME,
|
// VITE_APP_NAME, when set, overrides both name and short_name to
|
||||||
|
// preserve the pre-#95 behavior. Otherwise brand.shortName drives.
|
||||||
|
short_name: process.env.VITE_APP_NAME ?? brand.shortName ?? APP_NAME,
|
||||||
description: 'Discover events near you',
|
description: 'Discover events near you',
|
||||||
theme_color: '#1f2937',
|
theme_color: brand.themeColor ?? '#1f2937',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
function forumHtmlPlugin(): Plugin {
|
function forumHtmlPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
|
@ -72,8 +72,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Forum — Discussions',
|
name: 'Forum — Discussions',
|
||||||
short_name: 'Forum',
|
short_name: 'Forum',
|
||||||
description: 'Decentralized link aggregator and discussion forum on Nostr',
|
description: 'Decentralized link aggregator and discussion forum on Nostr',
|
||||||
theme_color: '#2563eb',
|
theme_color: brand.themeColor ?? '#2563eb',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to rewrite dev server requests to libra.html
|
* Plugin to rewrite dev server requests to libra.html
|
||||||
|
|
@ -79,8 +79,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Libra — Team Accounting',
|
name: 'Libra — Team Accounting',
|
||||||
short_name: 'Libra',
|
short_name: 'Libra',
|
||||||
description: 'Team accounting and expense management',
|
description: 'Team accounting and expense management',
|
||||||
theme_color: '#1f2937',
|
theme_color: brand.themeColor ?? '#1f2937',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
function marketHtmlPlugin(): Plugin {
|
function marketHtmlPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
|
@ -72,8 +72,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Market — Nostr',
|
name: 'Market — Nostr',
|
||||||
short_name: 'Market',
|
short_name: 'Market',
|
||||||
description: 'Decentralized marketplace on Nostr with Lightning payments',
|
description: 'Decentralized marketplace on Nostr with Lightning payments',
|
||||||
theme_color: '#dc2626',
|
theme_color: brand.themeColor ?? '#dc2626',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
function restaurantHtmlPlugin(): Plugin {
|
function restaurantHtmlPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
|
@ -79,8 +79,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
description: 'Order from your local Nostr-native restaurant with Lightning payments',
|
description: 'Order from your local Nostr-native restaurant with Lightning payments',
|
||||||
// Green to differentiate from market red. PDF tile is purple
|
// Green to differentiate from market red. PDF tile is purple
|
||||||
// (see ~/dev/shared/extensions/restaurant/static/image/restaurant.png).
|
// (see ~/dev/shared/extensions/restaurant/static/image/restaurant.png).
|
||||||
theme_color: '#16a34a',
|
theme_color: brand.themeColor ?? '#16a34a',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
function tasksHtmlPlugin(): Plugin {
|
function tasksHtmlPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
|
@ -72,8 +72,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Tasks — Work Orders',
|
name: 'Tasks — Work Orders',
|
||||||
short_name: 'Tasks',
|
short_name: 'Tasks',
|
||||||
description: 'Decentralized task management on Nostr',
|
description: 'Decentralized task management on Nostr',
|
||||||
theme_color: '#4338ca',
|
theme_color: brand.themeColor ?? '#4338ca',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import { brandAlias } from './vite-branding'
|
import { brand, brandAlias } from './vite-branding'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to rewrite dev server requests to wallet.html
|
* Plugin to rewrite dev server requests to wallet.html
|
||||||
|
|
@ -78,8 +78,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
name: 'Wallet — Lightning',
|
name: 'Wallet — Lightning',
|
||||||
short_name: 'Wallet',
|
short_name: 'Wallet',
|
||||||
description: 'Lightning Network wallet — send, receive, and manage sats',
|
description: 'Lightning Network wallet — send, receive, and manage sats',
|
||||||
theme_color: '#eab308',
|
theme_color: brand.themeColor ?? '#eab308',
|
||||||
background_color: '#ffffff',
|
background_color: brand.backgroundColor ?? '#ffffff',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
orientation: 'portrait-primary',
|
||||||
start_url: process.env.VITE_BASE_PATH || '/',
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue