feat(deps): Tailwind 3 → 4 + swap tailwindcss-animate for tw-animate-css

Ran the official @tailwindcss/upgrade tool:
- tailwind.config.ts → @theme directive in globals.css
- @tailwind base/components/utilities → @import 'tailwindcss'
- postcss.config switched from tailwindcss + autoprefixer to
  @tailwindcss/postcss (autoprefixer baked in)
- focus-visible:outline-none → focus-visible:outline-hidden (the v3
  utility was a footgun — outline still showed in forced-colors mode)

Reverted the migration tool's over-zealous variant="outline" →
variant="outline-solid" rename on CVA prop values; that rename was
meant for the Tailwind `outline:` utility, not our Button/Badge
component variants.

Swapped tailwindcss-animate (v3-style JS plugin) for tw-animate-css
(v4-native @import). Same utility surface (animate-spin, animate-in,
etc.), one fewer JS plugin in the bundle.

Fixed the upgrade tool's malformed dark variant
(@custom-variant dark (&:is(class *)) — `class` was being parsed as
a tag) to canonical &:where(.dark, .dark *).

Verified: tsc 0 errors, eslint 0 errors (16 pre-existing warnings),
vitest 1315/1315, next build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 22:14:38 +02:00
parent 3147923d91
commit 0ab96d74a8
69 changed files with 561 additions and 753 deletions

View File

@@ -200,7 +200,7 @@ export default function DocumensoSettingsPage() {
<ul className="space-y-1.5">
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -211,7 +211,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -222,7 +222,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -233,7 +233,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -245,7 +245,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -260,7 +260,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -271,7 +271,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>
@@ -283,7 +283,7 @@ export default function DocumensoSettingsPage() {
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600"
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600"
aria-hidden="true"
/>
<span>

View File

@@ -179,7 +179,7 @@ export default function ErrorEventDetailPage() {
<KV label="Name" value={event.errorName ?? '—'} mono />
<div>
<p className="text-xs text-muted-foreground">Message</p>
<p className="mt-0.5 font-mono whitespace-pre-wrap break-words">
<p className="mt-0.5 font-mono whitespace-pre-wrap wrap-break-word">
{event.errorMessage ?? '—'}
</p>
</div>
@@ -195,7 +195,7 @@ export default function ErrorEventDetailPage() {
<Copy className="mr-1.5 h-3 w-3" /> Copy
</Button>
</div>
<pre className="mt-1 max-h-96 overflow-auto rounded bg-muted p-2 text-xs font-mono whitespace-pre-wrap break-words">
<pre className="mt-1 max-h-96 overflow-auto rounded bg-muted p-2 text-xs font-mono whitespace-pre-wrap wrap-break-word">
{event.errorStack}
</pre>
</div>
@@ -211,7 +211,7 @@ export default function ErrorEventDetailPage() {
</CardTitle>
</CardHeader>
<CardContent>
<pre className="max-h-64 overflow-auto rounded bg-muted p-2 text-xs font-mono whitespace-pre-wrap break-words">
<pre className="max-h-64 overflow-auto rounded bg-muted p-2 text-xs font-mono whitespace-pre-wrap wrap-break-word">
{event.requestBodyExcerpt}
</pre>
</CardContent>

View File

@@ -165,7 +165,7 @@ export default function InvoicesPage() {
{/* Delete confirmation */}
{deleteTarget && (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-background/80 backdrop-blur-xs z-50 flex items-center justify-center">
<div className="bg-background border rounded-lg shadow-lg p-6 max-w-sm w-full space-y-4">
<h3 className="font-semibold">Delete Invoice?</h3>
<p className="text-sm text-muted-foreground">

View File

@@ -50,7 +50,7 @@ export default async function PortalDocumentsPage() {
{documents.map((doc) => (
<div key={doc.id} className="bg-white rounded-lg border p-5">
<div className="flex items-start gap-4">
<FileText className="h-5 w-5 text-gray-400 mt-0.5 flex-shrink-0" />
<FileText className="h-5 w-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex-1 min-w-0">
@@ -59,7 +59,7 @@ export default async function PortalDocumentsPage() {
{DOC_TYPE_LABELS[doc.documentType] ?? doc.documentType}
</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center gap-2 shrink-0">
<Badge variant={STATUS_COLORS[doc.status] ?? 'default'}>
{doc.status.replace(/_/g, ' ')}
</Badge>

View File

@@ -71,7 +71,7 @@ export default async function PortalInvoicesPage() {
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-right shrink-0">
<p className="text-lg font-semibold text-gray-900">
{formatCurrency(invoice.total, invoice.currency)}
</p>

View File

@@ -40,7 +40,7 @@ export default async function PortalMyYachtsPage() {
{yachts.map((y) => (
<div key={y.id} className="bg-white rounded-lg border p-5">
<div className="flex items-start gap-4">
<Sailboat className="h-5 w-5 text-gray-400 mt-0.5 flex-shrink-0" />
<Sailboat className="h-5 w-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex-1 min-w-0">

View File

@@ -65,7 +65,7 @@ export default async function ScannerLayout({
return (
<QueryProvider>
<PortProvider ports={[port]} defaultPortId={port.id}>
<div className="min-h-[100dvh] bg-background">{children}</div>
<div className="min-h-dvh bg-background">{children}</div>
</PortProvider>
</QueryProvider>
);

View File

@@ -1,6 +1,183 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-brand-50: #d8e5f4;
--color-brand-100: #b1cbe9;
--color-brand-200: #89b0de;
--color-brand-300: #6196d3;
--color-brand-400: #3a7bc8;
--color-brand-500: #2f6ab5;
--color-brand-600: #255a9e;
--color-brand-700: #1c4a87;
--color-brand: #3a7bc8;
--color-brand-dark: #1e2844;
--color-navy-50: #cdcfd6;
--color-navy-100: #9ea1af;
--color-navy-200: #71768a;
--color-navy-300: #474e66;
--color-navy-400: #1e2844;
--color-navy-500: #171f35;
--color-navy-600: #101625;
--color-navy: #1e2844;
--color-sage: #dae3c1;
--color-sage-light: #edf1e2;
--color-sage-dark: #b8c49e;
--color-mint: #add5b3;
--color-mint-light: #d6ead9;
--color-mint-dark: #7dba85;
--color-teal: #83aab1;
--color-teal-light: #b1cdd2;
--color-teal-dark: #5a8a92;
--color-purple: #685aa3;
--color-purple-light: #a49ac6;
--color-purple-dark: #4d4280;
--color-success: #2d8a4e;
--color-success-bg: #e8f5e9;
--color-success-border: #a5d6a7;
--color-warning: #e6a817;
--color-warning-bg: #fff8e1;
--color-warning-border: #ffe082;
--color-error: #d32f2f;
--color-error-bg: #ffebee;
--color-error-border: #ef9a9a;
--color-sidebar: #1e2844;
--color-sidebar-text: #cdcfd6;
--color-sidebar-hover: #171f35;
--color-sidebar-active: #3a7bc8;
--color-sidebar-divider: #474e66;
--font-sans: Inter, system-ui, -apple-system, Arial, sans-serif;
--font-mono: JetBrains Mono, ui-monospace, monospace;
--font-serif: Georgia, Times New Roman, serif;
--shadow-xs: 0 1px 2px 0 rgb(15 23 42 / 0.04);
--shadow-sm: 0 2px 4px -1px rgb(15 23 42 / 0.06);
--shadow: 0 1px 3px rgba(30, 40, 68, 0.1), 0 1px 2px rgba(30, 40, 68, 0.06);
--shadow-md: 0 4px 12px -2px rgb(15 23 42 / 0.08);
--shadow-lg: 0 12px 32px -8px rgb(15 23 42 / 0.12);
--shadow-xl: 0 20px 25px rgba(30, 40, 68, 0.1), 0 8px 10px rgba(30, 40, 68, 0.04);
--shadow-glow: 0 0 0 4px rgb(58 123 200 / 0.12);
--radius-sm: 0.375rem;
--radius: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.625rem;
--radius-xl: 0.875rem;
--background-image-gradient-brand: linear-gradient(135deg, #3a7bc8 0%, #2f6ab5 100%);
--background-image-gradient-brand-soft: linear-gradient(135deg, #d8e5f4 0%, #ffffff 100%);
--background-image-gradient-success: linear-gradient(135deg, #e8f5e9 0%, #ffffff 100%);
--background-image-gradient-warning: linear-gradient(135deg, #fef3c7 0%, #ffffff 100%);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--width-sidebar: 256px;
--width-sidebar-collapsed: 64px;
--transition-duration-sidebar: 200ms;
--transition-duration-fast: 150ms;
--transition-duration-base: 200ms;
--transition-duration-slow: 300ms;
--spacing-safe: env(safe-area-inset-bottom);
--spacing-safe-top: env(safe-area-inset-top);
--spacing-safe-bottom: env(safe-area-inset-bottom);
--spacing-safe-left: env(safe-area-inset-left);
--spacing-safe-right: env(safe-area-inset-right);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-badge-pop: badge-pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes badge-pop {
0% {
transform: scale(0.5);
opacity: 0;
}
60% {
transform: scale(1.18);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@layer base {
:root {