diff --git a/src/features/map/core/components/base-layers-control.tsx b/src/features/map/core/components/base-layers-control.tsx
index 30a05cd..a2757b3 100644
--- a/src/features/map/core/components/base-layers-control.tsx
+++ b/src/features/map/core/components/base-layers-control.tsx
@@ -114,7 +114,7 @@ export function BaseLayersControl({
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
active
- ? "border-blue-500 text-blue-700 shadow-sm ring-2 ring-blue-100"
+ ? "border-blue-500 text-blue-700 shadow-xs ring-2 ring-blue-100"
: "text-slate-700 hover:border-blue-300 hover:bg-blue-50/40",
option.disabled && "cursor-not-allowed opacity-50"
)}
@@ -166,7 +166,7 @@ export function BaseLayersControl({
"flex h-[84px] w-[84px] flex-col p-1 text-center transition duration-150",
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
- active ? "border-blue-500 shadow-sm ring-2 ring-blue-100" : "border-slate-200 hover:border-blue-300 hover:bg-blue-50/40",
+ active ? "border-blue-500 shadow-xs ring-2 ring-blue-100" : "border-slate-200 hover:border-blue-300 hover:bg-blue-50/40",
option.disabled && "cursor-not-allowed opacity-50"
)}
>
@@ -186,7 +186,7 @@ export function BaseLayersControl({
type="button"
aria-label={`${title}:当前 ${activeLayer?.label ?? ""}`}
title={`${title}:当前 ${activeLayer?.label ?? ""}`}
- className={cn("grid h-[84px] w-[84px] place-items-center p-1 text-slate-700 shadow-sm transition hover:border-blue-300 hover:text-blue-700", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME)}
+ className={cn("grid h-[84px] w-[84px] place-items-center p-1 text-slate-700 shadow-xs transition hover:border-blue-300 hover:text-blue-700", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME)}
>
{activeLayer ? (
@@ -219,7 +219,7 @@ function BaseLayerSwatch({ option, active }: BaseLayerSwatchProps) {
)}
>
{active ? (
-
+
) : null}
diff --git a/src/features/map/core/components/draw-toolbar.tsx b/src/features/map/core/components/draw-toolbar.tsx
index 5bb3ed4..d25ebf8 100644
--- a/src/features/map/core/components/draw-toolbar.tsx
+++ b/src/features/map/core/components/draw-toolbar.tsx
@@ -111,7 +111,7 @@ export function MapDrawToolbar({ items, label = "绘制工具", className = "",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-900 focus-visible:outline-offset-2",
item.active
- ? "bg-blue-600 text-white shadow-sm"
+ ? "bg-blue-600 text-white shadow-xs"
: "hover:bg-blue-50 hover:text-blue-700",
isDanger && "hover:bg-red-50 hover:text-red-700",
item.disabled && "cursor-not-allowed opacity-45 hover:bg-transparent hover:text-slate-600"
diff --git a/src/features/map/core/components/layer-panel.tsx b/src/features/map/core/components/layer-panel.tsx
index 85d7e8f..534baf2 100644
--- a/src/features/map/core/components/layer-panel.tsx
+++ b/src/features/map/core/components/layer-panel.tsx
@@ -64,7 +64,7 @@ function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySection
"relative flex min-h-[76px] w-full flex-col items-center justify-center gap-1.5 border px-1.5 py-2 text-center transition duration-150 active:translate-y-px active:scale-[0.98]",
MAP_COMPACT_RADIUS_CLASS_NAME,
item.visible
- ? "border-blue-200 bg-blue-50/90 text-blue-950 shadow-sm shadow-blue-950/5"
+ ? "border-blue-200 bg-blue-50/90 text-blue-950 shadow-xs shadow-blue-950/5"
: "surface-well border-transparent text-slate-500 hover:border-slate-200 hover:bg-white",
disabled && "cursor-not-allowed opacity-60 hover:bg-transparent"
)}
diff --git a/src/features/map/core/components/notice.tsx b/src/features/map/core/components/notice.tsx
index 75490b6..c4980a1 100644
--- a/src/features/map/core/components/notice.tsx
+++ b/src/features/map/core/components/notice.tsx
@@ -31,7 +31,7 @@ type SonnerPosition = NonNullable["position"]>;
const DEFAULT_POSITION: MapNoticePosition = "top-center";
const positionClassNames: Record = {
- "top-center": "!left-1/2 !right-auto top-[84px] [translate:-50%_0]",
+ "top-center": "left-1/2! right-auto! top-[84px] [translate:-50%_0]",
"top-right": "top-[84px] right-[72px]",
"bottom-left": "bottom-12 left-4",
"bottom-right": "bottom-12 right-4"
@@ -116,11 +116,11 @@ export function showMapNotice(options: MapNoticeOptions) {
: undefined,
classNames: {
cancelButton: cn(
- "absolute right-2 top-2 z-10 grid h-7 w-7 place-items-center border border-transparent bg-transparent p-0 text-slate-400 transition hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/25",
+ "absolute right-2 top-2 z-10 grid h-7 w-7 place-items-center border border-transparent bg-transparent p-0 text-slate-400 transition hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25",
MAP_ICON_CELL_RADIUS_CLASS_NAME
)
},
- className: cn("!fixed !z-[60] !w-[min(420px,calc(100vw-24px))]", positionClassNames[position])
+ className: cn("fixed! z-[60]! w-[min(420px,calc(100vw-24px))]!", positionClassNames[position])
}
);
}
diff --git a/src/features/workbench/components/feature-popover.tsx b/src/features/workbench/components/feature-popover.tsx
index d8512ae..e50cb6b 100644
--- a/src/features/workbench/components/feature-popover.tsx
+++ b/src/features/workbench/components/feature-popover.tsx
@@ -139,12 +139,12 @@ export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
{canCopy ? (
-
diff --git a/src/features/workbench/components/map-dev-panel.tsx b/src/features/workbench/components/map-dev-panel.tsx
index 5087d9a..4764c47 100644
--- a/src/features/workbench/components/map-dev-panel.tsx
+++ b/src/features/workbench/components/map-dev-panel.tsx
@@ -231,6 +231,6 @@ function RangeField({ label, min, max, step, value, onChange }: { label: string;
return onChange(Number(event.target.value))} className="h-6 w-full accent-blue-600" />;
}
-const inputClassName = "h-9 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";
+const inputClassName = "h-9 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-hidden transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";
const primaryButtonClassName = "inline-flex h-9 items-center justify-center gap-1.5 rounded-lg bg-blue-600 px-3 text-xs font-semibold text-white transition hover:bg-blue-700 active:scale-95 disabled:cursor-not-allowed disabled:scale-100 disabled:opacity-45";
const secondaryButtonClassName = "inline-flex h-9 items-center justify-center gap-1.5 rounded-lg border border-slate-200 bg-white/70 px-3 text-xs font-semibold text-slate-700 transition hover:bg-white active:scale-95 disabled:cursor-not-allowed disabled:scale-100 disabled:opacity-45";
diff --git a/src/features/workbench/components/scheduled-condition-feed.tsx b/src/features/workbench/components/scheduled-condition-feed.tsx
index ecf1a6b..b26f42d 100644
--- a/src/features/workbench/components/scheduled-condition-feed.tsx
+++ b/src/features/workbench/components/scheduled-condition-feed.tsx
@@ -517,7 +517,7 @@ function ConditionFilterDropdown({
@@ -707,7 +707,7 @@ function ConditionTimelineRow({ condition, selected, isLast, onSelect }: Conditi
aria-pressed={selected}
onClick={onSelect}
className={cn(
- "group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl border px-1 py-1 text-left outline-none transition-colors focus-visible:bg-blue-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-200",
+ "group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl border px-1 py-1 text-left outline-hidden transition-colors focus-visible:bg-blue-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-200",
selected ? "surface-reading border-blue-100 text-blue-900" : "border-transparent hover:bg-slate-50"
)}
>
diff --git a/src/features/workbench/components/workbench-top-bar-menus.tsx b/src/features/workbench/components/workbench-top-bar-menus.tsx
index e29e4ed..6e1efc9 100644
--- a/src/features/workbench/components/workbench-top-bar-menus.tsx
+++ b/src/features/workbench/components/workbench-top-bar-menus.tsx
@@ -372,7 +372,7 @@ function MenuPlainButton({
MAP_COMPACT_RADIUS_CLASS_NAME,
disabled
? "border-slate-200 bg-slate-50 text-slate-500"
- : "border-slate-200 bg-white text-slate-800 hover:border-blue-100 hover:bg-blue-50/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-100"
+ : "border-slate-200 bg-white text-slate-800 hover:border-blue-100 hover:bg-blue-50/55 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-100"
)}
>
{label}
@@ -402,7 +402,7 @@ function MenuPrimaryButton({
MAP_COMPACT_RADIUS_CLASS_NAME,
disabled
? "border-slate-200 bg-slate-50 text-slate-400"
- : "border-blue-200 bg-blue-600 text-white shadow-lg shadow-blue-600/20 hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
+ : "border-blue-200 bg-blue-600 text-white shadow-lg shadow-blue-600/20 hover:bg-blue-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-200"
)}
>
diff --git a/src/features/workbench/components/workbench-top-bar-styles.ts b/src/features/workbench/components/workbench-top-bar-styles.ts
index 08bd5da..137274c 100644
--- a/src/features/workbench/components/workbench-top-bar-styles.ts
+++ b/src/features/workbench/components/workbench-top-bar-styles.ts
@@ -1,5 +1,5 @@
export const headerControlButtonClassName =
- "inline-flex h-8 items-center gap-2 whitespace-nowrap rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
+ "inline-flex h-8 items-center gap-2 whitespace-nowrap rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
export const headerControlIconClassName =
"grid h-5 w-5 shrink-0 place-items-center rounded-full bg-slate-100/80 text-slate-500 transition-colors group-hover:bg-blue-50 group-hover:text-blue-700 group-data-[state=open]:bg-blue-50 group-data-[state=open]:text-blue-700";
diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx
index 39a6e1a..748c2db 100644
--- a/src/features/workbench/map-workbench-page.tsx
+++ b/src/features/workbench/map-workbench-page.tsx
@@ -956,7 +956,7 @@ export function MapWorkbenchPage() {
agentPanelWidth ?? getWorkbenchViewportLayout(viewportWidth).agentWidth
)}
className={cn(
- "group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-none",
+ "group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-hidden",
agentPanelResizing && "[&>span]:bg-blue-500 [&>span]:opacity-100"
)}
role="separator"
@@ -965,7 +965,7 @@ export function MapWorkbenchPage() {
onKeyDown={handleAgentPanelResizeKeyDown}
onPointerDown={handleAgentPanelResizeStart}
>
-
+
>
) : (
diff --git a/src/shared/ui/badge.tsx b/src/shared/ui/badge.tsx
index e87d62b..e725b7a 100644
--- a/src/shared/ui/badge.tsx
+++ b/src/shared/ui/badge.tsx
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
diff --git a/src/shared/ui/button-group.tsx b/src/shared/ui/button-group.tsx
index eda2173..36b8670 100644
--- a/src/shared/ui/button-group.tsx
+++ b/src/shared/ui/button-group.tsx
@@ -67,7 +67,7 @@ function ButtonGroupSeparator({
data-slot="button-group-separator"
orientation={orientation}
className={cn(
- "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
+ "bg-input relative m-0! self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
diff --git a/src/shared/ui/button.tsx b/src/shared/ui/button.tsx
index 660c242..9863877 100644
--- a/src/shared/ui/button.tsx
+++ b/src/shared/ui/button.tsx
@@ -5,18 +5,18 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
- "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
outline:
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
diff --git a/src/shared/ui/command.tsx b/src/shared/ui/command.tsx
index 3f853fb..0fa563c 100644
--- a/src/shared/ui/command.tsx
+++ b/src/shared/ui/command.tsx
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
{children}
-
+
Close
diff --git a/src/shared/ui/dropdown-menu.tsx b/src/shared/ui/dropdown-menu.tsx
index 5a20503..7447403 100644
--- a/src/shared/ui/dropdown-menu.tsx
+++ b/src/shared/ui/dropdown-menu.tsx
@@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
svg]:size-4 [&>svg]:shrink-0",
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
@@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
) {
data-slot="input-group"
role="group"
className={cn(
- "group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
+ "group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-hidden transition-[color,box-shadow]",
"h-9 has-[>textarea]:h-auto",
// Variants based on alignment.
diff --git a/src/shared/ui/input.tsx b/src/shared/ui/input.tsx
index 69b64fb..6f3073a 100644
--- a/src/shared/ui/input.tsx
+++ b/src/shared/ui/input.tsx
@@ -8,7 +8,7 @@ const Input = React.forwardRef>(
span]:line-clamp-1",
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
filePath !== migrationTestPath)
+ .map((filePath) => readFileSync(join(projectRoot, filePath), "utf8"))
+ .join("\n");
+
+describe("Tailwind v4 migration", () => {
+ it("does not reintroduce v3-only utility syntax", () => {
+ expect(runtimeSource).not.toMatch(/(?:max-h|origin)-\[--/);
+ expect(runtimeSource).not.toMatch(/\bshadow-sm\b/);
+ expect(runtimeSource).not.toMatch(/\boutline-none\b/);
+ expect(runtimeSource).not.toMatch(
+ /(?:^|\s)!(?:m-|left-|right-|fixed\b|z-|w-|border-|ring-)|:!(?:border-|ring-)/m
+ );
+ });
+
+ it("preserves the Tailwind v3 default border color", () => {
+ const styles = readFileSync(join(projectRoot, "src/styles.css"), "utf8");
+
+ expect(styles).toMatch(
+ /@layer base\s*{[\s\S]*?::file-selector-button\s*{[\s\S]*?border-color:\s*var\(--color-gray-200, currentColor\)/
+ );
+ });
+});