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>
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useState } from 'react';
|
|
import { Loader2, Pencil } from 'lucide-react';
|
|
|
|
import { TimezoneCombobox } from '@/components/shared/timezone-combobox';
|
|
import { toastError } from '@/lib/api/toast-error';
|
|
import { formatTimezoneLabel } from '@/lib/i18n/timezones';
|
|
import type { CountryCode } from '@/lib/i18n/countries';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface InlineTimezoneFieldProps {
|
|
value: string | null | undefined;
|
|
onSave: (next: string | null) => Promise<void>;
|
|
/** Optional country to surface "Suggested" zones in the picker. */
|
|
countryHint?: CountryCode | null;
|
|
emptyText?: string;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
'data-testid'?: string;
|
|
}
|
|
|
|
export function InlineTimezoneField({
|
|
value,
|
|
onSave,
|
|
countryHint,
|
|
emptyText = '-',
|
|
disabled,
|
|
className,
|
|
'data-testid': testId,
|
|
}: InlineTimezoneFieldProps) {
|
|
const [editing, setEditing] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
// Set true when the user picks a value from the dropdown, so the
|
|
// popover-close handler knows commit() will exit edit mode itself.
|
|
const pickedRef = useRef(false);
|
|
|
|
async function commit(next: string | null) {
|
|
pickedRef.current = true;
|
|
if (next === (value ?? null)) {
|
|
setEditing(false);
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
await onSave(next);
|
|
setEditing(false);
|
|
} catch (err) {
|
|
toastError(err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
if (editing) {
|
|
return (
|
|
<div className={cn('flex items-center gap-1', className)}>
|
|
<TimezoneCombobox
|
|
value={value}
|
|
onChange={(tz) => void commit(tz)}
|
|
countryHint={countryHint ?? undefined}
|
|
data-testid={testId}
|
|
defaultOpen
|
|
onOpenChange={(open) => {
|
|
// Auto-exit edit mode when the dropdown closes without a pick,
|
|
// so the user isn't stuck looking at the trigger. commit() owns
|
|
// the exit when a value was selected.
|
|
if (!open && !pickedRef.current) {
|
|
setEditing(false);
|
|
}
|
|
if (open) pickedRef.current = false;
|
|
}}
|
|
/>
|
|
{saving && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const display = value ? formatTimezoneLabel(value) : null;
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => setEditing(true)}
|
|
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-hidden focus-visible:ring-1 focus-visible:ring-ring',
|
|
disabled && 'cursor-not-allowed opacity-60 hover:bg-transparent',
|
|
className,
|
|
)}
|
|
>
|
|
<span className={cn('flex-1', !display && 'text-muted-foreground')}>
|
|
{display ?? emptyText}
|
|
</span>
|
|
{!disabled && (
|
|
<Pencil className="h-3 w-3 opacity-0 transition-opacity group-hover:opacity-50" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|