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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
38a890c8d1
commit
8f1b1fb5cd
@@ -120,7 +120,7 @@ function SetPasswordContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-background p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
|
||||
@@ -368,7 +368,7 @@ export function LoginClient({ initialMethod }: { initialMethod: LoginMethod | nu
|
||||
const webmailHint = detectWebmailHint(email, branding.authEmailFrom)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
@@ -428,7 +428,7 @@ export function LoginClient({ initialMethod }: { initialMethod: LoginMethod | nu
|
||||
// Reset password form
|
||||
if (showResetPassword) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
@@ -493,7 +493,7 @@ export function LoginClient({ initialMethod }: { initialMethod: LoginMethod | nu
|
||||
const chipCount = (showBankIdChip ? 1 : 0) + (showEmailChip ? 1 : 0) + (googleAuthEnabled ? 1 : 0)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<header className="text-center mb-8">
|
||||
<h1 className="sr-only">{tAuth('login_title')}</h1>
|
||||
|
||||
@@ -175,7 +175,7 @@ function MfaEnrollContent() {
|
||||
// Step 1: Show enroll button
|
||||
if (!qrCode) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
@@ -229,7 +229,7 @@ function MfaEnrollContent() {
|
||||
|
||||
// Step 2: Show QR code and verification
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
|
||||
@@ -170,7 +170,7 @@ function MfaVerifyContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
|
||||
@@ -384,7 +384,7 @@ function RegisterPageContent() {
|
||||
|
||||
if (duplicateEmail) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
@@ -437,7 +437,7 @@ function RegisterPageContent() {
|
||||
const webmailHint = detectWebmailHint(email, branding.authEmailFrom)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
@@ -485,7 +485,7 @@ function RegisterPageContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<header className="text-center mb-8">
|
||||
<h1 className="sr-only">{t('create_account')}</h1>
|
||||
|
||||
@@ -208,7 +208,7 @@ function ResetPasswordInner() {
|
||||
: t('subtitle')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
|
||||
@@ -39,7 +39,7 @@ const NO_COMPANY_ALLOWED_PATHS = ['/settings/account']
|
||||
* document flow with the bottom nav, exactly as before.
|
||||
*/
|
||||
const MAIN_PANEL_CLASS =
|
||||
'safe-area-main-padding md:!pb-0 relative bg-background min-h-screen ' +
|
||||
'safe-area-main-padding md:!pb-0 relative bg-background min-h-dvh ' +
|
||||
'md:min-h-0 md:ml-[var(--nav-w)] md:mt-[10px] md:mr-[var(--agent-dock-w)] md:h-[calc(100vh-20px)] ' +
|
||||
'md:overflow-y-auto md:rounded-xl md:border md:border-border ' +
|
||||
'md:transition-[margin-left,margin-right] md:duration-300 md:ease-[cubic-bezier(0.32,0.72,0,1)]'
|
||||
@@ -112,7 +112,7 @@ export default async function DashboardLayout({
|
||||
<SessionTimeoutController />
|
||||
<AgentSheetProvider>
|
||||
<CompanyTabSync />
|
||||
<div className="min-h-screen bg-frame md:flex md:flex-col">
|
||||
<div className="min-h-dvh bg-frame md:flex md:flex-col">
|
||||
<DashboardNav
|
||||
companyName={getBranding().appName.toLowerCase()}
|
||||
entityType="enskild_firma"
|
||||
@@ -238,7 +238,7 @@ export default async function DashboardLayout({
|
||||
<SessionTimeoutController />
|
||||
<AgentSheetProvider>
|
||||
<CompanyTabSync />
|
||||
<div className="min-h-screen bg-frame md:flex md:flex-col">
|
||||
<div className="min-h-dvh bg-frame md:flex md:flex-col">
|
||||
<DashboardNav
|
||||
companyName={getBranding().appName.toLowerCase()}
|
||||
entityType="enskild_firma"
|
||||
@@ -332,7 +332,7 @@ export default async function DashboardLayout({
|
||||
<CompanyTabSync />
|
||||
<div
|
||||
id="dash-shell"
|
||||
className="min-h-screen bg-frame md:flex md:flex-col"
|
||||
className="min-h-dvh bg-frame md:flex md:flex-col"
|
||||
style={{ '--nav-w': navCollapsed ? '64px' : '248px' } as React.CSSProperties}
|
||||
>
|
||||
{/* Skip to content link for keyboard/screen reader users */}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function OnboardingLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="min-h-dvh bg-background flex items-center justify-center">
|
||||
{user && <SessionTimeoutController />}
|
||||
<OnboardingBackdrop />
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateMetadata(): Metadata {
|
||||
export default function DPAPage() {
|
||||
const { appName, legalEntity, privacyEmail } = getBranding()
|
||||
return (
|
||||
<div className="min-h-screen bg-background py-12 px-4">
|
||||
<div className="min-h-dvh bg-background py-12 px-4">
|
||||
<div className="max-w-3xl mx-auto space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="font-display text-3xl md:text-4xl tracking-tight text-foreground">
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
|
||||
<div className="min-h-dvh bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Laddar...</p>
|
||||
@@ -113,7 +113,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
|
||||
if (error && !invoice) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center p-4">
|
||||
<div className="min-h-dvh bg-gradient-to-b from-slate-50 to-white flex items-center justify-center p-4">
|
||||
<Card className="max-w-md w-full">
|
||||
<CardContent className="pt-6 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
||||
@@ -132,7 +132,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
// Already responded view
|
||||
if (invoice.alreadyResponded || successMessage) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center p-4">
|
||||
<div className="min-h-dvh bg-gradient-to-b from-slate-50 to-white flex items-center justify-center p-4">
|
||||
<Card className="max-w-md w-full">
|
||||
<CardContent className="pt-6 text-center">
|
||||
<CheckCircle className="h-12 w-12 text-success mx-auto mb-4" />
|
||||
@@ -166,7 +166,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
const feeDueSeparately = invoice.feeDueSeparately ?? 0
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white py-12 px-4">
|
||||
<div className="min-h-dvh bg-gradient-to-b from-slate-50 to-white py-12 px-4">
|
||||
<div className="max-w-lg mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateMetadata(): Metadata {
|
||||
export default function PrivacyPolicyPage() {
|
||||
const { appName, legalEntity, privacyEmail } = getBranding()
|
||||
return (
|
||||
<div className="min-h-screen bg-background py-12 px-4">
|
||||
<div className="min-h-dvh bg-background py-12 px-4">
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export default function CompaniesLoading() {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-background p-4" aria-busy="true">
|
||||
<main className="flex min-h-dvh items-center justify-center bg-background p-4" aria-busy="true">
|
||||
<div className="w-full max-w-lg space-y-6 rounded-lg border bg-card p-6">
|
||||
<Skeleton className="h-7 w-48" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
|
||||
@@ -36,7 +36,7 @@ export default async function NewCompanyPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="min-h-dvh bg-background">
|
||||
<SessionTimeoutController />
|
||||
<OnboardingBackdrop />
|
||||
<OnboardingJourney teamId={teamId} mode="add" />
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function GlobalError({
|
||||
</head>
|
||||
<body>
|
||||
{phase === "fallback" ? (
|
||||
<div className="flex min-h-screen items-center justify-center p-8">
|
||||
<div className="flex min-h-dvh items-center justify-center p-8">
|
||||
<div className="text-center space-y-4">
|
||||
<h2 className="text-xl font-semibold">Något gick fel</h2>
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -364,6 +364,55 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ── Touch / mobile-web behavior ──
|
||||
Hover styling is already safe: Tailwind 4 gates every hover: variant
|
||||
behind @media (hover: hover), so no stuck-hover rules are needed here. */
|
||||
|
||||
/* Kill the gray/blue tap flash (inherited property, one declaration covers all) */
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Stop pull-to-refresh from hijacking list scrolling and stop inner scrollers
|
||||
from chaining into the document. `contain`, not `none`: none would also kill
|
||||
iOS rubber-banding, which should stay. */
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
/* Remove the double-tap-to-zoom wait on interactive elements so taps commit
|
||||
immediately; pinch zoom and panning are unaffected. */
|
||||
a,
|
||||
button,
|
||||
label,
|
||||
summary,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
[role="button"] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Long-press on a button must not enter text selection */
|
||||
button,
|
||||
summary,
|
||||
[role="button"] {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* iOS Safari zooms into any focused field whose font-size is under 16px.
|
||||
Touch-primary devices get a 16px floor; desktop keeps text-sm form fields.
|
||||
Unlayered on purpose so it also beats layered text-[13px]-style utilities
|
||||
on bespoke inputs. */
|
||||
@media (pointer: coarse) {
|
||||
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
|
||||
select,
|
||||
textarea {
|
||||
font-size: max(16px, 1em);
|
||||
}
|
||||
}
|
||||
|
||||
/* Display typography - Fraunces */
|
||||
.font-display {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export default function InviteLoading() {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-background p-4" aria-busy="true">
|
||||
<main className="flex min-h-dvh items-center justify-center bg-background p-4" aria-busy="true">
|
||||
<div className="w-full max-w-md space-y-5 rounded-lg border bg-card p-6">
|
||||
<Skeleton className="mx-auto h-12 w-12 rounded-full" />
|
||||
<Skeleton className="mx-auto h-7 w-56" />
|
||||
|
||||
@@ -197,14 +197,14 @@ export default function InvitePage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="min-h-dvh flex items-center justify-center bg-background">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<div className="min-h-dvh flex flex-col bg-background">
|
||||
<header className="relative bg-[#141414] text-white overflow-hidden">
|
||||
<div className="absolute inset-0 pointer-events-none" aria-hidden>
|
||||
<div
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
@@ -96,6 +97,7 @@ export default async function RootLayout({
|
||||
{children}
|
||||
<Toaster />
|
||||
<DeployReloadPrompt />
|
||||
<ThemeColorSync />
|
||||
<ScrollbarReveal />
|
||||
</TooltipProvider>
|
||||
</SWRProvider>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export default function PayslipLoading() {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-background px-5" aria-busy="true">
|
||||
<main className="flex min-h-dvh items-center justify-center bg-background px-5" aria-busy="true">
|
||||
<div className="w-full max-w-2xl space-y-6 rounded-lg border bg-card p-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-56" />
|
||||
|
||||
@@ -19,7 +19,7 @@ const MONTH_NAMES = [
|
||||
|
||||
function MessageShell({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<main className="min-h-screen bg-background flex items-center justify-center px-5">
|
||||
<main className="min-h-dvh bg-background flex items-center justify-center px-5">
|
||||
<div className="max-w-md w-full rounded-lg border border-border p-6 space-y-2">
|
||||
<h1 className="font-display text-2xl tracking-tight">{title}</h1>
|
||||
<p className="text-sm text-muted-foreground">{body}</p>
|
||||
@@ -99,7 +99,7 @@ export default async function PayslipPage({
|
||||
const monthName = MONTH_NAMES[run.period_month - 1]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background flex items-center justify-center px-5 py-10">
|
||||
<main className="min-h-dvh bg-background flex items-center justify-center px-5 py-10">
|
||||
<div className="max-w-md w-full rounded-lg border border-border p-6 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">{company.name}</p>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function SandboxPage() {
|
||||
// Loading state while checking auth
|
||||
if (isLoggedIn === null) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-background to-primary/[0.03]">
|
||||
<div className="min-h-dvh flex items-center justify-center bg-gradient-to-b from-background to-primary/[0.03]">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
@@ -79,7 +79,7 @@ export default function SandboxPage() {
|
||||
// Already logged in as a real user
|
||||
if (isLoggedIn) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-primary/[0.03] p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-gradient-to-b from-background to-primary/[0.03] p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<BrandWordmark size="hero" className="mb-2" />
|
||||
@@ -107,7 +107,7 @@ export default function SandboxPage() {
|
||||
|
||||
// Sandbox landing
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-primary/[0.03] p-4">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-gradient-to-b from-background to-primary/[0.03] p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<BrandWordmark size="hero" className="mb-2" />
|
||||
|
||||
Reference in New Issue
Block a user