feat(ui): frame-layout shell, pill buttons, 24px page titles (UI migration PR 1) (#1117)
* feat(ui): frame-layout shell, pill buttons, 24px page titles (UI migration PR 1) The visual shell from the concept, zero behavior change: - New --frame token pair (40 18% 96% light / 0 0% 5% dark); the dashboard wrapper is bg-frame and <main> becomes a rounded 12px panel with its own inner scroll (md-gated; mobile keeps document flow + bottom nav) - Sidebar goes borderless/transparent on the frame - Buttons are pills app-wide (radius 99px, default 7px/16px padding, 13px text, icon buttons become circles), set once in components/ui/button.tsx - PageHeader locked at exactly 24px/32px Hedvig serif - MainContainer resets panel scroll on route change (Next's window scroll-to-top never fires for an inner scroll container) - chat layout + extension workspaces switch viewport-height formulas to h-full so they fill the panel instead of overflowing it by 20px - .claude/rules/design.md rewritten with the 14 locked UI-migration conventions from dev_docs/ui_migration_plan.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): clamp hand-rolled page titles to the locked 24px/32px Transaktioner (TransactionStatusBar) and 13 other pages hand-roll their h1 instead of using PageHeader, so they kept text-3xl/4xl after the shell change. Clamp them all to font-display text-2xl leading-8. Onboarding heroes and headline numbers are intentionally untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): keep the frame strip above the panel on banner-less accounts <main>'s 10px top margin was the first in-flow margin inside the shell wrapper, so it collapsed through the wrapper and pushed the whole shell down, showing white body background above the panel instead of the warm frame strip (only visible on real accounts: the sandbox banner blocked the collapse). Flex containers never collapse child margins, so the shell wrappers become md:flex md:flex-col. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
288915c152
commit
6b506a9ca8
@@ -14,7 +14,7 @@ export default function ConsultantEmptyState({ firstName }: ConsultantEmptyState
|
||||
return (
|
||||
<div className="stagger-enter">
|
||||
<header className="mb-16">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">
|
||||
{greeting}{firstName ? `, ${firstName}` : ''}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
@@ -510,7 +510,8 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:fixed md:inset-y-0 md:flex md:w-64 md:flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col border-r border-border bg-background">
|
||||
{/* Borderless on the frame: the panel next to it carries the border */}
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-transparent">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-7 pb-4">
|
||||
{/* Company switcher pinned to the top: the active company is
|
||||
the strongest piece of context for everything below it. */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,15 @@ export function MainContainer({
|
||||
children: ReactNode
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// The panel (<main>) is its own scroll container on desktop, so Next's
|
||||
// built-in scroll-to-top on navigation (which targets the window) never
|
||||
// fires for it. Reset the panel scroll on every route change; hash-anchor
|
||||
// scrolling still works because pages call scrollIntoView themselves.
|
||||
useEffect(() => {
|
||||
document.getElementById('main-content')?.scrollTo(0, 0)
|
||||
}, [pathname])
|
||||
|
||||
// Full-bleed routes own their own padding + multi-pane layout. They
|
||||
// shouldn't sit inside max-w-5xl or any horizontal padding: that's what
|
||||
// causes a visible gap between the dashboard sidebar and the chat-sidebar
|
||||
|
||||
@@ -156,7 +156,7 @@ function deriveInboxStatus(item: InboxItem): InboxStatus {
|
||||
|
||||
function WorkspaceSkeleton() {
|
||||
return (
|
||||
<div className="h-[calc(100vh-1px)] p-4 md:p-6">
|
||||
<div className="h-[calc(100vh-1px)] md:h-full p-4 md:p-6">
|
||||
<div className="h-full flex flex-col rounded-lg border bg-card overflow-hidden">
|
||||
<header className="flex items-center justify-between gap-4 border-b px-4 py-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -712,7 +712,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-[calc(100vh-1px)] xl:h-[calc(100vh-1px)] p-4 md:p-6"
|
||||
className="min-h-[calc(100vh-1px)] md:min-h-full xl:h-full p-4 md:p-6"
|
||||
onDragOver={(e) => { e.preventDefault(); if (!isDragging) setIsDragging(true) }}
|
||||
onDragLeave={(e) => {
|
||||
// only clear when leaving the workspace itself, not children
|
||||
|
||||
@@ -1373,7 +1373,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Heading className={bare ? 'font-display text-xl tracking-tight' : 'font-display text-2xl md:text-3xl tracking-tight'}>
|
||||
<Heading className={bare ? 'font-display text-xl tracking-tight' : 'font-display text-2xl leading-8 tracking-tight'}>
|
||||
{titleText}
|
||||
{numberPreview && !isSelfBilled && (
|
||||
<span className={bare ? 'ml-2 text-muted-foreground tabular-nums text-lg' : 'ml-2 text-muted-foreground tabular-nums text-xl md:text-2xl'}>
|
||||
|
||||
@@ -1487,7 +1487,7 @@ export default function NewSupplierInvoiceForm({
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('page_title')}</h1>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{t('page_title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function TransactionStatusBar({
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl md:text-4xl tracking-tight">{t('page_title')}</h1>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{t('page_title')}</h1>
|
||||
{uncategorizedCount > 0 && mode === 'inbox' && (
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<span className="text-foreground font-semibold">{uncategorizedCount}</span> {t('subtitle_to_post')}
|
||||
|
||||
@@ -3,8 +3,11 @@ import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Buttons are pills (radius 99px): a deliberate app-wide divergence from the
|
||||
// shadcn 8px default, locked in the UI-migration conventions. Change it here,
|
||||
// never per call site.
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-full font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -24,10 +27,10 @@ const buttonVariants = cva(
|
||||
"bg-success text-success-foreground hover:bg-success/90",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-5 py-2 rounded-lg",
|
||||
sm: "h-9 px-4 text-xs rounded-lg",
|
||||
lg: "h-11 px-8 text-base rounded-lg",
|
||||
icon: "h-10 w-10 rounded-lg",
|
||||
default: "px-4 py-[7px] text-[13px]",
|
||||
sm: "h-9 px-4 text-xs",
|
||||
lg: "h-11 px-8 text-base",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -10,7 +10,8 @@ export function PageHeader({ title, description, action }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl md:text-4xl tracking-tight">{title}</h1>
|
||||
{/* Locked at exactly 24px/32px (UI-migration convention 2) */}
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mt-1 text-balance">{description}</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user