8f1b1fb5cd
* feat(ui): replace generic building icon with company monogram in user menu The company row and switcher flyout in the sidebar user menu showed lucide Building2 for every company. Render the company's initial in a small rounded square instead (square = company, circle = person), so each company gets a mark of its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mobile): touch behavior polish for the mobile web experience - kill -webkit-tap-highlight-color flash; touch-action: manipulation on interactive elements (no double-tap-to-zoom wait); user-select: none on buttons (long-press no longer enters text selection) - overscroll-behavior-y: contain on html/body: pull-to-refresh no longer hijacks list scrolling, inner scrollers stop chaining to the document (contain, not none, so iOS rubber-banding survives) - 16px font-size floor for form fields on coarse pointers: iOS Safari stops zooming into focused inputs; desktop keeps text-sm - min-h-screen -> min-h-dvh everywhere: correct height under collapsing mobile browser chrome, identical on desktop - active: variants mirror hover: on Button: Tailwind 4 gates hover: behind (hover: hover), so touch devices previously got zero pointer feedback - theme-color now tracks the app: default was a leftover blue #304D83; SSR emits white and ThemeColorSync mirrors the computed --background into the meta tag across dark mode and palette switches Hover-stuck-after-tap and viewport-fit/safe-area were already covered (Tailwind 4 hover gating; existing viewportFit: cover + safe-area utilities). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log monogram and overscroll decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
129 lines
5.1 KiB
TypeScript
129 lines
5.1 KiB
TypeScript
/**
|
|
* Branding Service
|
|
*
|
|
* Provides whitelabel-friendly branding values (app name, support emails,
|
|
* asset paths, theme colors) with three-tier resolution:
|
|
*
|
|
* defaults < env vars < extension override
|
|
*
|
|
* If nothing is set, Accounted defaults are returned: production behaviour
|
|
* is unchanged. A whitelabel sets env vars (NEXT_PUBLIC_BRANDING_* for
|
|
* client-readable, BRANDING_* for server-only) or registers a branding
|
|
* extension via registerBrandingService().
|
|
*
|
|
* See docs/WHITELABEL.md for the full env var reference and fork checklist.
|
|
*/
|
|
|
|
export interface BrandingConfig {
|
|
// Identity
|
|
appName: string
|
|
appDescription: string
|
|
legalEntity: string
|
|
|
|
// Contact
|
|
supportEmail: string
|
|
privacyEmail: string
|
|
securityEmail: string
|
|
|
|
// Address Supabase Auth sends verification / reset emails from. Used to build
|
|
// the `from:` query on the "check your email" screen's Gmail deep link.
|
|
// Must match the From in your Supabase Auth SMTP config.
|
|
authEmailFrom: string
|
|
|
|
// URLs
|
|
appUrl: string
|
|
|
|
// Asset paths
|
|
logoPath: string
|
|
faviconPath: string
|
|
appleTouchIconPath: string
|
|
pwaIconBasePath: string
|
|
|
|
// Colors
|
|
themeColor: string
|
|
manifestThemeColor: string
|
|
manifestBackgroundColor: string
|
|
|
|
// Navigation
|
|
hiddenNavHrefs: string[]
|
|
/**
|
|
* Sidebar density. `'standard'` renders the original full sidebar with
|
|
* every nav group at equal weight. `'slim'` renders an AI-first layout:
|
|
* four primary destinations (Översikt, Transaktioner, Fakturor, Anna)
|
|
* are visible at full weight, every other group is collapsed and muted,
|
|
* and Inställningar/Hjälp/Logga ut move into a profile dropdown.
|
|
* Set per-brand; default `'standard'` so self-hosted is unchanged.
|
|
*/
|
|
navDensity: 'standard' | 'slim'
|
|
}
|
|
|
|
const DEFAULT_BRANDING: BrandingConfig = {
|
|
appName: 'Accounted',
|
|
appDescription: 'Ekonomihantering',
|
|
legalEntity: 'Arcim Technology AB',
|
|
// Emails and URLs intentionally keep the gnubok.se hostname: the rebrand is
|
|
// visual only; we don't churn the support inbox or app domain alongside it.
|
|
supportEmail: 'support@gnubok.se',
|
|
privacyEmail: 'privacy@gnubok.se',
|
|
securityEmail: 'security@arcim.io',
|
|
authEmailFrom: 'noreply@gnubok.se',
|
|
appUrl: process.env.NEXT_PUBLIC_APP_URL || 'https://app.gnubok.se',
|
|
// The visible brand mark now renders as text via <BrandWordmark>; this
|
|
// image path is kept as a fallback for any surface still using <Image>
|
|
// (e.g. PWA-style metadata that demands a concrete file).
|
|
logoPath: '/accounted-icon.png',
|
|
faviconPath: '/favicon.ico',
|
|
appleTouchIconPath: '/icons/icon-192.png',
|
|
pwaIconBasePath: '/icons',
|
|
// SSR-initial browser-chrome color (iOS status bar / Android address bar).
|
|
// Matches light --background; after hydration ThemeColorSync keeps the meta
|
|
// tag in step with the active theme and palette.
|
|
themeColor: '#ffffff',
|
|
manifestThemeColor: '#1a1a1a',
|
|
manifestBackgroundColor: '#ffffff',
|
|
hiddenNavHrefs: [],
|
|
navDensity: 'standard',
|
|
}
|
|
|
|
let _override: Partial<BrandingConfig> = {}
|
|
|
|
export function registerBrandingService(partial: Partial<BrandingConfig>): void {
|
|
_override = { ...partial }
|
|
}
|
|
|
|
export function getBranding(): BrandingConfig {
|
|
return {
|
|
...DEFAULT_BRANDING,
|
|
...readEnvOverrides(),
|
|
..._override,
|
|
}
|
|
}
|
|
|
|
function readEnvOverrides(): Partial<BrandingConfig> {
|
|
const env = process.env
|
|
const o: Partial<BrandingConfig> = {}
|
|
if (env.NEXT_PUBLIC_BRANDING_APP_NAME) o.appName = env.NEXT_PUBLIC_BRANDING_APP_NAME
|
|
if (env.NEXT_PUBLIC_BRANDING_APP_DESCRIPTION) o.appDescription = env.NEXT_PUBLIC_BRANDING_APP_DESCRIPTION
|
|
if (env.BRANDING_LEGAL_ENTITY) o.legalEntity = env.BRANDING_LEGAL_ENTITY
|
|
if (env.BRANDING_SUPPORT_EMAIL) o.supportEmail = env.BRANDING_SUPPORT_EMAIL
|
|
if (env.BRANDING_PRIVACY_EMAIL) o.privacyEmail = env.BRANDING_PRIVACY_EMAIL
|
|
if (env.BRANDING_SECURITY_EMAIL) o.securityEmail = env.BRANDING_SECURITY_EMAIL
|
|
if (env.NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM) o.authEmailFrom = env.NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM
|
|
if (env.NEXT_PUBLIC_APP_URL) o.appUrl = env.NEXT_PUBLIC_APP_URL
|
|
if (env.NEXT_PUBLIC_BRANDING_LOGO_PATH) o.logoPath = env.NEXT_PUBLIC_BRANDING_LOGO_PATH
|
|
if (env.NEXT_PUBLIC_BRANDING_FAVICON_PATH) o.faviconPath = env.NEXT_PUBLIC_BRANDING_FAVICON_PATH
|
|
if (env.NEXT_PUBLIC_BRANDING_APPLE_ICON_PATH) o.appleTouchIconPath = env.NEXT_PUBLIC_BRANDING_APPLE_ICON_PATH
|
|
if (env.NEXT_PUBLIC_BRANDING_PWA_ICON_BASE) o.pwaIconBasePath = env.NEXT_PUBLIC_BRANDING_PWA_ICON_BASE
|
|
if (env.NEXT_PUBLIC_BRANDING_THEME_COLOR) o.themeColor = env.NEXT_PUBLIC_BRANDING_THEME_COLOR
|
|
if (env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR) o.manifestThemeColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR
|
|
if (env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR) o.manifestBackgroundColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR
|
|
if (env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV) {
|
|
const hrefs = env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV.split(',').map(s => s.trim()).filter(Boolean)
|
|
if (hrefs.length > 0) o.hiddenNavHrefs = hrefs
|
|
}
|
|
if (env.NEXT_PUBLIC_BRANDING_NAV_DENSITY === 'slim' || env.NEXT_PUBLIC_BRANDING_NAV_DENSITY === 'standard') {
|
|
o.navDensity = env.NEXT_PUBLIC_BRANDING_NAV_DENSITY
|
|
}
|
|
return o
|
|
}
|