* fix(design): stop synthesizing bold on Hedvig display headings
Hedvig Letters Serif ships weight 400 only, but the h1-h3 base rule
forced font-weight 500 and DialogTitle/SheetTitle stacked font-semibold
on top, so every display heading rendered browser-synthesized bold: the
smudged heavy look on dialog titles and page headings. Drop the base
rule to 400, remove the weight utilities from the title primitives, and
sweep the 41 files that hand-set font-medium/semibold/bold on serif
headings (a pattern design.md already forbids). Headings that opt into
font-sans keep their weight.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(design): drop empty className left by the weight sweep
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>
* 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>
* fix(app): stop transient login errors flashing the full-screen fallback
The BankID login landing (/auth/callback then the /select-company picker)
fires several Supabase auth/DB queries right as the session cookies are set,
so a transient failure there (most often a refresh-token rotation race, seen
in prod as "Invalid Refresh Token: Already Used/Not Found" on /middleware, or
a stale JS chunk after a deploy) threw during render. Only (dashboard) had an
error.tsx, so these escaped every boundary and hit app/global-error.tsx,
blanking the whole document with a bare "Nagot gick fel" screen for ~1s before
the next request repainted and logged the user in as usual.
Add an app-level error.tsx (AppErrorBoundary) that catches those segments and
their layouts, and harden global-error.tsx. Both recover via a single guarded
hard reload instead of React reset(): a reload re-runs middleware (fresh
rotated auth cookie) and fetches a fresh bundle (ChunkLoadError after a
deploy), matching the browser-navigation self-heal these transients already
relied on, whereas reset() re-renders against the same stale payload/bundle. A
per-path sessionStorage time-guard bounds it to one reload so a persistent
error shows the manual fallback instead of looping.
Reported via support: transient "Nagot gick fel" flash on BankID login.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app): make the error-boundary reload guard loop-proof (review)
CodeRabbit and the PR Agent both flagged that the 12s time-window guard could
still loop if a failing render takes longer than the window (e.g. a slow SSR
that eventually throws). Replace the time window with a per-path, per-tab-session
one-shot flag, so the auto-reload fires at most once per path regardless of
timing and a genuinely persistent error settles on the manual fallback.
sessionStorage is per-tab, so a fresh visit (or a different path) still gets a
fresh auto-recovery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app): claim the reload flag atomically + add support escape hatch (review)
CodeRabbit round 2:
- Major: the one-shot flag was written in the effect but the reload fired even
if the write threw (sessionStorage quota full), so it could reload forever
without ever recording the attempt. Claim the flag inside decideInitialPhase
instead, so entering the 'reloading' phase guarantees the flag persisted;
any write failure falls through to the manual 'fallback' (no reload).
- Minor: give global-error.tsx a support escape hatch. It can't use SupportLink
(no providers when the root layout fails), so use a dependency-free mailto to
the hardcoded support address.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Long-open tabs keep running the JS bundle they first loaded, so a shipped
change can look "missing" (e.g. a new settings field appearing only after a
full reload) until the whole app is reloaded. Add a small, unobtrusive prompt
that detects a newer deploy and offers a one-click reload.
- next.config: inline the deploy's commit SHA into the client bundle as
NEXT_PUBLIC_BUILD_ID (empty in dev / self-hosted, which disables the check).
- /api/version: public, no-store route returning the running deployment's SHA
at request time.
- DeployReloadPrompt: compares the two on load, on tab focus, and on a 30-min
backstop; shows a bottom banner with "Ladda om" on mismatch. Mounted once in
the root layout. No service worker, degrades to a no-op with no build id.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>