refactor(ui): system hygiene sweep from the UI craft audit (#1281)

Raw palette: SalaryCalendar was the only file in the salary cluster still
on bg-red-100 / text-amber-800 style classes, with zero dark: variants, so
every absence pill rendered paper-white on a near-black page. All 30 are
now alpha-over-token on the semantic scale, which needs no dark: variant
(the approach badge.tsx already takes). Eight absence types share four
tones, so fill carries a second axis: filled vs outlined separates the
types whose Lucide icon is identical (Heart is parental, pregnancy and
care_relative; Activity is study and other_leave).

Primitives:
- select and dropdown-menu now scale from their trigger via the Radix
  transform-origin variables instead of from their own middle. Dialog stays
  centred: modals are not anchored to a trigger.
- toast used fade-out-80, the one animate-* class with no @utility in
  globals.css, so --tw-exit-opacity fell back to 1 and the toast slid away
  at full opacity. Enter and exit now also share one path per breakpoint
  (top on mobile where the viewport is a full-width bar, right on desktop
  where it is a corner card) using max-sm:/sm: rather than stacking both,
  which would have produced a diagonal.
- slide-over exited on ease-in, which delays the moment the user has
  already decided to leave; it enters on the drawer curve and now leaves
  on it too.
- progress animated transition-all where only transform changes.
- One app-wide TooltipProvider in the root layout. skipDelayDuration is
  provider-scoped, so a provider per instance meant the grace window could
  never fire and every account number in a huvudbok re-paid the full 200ms.

Also: nine hand-rolled animate-pulse placeholders in three different greys
to the Skeleton primitive, 25 CardTitle text-lg to the locked text-base,
two always-on chips to muted text (a chip on every row is not an
exception), the three extensions titles onto the display face, the help
filter chips onto pill geometry, and the dead border-border/60 on the
/import row.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-29 12:33:40 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent bc5c673f7d
commit 3d02a74147
27 changed files with 119 additions and 88 deletions
+7 -3
View File
@@ -242,9 +242,13 @@ export default function ArticleDetailPage({
<div>
<h1 className="font-display text-2xl leading-8 tracking-tight">{article.name}</h1>
<div className="flex items-center gap-2 mt-1">
<Badge variant={article.active ? 'success' : 'secondary'}>
{article.active ? t('status_active') : t('status_inactive')}
</Badge>
{article.active ? (
<span className="text-sm text-muted-foreground">{t('status_active')}</span>
) : (
<Badge variant="outline" className="font-normal">
{t('status_inactive')}
</Badge>
)}
<span className="text-sm text-muted-foreground tabular-nums">
{t(ARTICLE_TYPE_KEY[article.type])}
{article.article_number ? ` · #${article.article_number}` : ''}
@@ -76,7 +76,7 @@ export default async function ExtensionDetailPage({
<Icon className="h-7 w-7 text-primary" />
</div>
<div>
<h1 className="text-xl font-semibold tracking-tight">{extensionName}</h1>
<h1 className="font-display text-2xl leading-8 tracking-tight">{extensionName}</h1>
<p className="text-sm text-muted-foreground mt-0.5">{extensionDescription}</p>
<div className="mt-2">
<CategoryBadge category={definition.category} />
+1 -1
View File
@@ -44,7 +44,7 @@ export default async function SectorExtensionsPage({
<Icon className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-xl font-semibold tracking-tight">{sectorName}</h1>
<h1 className="font-display text-2xl leading-8 tracking-tight">{sectorName}</h1>
<p className="text-sm text-muted-foreground mt-0.5">{sectorDescription}</p>
</div>
</div>
+1 -1
View File
@@ -18,7 +18,7 @@ export default async function ExtensionsPage() {
return (
<div>
<div className="mb-8">
<h1 className="text-xl font-semibold tracking-tight">{t('page_title')}</h1>
<h1 className="font-display text-2xl leading-8 tracking-tight">{t('page_title')}</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('page_description')}
</p>
+6 -6
View File
@@ -350,10 +350,10 @@ export default function HelpPage() {
<button
onClick={() => setSelectedCategory(null)}
className={cn(
'px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
'rounded-full px-4 py-[7px] text-[13px] transition-colors duration-150',
selectedCategory === null
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-muted-foreground hover:text-foreground'
: 'bg-secondary/60 text-muted-foreground hover:bg-secondary hover:text-foreground'
)}
>
{t('filter_all')}
@@ -363,10 +363,10 @@ export default function HelpPage() {
key={key}
onClick={() => setSelectedCategory(selectedCategory === key ? null : key)}
className={cn(
'px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
'rounded-full px-4 py-[7px] text-[13px] transition-colors duration-150',
selectedCategory === key
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-muted-foreground hover:text-foreground'
: 'bg-secondary/60 text-muted-foreground hover:bg-secondary hover:text-foreground'
)}
>
{t(config.labelKey)}
@@ -400,7 +400,7 @@ export default function HelpPage() {
{/* Document templates */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('templates_title')}</CardTitle>
<CardTitle className="text-base">{t('templates_title')}</CardTitle>
<CardDescription>{t('templates_subtitle')}</CardDescription>
</CardHeader>
<CardContent>
@@ -438,7 +438,7 @@ export default function HelpPage() {
{/* External resources */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('external_resources_title')}</CardTitle>
<CardTitle className="text-base">{t('external_resources_title')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-3 sm:grid-cols-2">
+1 -1
View File
@@ -2298,7 +2298,7 @@ function ImportRow({
disabled={disabled}
aria-expanded={expanded}
className={cn(
'group flex w-full items-center justify-between gap-4 border-b border-border/60 px-1 py-3 text-left',
'group flex w-full items-center justify-between gap-4 border-b border-border px-1 py-3 text-left',
'transition-colors duration-150 hover:bg-secondary/35',
'disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent',
)}
+2 -2
View File
@@ -261,9 +261,9 @@ export default function RecurringInvoicesPage() {
<TableCell className="tabular-nums">{formatDate(s.next_run_date)}</TableCell>
<TableCell>
{s.status === 'active' ? (
<Badge variant="success">{t('status_active')}</Badge>
<span className="text-xs text-muted-foreground">{t('status_active')}</span>
) : (
<Badge variant="secondary">{t('status_paused')}</Badge>
<Badge variant="outline" className="font-normal">{t('status_paused')}</Badge>
)}
</TableCell>
<TableCell className="tabular-nums text-right">
@@ -13,7 +13,6 @@ import { useToast } from '@/components/ui/use-toast'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/info-tooltip'
import {
@@ -193,7 +192,6 @@ export function KassaflodesanalysClient() {
)}
Ladda ner PDF
</Button>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<span>
@@ -205,7 +203,6 @@ export function KassaflodesanalysClient() {
</TooltipTrigger>
<TooltipContent>Snart tillgänglig</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
@@ -588,7 +588,7 @@ export default function SupplierInvoiceDetailPage() {
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('invoice_info_title')}</CardTitle>
<CardTitle className="text-base">{t('invoice_info_title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex justify-between">
@@ -629,7 +629,7 @@ export default function SupplierInvoiceDetailPage() {
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('amounts_title')}</CardTitle>
<CardTitle className="text-base">{t('amounts_title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex justify-between">
@@ -666,7 +666,7 @@ export default function SupplierInvoiceDetailPage() {
{invoice.supplier && (
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('supplier_section_title')}</CardTitle>
<CardTitle className="text-base">{t('supplier_section_title')}</CardTitle>
</CardHeader>
<CardContent className="text-sm">
<Link href={`/suppliers/${invoice.supplier.id}`} className="text-primary hover:underline font-medium">
@@ -683,7 +683,7 @@ export default function SupplierInvoiceDetailPage() {
{/* Line items */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('rows_title')}</CardTitle>
<CardTitle className="text-base">{t('rows_title')}</CardTitle>
</CardHeader>
<CardContent>
{/* Desktop table */}
@@ -766,7 +766,7 @@ export default function SupplierInvoiceDetailPage() {
{payments.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('payment_history_title')}</CardTitle>
<CardTitle className="text-base">{t('payment_history_title')}</CardTitle>
</CardHeader>
<CardContent>
{/* Desktop table */}
@@ -824,7 +824,7 @@ export default function SupplierInvoiceDetailPage() {
{invoice.document_id && (
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('document_title')}</CardTitle>
<CardTitle className="text-base">{t('document_title')}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -844,7 +844,7 @@ export default function SupplierInvoiceDetailPage() {
{/* Journal entries (sambandskrav) */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('vouchers_title')}</CardTitle>
<CardTitle className="text-base">{t('vouchers_title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{invoice.registration_journal_entry_id ? (
@@ -895,7 +895,7 @@ export default function SupplierInvoiceDetailPage() {
{invoice.notes && (
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('notes_title')}</CardTitle>
<CardTitle className="text-base">{t('notes_title')}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{invoice.notes}</p>
+4 -6
View File
@@ -126,9 +126,7 @@ export default function SupplierDetailPage() {
return (
<div className="space-y-8">
<Skeleton className="h-8 w-48" />
<Card className="animate-pulse">
<CardContent className="h-48" />
</Card>
<Skeleton className="h-48 w-full" />
</div>
)
}
@@ -240,7 +238,7 @@ export default function SupplierDetailPage() {
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('contact_section_title')}</CardTitle>
<CardTitle className="text-base">{t('contact_section_title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{supplier.email && <p>{t('email_inline', { email: supplier.email })}</p>}
@@ -252,7 +250,7 @@ export default function SupplierDetailPage() {
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('payment_section_title')}</CardTitle>
<CardTitle className="text-base">{t('payment_section_title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{supplier.bankgiro && <p>{t('bankgiro_inline', { value: supplier.bankgiro })}</p>}
@@ -269,7 +267,7 @@ export default function SupplierDetailPage() {
{/* Invoices */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">{t('invoices_section_title')}</CardTitle>
<CardTitle className="text-base">{t('invoices_section_title')}</CardTitle>
<Link href="/supplier-invoices?new=1">
<Button size="sm">
<FileText className="mr-2 h-4 w-4" />
+7 -4
View File
@@ -6,6 +6,7 @@ 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 { ThemeProvider } from "@/components/theme-provider";
import { SWRProvider } from "@/components/providers/SWRProvider";
@@ -89,10 +90,12 @@ export default async function RootLayout({
disableTransitionOnChange
>
<SWRProvider>
{children}
<Toaster />
<DeployReloadPrompt />
<ScrollbarReveal />
<TooltipProvider>
{children}
<Toaster />
<DeployReloadPrompt />
<ScrollbarReveal />
</TooltipProvider>
</SWRProvider>
</ThemeProvider>
</NextIntlClientProvider>