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
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export function AuthPageSkeleton() {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-frame p-4" aria-busy="true">
|
||||
<main className="flex min-h-dvh items-center justify-center bg-frame p-4" aria-busy="true">
|
||||
<div className="w-full max-w-sm">
|
||||
<Skeleton className="mx-auto mb-8 h-12 w-48" />
|
||||
<div className="space-y-4 rounded-xl border border-border bg-background p-6">
|
||||
|
||||
@@ -10,7 +10,6 @@ import { performCompanySwitch } from '@/lib/company/switch-client'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { SupportLink } from '@/components/ui/support-link'
|
||||
import {
|
||||
Building2,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
ChevronRight,
|
||||
@@ -58,6 +57,19 @@ function accountInitial(name: string | null, email: string | null): string {
|
||||
return '?'
|
||||
}
|
||||
|
||||
// Company monogram: first letter in a small rounded square. Square = company,
|
||||
// circle = person (the avatar above), so the two identity marks stay distinct.
|
||||
function CompanyMark({ name }: { name: string }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[4px] bg-secondary text-[9px] font-semibold uppercase leading-none text-foreground"
|
||||
>
|
||||
{name.trim().charAt(0) || '?'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sticky bottom-of-sidebar user block: avatar initials, name, active company,
|
||||
* chevron. Opens an upward popover aligned with the nav column holding
|
||||
@@ -246,7 +258,7 @@ export default function UserMenu({
|
||||
aria-expanded={companiesOpen}
|
||||
className={cn(menuRow, companiesOpen && 'bg-secondary/60 text-foreground')}
|
||||
>
|
||||
<Building2 className="h-4 w-4 flex-shrink-0" />
|
||||
<CompanyMark name={company?.name || tSwitcher('default_company_name')} />
|
||||
<span className="flex-1 truncate">
|
||||
{company?.name || tSwitcher('default_company_name')}
|
||||
</span>
|
||||
@@ -288,7 +300,7 @@ export default function UserMenu({
|
||||
isPending && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
<Building2 className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
<CompanyMark name={c.name} />
|
||||
<span className="min-w-0 flex-1 truncate">{c.name}</span>
|
||||
{role !== 'owner' && (
|
||||
<span className="flex-shrink-0 text-[10px] text-muted-foreground/60">
|
||||
|
||||
@@ -27,7 +27,7 @@ interface DocsLayoutProps {
|
||||
|
||||
export function DocsLayout({ currentPath, children }: DocsLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="min-h-dvh bg-background">
|
||||
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/docs/api" className="font-display text-xl tracking-tight" style={{ fontWeight: 700 }}>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Mirrors the app's real background into <meta name="theme-color"> so the
|
||||
* browser chrome (iOS status bar, Android address bar) always matches what
|
||||
* is on screen. A static viewport color cannot: dark mode is a class toggle
|
||||
* (next-themes) and color palettes swap --background at runtime via
|
||||
* data-palette, both on <html>.
|
||||
*/
|
||||
export function ThemeColorSync() {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
const sync = () => {
|
||||
const bg = getComputedStyle(root).getPropertyValue('--background').trim()
|
||||
if (!bg) return
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', `hsl(${bg})`)
|
||||
}
|
||||
sync()
|
||||
const observer = new MutationObserver(sync)
|
||||
observer.observe(root, { attributes: true, attributeFilter: ['class', 'data-palette'] })
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
@@ -11,20 +11,23 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
// active: mirrors hover: on every variant. Tailwind 4 gates hover:
|
||||
// behind (hover: hover), so on touch devices these are the only
|
||||
// pointer-down feedback a button gives.
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-secondary",
|
||||
"border border-input bg-transparent hover:bg-secondary active:bg-secondary",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/70",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/70 active:bg-secondary/70",
|
||||
ghost:
|
||||
"hover:bg-secondary hover:text-secondary-foreground",
|
||||
"hover:bg-secondary hover:text-secondary-foreground active:bg-secondary active:text-secondary-foreground",
|
||||
link:
|
||||
"text-primary underline-offset-4 hover:underline",
|
||||
"text-primary underline-offset-4 hover:underline active:underline",
|
||||
success:
|
||||
"bg-success text-success-foreground hover:bg-success/90",
|
||||
"bg-success text-success-foreground hover:bg-success/90 active:bg-success/90",
|
||||
},
|
||||
size: {
|
||||
default: "px-4 py-[7px] text-[13px]",
|
||||
|
||||
Reference in New Issue
Block a user