优化 TransformEditor:

- 本地化锚点、偏移及相关 UI 文本。
- 添加字段提示说明,提升编辑体验。
- 改进输入框布局和样式,增强可读性和交互性。
- 引入 `FieldLabel` 组件,支持带提示的字段标签渲染。
This commit is contained in:
2026-08-14 20:24:56 +08:00
parent d161e9efca
commit cf5e9bbc16
@@ -3,6 +3,7 @@ import {
ChevronDown,
ChevronUp,
Crosshair,
Info,
LockKeyhole,
Maximize2,
Move,
@@ -36,16 +37,16 @@ type AnchorPreset = {
const AXIS_LABELS = ['X', 'Y'] as const;
const AXIS_MODE_LABELS: Record<AnchorMode, string> = {
left: 'Left',
center: 'Center',
right: 'Right',
stretch: 'Stretch',
left: '左侧',
center: '居中',
right: '右侧',
stretch: '拉伸',
};
const ROW_MODE_LABELS: Record<AnchorMode, string> = {
left: 'Top',
center: 'Middle',
right: 'Bottom',
stretch: 'Stretch',
left: '顶部',
center: '居中',
right: '底部',
stretch: '拉伸',
};
const COLUMN_MODES: AnchorMode[] = ['left', 'center', 'right', 'stretch'];
const ROW_MODES: AnchorMode[] = ['left', 'center', 'right', 'stretch'];
@@ -61,11 +62,20 @@ const PRESETS: AnchorPreset[] = ROW_MODES.flatMap((y) =>
const CUSTOM_PRESET: AnchorPreset = {
id: 'custom',
label: 'Custom',
label: '自定义',
x: 'left',
y: 'left',
};
const FIELD_HINTS = {
anchor_min:
'锚点最小值:用父容器的比例位置(0 到 1)定义元素的左上边界。0 表示左侧或顶部,1 表示右侧或底部。',
anchor_max:
'锚点最大值:用父容器的比例位置(0 到 1)定义元素的右下边界。与最小值不同可让元素随父容器拉伸。',
offset_min: '偏移最小值:相对于最小锚点的像素偏移,控制元素左侧和顶部的位置。',
offset_max: '偏移最大值:相对于最大锚点的像素偏移,控制元素右侧和底部的位置。',
} as const;
function cloneTransform(transform: Transform): Transform {
return {
anchor_min: [...transform.anchor_min],
@@ -176,22 +186,22 @@ function formatValue(value: number): string {
function VectorInputRow({
label,
hint,
values,
step,
readOnly,
onCommit,
}: {
label: string;
hint: string;
values: readonly [number, number];
step: number;
readOnly: boolean;
onCommit: (axis: Axis, value: number) => void;
}) {
return (
<div className="grid grid-cols-[5.6rem_minmax(0,1fr)_minmax(0,1fr)] items-center gap-2">
<span className="text-[11px] font-semibold tracking-wide text-(--platform-text-soft)">
{label}
</span>
<div className="grid grid-cols-[6.5rem_minmax(0,1fr)_minmax(0,1fr)] items-center gap-2">
<FieldLabel label={label} hint={hint} />
{AXIS_LABELS.map((axisLabel, axis) => (
<ScalarInput
key={axisLabel}
@@ -206,6 +216,31 @@ function VectorInputRow({
);
}
function FieldLabel({ label, hint }: { label: string; hint: string }) {
return (
<span
className="group/field relative inline-flex items-center gap-1 text-[11px] font-semibold tracking-wide text-(--platform-text-soft)"
title={hint}
>
{label}
<button
type="button"
aria-label={`${label}说明`}
title={hint}
className="grid size-4 shrink-0 place-items-center rounded-full text-(--platform-text-soft) outline-none transition hover:bg-orange-100 hover:text-orange-700 focus-visible:ring-2 focus-visible:ring-orange-200"
>
<Info size={11} aria-hidden="true" />
</button>
<span
role="tooltip"
className="pointer-events-none absolute bottom-[calc(100%+0.4rem)] left-0 z-40 hidden w-64 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-neutral-bg) px-2.5 py-2 text-[10px] font-normal leading-relaxed text-(--platform-neutral-text) shadow-lg group-hover/field:block group-focus-within/field:block"
>
{hint}
</span>
</span>
);
}
function ScalarInput({
ariaLabel,
value,
@@ -276,7 +311,7 @@ function ScalarInput({
<span className="absolute right-1 top-1/2 flex -translate-y-1/2 flex-col gap-0.5">
<button
type="button"
aria-label={`${ariaLabel} increase`}
aria-label={`${ariaLabel} 增加`}
disabled={readOnly}
className="grid size-5 place-items-center rounded text-(--platform-text-soft) transition hover:bg-orange-100 hover:text-orange-700 disabled:cursor-default disabled:opacity-40"
onMouseDown={(event) => event.preventDefault()}
@@ -286,7 +321,7 @@ function ScalarInput({
</button>
<button
type="button"
aria-label={`${ariaLabel} decrease`}
aria-label={`${ariaLabel} 减少`}
disabled={readOnly}
className="grid size-5 place-items-center rounded text-(--platform-text-soft) transition hover:bg-orange-100 hover:text-orange-700 disabled:cursor-default disabled:opacity-40"
onMouseDown={(event) => event.preventDefault()}
@@ -361,21 +396,24 @@ export function TransformEditor({
<SlidersHorizontal size={15} aria-hidden="true" />
</span>
<div>
<h2 className="text-sm font-semibold">Rect Transform</h2>
<p className="text-[10px] text-(--platform-text-soft)">
Anchors &amp; offsets
<h2 className="text-sm font-semibold"></h2>
<p
className="text-[10px] text-(--platform-text-soft)"
title="通过锚点比例和像素偏移控制元素在父容器中的位置与尺寸。"
>
</p>
</div>
</div>
{readOnly ? (
<span className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-slate-200 px-2 py-1 text-[10px] font-semibold text-slate-600">
<LockKeyhole size={12} aria-hidden="true" />
Read only
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-[10px] font-semibold text-emerald-600">
<Unlock size={12} aria-hidden="true" />
Editable
</span>
)}
</header>
@@ -386,6 +424,7 @@ export function TransformEditor({
type="button"
aria-expanded={presetOpen}
aria-haspopup="dialog"
title="锚点预设:选择元素相对于父容器的锚点位置;拉伸模式会让元素边界随父容器变化。"
disabled={readOnly}
className="flex h-11 w-full items-center justify-between rounded-xl border border-(--platform-subpanel-border) bg-white/65 px-3 text-left transition hover:border-orange-300 hover:bg-white disabled:cursor-default disabled:opacity-65"
onClick={() => setPresetOpen((open) => !open)}
@@ -396,7 +435,7 @@ export function TransformEditor({
</span>
<span>
<span className="block text-[10px] font-semibold uppercase tracking-[0.08em] text-(--platform-text-soft)">
Anchor preset
</span>
<span className="block text-xs font-semibold">
{selectedPreset.label}
@@ -414,11 +453,11 @@ export function TransformEditor({
<div
className="absolute inset-x-0 top-[calc(100%+0.5rem)] z-30 rounded-2xl border border-(--platform-subpanel-border) bg-white p-3 shadow-[0_18px_42px_rgb(41_31_24/18%)]"
role="dialog"
aria-label="Anchor presets"
aria-label="锚点预设"
>
<div className="mb-2 flex items-center justify-between px-1">
<span className="text-[10px] font-bold uppercase tracking-widest text-(--platform-text-soft)">
Anchor
</span>
<span className="font-mono text-[10px] text-(--platform-text-soft)">
4 × 4
@@ -485,7 +524,7 @@ export function TransformEditor({
setPresetOpen(false);
}}
>
<span>Custom Anchor</span>
<span></span>
</button>
</div>
) : null}
@@ -495,10 +534,11 @@ export function TransformEditor({
<div className="grid gap-2 rounded-xl border border-dashed border-orange-200 bg-orange-50/45 p-3">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-[0.08em] text-orange-700">
<Move size={13} aria-hidden="true" />
Custom Anchor
</div>
<VectorInputRow
label="Anchor Min"
label="锚点最小值"
hint={FIELD_HINTS.anchor_min}
values={transform.anchor_min}
step={0.05}
readOnly={readOnly}
@@ -507,7 +547,8 @@ export function TransformEditor({
}
/>
<VectorInputRow
label="Anchor Max"
label="锚点最大值"
hint={FIELD_HINTS.anchor_max}
values={transform.anchor_max}
step={0.05}
readOnly={readOnly}
@@ -529,22 +570,23 @@ export function TransformEditor({
aria-hidden="true"
/>
<span>
Anchor values are outside the standard range or produce an invalid
rectangle.
</span>
</div>
) : null}
<div className="grid gap-2">
<VectorInputRow
label="Offset Min"
label="偏移最小值"
hint={FIELD_HINTS.offset_min}
values={transform.offset_min}
step={1}
readOnly={readOnly}
onCommit={(axis, value) => updateVector('offset_min', axis, value)}
/>
<VectorInputRow
label="Offset Max"
label="偏移最大值"
hint={FIELD_HINTS.offset_max}
values={transform.offset_max}
step={1}
readOnly={readOnly}