Files
next-tjwater-frontend/src/features/workbench/components/map-dev-panel.tsx
T

779 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { RotateCcw, X } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
import { Disclosure } from "@/shared/ui/disclosure";
import { FieldSelect } from "@/shared/ui/field-select";
import { Input } from "@/shared/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import {
LAYER_GROUP_GEOMETRIES,
type LayerGroupId,
type LayerGroupStylePatch
} from "../map/layer-group-style";
import { MAP_STYLE_TOKENS } from "../map/map-colors";
import { getNumericFeatureProperties } from "../map/value-label";
import type {
WorkbenchMapCommands,
WorkbenchMapControllerState
} from "../map/workbench-map-controller";
import {
SUPPLY_LAYER_CATALOG,
type AvailableWaterNetworkSourceId
} from "../map/sources";
import type { DetailFeature } from "../types";
import { INVESTIGATION_DEV_NETWORK } from "../utils/investigation-dev-network";
import { InvestigationDevTools, type InvestigationDevToolsProps } from "./investigation-dev-tools";
import { FeatureLocationPanel } from "./feature-location-panel";
import { FEATURE_LOCATION_ERROR_LABELS } from "../map/map-feature-feedback";
import type { ScadaAlertIntensity } from "./scada-alert-atmosphere";
import { NoticeDevTools } from "./notice-dev-tools";
type MapDevPanelProps = {
commands: WorkbenchMapCommands;
controllerState: WorkbenchMapControllerState;
detailFeature: DetailFeature | null;
flowDataSource?: "realtime" | "mock";
onClose: () => void;
investigationFollow?: boolean;
onInvestigationFollowChange?: (enabled: boolean) => void;
investigationTools?: InvestigationDevToolsProps;
onPreviewScadaAlert?: () => void;
onEndScadaAlertPreview?: () => void;
scadaAlertPreviewActive?: boolean;
scadaAlertIntensity?: ScadaAlertIntensity;
onScadaAlertIntensityChange?: (intensity: ScadaAlertIntensity) => void;
};
const GROUP_LABELS = Object.fromEntries(
SUPPLY_LAYER_CATALOG.map((layer) => [layer.id, layer.label])
) as Record<AvailableWaterNetworkSourceId, string>;
const STYLE_GROUP_IDS = Object.keys(LAYER_GROUP_GEOMETRIES) as LayerGroupId[];
const LABEL_PRESETS: Record<
LayerGroupId,
{
property: string;
precision: number;
unit: string;
options: { value: string; label: string }[];
}
> = {
pipes: {
property: "diameter",
precision: 0,
unit: "mm",
options: [
{ value: "diameter", label: "管径 diameter" },
{ value: "length", label: "长度 length" },
{ value: "roughness", label: "粗糙系数 roughness" },
{ value: "minor_loss", label: "局部损失 minor_loss" }
]
},
junctions: {
property: "elevation",
precision: 1,
unit: "m",
options: [
{ value: "elevation", label: "高程 elevation" },
{ value: "base_demand", label: "基础需水量 base_demand" }
]
},
valves: {
property: "diameter",
precision: 0,
unit: "mm",
options: [
{ value: "diameter", label: "管径 diameter" },
{ value: "setting", label: "设定值 setting" },
{ value: "minor_loss", label: "局部损失 minor_loss" }
]
},
reservoirs: {
property: "head",
precision: 1,
unit: "m",
options: [{ value: "head", label: "水头 head" }]
}
};
const ERROR_LABELS: Record<string, string> = {
...FEATURE_LOCATION_ERROR_LABELS,
FLOW_UNAVAILABLE: "水流图层初始化失败",
INVALID_FLOW_CALCULATION: "管道 ID 或有符号流速无效",
INVALID_STYLE_PATCH: "参数不符合受控范围"
};
export function MapDevPanel({
commands,
controllerState,
detailFeature,
flowDataSource = "mock",
onClose,
investigationFollow = true,
onInvestigationFollowChange,
investigationTools,
onPreviewScadaAlert,
onEndScadaAlertPreview,
scadaAlertPreviewActive = false,
scadaAlertIntensity = "standard",
onScadaAlertIntensityChange
}: MapDevPanelProps) {
const [activeTab, setActiveTab] = useState(onInvestigationFollowChange ? "investigation" : "map");
const [locationDraft, setLocationDraft] = useState<{ sourceId: AvailableWaterNetworkSourceId; featureId: string }>({
sourceId: "pipes", featureId: INVESTIGATION_DEV_NETWORK.pipeIds[0]
});
const { sourceId, featureId } = locationDraft;
const featureIds = [...new Set(featureId.split(/[\s,,;;、]+/u).filter(Boolean))];
const [property, setProperty] = useState("diameter");
const [flowPipeId, setFlowPipeId] = useState<string>(INVESTIGATION_DEV_NETWORK.pipeIds[0]);
const [flowVelocity, setFlowVelocity] = useState("-1");
const [precision, setPrecision] = useState(0);
const [unit, setUnit] = useState("mm");
const [groupId, setGroupId] = useState<LayerGroupId>("pipes");
const [showValueLabels, setShowValueLabels] = useState(true);
const [showFeatureIds, setShowFeatureIds] = useState(false);
const [labelSize, setLabelSize] = useState(12);
const [labelColor, setLabelColor] = useState<string>(MAP_STYLE_TOKENS.label.primary);
const [draft, setDraft] = useState<Record<string, string | number>>({
color: MAP_STYLE_TOKENS.supply.pipeMedium,
fillColor: MAP_STYLE_TOKENS.supply.junction,
strokeColor: "#FFFFFF",
opacity: 0.9,
widthMultiplier: 1,
radiusMultiplier: 1,
strokeMultiplier: 1,
iconMultiplier: 1,
dash: "original"
});
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !event.defaultPrevented) onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
const geometry = LAYER_GROUP_GEOMETRIES[groupId];
const activeLabel = controllerState.valueLabels[groupId];
const hasActiveLabels = Object.keys(controllerState.valueLabels).length > 0;
function selectStyleGroup(nextGroupId: LayerGroupId) {
const preset = LABEL_PRESETS[nextGroupId];
const saved = controllerState.valueLabels[nextGroupId];
setGroupId(nextGroupId);
setProperty(saved?.property ?? preset.property);
setPrecision(saved?.precision ?? preset.precision);
setUnit(saved?.unit ?? preset.unit);
setShowValueLabels(saved?.showValue ?? true);
setShowFeatureIds(saved?.showId ?? false);
setLabelSize(saved?.textSize ?? 12);
setLabelColor(saved?.textColor ?? MAP_STYLE_TOKENS.label.primary);
}
function useSelectedFeature() {
if (!detailFeature) return;
if (detailFeature.layer === "pipes") setFlowPipeId(detailFeature.id);
if (detailFeature.layer in LAYER_GROUP_GEOMETRIES) {
const nextGroupId = detailFeature.layer as LayerGroupId;
selectStyleGroup(nextGroupId);
const allowedProperties = new Set(
LABEL_PRESETS[nextGroupId].options.map((option) => option.value)
);
const firstNumeric = getNumericFeatureProperties(detailFeature.properties).find(([name]) =>
allowedProperties.has(name)
);
if (firstNumeric) setProperty(firstNumeric[0]);
}
}
function applyLayerStyle() {
let patch: LayerGroupStylePatch;
if (geometry === "line") {
patch = {
color: String(draft.color),
opacity: Number(draft.opacity),
widthMultiplier: Number(draft.widthMultiplier),
dash: draft.dash as "original" | "solid" | "dashed"
};
} else {
patch = {
fillColor: String(draft.fillColor),
strokeColor: String(draft.strokeColor),
opacity: Number(draft.opacity),
radiusMultiplier: Number(draft.radiusMultiplier),
strokeMultiplier: Number(draft.strokeMultiplier),
iconMultiplier: Number(draft.iconMultiplier)
};
}
commands.applyLayerGroupStyle(groupId, patch);
}
function applyLabels() {
if (showValueLabels || showFeatureIds) {
void commands.showValueLabel({
sourceId: groupId,
property,
precision,
unit,
showValue: showValueLabels,
showId: showFeatureIds,
textSize: labelSize,
textColor: labelColor
});
} else {
commands.clearValueLabel(groupId);
}
}
function applyFlowCalculation() {
commands.updatePipeFlowCalculation({
pipeId: flowPipeId.trim(),
velocity: Number(flowVelocity)
});
}
const flowSummary = controllerState.flowSummary;
const validFlowCalculation =
flowPipeId.trim().length > 0 && Number.isFinite(Number(flowVelocity));
return (
<aside
aria-label="地图开发面板"
className="workbench-shadcn-surface bg-background text-foreground shadow-lg pointer-events-auto absolute right-3 top-[4.5rem] z-[80] hidden max-h-[calc(100dvh-5.5rem)] w-[400px] max-w-[calc(100vw-1.5rem)] flex-col overflow-hidden rounded-2xl border lg:flex"
data-testid="map-dev-panel"
>
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
<div>
<h2 className="text-sm font-semibold text-foreground">
开发测试{" "}
<span className="ml-1 text-[11px] font-medium text-muted-foreground">Dev Panel</span>
</h2>
<p className="mt-1 text-[11px] leading-4 text-muted-foreground">
调查、通知与地图工具 · 仅当前页面生效
</p>
</div>
<Button
size="icon"
variant="ghost"
type="button"
aria-label="关闭 Dev Panel"
onClick={onClose}
>
<X size={16} />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex min-h-0 flex-1 flex-col">
<div className="shrink-0 border-b border-border px-4 py-2">
<TabsList aria-label="开发测试类型" className="grid w-full grid-cols-3">
<TabsTrigger value="investigation" disabled={!onInvestigationFollowChange}>调查测试</TabsTrigger>
<TabsTrigger value="notices">通知测试</TabsTrigger>
<TabsTrigger value="map">地图调试</TabsTrigger>
</TabsList>
</div>
<TabsContent value="investigation" hidden={activeTab !== "investigation"} forceMount className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 data-[state=inactive]:hidden [scrollbar-gutter:stable]">
{onPreviewScadaAlert ? <section aria-label="爆管视觉警告测试" className="mb-4 space-y-2 border-b border-border pb-4">
<h3 className="text-xs font-semibold">全屏爆管视觉警告</h3>
<p className="text-[11px] leading-5 text-muted-foreground">本地持续预览,可模拟事件处理结束。</p>
{onScadaAlertIntensityChange ? <div role="group" aria-label="告警灯效强度" className="flex gap-2">
{([ ["soft", "柔和"], ["standard", "标准"], ["strong", "醒目"] ] as const).map(([value, label]) =>
<Button key={value} type="button" size="sm" variant="outline" aria-pressed={scadaAlertIntensity === value}
onClick={() => onScadaAlertIntensityChange(value)}>{label}</Button>)}
</div> : null}
<div className="flex flex-wrap gap-2">
<Button type="button" size="sm" variant="outline" onClick={onPreviewScadaAlert}>测试爆管告警效果</Button>
<Button type="button" size="sm" variant="ghost" disabled={!scadaAlertPreviewActive}
onClick={onEndScadaAlertPreview}>模拟事件处理结束</Button>
</div>
</section> : null}
{onInvestigationFollowChange ? (
<section aria-label="调查联动" className="space-y-4">
<div className="space-y-2">
<ToggleField label="跟随所选调查" checked={investigationFollow} onChange={onInvestigationFollowChange} />
<p className="text-xs leading-5 text-muted-foreground">选中调查后,跟随新的地图高亮、图表和影响范围指令。</p>
</div>
{investigationTools ? <InvestigationDevTools key={`${investigationTools.task?.task_id}:${investigationTools.task?.execution_generation}`} {...investigationTools} /> : null}
</section>
) : null}
</TabsContent>
<TabsContent value="notices" hidden={activeTab !== "notices"} forceMount className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 data-[state=inactive]:hidden [scrollbar-gutter:stable]">
<NoticeDevTools />
</TabsContent>
<TabsContent value="map" hidden={activeTab !== "map"} forceMount className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 data-[state=inactive]:hidden [scrollbar-gutter:stable]">
{controllerState.errorCode ? (
<div role="alert" className="status-tone-danger my-3 break-words rounded-lg border border-[var(--status-border)] bg-[var(--status-soft)] px-3 py-2 text-xs text-[var(--status-foreground)]">
{ERROR_LABELS[controllerState.errorCode] ?? controllerState.errorCode}
</div>
) : null}
<PanelSection
title="要素定位"
description="按业务源与要素 ID 定位,也可从地图工具的定位面板使用。"
initiallyOpen
status={controllerState.pending ? "执行中" : undefined}
>
<FeatureLocationPanel commands={commands} controllerState={controllerState}
detailFeature={detailFeature} value={locationDraft} onValueChange={setLocationDraft}
onUseSelectedFeature={useSelectedFeature} showError={false}
contentClassName="workbench-shadcn-surface" />
</PanelSection>
<PanelSection
title="水流调试"
status={controllerState.flowVisible ? "已开启" : "未开启"}
description={
flowDataSource === "realtime"
? "已读取后端最近 15 分钟的管道流速;可用有符号流速覆盖单管方向。"
: "后端流速不可用,开启时使用正反向演示流向;有符号流速可覆盖单管方向。"
}
>
<div className="grid grid-cols-2 gap-2">
<Button
type="button"
disabled={controllerState.pending || controllerState.flowVisible}
onClick={() => void commands.setFlowVisible(true)}
className="w-full text-xs"
>
开启水流
</Button>
<Button
type="button"
disabled={!controllerState.flowVisible}
onClick={() => void commands.setFlowVisible(false)}
className="w-full text-xs"
variant="outline"
>
关闭水流
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<Field label="管道 ID">
<Input
value={flowPipeId}
onChange={(event) => setFlowPipeId(event.target.value)}
className={inputClassName}
/>
</Field>
<Field label="有符号流速">
<Input
type="number"
step="any"
value={flowVelocity}
onChange={(event) => setFlowVelocity(event.target.value)}
className={inputClassName}
/>
</Field>
</div>
<Button
type="button"
disabled={!validFlowCalculation || controllerState.pending}
onClick={applyFlowCalculation}
className="w-full text-xs"
>
更新该管道方向
</Button>
<div className="grid grid-cols-2 gap-2">
<Button
type="button"
disabled={sourceId !== "pipes" || featureIds.length !== 1}
onClick={() => setFlowPipeId(featureIds[0])}
className="w-full text-xs"
variant="outline"
>
使用 Feature ID
</Button>
<Button
type="button"
onClick={() => void commands.clearPipeFlowCalculations()}
className="w-full text-xs"
variant="outline"
>
清空流速
</Button>
</div>
{flowSummary.total > 0 ? (
<StatusText>
{flowDataSource === "realtime" ? "后端实时流速" : "演示流向"} · 已赋值{" "}
{flowSummary.total} · 正向 {flowSummary.forward} · 反向 {flowSummary.reverse} · 停止{" "}
{flowSummary.stopped}
</StatusText>
) : controllerState.flowVisible ? (
<StatusText>演示流向已启用 · 正反向交错显示</StatusText>
) : (
<StatusText>开启后显示演示流向</StatusText>
)}
</PanelSection>
<PanelSection
title="图层样式"
status={
controllerState.styledGroups.length > 0
? `${controllerState.styledGroups.length} 组已覆盖`
: undefined
}
description="调整业务图层的颜色、透明度和尺寸,交互语义色保持不变。"
>
<FieldSelect
contentClassName="workbench-shadcn-surface"
label="业务图层组"
value={groupId}
onValueChange={(value) => selectStyleGroup(value as LayerGroupId)}
options={STYLE_GROUP_IDS.map((id) => ({ value: id, label: GROUP_LABELS[id] }))}
/>
{geometry === "line" ? (
<>
<ColorField
label="颜色"
value={String(draft.color)}
onChange={(value) => setDraft((current) => ({ ...current, color: value }))}
/>
<RangeField
label="透明度"
min={0}
max={1}
step={0.05}
value={Number(draft.opacity)}
onChange={(value) => setDraft((current) => ({ ...current, opacity: value }))}
/>
<RangeField
label="宽度倍率"
min={0.5}
max={3}
step={0.1}
value={Number(draft.widthMultiplier)}
onChange={(value) =>
setDraft((current) => ({ ...current, widthMultiplier: value }))
}
/>
<FieldSelect
contentClassName="workbench-shadcn-surface"
label="线型"
value={String(draft.dash)}
onValueChange={(value) => setDraft((current) => ({ ...current, dash: value }))}
options={[
{ value: "original", label: "原始" },
{ value: "solid", label: "实线" },
{ value: "dashed", label: "虚线" }
]}
/>
</>
) : (
<>
<div className="grid grid-cols-2 gap-2">
<ColorField
label="填充"
value={String(draft.fillColor)}
onChange={(value) => setDraft((current) => ({ ...current, fillColor: value }))}
/>
<ColorField
label="描边"
value={String(draft.strokeColor)}
onChange={(value) => setDraft((current) => ({ ...current, strokeColor: value }))}
/>
</div>
<RangeField
label="透明度"
min={0}
max={1}
step={0.05}
value={Number(draft.opacity)}
onChange={(value) => setDraft((current) => ({ ...current, opacity: value }))}
/>
<RangeField
label="半径倍率"
min={0.5}
max={3}
step={0.1}
value={Number(draft.radiusMultiplier)}
onChange={(value) =>
setDraft((current) => ({ ...current, radiusMultiplier: value }))
}
/>
<RangeField
label="描边倍率"
min={0.5}
max={3}
step={0.1}
value={Number(draft.strokeMultiplier)}
onChange={(value) =>
setDraft((current) => ({ ...current, strokeMultiplier: value }))
}
/>
{groupId !== "junctions" ? (
<RangeField
label="图标倍率"
min={0.5}
max={3}
step={0.1}
value={Number(draft.iconMultiplier)}
onChange={(value) =>
setDraft((current) => ({ ...current, iconMultiplier: value }))
}
/>
) : null}
</>
)}
<div className="grid grid-cols-2 gap-2">
<Button type="button" onClick={applyLayerStyle} className="w-full text-xs">
应用图层样式
</Button>
<Button
type="button"
onClick={() => commands.resetLayerGroupStyle(groupId)}
className="w-full text-xs"
variant="outline"
>
重置本组
</Button>
</div>
{controllerState.styledGroups.includes(groupId) ? (
<StatusText>已覆盖当前图层组样式</StatusText>
) : null}
</PanelSection>
<PanelSection
title="数值标注"
description="作用于所选图层的全部可见要素,使用原生 MVT 标注并自动避让。"
status={
hasActiveLabels
? `${Object.keys(controllerState.valueLabels).length} 组已标注`
: undefined
}
>
<FieldSelect
contentClassName="workbench-shadcn-surface"
label="标注图层组"
value={groupId}
onValueChange={(value) => selectStyleGroup(value as LayerGroupId)}
options={STYLE_GROUP_IDS.map((id) => ({ value: id, label: GROUP_LABELS[id] }))}
/>
<div className="grid grid-cols-2 gap-2">
<ToggleField label="数值" checked={showValueLabels} onChange={setShowValueLabels} />
<ToggleField label="设施 ID" checked={showFeatureIds} onChange={setShowFeatureIds} />
</div>
{showValueLabels ? (
<>
<FieldSelect
contentClassName="workbench-shadcn-surface"
label="数值属性"
value={property}
onValueChange={setProperty}
options={LABEL_PRESETS[groupId].options}
/>
<div className="grid grid-cols-2 gap-2">
<Field label="精度 0-3">
<Input
type="number"
min={0}
max={3}
value={precision}
onChange={(event) => setPrecision(Number(event.target.value))}
className={inputClassName}
/>
</Field>
<Field label="单位">
<Input
maxLength={16}
value={unit}
onChange={(event) => setUnit(event.target.value)}
className={inputClassName}
/>
</Field>
</div>
</>
) : null}
<RangeField
label="字号"
min={10}
max={18}
step={1}
value={labelSize}
onChange={setLabelSize}
/>
<ColorField label="文字颜色" value={labelColor} onChange={setLabelColor} />
<div className="grid grid-cols-2 gap-2">
<Button
type="button"
disabled={!showValueLabels && !showFeatureIds}
onClick={applyLabels}
className="w-full text-xs"
>
应用标注
</Button>
<Button
type="button"
disabled={!hasActiveLabels}
onClick={() => {
setShowValueLabels(false);
setShowFeatureIds(false);
commands.clearValueLabel();
}}
className="w-full text-xs"
variant="outline"
>
清除全部标注
</Button>
</div>
{activeLabel ? (
<StatusText>
已标注全部 {GROUP_LABELS[groupId]} Feature · {activeLabel.showId ? "ID + " : ""}
{activeLabel.showValue ? activeLabel.property : "仅 ID"}
</StatusText>
) : null}
</PanelSection>
</TabsContent>
</Tabs>
<div className="shrink-0 border-t border-border p-3" hidden={activeTab !== "map"}>
<Button
type="button"
onClick={() => commands.resetAll()}
className="w-full text-xs"
variant="outline"
>
<RotateCcw size={14} />
重置地图调试效果
</Button>
</div>
</aside>
);
}
function PanelSection({
title,
description,
initiallyOpen = false,
status,
children
}: {
title: string;
description: string;
initiallyOpen?: boolean;
status?: string;
children: React.ReactNode;
}) {
return (
<Disclosure
initiallyOpen={initiallyOpen}
className="border-b border-border py-2 last:border-b-0 [&>summary]:px-2 [&>summary]:text-foreground [&>summary]:text-sm"
label={
<span className="flex min-w-0 items-center justify-between gap-2">
<span className="font-semibold">{title}</span>
{status ? (
<span className="shrink-0 text-[11px] font-normal text-muted-foreground">
{status}
</span>
) : null}
</span>
}
>
<div className="space-y-3 px-2 pb-3 pt-1">
<p className="text-[11px] leading-5 text-muted-foreground">{description}</p>
{children}
</div>
</Disclosure>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<label className="block min-w-0">
<span className="mb-1.5 block text-xs font-medium text-muted-foreground">{label}</span>
{children}
</label>
);
}
function ToggleField({
label,
checked,
onChange
}: {
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<label className="flex min-h-10 cursor-pointer items-center gap-2 rounded-lg border border-input bg-background px-2.5 py-2 text-xs font-medium text-foreground">
<Checkbox
checked={checked}
onChange={(event) => onChange(event.target.checked)}
/>
{label}
</label>
);
}
function ColorField({
label,
value,
onChange
}: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
return (
<Field label={label}>
<div className="flex min-h-10 items-center gap-2 rounded-lg border border-input bg-background px-2">
<input
type="color"
value={value}
onChange={(event) => onChange(event.target.value.toUpperCase())}
aria-label={label}
className="h-10 w-10 shrink-0 cursor-pointer rounded border-0 bg-transparent p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--action-blue)]"
/>
<span className="text-xs font-medium tabular-nums text-muted-foreground">{value}</span>
</div>
</Field>
);
}
function RangeField({
label,
min,
max,
step,
value,
onChange
}: {
label: string;
min: number;
max: number;
step: number;
value: number;
onChange: (value: number) => void;
}) {
return (
<Field label={`${label} · ${value}`}>
<input
aria-label={label}
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(event) => onChange(Number(event.target.value))}
className="h-10 w-full rounded accent-[var(--action-blue)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--action-blue)]"
/>
</Field>
);
}
function StatusText({ children, tone = "success" }: { children: React.ReactNode; tone?: "success" | "warning" }) {
return (
<p
role="status"
className={`status-tone-${tone} break-words rounded-lg border border-[var(--status-border)] bg-[var(--status-soft)] px-2.5 py-1.5 text-[11px] font-medium leading-5 text-[var(--status-foreground)] [overflow-wrap:anywhere]`}
>
{children}
</p>
);
}
const inputClassName =
"h-10 w-full rounded-lg border border-input bg-background px-2.5 text-xs text-foreground transition-colors";