Reverses the events-only hide (c037d45) now that the link has a real
home. Three parts:
- Add a @brand-hub-logo alias (brandHubLogoAliasEntry) resolving to the
brand's primary/global logo — the HUB's logo, never the per-standalone
@brand-app-logo. Wired into all 9 app vite configs since the shared
ProfileSheetContent renders it.
- Restructure the profile sheet into a fixed-height flex column: a
flex-1 min-h-0 overflow-y-auto scroll region over a shrink-0 footer,
so "Back to hub" + the log-in/out bar stay pinned to the bottom while
the identity/preferences area scrolls.
- Move the edit-profile Dialog out of the flex root (it portals to body,
so it's not part of the sheet flow).
Logo bumped to w-8 h-8, centered, with tightened footer padding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import type { Plugin } from 'vite'
|
|
|
|
/**
|
|
* Absolute path to the active brand kit. Deployers point this at their
|
|
* own `branding/<name>/` directory (see branding/README.md).
|
|
*
|
|
* Defaults to the committed aiolabs default brand. Used by vite configs
|
|
* for the `@brand` import alias and by pwa-assets.config.ts.
|
|
*/
|
|
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
|
|
/**
|
|
* Optional default in-app theme mode (light / dark / system). Sets the
|
|
* INITIAL value the theme-provider uses when the user has no saved
|
|
* preference; a user's later choice still wins and persists. Unset →
|
|
* the app's built-in default ('dark'). Distinct from `themeColor`,
|
|
* which is PWA chrome only.
|
|
*/
|
|
theme?: 'light' | 'dark' | 'system'
|
|
/**
|
|
* Optional default in-app color palette (e.g. 'darkmatter'). Same
|
|
* initial-default semantics as `theme`. Must be one of the palettes in
|
|
* src/components/theme-provider (PALETTES). Unset → 'catppuccin'.
|
|
*/
|
|
palette?: string
|
|
}
|
|
|
|
export const brand: Brand = JSON.parse(
|
|
readFileSync(resolve(BRAND_DIR, 'brand.json'), 'utf-8'),
|
|
)
|
|
|
|
// Surface the brand's in-app theme defaults to the client as VITE_*
|
|
// env vars (read by the theme-provider). Set here at module load — every
|
|
// vite.<app>.config.ts imports this file — so the default applies
|
|
// app-wide (hub + all standalones) without per-config wiring. Always
|
|
// assigned (empty when unset) so a prior brand's value can't leak into a
|
|
// later build in the same process.
|
|
process.env.VITE_BRAND_THEME = brand.theme ?? ''
|
|
process.env.VITE_BRAND_PALETTE = brand.palette ?? ''
|
|
|
|
/**
|
|
* Spread into a vite config's `resolve.alias` map. Lets components
|
|
* import deployer-provided assets via `@brand/<file>` instead of
|
|
* hardcoding `@/assets/logo.png`.
|
|
*/
|
|
export const brandAlias = {
|
|
'@brand': BRAND_DIR,
|
|
} as const
|
|
|
|
/**
|
|
* Resolution order for the in-app logo of a given standalone. Mirrors
|
|
* what pwa-assets.config.ts does for PWA icons: per-standalone override
|
|
* first (SVG then PNG), then the brand's primary logo (SVG then PNG).
|
|
*
|
|
* Returned path is absolute so vite alias can map directly to it.
|
|
*/
|
|
export function resolveAppLogo(app?: string): string {
|
|
const candidates: string[] = []
|
|
if (app) {
|
|
candidates.push(
|
|
join(BRAND_DIR, 'icons', app, 'logo.svg'),
|
|
join(BRAND_DIR, 'icons', app, 'logo.png'),
|
|
)
|
|
}
|
|
candidates.push(
|
|
join(BRAND_DIR, 'logo.svg'),
|
|
join(BRAND_DIR, 'logo.png'),
|
|
)
|
|
const found = candidates.find((p) => existsSync(p))
|
|
if (!found) {
|
|
throw new Error(
|
|
`No brand logo found for app="${app ?? ''}". Tried:\n ${candidates.join('\n ')}\n` +
|
|
`See branding/README.md for the brand kit contract.`,
|
|
)
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* Standalone-aware brand-logo alias entry. Append to a vite config's
|
|
* `resolve.alias` array alongside the rest of the alias map. The
|
|
* regex matches `@brand-app-logo` with or without a `?url` query so
|
|
* `import logoUrl from '@brand-app-logo?url'` resolves to the active
|
|
* standalone's logo file (per-app override or global), with no
|
|
* fallback chain in the component itself.
|
|
*
|
|
* Note: when used with the object form of resolve.alias, a bare
|
|
* `@brand` entry would shadow this — combine the two as an array
|
|
* (see vite.events.config.ts).
|
|
*/
|
|
export function brandAppLogoAliasEntry(app?: string) {
|
|
const resolved = resolveAppLogo(app)
|
|
return {
|
|
find: /^@brand-app-logo(\?.*)?$/,
|
|
replacement: `${resolved}$1`,
|
|
} as const
|
|
}
|
|
|
|
/**
|
|
* Hub-logo alias entry. Resolves `@brand-hub-logo` to the brand's
|
|
* primary/global logo (the hub's logo), independent of which standalone
|
|
* is building. Unlike {@link brandAppLogoAliasEntry}, this never takes an
|
|
* `app` argument — the "Back to hub" link in every standalone must point
|
|
* at the HUB's logo, not the current standalone's own logo. Wire it into
|
|
* every vite.<app>.config.ts that builds ProfileSheetContent.vue.
|
|
*/
|
|
export function brandHubLogoAliasEntry() {
|
|
const resolved = resolveAppLogo()
|
|
return {
|
|
find: /^@brand-hub-logo(\?.*)?$/,
|
|
replacement: `${resolved}$1`,
|
|
} as const
|
|
}
|
|
|
|
/**
|
|
* Optional brand banner — a wide lockup (logo + wordmark in one image)
|
|
* that replaces the logo + app-name pair in a standalone's header.
|
|
*
|
|
* Resolution mirrors {@link resolveAppLogo} (per-standalone override
|
|
* first, then the brand's primary banner), but a banner is OPTIONAL:
|
|
* returns `null` when none is found instead of throwing. Brands that
|
|
* don't ship a banner keep the default logo + name rendering.
|
|
*/
|
|
export function resolveAppBanner(app?: string): string | null {
|
|
const candidates: string[] = []
|
|
if (app) {
|
|
candidates.push(
|
|
join(BRAND_DIR, 'icons', app, 'banner.svg'),
|
|
join(BRAND_DIR, 'icons', app, 'banner.png'),
|
|
)
|
|
}
|
|
candidates.push(
|
|
join(BRAND_DIR, 'banner.svg'),
|
|
join(BRAND_DIR, 'banner.png'),
|
|
)
|
|
return candidates.find((p) => existsSync(p)) ?? null
|
|
}
|
|
|
|
/**
|
|
* Standalone-aware brand-banner alias entry, the banner sibling of
|
|
* {@link brandAppLogoAliasEntry}. Always registers the
|
|
* `@brand-app-banner` alias so the static `import '@brand-app-banner?url'`
|
|
* in the component resolves cleanly — when the active brand has no
|
|
* banner it falls back to the resolved logo, which the component never
|
|
* renders (it gates on the `VITE_APP_BANNER` flag instead).
|
|
*/
|
|
export function brandAppBannerAliasEntry(app?: string) {
|
|
const resolved = resolveAppBanner(app) ?? resolveAppLogo(app)
|
|
return {
|
|
find: /^@brand-app-banner(\?.*)?$/,
|
|
replacement: `${resolved}$1`,
|
|
} 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
|
|
}
|
|
|
|
/**
|
|
* Vite plugin: regenerate PWA icons under public/icons/ once per build
|
|
* / dev-server start, so vite.<app>.config.ts's includeAssets +
|
|
* manifest.icons always have something to include. Source resolution
|
|
* lives in pwa-assets.config.ts.
|
|
*/
|
|
export function brandAssetsPlugin(): Plugin {
|
|
let generated = false
|
|
return {
|
|
name: 'brand-assets-generator',
|
|
buildStart() {
|
|
if (generated) return
|
|
const { status } = spawnSync(
|
|
'node',
|
|
[resolve('scripts/generate-pwa-assets.mjs')],
|
|
{ stdio: 'inherit' },
|
|
)
|
|
if (status !== 0) {
|
|
throw new Error('pwa-assets-generator failed; see output above')
|
|
}
|
|
generated = true
|
|
},
|
|
}
|
|
}
|