Files
accounted/app/layout.tsx
T
Jakob Wennberg 8f1b1fb5cd feat(ui): company monogram in user menu + mobile web touch polish (#1531)
* 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>
2026-08-13 10:41:45 +02:00

113 lines
3.7 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Hedvig_Letters_Serif } from "next/font/google";
import Script from "next/script";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip-provider";
import { DeployReloadPrompt } from "@/components/system/DeployReloadPrompt";
import { ThemeColorSync } from "@/components/system/ThemeColorSync";
import { ThemeProvider } from "@/components/theme-provider";
import { PaletteProvider } from "@/components/providers/PaletteProvider";
import { SWRProvider } from "@/components/providers/SWRProvider";
import { ScrollbarReveal } from "@/components/ScrollbarReveal";
import { ensureInitialized } from "@/lib/init";
import { getBranding } from "@/lib/branding/service";
import { APP_TIME_ZONE } from "@/i18n/config";
import "./globals.css";
// Load extensions before metadata/viewport functions read the branding service.
// Without this, an extension that calls registerBrandingService() at its module
// load time would not have run yet when the first request hits this layout.
ensureInitialized();
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const hedvigSerif = Hedvig_Letters_Serif({
variable: "--font-hedvig-serif",
subsets: ["latin"],
display: "swap",
// Hedvig Letters Serif on Google Fonts only ships 400; the brand wordmark
// requests 700, which browsers synthesize from this file. Keep the load
// single-file to stay within the existing display-font budget.
weight: "400",
});
export function generateMetadata(): Metadata {
const b = getBranding();
return {
title: b.appName,
description: b.appDescription,
manifest: "/manifest.webmanifest",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: b.appName,
},
};
}
export function generateViewport(): Viewport {
return {
themeColor: getBranding().themeColor,
width: "device-width",
initialScale: 1,
viewportFit: "cover",
};
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const branding = getBranding();
const locale = await getLocale();
const messages = await getMessages();
return (
<html lang={locale} suppressHydrationWarning className={`${geistSans.variable} ${geistMono.variable} ${hedvigSerif.variable}`}>
<head>
<link rel="apple-touch-icon" href={branding.appleTouchIconPath} />
</head>
<body
className="antialiased"
>
{/* timeZone is passed explicitly: client components must format in the
same zone the server rendered with, or timestamps shift on hydration. */}
<NextIntlClientProvider locale={locale} messages={messages} timeZone={APP_TIME_ZONE}>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
<PaletteProvider>
<SWRProvider>
<TooltipProvider>
{children}
<Toaster />
<DeployReloadPrompt />
<ThemeColorSync />
<ScrollbarReveal />
</TooltipProvider>
</SWRProvider>
</PaletteProvider>
</ThemeProvider>
</NextIntlClientProvider>
<SpeedInsights />
<Script src="/sw-register.js" strategy="afterInteractive" />
</body>
</html>
);
}