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:
@@ -14,7 +14,7 @@ export function ActionRow({ children, className }: { children: ReactNode; classN
|
||||
'flex gap-2',
|
||||
'overflow-x-auto snap-x snap-mandatory scroll-smooth -mx-3 px-3 sm:mx-0 sm:px-0',
|
||||
'sm:flex-wrap sm:overflow-visible',
|
||||
'[&>*]:snap-start [&>*]:shrink-0 sm:[&>*]:snap-none',
|
||||
'*:snap-start *:shrink-0 sm:*:snap-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function BrandedAuthShell({ children, branding }: BrandedAuthShellProps)
|
||||
const altText = branding?.appName || 'Port Nimara';
|
||||
// fixed inset-0 anchors the auth surface to the viewport directly —
|
||||
// iOS Safari ignores overflow-hidden on inner divs for body-level
|
||||
// scrolling, so a regular `h-[100dvh] overflow-hidden` wrapper doesn't
|
||||
// scrolling, so a regular `h-dvh overflow-hidden` wrapper doesn't
|
||||
// stop the rubber-band bounce. Pinning to the viewport via position
|
||||
// fixed does. The fixed-position shell then uses flex to center the
|
||||
// card within the visible area.
|
||||
|
||||
@@ -122,10 +122,7 @@ export function CountryCombobox({
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popper-anchor-width)] min-w-[280px] p-0"
|
||||
align="start"
|
||||
>
|
||||
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search country or code…" />
|
||||
<CommandList>
|
||||
|
||||
@@ -408,7 +408,7 @@ export function DataTable<TData>({
|
||||
const next = e.target.value === 'all' ? 1000 : Number(e.target.value);
|
||||
onPaginationChange?.(1, next);
|
||||
}}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function DetailPageShell({
|
||||
return (
|
||||
<div className={cn('flex flex-col min-h-full', className)}>
|
||||
{/* Desktop-only sticky header - mobile topbar covers this on small viewports. */}
|
||||
<div className="hidden sm:block sticky top-0 z-10 bg-background/95 backdrop-blur border-b border-border px-4 py-3 sm:px-6">
|
||||
<div className="hidden sm:block sticky top-0 z-10 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 sm:px-6">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<h2 className="truncate text-lg font-semibold text-foreground">{entityName}</h2>
|
||||
{status ? <div className="ml-auto shrink-0">{status}</div> : null}
|
||||
@@ -65,7 +65,7 @@ export function DetailPageShell({
|
||||
className={cn(
|
||||
'sm:hidden',
|
||||
'fixed inset-x-0 bottom-[calc(56px+env(safe-area-inset-bottom))] z-30',
|
||||
'border-t border-border bg-background/95 backdrop-blur px-4 py-3',
|
||||
'border-t border-border bg-background/95 backdrop-blur-sm px-4 py-3',
|
||||
'flex items-center gap-2',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -119,7 +119,7 @@ export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }:
|
||||
const actor = row.actor?.name || row.actor?.email || 'System';
|
||||
return (
|
||||
<li key={row.id} className="relative">
|
||||
<span className="absolute -left-[31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
|
||||
<span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">{summarize(row)}</span>
|
||||
<span className="text-muted-foreground"> · {actor}</span>
|
||||
|
||||
@@ -89,7 +89,7 @@ export function InlineCountryField({
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1.5 rounded px-1 -mx-1 py-0.5 text-left text-sm',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring',
|
||||
disabled && 'cursor-not-allowed opacity-60 hover:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -293,7 +293,7 @@ function ReadButton({
|
||||
className={cn(
|
||||
'group rounded px-1 -mx-1 py-0.5 text-left text-sm',
|
||||
multiline ? 'flex w-full items-start gap-1.5' : 'inline-flex items-center gap-1.5',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring',
|
||||
// Select-kind buttons get a faint border so they read as a
|
||||
// distinct interactive element rather than text-with-an-icon.
|
||||
kind === 'select' && 'border border-border bg-background hover:bg-accent',
|
||||
|
||||
@@ -137,7 +137,7 @@ export function InlinePhoneField({
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1.5 rounded px-1 -mx-1 py-0.5 text-left text-sm',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring',
|
||||
disabled && 'cursor-not-allowed opacity-60 hover:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function InlineTimezoneField({
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1.5 rounded px-1 -mx-1 py-0.5 text-left text-sm',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
'hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring',
|
||||
disabled && 'cursor-not-allowed opacity-60 hover:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function ListCard({
|
||||
const innerClassName = cn(
|
||||
'block p-3',
|
||||
accentClassName && 'pl-4',
|
||||
'rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
'rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ResponsiveTabs({ tabs, value, onValueChange }: ResponsiveTabsPro
|
||||
slides under the wrapper. */}
|
||||
<div
|
||||
ref={listRef}
|
||||
className="overflow-x-auto -mx-2 px-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="overflow-x-auto -mx-2 px-2 scrollbar-none [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
<TabsList className="inline-flex w-max">
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function TagPicker({
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search tags..." />
|
||||
<CommandList>
|
||||
|
||||
@@ -97,10 +97,7 @@ export function TimezoneCombobox({
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popper-anchor-width)] min-w-[360px] p-0"
|
||||
align="start"
|
||||
>
|
||||
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[360px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search timezones…" />
|
||||
<CommandList>
|
||||
|
||||
Reference in New Issue
Block a user