import React, { useCallback, useRef, useEffect, useMemo, useState } from 'react';
import JsBarcode from 'jsbarcode';
import { QRCodeSVG } from 'qrcode.react';
import type {
  LabelTemplate,
  LabelElement,
  NutritionExtraItem,
  PrintOrientation,
} from '../../../types/labelTemplate';
import {
  canonicalElementType,
  isPrintInputElement,
  isCompanyAutoElement,
  resolveLabelEditorElementFontFamily,
} from '../../../types/labelTemplate';
import {
  INVERT_COLORS_BG,
  INVERT_COLORS_FG,
  readInvertColors,
} from '../../../utils/invertColorsConfig';
import { PRESET_LABEL_SIZES } from '../../../types/labelTemplate';
import { NUTRITION_FIXED_ITEMS } from '../../../types/labelTemplate';
import { cn } from '../../ui/utils';
import { resolvePictureUrlForDisplay } from '../../../services/imageUploadService';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../../ui/select';
import { normalizeBarcodeType, toJsBarcodeFormat } from '../../../lib/barcodeFormat';
import { formatCompanyPrintPreviewText } from '../../../utils/companyPrintFields';
import { pxToUnit, unitToPx } from '@/utils/labelTemplateUnits';
import {
  type PreviewRulerDisplayUnit,
  convertTemplateLengthToDisplay,
  elementPxToDisplayLength,
  formatPreviewRulerDisplayValue,
  previewRulerUnitLabel,
} from '@/utils/previewRulerUnits';
import { isLikelyResolvedDateTimeLiteral } from '../../../lib/labelFormDatePreview';

export type { PrintOrientation } from '../../../types/labelTemplate';

export function printContentLayerStyle(
  orientation: PrintOrientation,
  baseW: number,
  baseH: number,
): React.CSSProperties {
  if (orientation !== 'horizontal') return {};
  return {
    transform: 'rotate(90deg)',
    transformOrigin: `${baseW / 2}px ${baseH / 2}px`,
  };
}

/** 横打 preview 层 rotate(90deg) 的逆变换：画布本地坐标 → 模板坐标 */
export function canvasLocalToTemplateSpace(
  localX: number,
  localY: number,
  orientation: PrintOrientation,
  baseW: number,
  baseH: number,
): { x: number; y: number } {
  if (orientation !== 'horizontal') {
    return { x: localX, y: localY };
  }
  const cx = baseW / 2;
  const cy = baseH / 2;
  // 与 printContentLayerStyle rotate(90deg) 成对
  return {
    x: cx + localY - cy,
    y: cx - localX + cy,
  };
}

export function clientToCanvasLocalPoint(
  clientX: number,
  clientY: number,
  canvasEl: HTMLElement | null,
  scale: number,
): { x: number; y: number } | null {
  if (!canvasEl) return null;
  const rect = canvasEl.getBoundingClientRect();
  return {
    x: (clientX - rect.left) / scale,
    y: (clientY - rect.top) / scale,
  };
}

/** 横打时将画布位移增量映射到模板坐标增量 */
export function mapPointerDeltaToTemplateSpace(
  dx: number,
  dy: number,
  orientation: PrintOrientation,
): { dx: number; dy: number } {
  if (orientation !== 'horizontal') return { dx, dy };
  return { dx: dy, dy: -dx };
}

/** 根据指针位置与抓取偏移计算模板坐标（竖打跟随视觉方向） */
export function templatePositionFromPointerGrab(
  localX: number,
  localY: number,
  grabOffsetX: number,
  grabOffsetY: number,
  orientation: PrintOrientation,
  baseW: number,
  baseH: number,
): { x: number; y: number } {
  const templatePtr = canvasLocalToTemplateSpace(localX, localY, orientation, baseW, baseH);
  return {
    x: templatePtr.x - grabOffsetX,
    y: templatePtr.y - grabOffsetY,
  };
}

const ELEMENT_DRAG_THRESHOLD_PX = 4;

/** 横打预览 rotate(90deg) 时，屏幕拖拽增量映射到模板宽/高增量 */
export function elementResizeDeltaFromPointer(
  dlx: number,
  dly: number,
  handleId: string,
  orientation: PrintOrientation,
): { dw: number; dh: number } {
  if (orientation === 'horizontal') {
    let dw = 0;
    let dh = 0;
    if (handleId.includes('e')) dw += dly;
    if (handleId.includes('w')) dw -= dly;
    if (handleId.includes('s')) dh -= dlx;
    if (handleId.includes('n')) dh += dlx;
    return { dw, dh };
  }
  let dw = 0;
  let dh = 0;
  if (handleId.includes('e')) dw += dlx;
  if (handleId.includes('w')) dw -= dlx;
  if (handleId.includes('s')) dh += dly;
  if (handleId.includes('n')) dh -= dly;
  return { dw, dh };
}

/** 将画布本地坐标系中的指针位移转为模板坐标系增量（竖打已含 rotate 逆变换） */
export function templatePointerDeltaFromCanvasLocals(
  startLocalX: number,
  startLocalY: number,
  currentLocalX: number,
  currentLocalY: number,
  orientation: PrintOrientation,
  baseW: number,
  baseH: number,
): { dtx: number; dty: number } {
  const start = canvasLocalToTemplateSpace(startLocalX, startLocalY, orientation, baseW, baseH);
  const current = canvasLocalToTemplateSpace(currentLocalX, currentLocalY, orientation, baseW, baseH);
  return {
    dtx: current.x - start.x,
    dty: current.y - start.y,
  };
}

export function computeResizedElementBox(
  elX: number,
  elY: number,
  w: number,
  h: number,
  handleId: string,
  dlx: number,
  dly: number,
  orientation: PrintOrientation,
): { x: number; y: number; width: number; height: number } {
  const { dw, dh } = elementResizeDeltaFromPointer(dlx, dly, handleId, orientation);
  let nw = w;
  let nh = h;
  let nx = elX;
  let ny = elY;
  if (handleId.includes('e')) nw = Math.max(20, w + dw);
  if (handleId.includes('w')) {
    nw = Math.max(20, w - dw);
    nx = elX + (w - nw);
  }
  if (handleId.includes('s')) nh = Math.max(12, h + dh);
  if (handleId.includes('n')) {
    nh = Math.max(12, h - dh);
    ny = elY + (h - nh);
  }
  return { x: nx, y: ny, width: nw, height: nh };
}

function PrintOrientationToggle({
  value,
  onChange,
}: {
  value: PrintOrientation;
  onChange: (v: PrintOrientation) => void;
}) {
  const btnBase =
    'h-8 w-8 rounded border flex items-center justify-center shrink-0 transition-colors shadow-sm active:scale-95';
  const btnActive = 'bg-blue-600 border-blue-600 text-white';
  const btnIdle = 'bg-white border-gray-300 text-gray-800 hover:bg-gray-50';

  return (
    <div className="flex items-center gap-1 shrink-0" role="group" aria-label="Print orientation">
      <button
        type="button"
        title="Landscape"
        aria-label="Landscape"
        aria-pressed={value === 'horizontal'}
        className={cn(btnBase, value === 'horizontal' ? btnActive : btnIdle)}
        onClick={() => onChange('horizontal')}
      >
        <svg width="18" height="14" viewBox="0 0 18 14" aria-hidden className="block">
          <rect x="1" y="3" width="16" height="8" rx="1" fill="none" stroke="currentColor" strokeWidth="1.25" />
          <path d="M5 7h8M11 5.5l1.5 1.5L11 8.5M7 5.5L5.5 7 7 8.5" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </button>
      <button
        type="button"
        title="Portrait"
        aria-label="Portrait"
        aria-pressed={value === 'vertical'}
        className={cn(btnBase, value === 'vertical' ? btnActive : btnIdle)}
        onClick={() => onChange('vertical')}
      >
        <svg width="14" height="18" viewBox="0 0 14 18" aria-hidden className="block">
          <rect x="3" y="1" width="8" height="16" rx="1" fill="none" stroke="currentColor" strokeWidth="1.25" />
          <path d="M7 5v8M5.5 11l1.5 1.5 1.5-1.5M5.5 7l1.5-1.5 1.5 1.5" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </button>
    </div>
  );
}

/** 真实条形码渲染（JsBarcode），支持水平/竖排与制式 */
function BarcodeBlock({
  data,
  width,
  height,
  showText,
  orientation = 'horizontal',
  barcodeType,
  fontSize = 14,
  textAlign = 'center',
}: {
  data: string;
  width: number;
  height: number;
  showText?: boolean;
  orientation?: 'horizontal' | 'vertical';
  barcodeType?: unknown;
  fontSize?: number;
  textAlign?: 'left' | 'center' | 'right' | string;
}) {
  const svgRef = useRef<SVGSVGElement>(null);
  const isVertical = orientation === 'vertical';
  const labelReserve = showText !== false ? Math.max(12, Math.round(fontSize) + 4) : 4;
  const barHeight = Math.max(20, (isVertical ? width : height) - labelReserve);
  const jsFormat = toJsBarcodeFormat(barcodeType);
  const align =
    textAlign === 'right' ? 'flex-end' : textAlign === 'center' ? 'center' : 'flex-start';
  useEffect(() => {
    if (svgRef.current && data) {
      try {
        JsBarcode(svgRef.current, data, {
          format: jsFormat,
          width: 1,
          height: barHeight,
          displayValue: showText !== false,
          margin: 2,
          fontOptions: '',
          fontSize: Math.max(8, Math.round(fontSize)),
          textAlign: textAlign === 'right' ? 'right' : textAlign === 'center' ? 'center' : 'left',
        });
      } catch {
        // invalid data, ignore
      }
    }
  }, [data, barHeight, showText, jsFormat, fontSize, textAlign]);
  const svg = <svg ref={svgRef} className="w-full h-full min-h-0" style={{ maxHeight: isVertical ? width : height }} />;
  if (isVertical) {
    return (
      <div className="w-full h-full flex items-center justify-center">
        <div
          style={{
            transform: 'rotate(-90deg)',
            transformOrigin: 'center center',
            width: height,
            height: width,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <div className="w-full min-h-0 flex flex-col" style={{ alignItems: align }}>{svg}</div>
        </div>
      </div>
    );
  }
  return (
    <div className="w-full h-full flex flex-col justify-center" style={{ alignItems: align }}>
      <div className="w-full min-h-0 flex flex-col" style={{ alignItems: align }}>{svg}</div>
    </div>
  );
}

/** 画布网格步长（px），控件吸附到该步长 */
const GRID_SIZE = 8;

/** 将数值对齐到网格 */
function snapToGrid(value: number): number {
  return Math.round(value / GRID_SIZE) * GRID_SIZE;
}

/** 画布内安全边距（px），与主题色虚线框一致；控件不可移出 */
export const LABEL_CANVAS_SAFE_MARGIN_PX = 16;

/** 蓝色虚线安全区（固定在纸张坐标系，横打时不随控件旋转） */
function CanvasSafeMarginOverlay({ baseW, baseH }: { baseW: number; baseH: number }) {
  if (baseW <= LABEL_CANVAS_SAFE_MARGIN_PX * 2 || baseH <= LABEL_CANVAS_SAFE_MARGIN_PX * 2) {
    return null;
  }
  const m = LABEL_CANVAS_SAFE_MARGIN_PX;
  const stroke = '#2563eb';
  const sw = 2;
  const dash = '10 6';
  return (
    <svg
      className="pointer-events-none absolute left-0 top-0 z-[12]"
      width={baseW}
      height={baseH}
      style={{ overflow: 'visible' }}
      aria-hidden
    >
      <line x1={0} y1={m} x2={baseW} y2={m} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
      <line x1={0} y1={baseH - m} x2={baseW} y2={baseH - m} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
      <line x1={m} y1={0} x2={m} y2={baseH} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
      <line x1={baseW - m} y1={0} x2={baseW - m} y2={baseH} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
    </svg>
  );
}

/** 将控件矩形限制在虚线安全区内（画布像素坐标；数据始终为纸张 WxH 坐标） */
export function clampLabelElementBox(
  x: number,
  y: number,
  w: number,
  h: number,
  baseW: number,
  baseH: number,
  marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX,
  orientation: PrintOrientation = 'vertical',
): { x: number; y: number; w: number; h: number } {
  if (orientation === 'horizontal') {
    const cx = baseW / 2;
    const cy = baseH / 2;
    const minX = cx - cy + marginPx;
    const minY = cx + cy - baseW + marginPx;
    const innerW = baseH - marginPx * 2;
    const innerH = baseW - marginPx * 2;
    if (innerW < GRID_SIZE || innerH < GRID_SIZE) {
      return {
        x: minX,
        y: minY,
        w: Math.max(GRID_SIZE, snapToGrid(innerW)),
        h: Math.max(GRID_SIZE, snapToGrid(innerH)),
      };
    }
    let cw = Math.min(Math.max(20, snapToGrid(w)), snapToGrid(innerW));
    let ch = Math.min(Math.max(12, snapToGrid(h)), snapToGrid(innerH));
    const maxX = baseH - marginPx + cx - cy - cw;
    const maxY = cx + cy - marginPx - ch;
    if (maxX < minX || maxY < minY) {
      return { x: minX, y: minY, w: snapToGrid(innerW), h: snapToGrid(innerH) };
    }
    let nx = snapToGrid(x);
    let ny = snapToGrid(y);
    nx = Math.min(Math.max(nx, minX), snapToGrid(maxX));
    ny = Math.min(Math.max(ny, minY), snapToGrid(maxY));
    return { x: nx, y: ny, w: cw, h: ch };
  }

  const minX = marginPx;
  const minY = marginPx;
  const maxR = baseW - marginPx;
  const maxB = baseH - marginPx;
  let cw = Math.max(20, snapToGrid(w));
  let ch = Math.max(12, snapToGrid(h));
  const innerW = maxR - minX;
  const innerH = maxB - minY;
  if (innerW < GRID_SIZE || innerH < GRID_SIZE) {
    return {
      x: minX,
      y: minY,
      w: Math.max(GRID_SIZE, snapToGrid(innerW)),
      h: Math.max(GRID_SIZE, snapToGrid(innerH)),
    };
  }
  cw = Math.min(cw, snapToGrid(innerW));
  ch = Math.min(ch, snapToGrid(innerH));
  let cx = snapToGrid(x);
  let cy = snapToGrid(y);
  cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw));
  cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch));
  return { x: cx, y: cy, w: cw, h: ch };
}

/** 拖拽时仅限制位置，不改变宽高 */
function clampDragPosition(
  x: number,
  y: number,
  w: number,
  h: number,
  baseW: number,
  baseH: number,
  marginPx: number,
  orientation: PrintOrientation = 'vertical',
): { x: number; y: number } {
  if (orientation === 'horizontal') {
    const cx = baseW / 2;
    const cy = baseH / 2;
    const minX = cx - cy + marginPx;
    const minY = cx + cy - baseW + marginPx;
    const maxX = baseH - marginPx + cx - cy - w;
    const maxY = cx + cy - marginPx - h;
    if (!Number.isFinite(maxX) || !Number.isFinite(maxY) || maxX < minX || maxY < minY) {
      return { x: minX, y: minY };
    }
    let nx = snapToGrid(x);
    let ny = snapToGrid(y);
    nx = Math.min(Math.max(nx, minX), snapToGrid(maxX));
    ny = Math.min(Math.max(ny, minY), snapToGrid(maxY));
    return { x: nx, y: ny };
  }

  const minX = marginPx;
  const minY = marginPx;
  const maxR = baseW - marginPx;
  const maxB = baseH - marginPx;
  const maxX = maxR - w;
  const maxY = maxB - h;
  if (!Number.isFinite(maxX) || !Number.isFinite(maxY) || maxX < minX || maxY < minY) {
    return { x: minX, y: minY };
  }
  let cx = snapToGrid(x);
  let cy = snapToGrid(y);
  cx = Math.min(Math.max(cx, minX), snapToGrid(maxX));
  cy = Math.min(Math.max(cy, minY), snapToGrid(maxY));
  return { x: cx, y: cy };
}

const RULER_H = 24;
const RULER_W = 24;

/** 拖拽/缩放过程中尚未落库的控件几何补丁 */
export type LabelElementLivePatch = {
  id: string;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
};

export function mergeLabelElementLivePatch(
  el: LabelElement,
  patch: LabelElementLivePatch | null | undefined,
): LabelElement {
  if (!patch || patch.id !== el.id) return el;
  return { ...el, ...patch };
}

export function mergeTemplateElementsLivePatch(
  elements: LabelElement[],
  patch: LabelElementLivePatch | null | undefined,
): LabelElement[] {
  if (!patch?.id) return elements;
  return elements.map((el) => mergeLabelElementLivePatch(el, patch));
}

/** @deprecated 请从 `@/utils/previewRulerUnits` 导入 */
export type { PreviewRulerDisplayUnit } from '@/utils/previewRulerUnits';

function formatSelectionLengthForPreviewRuler(
  lengthPx: number,
  basePaperPx: number,
  paperSizeTemplate: number,
  templateUnit: "cm" | "inch",
  displayUnit: PreviewRulerDisplayUnit,
): string | null {
  if (
    !Number.isFinite(lengthPx) ||
    !Number.isFinite(basePaperPx) ||
    basePaperPx <= 0 ||
    !Number.isFinite(paperSizeTemplate) ||
    paperSizeTemplate <= 0
  ) {
    return null;
  }
  const d = elementPxToDisplayLength(
    lengthPx,
    basePaperPx,
    paperSizeTemplate,
    templateUnit,
    displayUnit,
  );
  const label = formatPreviewRulerDisplayValue(d, displayUnit);
  return `${label}${previewRulerUnitLabel(displayUnit)}`;
}

/** 画布选中框旁尺寸读数（仅数值，与参考图一致） */
function formatElementDimensionValue(
  lengthPx: number,
  basePaperPx: number,
  paperSizeTemplate: number,
  templateUnit: "cm" | "inch",
  displayUnit: PreviewRulerDisplayUnit,
): string {
  const d = elementPxToDisplayLength(
    lengthPx,
    basePaperPx,
    paperSizeTemplate,
    templateUnit,
    displayUnit,
  );
  return formatPreviewRulerDisplayValue(d, displayUnit);
}

const ELEMENT_RESIZE_HANDLE_SIZE = 8;
const ELEMENT_RESIZE_HANDLE_HIT = 14;
const ELEMENT_RESIZE_HANDLES = [
  { id: "nw", cx: 0, cy: 0, cursor: "nwse-resize" },
  { id: "n", cx: 0.5, cy: 0, cursor: "ns-resize" },
  { id: "ne", cx: 1, cy: 0, cursor: "nesw-resize" },
  { id: "e", cx: 1, cy: 0.5, cursor: "ew-resize" },
  { id: "se", cx: 1, cy: 1, cursor: "nwse-resize" },
  { id: "s", cx: 0.5, cy: 1, cursor: "ns-resize" },
  { id: "sw", cx: 0, cy: 1, cursor: "nesw-resize" },
  { id: "w", cx: 0, cy: 0.5, cursor: "ew-resize" },
] as const;

function dimensionArrowHead(
  tipX: number,
  tipY: number,
  dir: "left" | "right" | "up" | "down",
): string {
  const s = 3;
  if (dir === "left") return `${tipX},${tipY} ${tipX + s},${tipY - s} ${tipX + s},${tipY + s}`;
  if (dir === "right") return `${tipX},${tipY} ${tipX - s},${tipY - s} ${tipX - s},${tipY + s}`;
  if (dir === "up") return `${tipX},${tipY} ${tipX - s},${tipY + s} ${tipX + s},${tipY + s}`;
  return `${tipX},${tipY} ${tipX - s},${tipY - s} ${tipX + s},${tipY - s}`;
}

/** 选中控件：方形虚线框 + 宽高标注 + 8 个红色拖拽点 */
function ElementSelectionFrame({
  el,
  templateUnit,
  paperWidthPx,
  paperHeightPx,
  paperWidthTemplate,
  paperHeightTemplate,
  displayUnit,
  printOrientation = 'vertical',
  interactive = false,
  onResizePointerDown,
}: {
  el: LabelElement;
  templateUnit: "cm" | "inch";
  paperWidthPx: number;
  paperHeightPx: number;
  paperWidthTemplate: number;
  paperHeightTemplate: number;
  displayUnit: PreviewRulerDisplayUnit;
  printOrientation?: PrintOrientation;
  interactive?: boolean;
  onResizePointerDown?: (e: React.PointerEvent, handleId: string) => void;
}) {
  const x = el.x;
  const y = el.y;
  const w = Math.max(1, el.width);
  const h = Math.max(1, el.height);
  const wValue = formatElementDimensionValue(
    w,
    paperWidthPx,
    paperWidthTemplate,
    templateUnit,
    displayUnit,
  );
  const hValue = formatElementDimensionValue(
    h,
    paperHeightPx,
    paperHeightTemplate,
    templateUnit,
    displayUnit,
  );

  const widthAbove = y >= 28;
  const widthLineY = widthAbove ? y - 12 : y + h + 12;
  const widthTextY = widthAbove ? widthLineY - 6 : widthLineY + 14;

  const heightOnLeft = x >= 36;
  const heightLineX = heightOnLeft ? x - 12 : x + w + 12;
  const heightTextX = heightOnLeft ? heightLineX - 8 : heightLineX + 8;
  // 横打预览旋转后，屏幕上的横/竖跨度与模板宽/高对调，标注跟随视觉
  const isRotatedPrint = printOrientation === 'horizontal';
  const displayWidthLabel = isRotatedPrint ? hValue : wValue;
  const displayHeightLabel = isRotatedPrint ? wValue : hValue;

  return (
    <>
      <div
        className="pointer-events-none absolute left-0 top-0 z-[40]"
        style={{ width: paperWidthPx, height: paperHeightPx }}
      >
        <svg
          className="pointer-events-none absolute left-0 top-0 overflow-visible select-none"
          width={paperWidthPx}
          height={paperHeightPx}
          aria-hidden
        >
          <rect
            x={x}
            y={y}
            width={w}
            height={h}
            fill="none"
            stroke="#111827"
            strokeWidth={1}
            strokeDasharray="4 3"
          />
          {/* 宽度标注 */}
          <line x1={x} y1={widthLineY} x2={x + w} y2={widthLineY} stroke="#111827" strokeWidth={1} />
          <polygon points={dimensionArrowHead(x, widthLineY, "left")} fill="#111827" />
          <polygon points={dimensionArrowHead(x + w, widthLineY, "right")} fill="#111827" />
          <text
            x={x + w / 2}
            y={widthTextY}
            textAnchor="middle"
            fontSize={11}
            fill="#111827"
            className="font-mono"
          >
          {displayWidthLabel}
        </text>
        {/* 高度标注 */}
        <line x1={heightLineX} y1={y} x2={heightLineX} y2={y + h} stroke="#111827" strokeWidth={1} />
        <polygon points={dimensionArrowHead(heightLineX, y, "up")} fill="#111827" />
        <polygon points={dimensionArrowHead(heightLineX, y + h, "down")} fill="#111827" />
        <text
          x={heightTextX}
          y={y + h / 2}
          textAnchor="middle"
          fontSize={11}
          fill="#111827"
          className="font-mono"
          transform={`rotate(-90, ${heightTextX}, ${y + h / 2})`}
        >
          {displayHeightLabel}
          </text>
        </svg>
      </div>
      {interactive && onResizePointerDown
        ? ELEMENT_RESIZE_HANDLES.map((handle) => {
            const left = x + w * handle.cx - ELEMENT_RESIZE_HANDLE_HIT / 2;
            const top = y + h * handle.cy - ELEMENT_RESIZE_HANDLE_HIT / 2;
            const inset = (ELEMENT_RESIZE_HANDLE_HIT - ELEMENT_RESIZE_HANDLE_SIZE) / 2;
            return (
              <div
                key={handle.id}
                data-element-resize-handle="true"
                className="absolute z-[41] touch-none"
                style={{
                  left,
                  top,
                  width: ELEMENT_RESIZE_HANDLE_HIT,
                  height: ELEMENT_RESIZE_HANDLE_HIT,
                  cursor: handle.cursor,
                }}
                onPointerDown={(e) => {
                  if (e.button !== 0) return;
                  e.stopPropagation();
                  e.preventDefault();
                  onResizePointerDown(e, handle.id);
                }}
                onClick={(e) => e.stopPropagation()}
              >
                <div
                  className="pointer-events-none absolute border border-red-700 bg-red-500 shadow-sm transition-transform hover:scale-110"
                  style={{
                    left: inset,
                    top: inset,
                    width: ELEMENT_RESIZE_HANDLE_SIZE,
                    height: ELEMENT_RESIZE_HANDLE_SIZE,
                  }}
                />
              </div>
            );
          })
        : null}
    </>
  );
}

/**
 * 贯穿预览区全宽的横向标尺：刻度按「预览标尺单位」绘制；与模板画布单位无关。
 */
function RulerBarHorizontal({
  rulerTotalWidthPx,
  paperWidthPx,
  paperOffsetLeftPx,
  paperWidthTemplate,
  templateUnit,
  displayUnit,
  baseW,
  selection,
}: {
  rulerTotalWidthPx: number;
  paperWidthPx: number;
  paperOffsetLeftPx: number;
  /** 模板定义的纸张宽度（模板单位） */
  paperWidthTemplate: number;
  templateUnit: "cm" | "inch";
  displayUnit: PreviewRulerDisplayUnit;
  baseW: number;
  selection: { x: number; width: number } | null;
}) {
  const displaySpan = convertTemplateLengthToDisplay(paperWidthTemplate, templateUnit, displayUnit);
  if (!Number.isFinite(displaySpan) || displaySpan <= 0 || !Number.isFinite(paperWidthPx) || paperWidthPx < 1) {
    return null;
  }
  if (!Number.isFinite(rulerTotalWidthPx) || rulerTotalWidthPx < 1) {
    return null;
  }
  const h = RULER_H;
  const pxPerDisplayUnit = paperWidthPx / displaySpan;
  /** 标尺几何中心为刻度 0，向左为负、向右为正 */
  const centerPx = rulerTotalWidthPx / 2;
  const xAtSignedUnit = (u: number) => centerPx + u * pxPerDisplayUnit;
  const nodes: React.ReactNode[] = [];

  let labelStep = 1;
  if (displayUnit === "mm") {
    if (displaySpan > 120) labelStep = 20;
    else if (displaySpan > 60) labelStep = 10;
    else if (displaySpan > 25) labelStep = 5;
  }

  const minorDivisions = displayUnit === "inch" ? 8 : 10;
  const kMin = Math.floor((0 - centerPx) / pxPerDisplayUnit) - 2;
  const kMax = Math.ceil((rulerTotalWidthPx - centerPx) / pxPerDisplayUnit) + 2;
  const kLo = Math.max(-5000, Math.min(5000, kMin));
  const kHi = Math.max(-5000, Math.min(5000, kMax));

  for (let k = kLo; k <= kHi; k++) {
    const x = xAtSignedUnit(k);
    if (x < -8 || x > rulerTotalWidthPx + 8) continue;
    const showLabel = k === 0 || k % labelStep === 0;
    nodes.push(
      <g key={`maj-${k}`}>
        <line x1={x} y1={h} x2={x} y2={4} stroke="#9ca3af" strokeWidth={1} />
        {showLabel ? (
          <text
            x={x}
            y={12}
            fontSize={8}
            fill="#4b5563"
            className="select-none font-mono"
            textAnchor="middle"
          >
            {k}
          </text>
        ) : null}
      </g>,
    );
    const midMinor = Math.floor(minorDivisions / 2);
    for (let s = 1; s < minorDivisions; s++) {
      const u = k + s / minorDivisions;
      const x2 = xAtSignedUnit(u);
      if (x2 < -4 || x2 > rulerTotalWidthPx + 4) continue;
      const y2 = s === midMinor ? 10 : 12;
      nodes.push(
        <line
          key={`min-${k}-${s}`}
          x1={x2}
          y1={h}
          x2={x2}
          y2={y2}
          stroke="#d1d5db"
          strokeWidth={0.5}
        />,
      );
    }
  }

  let selLeft = 0;
  let selW = 0;
  if (selection && Number.isFinite(baseW) && baseW > 0) {
    selLeft = paperOffsetLeftPx + (selection.x / baseW) * paperWidthPx;
    selW = (selection.width / baseW) * paperWidthPx;
  }
  const wLabel = selection
    ? formatSelectionLengthForPreviewRuler(selection.width, baseW, paperWidthTemplate, templateUnit, displayUnit)
    : null;

  return (
    <svg
      width="100%"
      height={h}
      viewBox={`0 0 ${rulerTotalWidthPx} ${h}`}
      preserveAspectRatio="none"
      className="block bg-slate-100/90 border-b border-slate-300 pointer-events-none select-none shrink-0"
      aria-hidden
    >
      <line
        x1={0}
        y1={h - 0.5}
        x2={rulerTotalWidthPx}
        y2={h - 0.5}
        stroke="#e5e7eb"
      />
      {nodes}
      {selection && Number.isFinite(selW) && selW > 0.5 && (
        <>
          <rect
            x={selLeft}
            y={1}
            width={Math.max(1, Math.min(selW, rulerTotalWidthPx - selLeft + 1))}
            height={h - 4}
            fill="rgb(59 130 246 / 0.22)"
            stroke="rgb(37 99 235 / 0.85)"
            strokeWidth={1}
            rx={2}
          />
          {wLabel && (
            <text
              x={selLeft + Math.max(2, Math.min(selW, rulerTotalWidthPx - selLeft) / 2)}
              y={h - 6}
              textAnchor="middle"
              fontSize={8}
              fill="#1d4ed8"
              className="select-none font-mono"
            >
              {wLabel}
            </text>
          )}
        </>
      )}
    </svg>
  );
}

/**
 * 贯穿预览区全高的纵向标尺：刻度与读数逻辑与横向标尺一致。
 */
function RulerBarVertical({
  rulerTotalHeightPx,
  paperHeightPx,
  paperOffsetTopPx,
  paperHeightTemplate,
  templateUnit,
  displayUnit,
  baseH,
  selection,
}: {
  rulerTotalHeightPx: number;
  paperHeightPx: number;
  paperOffsetTopPx: number;
  /** 模板定义的纸张高度（模板单位） */
  paperHeightTemplate: number;
  templateUnit: "cm" | "inch";
  displayUnit: PreviewRulerDisplayUnit;
  baseH: number;
  selection: { y: number; height: number } | null;
}) {
  const displaySpan = convertTemplateLengthToDisplay(paperHeightTemplate, templateUnit, displayUnit);
  if (!Number.isFinite(displaySpan) || displaySpan <= 0 || !Number.isFinite(paperHeightPx) || paperHeightPx < 1) {
    return null;
  }
  if (!Number.isFinite(rulerTotalHeightPx) || rulerTotalHeightPx < 1) {
    return null;
  }
  const w = RULER_W;
  const pxPerDisplayUnit = paperHeightPx / displaySpan;
  /** 标尺几何中心为刻度 0，向上为负、向下为正 */
  const centerPx = rulerTotalHeightPx / 2;
  const yAtSignedUnit = (u: number) => centerPx + u * pxPerDisplayUnit;
  const nodes: React.ReactNode[] = [];

  let labelStep = 1;
  if (displayUnit === "mm") {
    if (displaySpan > 120) labelStep = 20;
    else if (displaySpan > 60) labelStep = 10;
    else if (displaySpan > 25) labelStep = 5;
  }

  const minorDivisions = displayUnit === "inch" ? 8 : 10;
  const kMin = Math.floor((0 - centerPx) / pxPerDisplayUnit) - 2;
  const kMax = Math.ceil((rulerTotalHeightPx - centerPx) / pxPerDisplayUnit) + 2;
  const kLo = Math.max(-5000, Math.min(5000, kMin));
  const kHi = Math.max(-5000, Math.min(5000, kMax));

  for (let k = kLo; k <= kHi; k++) {
    const y = yAtSignedUnit(k);
    if (y < -8 || y > rulerTotalHeightPx + 8) continue;
    const showLabel = k === 0 || k % labelStep === 0;
    nodes.push(
      <g key={`maj-${k}`}>
        <line x1={w} y1={y} x2={4} y2={y} stroke="#9ca3af" strokeWidth={1} />
        {showLabel ? (
          <text
            x={10}
            y={y}
            fontSize={8}
            fill="#4b5563"
            className="select-none font-mono"
            textAnchor="middle"
            transform={`rotate(-90, 10, ${y})`}
          >
            {k}
          </text>
        ) : null}
      </g>,
    );
    const midMinor = Math.floor(minorDivisions / 2);
    for (let s = 1; s < minorDivisions; s++) {
      const u = k + s / minorDivisions;
      const y2 = yAtSignedUnit(u);
      if (y2 < -4 || y2 > rulerTotalHeightPx + 4) continue;
      const x2 = s === midMinor ? 10 : 12;
      nodes.push(
        <line
          key={`min-${k}-${s}`}
          x1={w}
          y1={y2}
          x2={x2}
          y2={y2}
          stroke="#d1d5db"
          strokeWidth={0.5}
        />,
      );
    }
  }

  let selTop = 0;
  let selH = 0;
  if (selection && Number.isFinite(baseH) && baseH > 0) {
    selTop = paperOffsetTopPx + (selection.y / baseH) * paperHeightPx;
    selH = (selection.height / baseH) * paperHeightPx;
  }
  const hLabel = selection
    ? formatSelectionLengthForPreviewRuler(selection.height, baseH, paperHeightTemplate, templateUnit, displayUnit)
    : null;

  return (
    <svg
      width={w}
      height={rulerTotalHeightPx}
      viewBox={`0 0 ${w} ${rulerTotalHeightPx}`}
      preserveAspectRatio="none"
      className="block h-full w-full bg-slate-100/90 border-r border-slate-300 pointer-events-none select-none shrink-0"
      aria-hidden
    >
      <line
        x1={w - 0.5}
        y1={0}
        x2={w - 0.5}
        y2={rulerTotalHeightPx}
        stroke="#e5e7eb"
      />
      {nodes}
      {selection && Number.isFinite(selH) && selH > 0.5 && (
        <>
          <rect
            x={1}
            y={selTop}
            width={w - 4}
            height={Math.max(1, Math.min(selH, rulerTotalHeightPx - selTop + 1))}
            fill="rgb(59 130 246 / 0.22)"
            stroke="rgb(37 99 235 / 0.85)"
            strokeWidth={1}
            rx={2}
          />
          {hLabel && (
            <text
              x={w - 6}
              y={selTop + Math.max(2, Math.min(selH, rulerTotalHeightPx - selTop) / 2)}
              textAnchor="middle"
              fontSize={8}
              fill="#1d4ed8"
              className="select-none font-mono"
              transform={`rotate(-90, ${w - 6}, ${selTop + Math.max(2, Math.min(selH, rulerTotalHeightPx - selTop) / 2)})`}
            >
              {hLabel}
            </text>
          )}
        </>
      )}
    </svg>
  );
}

/**
 * 多选项在画布上的文案：有 prefix 时与 App 打印一致（prefix + 答案/占位）；否则在已选字典时显示「字典名称: 内容」。
 */
function formatMultipleOptionsCanvasLine(
  cfg: Record<string, unknown>,
  text: string,
  selected: string[],
): string {
  const prefix = String(cfg.prefix ?? '').trim();
  const dictLabel = String(cfg.multipleOptionName ?? cfg.MultipleOptionName ?? '').trim();
  const answers = selected.filter(Boolean).join(', ');
  const fallback = text || '…';
  if (prefix) {
    return answers ? `${prefix}${answers}` : `${prefix}${fallback}`;
  }
  if (dictLabel) {
    const body = answers || fallback;
    return `${dictLabel}: ${body}`;
  }
  return answers || fallback;
}

function nutritionExtraRows(cfg: Record<string, unknown>): NutritionExtraItem[] {
  const raw = cfg.extraNutrients;
  if (!Array.isArray(raw)) return [];
  return raw.map((item, idx) => {
    const row = item as Record<string, unknown>;
    return {
      id: String(row.id ?? `extra-${idx}`),
      name: String(row.name ?? ''),
      value: String(row.value ?? ''),
      unit: String(row.unit ?? ''),
    };
  });
}

function nutritionFixedField(
  cfg: Record<string, unknown>,
  key: string,
  field: 'value' | 'unit',
): string {
  const fixedRows = Array.isArray(cfg.fixedNutrients)
    ? (cfg.fixedNutrients as Record<string, unknown>[])
    : [];
  const row = fixedRows.find((item) => String(item.key ?? '').trim() === key);
  if (row) {
    const fromRow = String(row[field] ?? '').trim();
    if (fromRow !== '') return fromRow;
  }
  const directKey = field === 'value' ? key : `${key}Unit`;
  const direct = cfg[directKey];
  if (direct != null && String(direct).trim() !== '') return String(direct).trim();
  return String(row?.[field] ?? '').trim();
}

function formatDateByPreset(format: string, date: Date): string {
  const yyyy = String(date.getFullYear());
  const yy = yyyy.slice(-2);
  const mm = String(date.getMonth() + 1).padStart(2, '0');
  const dd = String(date.getDate()).padStart(2, '0');
  const hh = String(date.getHours()).padStart(2, '0');
  const min = String(date.getMinutes()).padStart(2, '0');
  const monthLong = date.toLocaleString('en-US', { month: 'long' }).toUpperCase();
  const dayLong = date.toLocaleString('en-US', { weekday: 'long' }).toUpperCase();
  const dayShort = date.toLocaleString('en-US', { weekday: 'short' }).toUpperCase();
  const monthShort = date.toLocaleString('en-US', { month: 'short' }).toUpperCase();
  switch (format) {
    case 'DD/MM/YYYY':
      return `${dd}/${mm}/${yyyy}`;
    case 'MM/DD/YYYY':
      return `${mm}/${dd}/${yyyy}`;
    case 'DD/MM/YY':
      return `${dd}/${mm}/${yy}`;
    case 'MM/DD/YY':
      return `${mm}/${dd}/${yy}`;
    case 'MM/YY':
      return `${mm}/${yy}`;
    case 'MM/DD':
      return `${mm}/${dd}`;
    case 'MM':
      return mm;
    case 'DD':
      return dd;
    case 'YY':
      return yy;
    case 'FULLY DAY(WEDNESDAY)':
      return dayLong;
    case 'DAY (WED)':
      return dayShort;
    case 'MONTH (DECEMBER)':
      return monthLong;
    case 'YEAR (2025)':
      return yyyy;
    case 'DD MONTH YEAR (25 DECEMBER 2025)':
      return `${dd} ${monthLong} ${yyyy}`;
    default:
      return format
        .replace('YYYY', yyyy)
        .replace('YY', yy)
        .replace('MM', mm)
        .replace('DD', dd)
        .replace('HH', hh)
        .replace('mm', min);
  }
}

const DURATION_UNITS = new Set([
  'Minutes',
  'Hours',
  'Days',
  'Weeks',
  'Months (30 Day)',
  'Years',
]);

function normalizeWeightUnit(raw: unknown): 'lb' | 'kg' | 'mg' | 'g' | 'oz' {
  const unit = String(raw ?? '').trim().toLowerCase();
  if (unit === 'milligrams') return 'mg';
  if (unit === 'grams') return 'g';
  if (unit === 'ounces') return 'oz';
  if (unit === 'pounds') return 'lb';
  if (unit === 'kilograms') return 'kg';
  if (unit === 'lb' || unit === 'kg' || unit === 'mg' || unit === 'g' || unit === 'oz') return unit;
  return 'g';
}

/** 根据元素类型与 config 渲染画布上的默认内容 */
function ElementContent({
  el,
  isAppPrintField,
  previewAsPrinted,
  previewSelectionHighlight = false,
}: {
  el: LabelElement;
  isAppPrintField?: boolean;
  /** 新建/编辑标签弹窗：按模板控件尺寸与字号展示已填值，而非 APP 占位样式 */
  previewAsPrinted?: boolean;
  /** 右侧 Print preview 选中高亮：避免子层 bg-white 盖住父级背景 */
  previewSelectionHighlight?: boolean;
}) {
  const cfg = el.config as Record<string, unknown>;
  const type = canonicalElementType(el.type);
  const isVerticalRotation = el.rotation === 'vertical';
  const inverted = readInvertColors(cfg);

  // Common styles
  const resolvedFontFamily = resolveLabelEditorElementFontFamily(cfg);
  const commonStyle: React.CSSProperties = {
    fontSize: (cfg?.fontSize as number) ?? 14,
    fontFamily: resolvedFontFamily,
    fontWeight: (cfg?.fontWeight as string) ?? 'normal',
    textAlign: (cfg?.textAlign as any) ?? 'left',
    color: inverted ? INVERT_COLORS_FG : ((cfg?.color as string) ?? '#000'),
    backgroundColor: inverted ? INVERT_COLORS_BG : undefined,
  };
  const invertedInputClass = inverted
    ? 'border-gray-600 bg-black text-white'
    : previewSelectionHighlight
      ? 'border-gray-300 bg-transparent'
      : 'border-gray-300 bg-white';

  // Rotation support:
  // The editor's Rotation is currently a simple horizontal/vertical toggle.
  // For text-like elements we render vertical via writing-mode to avoid layout clipping.
  const textLike =
    type === 'TEXT_STATIC' || type === 'TEXT_PRODUCT' || type === 'TEXT_PRICE';
  const textRotationStyle: React.CSSProperties =
    isVerticalRotation && textLike
      ? { writingMode: 'vertical-rl', textOrientation: 'mixed' as any }
      : {};
  const rotateBoxStyle: React.CSSProperties = isVerticalRotation
    ? { transform: 'rotate(-90deg)', transformOrigin: 'center center' }
    : {};

  // 文本类
  const inputType = cfg?.inputType as string | undefined;
  if (type === 'TEXT_STATIC' && isCompanyAutoElement(el) && !isAppPrintField) {
    const previewText = formatCompanyPrintPreviewText(cfg);
    return (
      <div
        className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-words leading-tight italic text-gray-600"
        style={{ ...commonStyle, ...textRotationStyle }}
        title="Filled from company profile when printing"
      >
        {previewText}
      </div>
    );
  }
  if (type === 'TEXT_STATIC') {
    const text = (cfg?.text as string) ?? 'Text';
    if (isAppPrintField && previewAsPrinted) {
      if (inputType === 'options') {
        const selected = Array.isArray(cfg?.selectedOptionValues)
          ? (cfg.selectedOptionValues as string[])
          : [];
        const line = formatMultipleOptionsCanvasLine(cfg, text, selected);
        const muted = selected.length === 0;
        return (
          <div
            className={cn(
              'w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-words leading-tight',
              muted && !inverted && 'text-gray-400',
              muted && inverted && 'text-gray-300',
            )}
            style={{ ...commonStyle, ...textRotationStyle }}
            title={line}
          >
            {line}
          </div>
        );
      }
      const display =
        inputType === 'number' ? ((cfg?.text as string) ?? '0') : text;
      return (
        <div
          className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-words leading-tight"
          style={{ ...commonStyle, ...textRotationStyle }}
        >
          {display}
        </div>
      );
    }
    if (isAppPrintField) {
      if (inputType === 'options') {
        const selected = Array.isArray(cfg?.selectedOptionValues)
          ? (cfg.selectedOptionValues as string[])
          : [];
        const line = formatMultipleOptionsCanvasLine(cfg, text, selected);
        return (
          <div
            className="w-full h-full px-1 flex flex-col justify-center overflow-hidden pointer-events-none italic text-[11px] leading-tight break-all"
            style={{ ...commonStyle, ...textRotationStyle }}
            title="Filled in mobile app when printing"
          >
            {line}
          </div>
        );
      }
      const display =
        inputType === 'number' ? ((cfg?.text as string) ?? '0') : text;
      return (
        <div
          className={cn(
            'w-full h-full px-1 flex items-center overflow-hidden pointer-events-none italic text-[11px]',
            !inverted && 'text-gray-600',
          )}
          style={{ ...commonStyle, ...textRotationStyle }}
          title="Filled in mobile app when printing"
        >
          {display}
        </div>
      );
    }
    if (inputType === 'number') {
      return (
        <input
          type="number"
          readOnly
          value={(cfg?.text as string) ?? '0'}
          className={cn(
            'w-full h-full min-w-0 border rounded px-1 pointer-events-none',
            invertedInputClass,
          )}
          style={{ ...commonStyle, ...textRotationStyle, textAlign: 'right' }}
        />
      );
    }
    if (inputType === 'options') {
      // 画布只展示「答案」纯文本，不出现勾选、下拉箭头等控件样式；实际选择在 APP 打印流程中完成
      const selected = Array.isArray(cfg?.selectedOptionValues)
        ? (cfg.selectedOptionValues as string[])
        : [];
      const line = formatMultipleOptionsCanvasLine(cfg, text, selected);
      const muted = selected.length === 0;
      return (
        <div
          className={cn(
            'w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight',
            muted && !inverted && 'text-gray-400',
            muted && inverted && 'text-gray-300',
          )}
          style={{ ...commonStyle, ...textRotationStyle }}
          title={line}
        >
          {line}
        </div>
      );
    }
    if (inputType === 'text') {
      return (
        <input
          type="text"
          readOnly
          value={text}
          className={cn(
            'w-full h-full min-w-0 border rounded px-1 pointer-events-none',
            invertedInputClass,
          )}
          style={{ ...commonStyle, ...textRotationStyle }}
        />
      );
    }
    return (
      <div
        className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight"
        style={{ ...commonStyle, ...textRotationStyle }}
      >
        {text}
      </div>
    );
  }
  if (type === 'TEXT_PRODUCT') {
    const text = (cfg?.text as string) ?? 'Product name';
    return (
      <div
        className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight"
        style={{ ...commonStyle, ...textRotationStyle }}
      >
        {text}
      </div>
    );
  }
  if (type === 'TEXT_PRICE') {
    const text = (cfg?.text as string) ?? '0.00';
    return (
      <div
        className="w-full h-full px-1 overflow-hidden flex items-center"
        style={{
          ...commonStyle,
          ...textRotationStyle,
          justifyContent:
            commonStyle.textAlign === 'center'
              ? 'center'
              : commonStyle.textAlign === 'right'
                ? 'flex-end'
                : 'flex-start',
        }}
      >
        <span>{text}</span>
      </div>
    );
  }

  // 条码（支持水平/竖排、制式、字号与对齐）
  if (type === 'BARCODE') {
    const data = (cfg?.data as string) ?? '123456789';
    const showText = (cfg?.showText as boolean) !== false;
    const orientation = (
      el.rotation === 'vertical' || (cfg?.orientation as string) === 'vertical'
        ? 'vertical'
        : 'horizontal'
    ) as 'horizontal' | 'vertical';
    const textAlign = (cfg?.textAlign as string) ?? 'center';
    const fontSize = (cfg?.fontSize as number) ?? 14;
    return (
      <div className="flex flex-col w-full h-full overflow-hidden p-0.5">
        <div className="flex-1 w-full min-h-0 flex items-center justify-center">
          <BarcodeBlock
            data={data}
            width={el.width}
            height={el.height}
            showText={showText}
            orientation={orientation}
            barcodeType={normalizeBarcodeType(cfg?.barcodeType)}
            fontSize={fontSize}
            textAlign={textAlign}
          />
        </div>
      </div>
    );
  }

  // 二维码
  if (type === 'QRCODE') {
    const data = (cfg?.data as string) ?? 'https://example.com';
    const size = Math.min(el.width, el.height) - 4;
    return (
      <div className="w-full h-full flex items-center justify-center p-0.5">
        <QRCodeSVG value={data} size={Math.max(20, size)} level="M" includeMargin={false} />
      </div>
    );
  }

  // 图片/Logo
  if (type === 'IMAGE') {
    const src = cfg?.src as string | undefined;
    const imageRotateStyle: React.CSSProperties = isVerticalRotation
      ? { transform: 'rotate(-90deg)' }
      : {};
    if (src) {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <img
            src={resolvePictureUrlForDisplay(src)}
            alt=""
            className="max-w-full max-h-full object-contain"
            style={imageRotateStyle}
          />
        </div>
      );
    }
    return (
      <div
        className="w-full h-full flex flex-col items-center justify-center bg-gray-100 text-gray-500 text-[10px] border border-dashed border-gray-300"
        style={imageRotateStyle}
      >
        <span className="font-medium">Logo</span>
      </div>
    );
  }

  // 日期/时间
  if (type === 'DATE') {
    const previewFmtRaw = cfg?.__previewFormatted;
    if (typeof previewFmtRaw === 'string') {
      const previewFmt = previewFmtRaw.trim();
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewFmt || '—'}
          </div>
        </div>
      );
    }
    const it = String(cfg?.inputType ?? cfg?.InputType ?? '').toLowerCase();
    const formatRaw =
      (typeof cfg?.format === 'string' && cfg.format.trim()
        ? cfg.format
        : typeof cfg?.Format === 'string' && cfg.Format.trim()
          ? cfg.Format
          : it === 'datetime'
            ? 'YYYY-MM-DD HH:mm'
            : 'DD/MM/YYYY') ?? (it === 'datetime' ? 'YYYY-MM-DD HH:mm' : 'DD/MM/YYYY');
    const format = isLikelyResolvedDateTimeLiteral(formatRaw)
      ? it === 'datetime'
        ? 'YYYY-MM-DD HH:mm'
        : 'DD/MM/YYYY'
      : formatRaw;
    const offset = Number(cfg?.offsetDays ?? cfg?.OffsetDays ?? 0) || 0;
    const d = new Date();
    d.setDate(d.getDate() + offset);
    const example = formatDateByPreset(format, d);
    const isInput = it === 'datetime' || it === 'date';
    if (isInput) {
      if (isAppPrintField) {
        return (
          <div className="w-full h-full flex items-center justify-center overflow-hidden">
            <div
              className="px-1 flex items-center justify-center overflow-hidden pointer-events-none text-[10px] text-center whitespace-nowrap"
              style={{ ...commonStyle, ...rotateBoxStyle }}
              title={`Format: ${format}`}
            >
              {format}
            </div>
          </div>
        );
      }
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <input
            type="date"
            readOnly
            value="2025-02-01"
            className="w-full h-full min-w-0 border border-gray-300 bg-white rounded px-1 pointer-events-none text-[10px]"
            style={{ ...commonStyle, ...rotateBoxStyle }}
          />
        </div>
      );
    }
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  // (Simplified other types similarly for brevity, ensuring style prop is passed)
  if (type === 'TIME') {
    const previewTime = cfg?.__previewFormatted;
    if (typeof previewTime === 'string') {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewTime.trim() || '—'}
          </div>
        </div>
      );
    }
    const d = new Date();
    const example = formatDateByPreset('HH:mm', d);
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  if (type === 'DURATION') {
    const previewDur = cfg?.__previewFormatted;
    if (typeof previewDur === 'string') {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewDur.trim() || '—'}
          </div>
        </div>
      );
    }
    const rawFormat =
      (typeof cfg?.format === 'string' && cfg.format.trim()
        ? cfg.format
        : typeof cfg?.Format === 'string' && cfg.Format.trim()
          ? cfg.Format
          : 'Days') ?? 'Days';
    const unit = DURATION_UNITS.has(rawFormat) ? rawFormat : 'Days';
    const rawV = cfg?.durationValue ?? cfg?.value ?? cfg?.offsetDays ?? cfg?.DurationValue ?? cfg?.Value ?? cfg?.OffsetDays;
    const durationValue = Number.isFinite(Number(rawV)) ? Number(rawV) : 3;
    const example = `${durationValue} ${unit}`;
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  if (type === 'WEIGHT') {
    const rawV = cfg?.value ?? cfg?.Value;
    const numVal =
      rawV == null || rawV === ''
        ? 500
        : typeof rawV === 'number'
          ? rawV
          : Number(rawV);
    const weightNum = Number.isFinite(numVal) ? numVal : 500;
    const weightUnit = normalizeWeightUnit(
      (typeof cfg?.unit === 'string' && cfg.unit.trim()
        ? cfg.unit
        : typeof cfg?.Unit === 'string' && cfg.Unit.trim()
          ? cfg.Unit
          : 'g') ?? 'g',
    );
    const weightFontSizeRaw = cfg?.fontSize ?? cfg?.FontSize;
    const weightFontSize = Number.isFinite(Number(weightFontSizeRaw)) ? Number(weightFontSizeRaw) : 14;
    const weightTextAlignRaw = String(cfg?.textAlign ?? cfg?.TextAlign ?? 'left').toLowerCase();
    const weightTextAlign: 'left' | 'center' | 'right' =
      weightTextAlignRaw === 'center' || weightTextAlignRaw === 'right' ? weightTextAlignRaw : 'left';
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div
          className="px-1 overflow-hidden whitespace-nowrap"
          style={{ ...commonStyle, ...rotateBoxStyle, fontSize: weightFontSize, textAlign: weightTextAlign }}
        >
          {weightNum}
          {weightUnit}
        </div>
      </div>
    );
  }

  if (type === 'WEIGHT_PRICE') {
    const unitPrice = (cfg?.unitPrice as number) ?? 10;
    const weight = (cfg?.weight as number) ?? 0.5;
    const currency = (cfg?.currency as string) ?? '$';
    return <div className="w-full h-full px-1 overflow-hidden whitespace-nowrap" style={commonStyle}>{currency}{(unitPrice * weight).toFixed(2)}</div>;
  }

  // 营养成分表
  if (type === 'NUTRITION') {
    const servingsPerContainer = String(cfg.servingsPerContainer ?? cfg.ServingsPerContainer ?? '').trim();
    const servingSize = String(cfg.servingSize ?? cfg.ServingSize ?? '').trim();
    const calories = String(cfg.calories ?? cfg.Calories ?? nutritionFixedField(cfg, 'calories', 'value') ?? '').trim();
    const nutritionTitleSize = Number(cfg.nutritionTitleFontSize ?? cfg.NutritionTitleFontSize ?? 16) || 16;
    const baseRows = NUTRITION_FIXED_ITEMS.map((item) => {
      const value = nutritionFixedField(cfg, item.key, 'value');
      const unit = nutritionFixedField(cfg, item.key, 'unit') || (item.defaultUnit ?? '');
      return {
        id: item.key,
        label: item.label,
        value,
        unit,
      };
    });
    const extraRows = nutritionExtraRows(cfg).map((item) => ({
      id: item.id,
      label: item.name.trim() || 'Other',
      value: item.value.trim(),
      unit: item.unit.trim(),
    }));
    const rows = [...baseRows, ...extraRows];
    const formatNutritionValue = (value: string, unit: string): string => {
      const v = String(value ?? '').trim();
      const u = String(unit ?? '').trim();
      if (!v && !u) return '';
      return `<${v}${u ? ` ${u}` : ''}`;
    };
    const nutritionContent = (
      <div
        className="text-[10px] p-1 w-full h-full overflow-hidden flex flex-col leading-tight bg-white"
        style={{ fontFamily: resolvedFontFamily }}
      >
        <div className="font-bold border-b border-black pb-0.5" style={{ fontSize: `${nutritionTitleSize}px` }}>
          Nutrition Facts
        </div>
        <div className="flex items-center justify-between py-0.5 mt-0.5 border-b border-black">
          <span className="font-semibold text-[10px]">Calories</span>
          <span className="font-semibold text-[10px]">
            {calories ? formatNutritionValue(calories, '') : ''}
          </span>
        </div>
        <div className="flex items-center justify-between py-0.5 text-[10px]">
          <span>Servings Per Container</span>
          <span>{servingsPerContainer}</span>
        </div>
        <div className="flex items-center justify-between pb-0.5 text-[10px] border-b border-black">
          <span>Serving Size</span>
          <span>{servingSize}</span>
        </div>
        <div className="flex-1 min-h-0 overflow-hidden pt-0.5">
          {rows.map((row) => (
            <div key={row.id} className="flex items-center justify-between py-[1px] text-[10px]">
              <span className="truncate font-medium">{row.label}</span>
              <span className="shrink-0 font-medium">
                {formatNutritionValue(row.value, row.unit)}
              </span>
            </div>
          ))}
        </div>
      </div>
    );
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div
          className="shrink-0"
          style={
            isVerticalRotation
              ? {
                  width: el.height,
                  height: el.width,
                  transform: 'rotate(-90deg)',
                  transformOrigin: 'center center',
                }
              : { width: '100%', height: '100%' }
          }
        >
          {nutritionContent}
        </div>
      </div>
    );
  }

  // 空白占位：预印刷 Logo/Image 区域，预览区展示占位字样
  if (type === 'BLANK') {
    const fontSize = Math.max(11, Math.min(el.width * 0.2, el.height * 0.36, 56));
    return (
      <div
        className="w-full h-full flex flex-col items-center justify-center text-center font-bold text-gray-900 leading-none select-none pointer-events-none overflow-hidden px-0.5"
        style={{ fontFamily: resolvedFontFamily }}
      >
        <span style={{ fontSize, lineHeight: 1.05 }}>Blank</span>
        <span style={{ fontSize, lineHeight: 1.05 }}>Space</span>
      </div>
    );
  }

  return (
    <div className="text-gray-500 text-[10px] px-1 truncate w-full flex items-center justify-center">
      {el.type.replace(/_/g, ' ')}
    </div>
  );
}

interface LabelCanvasProps {
  template: LabelTemplate;
  canvasBorder?: 'none' | 'line' | 'dotted';
  selectedId: string | null;
  onSelect: (id: string | null) => void;
  onUpdateElement: (id: string, patch: Partial<LabelElement>) => void;
  onDeleteElement: (id: string) => void;
  onTemplateChange?: (patch: Partial<LabelTemplate>) => void;
  scale?: number;
  onZoomIn?: () => void;
  onZoomOut?: () => void;
  /** 将缩放还原为 100%（与顶部标尺物理尺寸一致），并重新居中画布 */
  onResetZoom?: () => void;
  onPreview?: () => void;
  /** 为 true 时不在预览工具栏显示画布尺寸预设（改由顶部表单控制） */
  hideToolbarPresetSize?: boolean;
  /** 预览标尺单位（与右侧属性 W/H 读数一致） */
  previewRulerUnit?: PreviewRulerDisplayUnit;
  onPreviewRulerUnitChange?: (unit: PreviewRulerDisplayUnit) => void;
  /** 拖拽/缩放过程中的临时几何，用于选中框与右侧预览实时同步 */
  liveElementPatch?: LabelElementLivePatch | null;
  onLiveElementPatchChange?: (patch: LabelElementLivePatch | null) => void;
  /** 打印方向：竖打时纸张尺寸不变，内容整体旋转 90° 展示 */
  printOrientation?: PrintOrientation;
  onPrintOrientationChange?: (orientation: PrintOrientation) => void;
}

type PaperResizeEdge =
  | 'bottom'
  | 'right'
  | 'top'
  | 'left'
  | 'top-left'
  | 'top-right'
  | 'bottom-left'
  | 'bottom-right';

function cursorForPaperResizeEdge(edge: PaperResizeEdge): string {
  if (edge === 'top' || edge === 'bottom') return 'ns-resize';
  if (edge === 'left' || edge === 'right') return 'ew-resize';
  if (edge === 'top-left' || edge === 'bottom-right') return 'nwse-resize';
  return 'nesw-resize';
}

export function LabelCanvas({
  template,
  canvasBorder,
  selectedId,
  onSelect,
  onUpdateElement,
  onDeleteElement,
  onTemplateChange,
  scale = 1,
  onZoomIn,
  onZoomOut,
  onResetZoom,
  onPreview,
  hideToolbarPresetSize = false,
  previewRulerUnit = "cm",
  onPreviewRulerUnitChange,
  liveElementPatch = null,
  onLiveElementPatchChange,
  printOrientation = 'vertical',
  onPrintOrientationChange,
}: LabelCanvasProps) {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const rulerWorkspaceRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLDivElement>(null);
  const dragRef = useRef<{
    id: string;
    grabOffsetX: number;
    grabOffsetY: number;
    startLocalX: number;
    startLocalY: number;
    w: number;
    h: number;
    active: boolean;
  } | null>(null);
  const suppressCanvasClickDeselectRef = useRef(false);
  const capturedPointerIdRef = useRef<number | null>(null);
  const resizeRef = useRef<{
    id: string;
    corner: string;
    startLocalX: number;
    startLocalY: number;
    w: number;
    h: number;
    elX: number;
    elY: number;
  } | null>(null);
  const resizeDocumentListenersRef = useRef<{
    move: (e: PointerEvent) => void;
    up: (e: PointerEvent) => void;
  } | null>(null);
  const paperResizeRef = useRef<{
    edge: PaperResizeEdge;
    startX: number;
    startY: number;
    startW: number;
    startH: number;
    startElements: { id: string; x: number; y: number }[];
  } | null>(null);
  const lastUpdateRef = useRef<{ id: string; x?: number; y?: number; width?: number; height?: number } | null>(null);

  const nextFrameRef = useRef<number | null>(null);
  const [isSpacePressed, setIsSpacePressed] = React.useState(false);
  const [isPanning, setIsPanning] = React.useState(false);
  const [paperResizeCursor, setPaperResizeCursor] = React.useState<string | null>(null);
  const panStartRef = useRef<{ x: number; y: number; scrollLeft: number; scrollTop: number } | null>(null);
  const [panOffset, setPanOffset] = React.useState({ x: 0, y: 0 });
  const panOffsetStartRef = useRef<{ x: number; y: number; startX: number; startY: number } | null>(null);
  /** 滚动容器可视尺寸，用于标尺铺满灰色背景且小画布时不虚增滚动高度 */
  const [scrollViewport, setScrollViewport] = React.useState({ width: 0, height: 0 });
  /** 标尺工作区实测尺寸（1fr 网格铺满后用于刻度与画布居中） */
  const [measuredWorkspace, setMeasuredWorkspace] = React.useState({ width: 0, height: 0 });
  /** 仅影响预览区标尺刻度/读数，与顶部表单画布单位（template.unit）不同步 */
  const setPreviewRulerUnit = onPreviewRulerUnitChange ?? (() => {});

  useEffect(() => {
    const el = scrollContainerRef.current;
    if (!el) return;
    const update = () => {
      setScrollViewport({ width: el.clientWidth, height: el.clientHeight });
    };
    const ro = new ResizeObserver(update);
    ro.observe(el);
    update();
    return () => ro.disconnect();
  }, []);

  const baseW = unitToPx(Number(template.width) || 0, template.unit);
  const baseH = unitToPx(Number(template.height) || 0, template.unit);
  /** 缩放后的实际占位，用于滚动区域与居中，避免放大后画布被裁切 */
  const widthPx = baseW * scale;
  const heightPx = baseH * scale;
  const showGrid = template.showGrid !== false;
  const isRotatedPrint = printOrientation === 'horizontal';
  const effectiveCanvasBorder = canvasBorder ?? template.border ?? 'none';
  const canvasBorderClass =
    effectiveCanvasBorder === 'line'
      ? 'border border-gray-500'
      : effectiveCanvasBorder === 'dotted'
        ? 'border border-dotted border-gray-500'
        : 'border border-transparent';

  const innerAvailW = Math.max(0, scrollViewport.width - RULER_W);
  const innerAvailH = Math.max(0, scrollViewport.height - RULER_H);
  /** 标尺区域至少铺满灰色可视区；画布更大时随画布扩展 */
  const rulerCanvasWidth = Math.max(1, widthPx, innerAvailW);
  const rulerTotalHeight = Math.max(1, heightPx, innerAvailH);
  const effectiveRulerW = measuredWorkspace.width > 0 ? measuredWorkspace.width : rulerCanvasWidth;
  const effectiveRulerH = measuredWorkspace.height > 0 ? measuredWorkspace.height : rulerTotalHeight;
  const paperOffsetX = Math.max(0, (effectiveRulerW - widthPx) / 2);
  const paperOffsetY = Math.max(0, (effectiveRulerH - heightPx) / 2);
  const gridContentW = RULER_W + rulerCanvasWidth;
  const gridContentH = RULER_H + rulerTotalHeight;
  const wrapperW =
    scrollViewport.width > 0
      ? Math.max(scrollViewport.width, gridContentW)
      : gridContentW;
  const wrapperH =
    scrollViewport.height > 0
      ? Math.max(scrollViewport.height, gridContentH)
      : gridContentH;

  useEffect(() => {
    const el = rulerWorkspaceRef.current;
    if (!el) return;
    const update = () => {
      setMeasuredWorkspace({
        width: Math.max(1, el.clientWidth),
        height: Math.max(1, el.clientHeight),
      });
    };
    const ro = new ResizeObserver(update);
    ro.observe(el);
    update();
    return () => ro.disconnect();
  }, [scrollViewport.width, scrollViewport.height, widthPx, heightPx, scale]);

  const resetScrollToFit = useCallback(() => {
    const el = scrollContainerRef.current;
    if (!el) return;
    const run = () => {
      if (el.scrollWidth > el.clientWidth + 1) {
        el.scrollLeft = Math.max(0, (el.scrollWidth - el.clientWidth) / 2);
      } else {
        el.scrollLeft = 0;
      }
      el.scrollTop = 0;
    };
    requestAnimationFrame(() => requestAnimationFrame(run));
  }, []);

  const rulerSelection = useMemo(() => {
    if (!selectedId) {
      return { x: 0, width: baseW };
    }
    const el = template.elements.find((x) => x.id === selectedId);
    if (!el) return { x: 0, width: baseW };
    const live = mergeLabelElementLivePatch(el, liveElementPatch);
    return { x: live.x, width: live.width };
  }, [selectedId, template.elements, liveElementPatch, baseW]);
  const rulerVerticalSelection = useMemo(() => {
    if (!selectedId) {
      return { y: 0, height: baseH };
    }
    const el = template.elements.find((x) => x.id === selectedId);
    if (!el) return { y: 0, height: baseH };
    const live = mergeLabelElementLivePatch(el, liveElementPatch);
    return { y: live.y, height: live.height };
  }, [selectedId, template.elements, liveElementPatch, baseH]);

  const selectedElementLive = useMemo(() => {
    if (!selectedId) return null;
    const el = template.elements.find((x) => x.id === selectedId);
    if (!el) return null;
    return mergeLabelElementLivePatch(el, liveElementPatch);
  }, [selectedId, template.elements, liveElementPatch]);

  const handlePointerDown = useCallback(
    (e: React.PointerEvent, id: string) => {
      // 如果按住了空格，直接返回，交给外层 panning 处理
      // 允许中键 (button 1) 拖动
      if (isSpacePressed || e.button === 1) return;

      e.stopPropagation();
      suppressCanvasClickDeselectRef.current = true;
      onSelect(id);

      // Focus canvas for keyboard events
      canvasRef.current?.focus();

      const el = template.elements.find((x) => x.id === id);
      if (!el) return;

      const local = clientToCanvasLocalPoint(e.clientX, e.clientY, canvasRef.current, scale);
      if (!local) return;

      const templatePtr = canvasLocalToTemplateSpace(
        local.x,
        local.y,
        printOrientation,
        baseW,
        baseH,
      );

      dragRef.current = {
        id,
        grabOffsetX: templatePtr.x - el.x,
        grabOffsetY: templatePtr.y - el.y,
        startLocalX: local.x,
        startLocalY: local.y,
        w: el.width,
        h: el.height,
        active: false,
      };
    },
    [template.elements, onSelect, isSpacePressed, scale, printOrientation, baseW, baseH]
  );

  const requestUpdate = useCallback((updateFn: () => void) => {
    if (nextFrameRef.current !== null) {
      cancelAnimationFrame(nextFrameRef.current);
    }
    nextFrameRef.current = requestAnimationFrame(() => {
      updateFn();
      nextFrameRef.current = null;
    });
  }, []);

  const beginPaperResize = useCallback((e: React.PointerEvent, edge: PaperResizeEdge) => {
    e.stopPropagation();
    paperResizeRef.current = {
      edge,
      startX: e.clientX,
      startY: e.clientY,
      startW: template.width,
      startH: template.height,
      startElements: template.elements.map((el) => ({ id: el.id, x: el.x, y: el.y })),
    };
    const cursor = cursorForPaperResizeEdge(edge);
    setPaperResizeCursor(cursor);
    document.body.style.cursor = cursor;
    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
  }, [template.width, template.height, template.elements]);

  const detachResizeDocumentListeners = useCallback(() => {
    const listeners = resizeDocumentListenersRef.current;
    if (!listeners) return;
    document.removeEventListener('pointermove', listeners.move);
    document.removeEventListener('pointerup', listeners.up);
    document.removeEventListener('pointercancel', listeners.up);
    resizeDocumentListenersRef.current = null;
  }, []);

  const applyElementResizeAtClient = useCallback(
    (clientX: number, clientY: number) => {
      const session = resizeRef.current;
      if (!session || !canvasRef.current) return;

      const local = clientToCanvasLocalPoint(clientX, clientY, canvasRef.current, scale);
      if (!local) return;

      const { id, corner, startLocalX, startLocalY, w, h, elX, elY } = session;
      const { dtx, dty } = templatePointerDeltaFromCanvasLocals(
        startLocalX,
        startLocalY,
        local.x,
        local.y,
        printOrientation,
        baseW,
        baseH,
      );
      const resized = computeResizedElementBox(elX, elY, w, h, corner, dtx, dty, printOrientation);
      const clamped = clampLabelElementBox(
        snapToGrid(resized.x),
        snapToGrid(resized.y),
        snapToGrid(resized.width),
        snapToGrid(resized.height),
        baseW,
        baseH,
        LABEL_CANVAS_SAFE_MARGIN_PX,
        printOrientation,
      );

      lastUpdateRef.current = {
        id,
        width: clamped.w,
        height: clamped.h,
        x: clamped.x,
        y: clamped.y,
      };
      onLiveElementPatchChange?.({
        id,
        x: clamped.x,
        y: clamped.y,
        width: clamped.w,
        height: clamped.h,
      });
    },
    [scale, printOrientation, baseW, baseH, onLiveElementPatchChange],
  );

  const commitElementResize = useCallback(() => {
    if (!resizeDocumentListenersRef.current && !resizeRef.current && !lastUpdateRef.current) {
      return;
    }
    detachResizeDocumentListeners();
    if (lastUpdateRef.current) {
      const { id, ...patch } = lastUpdateRef.current;
      onUpdateElement(id, patch);
    }
    lastUpdateRef.current = null;
    resizeRef.current = null;
    onLiveElementPatchChange?.(null);
    suppressCanvasClickDeselectRef.current = true;
  }, [detachResizeDocumentListeners, onUpdateElement, onLiveElementPatchChange]);

  const beginElementResize = useCallback(
    (e: React.PointerEvent, elId: string, handleId: string) => {
      e.stopPropagation();
      e.preventDefault();
      suppressCanvasClickDeselectRef.current = true;
      onSelect(elId);

      detachResizeDocumentListeners();

      const el0 = template.elements.find((x) => x.id === elId);
      if (!el0) return;

      const local = clientToCanvasLocalPoint(e.clientX, e.clientY, canvasRef.current, scale);
      if (!local) return;
      resizeRef.current = {
        id: elId,
        corner: handleId,
        startLocalX: local.x,
        startLocalY: local.y,
        w: el0.width,
        h: el0.height,
        elX: el0.x,
        elY: el0.y,
      };
      onLiveElementPatchChange?.({
        id: elId,
        x: el0.x,
        y: el0.y,
        width: el0.width,
        height: el0.height,
      });

      const onMove = (ev: PointerEvent) => {
        ev.preventDefault();
        applyElementResizeAtClient(ev.clientX, ev.clientY);
      };
      const onUp = (ev: PointerEvent) => {
        applyElementResizeAtClient(ev.clientX, ev.clientY);
        commitElementResize();
      };
      resizeDocumentListenersRef.current = { move: onMove, up: onUp };
      document.addEventListener('pointermove', onMove);
      document.addEventListener('pointerup', onUp);
      document.addEventListener('pointercancel', onUp);
      (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
    },
    [
      template.elements,
      onLiveElementPatchChange,
      onSelect,
      scale,
      printOrientation,
      baseW,
      baseH,
      detachResizeDocumentListeners,
      applyElementResizeAtClient,
      commitElementResize,
    ],
  );

  const handlePointerMove = useCallback(
    (e: React.PointerEvent) => {
      // 画布平移：优先处理（translate 方式，不依赖滚动）
      if (isPanning && panOffsetStartRef.current) {
        const dx = e.clientX - panOffsetStartRef.current.startX;
        const dy = e.clientY - panOffsetStartRef.current.startY;
        setPanOffset({
          x: panOffsetStartRef.current.x + dx,
          y: panOffsetStartRef.current.y + dy,
        });
        return;
      }
      if (isPanning && panStartRef.current && scrollContainerRef.current) {
        const dx = e.clientX - panStartRef.current.x;
        const dy = e.clientY - panStartRef.current.y;
        scrollContainerRef.current.scrollLeft = panStartRef.current.scrollLeft - dx;
        scrollContainerRef.current.scrollTop = panStartRef.current.scrollTop - dy;
        return;
      }
      // Drag Element
      if (dragRef.current) {
        const { id, grabOffsetX, grabOffsetY, startLocalX, startLocalY, w, h, active } =
          dragRef.current;
        const local = clientToCanvasLocalPoint(e.clientX, e.clientY, canvasRef.current, scale);
        if (!local) return;

        if (!active) {
          const dlx = local.x - startLocalX;
          const dly = local.y - startLocalY;
          if (Math.hypot(dlx, dly) < ELEMENT_DRAG_THRESHOLD_PX) return;
          dragRef.current = { ...dragRef.current, active: true };
          const domEl = document.getElementById(`element-${id}`);
          if (domEl) {
            domEl.classList.add('z-50', 'opacity-90', 'shadow-xl', 'ring-2', 'ring-blue-400', 'ring-offset-2');
            domEl.style.cursor = 'grabbing';
          }
          canvasRef.current?.setPointerCapture?.(e.pointerId);
          capturedPointerIdRef.current = e.pointerId;
        }

        requestUpdate(() => {
          const { x: rawX, y: rawY } = templatePositionFromPointerGrab(
            local.x,
            local.y,
            grabOffsetX,
            grabOffsetY,
            printOrientation,
            baseW,
            baseH,
          );
          const { x: snappedX, y: snappedY } = clampDragPosition(
            rawX,
            rawY,
            w,
            h,
            baseW,
            baseH,
            LABEL_CANVAS_SAFE_MARGIN_PX,
            printOrientation,
          );

          lastUpdateRef.current = { id, x: snappedX, y: snappedY };
          onLiveElementPatchChange?.({
            id,
            x: snappedX,
            y: snappedY,
            width: w,
            height: h,
          });
        });
      }

      // Resize Element（document 级监听处理缩放，此处跳过）
      if (resizeRef.current && !resizeDocumentListenersRef.current) {
        const { id, corner, startLocalX, startLocalY, w, h, elX, elY } = resizeRef.current;
        const local = clientToCanvasLocalPoint(e.clientX, e.clientY, canvasRef.current, scale);
        if (!local) return;

        requestUpdate(() => {
          const { dtx, dty } = templatePointerDeltaFromCanvasLocals(
            startLocalX,
            startLocalY,
            local.x,
            local.y,
            printOrientation,
            baseW,
            baseH,
          );
          const resized = computeResizedElementBox(elX, elY, w, h, corner, dtx, dty, printOrientation);
          const clamped = clampLabelElementBox(
            snapToGrid(resized.x),
            snapToGrid(resized.y),
            snapToGrid(resized.width),
            snapToGrid(resized.height),
            baseW,
            baseH,
            LABEL_CANVAS_SAFE_MARGIN_PX,
            printOrientation,
          );

          lastUpdateRef.current = {
            id,
            width: clamped.w,
            height: clamped.h,
            x: clamped.x,
            y: clamped.y,
          };
          onLiveElementPatchChange?.({
            id,
            x: clamped.x,
            y: clamped.y,
            width: clamped.w,
            height: clamped.h,
          });
        });
      }

      // Resize Paper
      if (paperResizeRef.current && onTemplateChange) {
        const { edge, startX, startY, startW, startH, startElements } = paperResizeRef.current;
        const clientX = e.clientX;
        const clientY = e.clientY;

        requestUpdate(() => {
          const deltaPxX = (clientX - startX) / scale;
          const deltaPxY = (clientY - startY) / scale;

          const minPaperUnit = 1;
          const startWPx = unitToPx(startW, template.unit);
          const startHPx = unitToPx(startH, template.unit);
          const minWPx = unitToPx(minPaperUnit, template.unit);
          const minHPx = unitToPx(minPaperUnit, template.unit);

          const affectsTop = edge === 'top' || edge === 'top-left' || edge === 'top-right';
          const affectsBottom = edge === 'bottom' || edge === 'bottom-left' || edge === 'bottom-right';
          const affectsLeft = edge === 'left' || edge === 'top-left' || edge === 'bottom-left';
          const affectsRight = edge === 'right' || edge === 'top-right' || edge === 'bottom-right';

          let nextWUnit = startW;
          let nextHUnit = startH;
          let offsetContentPxX = 0;
          let offsetContentPxY = 0;

          if (affectsRight) {
            const proposedPx = Math.max(minWPx, startWPx + deltaPxX);
            nextWUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
          }
          if (affectsBottom) {
            const proposedPx = Math.max(minHPx, startHPx + deltaPxY);
            nextHUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
          }
          if (affectsLeft) {
            const proposedPx = Math.max(minWPx, startWPx - deltaPxX);
            nextWUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
            const snappedPx = unitToPx(nextWUnit, template.unit);
            const appliedDelta = startWPx - snappedPx;
            offsetContentPxX = appliedDelta;
          }
          if (affectsTop) {
            const proposedPx = Math.max(minHPx, startHPx - deltaPxY);
            nextHUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
            const snappedPx = unitToPx(nextHUnit, template.unit);
            const appliedDelta = startHPx - snappedPx;
            offsetContentPxY = appliedDelta;
          }

          const patch: Partial<LabelTemplate> = {};
          if (affectsLeft || affectsRight) patch.width = nextWUnit;
          if (affectsTop || affectsBottom) patch.height = nextHUnit;

          if ((offsetContentPxX !== 0 || offsetContentPxY !== 0) && startElements.length > 0) {
            const byId = new Map(startElements.map(s => [s.id, s]));
            patch.elements = template.elements.map((el) => {
              const s = byId.get(el.id);
              if (!s) return el;
              const nx = Math.max(0, s.x - offsetContentPxX);
              const ny = Math.max(0, s.y - offsetContentPxY);
              return (nx === el.x && ny === el.y) ? el : { ...el, x: nx, y: ny };
            });
          }

          onTemplateChange(patch);
        });
      }
    },
    [isPanning, onTemplateChange, scale, template.unit, requestUpdate, baseW, baseH, onLiveElementPatchChange, printOrientation]
  );

  const handlePointerUp = useCallback(
    (e?: React.PointerEvent) => {
    // 结束画布平移
    if (isPanning) {
      setIsPanning(false);
      panStartRef.current = null;
      panOffsetStartRef.current = null;
    }
    // Cancel pending animation frame
    if (nextFrameRef.current !== null) {
      cancelAnimationFrame(nextFrameRef.current);
      nextFrameRef.current = null;
    }

    const activeId = dragRef.current?.id || resizeRef.current?.id;
    const dragWasActive = dragRef.current?.active ?? false;
    if (activeId) {
      const domEl = document.getElementById(`element-${activeId}`);
      if (domEl) {
        domEl.classList.remove('z-50', 'opacity-90', 'shadow-xl', 'ring-2', 'ring-blue-400', 'ring-offset-2');
        domEl.style.cursor = '';
      }
    }

    const pointerId = e?.pointerId ?? capturedPointerIdRef.current;
    if (pointerId != null) {
      try {
        canvasRef.current?.releasePointerCapture?.(pointerId);
      } catch {
        // ignore
      }
    }
    capturedPointerIdRef.current = null;

    if (resizeDocumentListenersRef.current) {
      commitElementResize();
    } else if (lastUpdateRef.current && (dragWasActive || resizeRef.current)) {
      const { id, ...patch } = lastUpdateRef.current;
      onUpdateElement(id, patch);
      lastUpdateRef.current = null;
      onLiveElementPatchChange?.(null);
    } else {
      lastUpdateRef.current = null;
    }

    detachResizeDocumentListeners();
    dragRef.current = null;
    resizeRef.current = null;
    paperResizeRef.current = null;
    setPaperResizeCursor(null);
    document.body.style.cursor = '';
  },
    [onUpdateElement, onLiveElementPatchChange, isPanning, detachResizeDocumentListeners, commitElementResize],
  );

  useEffect(() => () => detachResizeDocumentListeners(), [detachResizeDocumentListeners]);

  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.code === 'Space' && !e.repeat) {
        setIsSpacePressed(true);
      }
    };
    const onKeyUp = (e: KeyboardEvent) => {
      if (e.code === 'Space') {
        setIsSpacePressed(false);
        setIsPanning(false);
        panStartRef.current = null;
        panOffsetStartRef.current = null;
      }
    };
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    return () => {
      window.removeEventListener('keydown', onKeyDown);
      window.removeEventListener('keyup', onKeyUp);
    };
  }, []);

  useEffect(() => {
    if (!paperResizeCursor) return;
    document.body.style.cursor = paperResizeCursor;
    return () => {
      document.body.style.cursor = '';
    };
  }, [paperResizeCursor]);

  // 缩放或纸张尺寸变化：清空平移偏移；内容未超出视口时不产生纵向滚动
  useEffect(() => {
    setPanOffset({ x: 0, y: 0 });
    resetScrollToFit();
    const t = window.setTimeout(resetScrollToFit, 80);
    return () => window.clearTimeout(t);
  }, [scale, baseW, baseH, widthPx, heightPx, resetScrollToFit]);

  // Keyboard navigation for elements
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (!selectedId) return;
    if (e.key === 'Delete' || e.key === 'Backspace') {
      // ... existing delete logic
      e.preventDefault();
      const idx = template.elements.findIndex((x) => x.id === selectedId);
      if (idx >= 0) {
        const next = template.elements.filter((x) => x.id !== selectedId);
        onDeleteElement(selectedId);
        onSelect(next[idx]?.id ?? next[idx - 1]?.id ?? null);
      }
      return;
    }

    const el = template.elements.find(x => x.id === selectedId);
    if (!el) return;

    // allow typing in inputs without triggering move? 
    // Actually our elements are not inputs (unless we implement inline edit).
    // But preventDefault is good.

    const step = e.shiftKey ? 1 : GRID_SIZE;
    let dx = 0;
    let dy = 0;

    switch (e.key) {
      case 'ArrowLeft': dx = -step; break;
      case 'ArrowRight': dx = step; break;
      case 'ArrowUp': dy = -step; break;
      case 'ArrowDown': dy = step; break;
      default: return;
    }

    e.preventDefault();
    const nx = el.x + dx;
    const ny = el.y + dy;
    const { x, y } = clampDragPosition(
      nx,
      ny,
      el.width,
      el.height,
      baseW,
      baseH,
      LABEL_CANVAS_SAFE_MARGIN_PX,
      printOrientation,
    );
    onUpdateElement(el.id, { x, y });

  }, [selectedId, template.elements, onUpdateElement, onDeleteElement, onSelect, baseW, baseH, printOrientation]);

  const canvasClick = () => onSelect(null);

  // 容器的 Pan 处理
  // 容器的 Pan 处理
  const handleContainerPointerDown = (e: React.PointerEvent) => {
    if (isSpacePressed || e.button === 1) {
      e.preventDefault();
      setIsPanning(true);
      panStartRef.current = {
        x: e.clientX,
        y: e.clientY,
        scrollLeft: scrollContainerRef.current?.scrollLeft || 0,
        scrollTop: scrollContainerRef.current?.scrollTop || 0
      };
      (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
    }
  };

  const handleContainerPointerMove = (e: React.PointerEvent) => {
    if (isPanning && panStartRef.current && scrollContainerRef.current) {
      const dx = e.clientX - panStartRef.current.x;
      const dy = e.clientY - panStartRef.current.y;
      scrollContainerRef.current.scrollLeft = panStartRef.current.scrollLeft - dx;
      scrollContainerRef.current.scrollTop = panStartRef.current.scrollTop - dy;
    }
  };

  const handleContainerPointerUp = (e: React.PointerEvent) => {
    if (isPanning) {
      setIsPanning(false);
      panStartRef.current = null;
    }
  };

  return (
    <div
      className="flex flex-col overflow-hidden bg-gray-100"
      style={{
        flex: "1 1 0%",
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        overflow: "hidden",
      }}
    >
      {/* Label Preview 标题 + 网格/预览/缩放 */}
      <div className="shrink-0 px-4 py-2 border-b border-gray-200 bg-white flex flex-nowrap items-center justify-between gap-3 z-10 min-h-[44px]">
        <span className="text-sm font-medium text-gray-700 shrink-0 min-w-0 truncate">Label Preview</span>
        <div className="flex flex-nowrap items-center justify-end gap-2 shrink-0 min-w-0">
          {onPrintOrientationChange ? (
            <PrintOrientationToggle
              value={printOrientation}
              onChange={onPrintOrientationChange}
            />
          ) : null}
          {onPreview && (
            <button
              type="button"
              onClick={onPreview}
              className="h-8 px-3 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 text-xs font-medium shadow-sm transition-all active:scale-95 shrink-0"
            >
              Preview
            </button>
          )}
          {onTemplateChange && !hideToolbarPresetSize ? (
            <Select
              value={(() => {
                const i = PRESET_LABEL_SIZES.findIndex(
                  (p) => p.width === template.width && p.height === template.height && p.unit === template.unit
                );
                return i >= 0 ? String(i) : 'custom';
              })()}
              onValueChange={(v: string) => {
                if (v === 'custom') return;
                const p = PRESET_LABEL_SIZES[Number(v)];
                if (p) onTemplateChange({ width: p.width, height: p.height, unit: p.unit });
              }}
            >
              <SelectTrigger className="h-8 w-[130px] max-w-[130px] text-xs shrink-0">
                <SelectValue placeholder="Canvas size" />
              </SelectTrigger>
              <SelectContent>
                {PRESET_LABEL_SIZES.map((p, i) => (
                  <SelectItem key={i} value={String(i)} className="text-xs">
                    {p.name}
                  </SelectItem>
                ))}
                <SelectItem value="custom" className="text-xs text-gray-500">
                  Custom
                </SelectItem>
              </SelectContent>
            </Select>
          ) : null}
          {onTemplateChange ? (
            <button
              type="button"
              onClick={() => onTemplateChange({ showGrid: !showGrid })}
              className={cn(
                'h-8 px-3 rounded border text-xs font-medium shadow-sm transition-colors shrink-0',
                showGrid ? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50' : 'border-gray-300 bg-gray-100 text-gray-500'
              )}
            >
              {showGrid ? 'Hide grid' : 'Show grid'}
            </button>
          ) : null}
          <div className="flex items-center gap-1 bg-white rounded border border-gray-300 p-0.5 shadow-sm h-8 shrink-0">
            <button
              type="button"
              onClick={onZoomOut}
              disabled={!onZoomOut}
              className="h-6 w-6 rounded hover:bg-gray-100 text-gray-600 disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center text-sm font-medium active:scale-90 transition-transform"
              title="Zoom out"
            >
              −
            </button>
            <span className="min-w-[3rem] text-center text-xs text-gray-600 font-medium">
              {Math.round(scale * 100)}%
            </span>
            <button
              type="button"
              onClick={onZoomIn}
              disabled={!onZoomIn}
              className="h-6 w-6 rounded hover:bg-gray-100 text-gray-600 disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center text-sm font-medium active:scale-90 transition-transform"
              title="Zoom in"
            >
              +
            </button>
          </div>
          {onResetZoom ? (
            <button
              type="button"
              onClick={onResetZoom}
              className="h-8 px-3 rounded border border-blue-200 bg-blue-50 text-blue-800 hover:bg-blue-100 text-xs font-medium shadow-sm transition-all active:scale-95 shrink-0"
              title="Reset zoom to 100% (match ruler canvas size, e.g. 3×2 inch)"
            >
              Restore size
            </button>
          ) : null}
          <Select
            value={previewRulerUnit}
            onValueChange={(v: PreviewRulerDisplayUnit) => setPreviewRulerUnit(v)}
          >
            <SelectTrigger
              className={cn(
                "h-8 w-auto min-w-[4.25rem] max-w-[5.5rem] px-3 text-xs font-medium shrink-0 rounded border border-gray-300 bg-white text-gray-700 shadow-sm",
                "hover:bg-gray-50 transition-all active:scale-95",
                "focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-gray-50",
              )}
              title="Preview ruler unit (does not change canvas size)"
            >
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="cm" className="text-xs">
                cm
              </SelectItem>
              <SelectItem value="mm" className="text-xs">
                mm
              </SelectItem>
              <SelectItem value="inch" className="text-xs">
                inch
              </SelectItem>
            </SelectContent>
          </Select>
        </div>
      </div>

      {/* Canvas Container：底层灰底铺满可视区，内容层可滚动 */}
      <div
        ref={scrollContainerRef}
        className={cn(
          "overflow-auto relative",
          isSpacePressed ? "cursor-grab active:cursor-grabbing" : ""
        )}
        style={{ flex: "1 1 0%", minHeight: 0, overflow: "auto", position: "relative" }}
        onPointerDown={handleContainerPointerDown}
        onPointerMove={handleContainerPointerMove}
        onPointerUp={handleContainerPointerUp}
        onPointerLeave={handleContainerPointerUp}
      >
        <div
          className="pointer-events-none absolute inset-0 z-0 bg-gray-100"
          aria-hidden
        />
        <div
          className="relative z-[1] box-border shrink-0"
          style={{
            transform: `translate(${panOffset.x}px, ${panOffset.y}px)`,
            width: wrapperW,
            height: wrapperH,
            minWidth: '100%',
            minHeight: '100%',
          }}
        >
          <div
            className="grid shrink-0"
            style={{
              gridTemplateColumns: `${RULER_W}px 1fr`,
              gridTemplateRows: `${RULER_H}px 1fr`,
              width: '100%',
              height: '100%',
              minWidth: gridContentW,
              minHeight: gridContentH,
            }}
          >
            {/* 标尺交汇角：仅占位，不渲染背景/边框/拖拽控件 */}
            <div
              aria-hidden
              className="col-start-1 row-start-1 shrink-0"
              style={{ width: RULER_W, height: RULER_H }}
            />
            <div
              className="col-start-2 row-start-1 min-w-0 w-full"
              style={{ height: RULER_H }}
            >
              <RulerBarHorizontal
                rulerTotalWidthPx={effectiveRulerW}
                paperWidthPx={widthPx}
                paperOffsetLeftPx={paperOffsetX}
                paperWidthTemplate={template.width}
                templateUnit={template.unit}
                displayUnit={previewRulerUnit}
                baseW={baseW}
                selection={rulerSelection}
              />
            </div>
            <div
              className="col-start-1 row-start-2 shrink-0 w-full h-full"
              style={{ width: RULER_W }}
            >
              <RulerBarVertical
                rulerTotalHeightPx={effectiveRulerH}
                paperHeightPx={heightPx}
                paperOffsetTopPx={paperOffsetY}
                paperHeightTemplate={template.height}
                templateUnit={template.unit}
                displayUnit={previewRulerUnit}
                baseH={baseH}
                selection={rulerVerticalSelection}
              />
            </div>
            <div
              ref={rulerWorkspaceRef}
              className="col-start-2 row-start-2 relative shrink-0 w-full h-full min-h-0 min-w-0"
            >
            <div
              className="absolute"
              style={{
                left: paperOffsetX,
                top: paperOffsetY,
                width: widthPx,
                height: heightPx,
              }}
            >
            <div
              ref={canvasRef}
              tabIndex={0}
              className={cn(
                'absolute left-0 top-0 shadow-lg outline-none',
                isRotatedPrint ? 'overflow-visible' : 'overflow-hidden',
                canvasBorderClass,
                isPanning ? 'cursor-grabbing' : 'cursor-grab'
              )}
              style={{
                width: baseW,
                height: baseH,
                transform: `scale(${scale})`,
                transformOrigin: 'top left',
                pointerEvents: isSpacePressed ? 'none' : 'auto',
                cursor: paperResizeCursor ?? undefined,
              }}
              onClick={(e) => {
                if (suppressCanvasClickDeselectRef.current) {
                  suppressCanvasClickDeselectRef.current = false;
                  return;
                }
                // 点击画布空白处取消选中
                const target = e.target as HTMLElement;
                const isOnElement = target.closest('[id^="element-"]');
                const isOnResizeHandle = target.closest('[data-element-resize-handle="true"]');
                const isOnPaperResize =
                  target.closest('[data-paper-resize-handle="true"]') ||
                  target.closest('[title*="Drag to resize paper"]') ||
                  target.closest('[title*="Drag to increase paper height"]') ||
                  target.closest('[title*="Drag to increase paper width"]');
                if (!isOnElement && !isOnResizeHandle && !isOnPaperResize) {
                  onSelect(null);
                }
              }}
              onPointerDown={(e) => {
                // 空白处按下即开始平移（在画布内且未点到元素/纸张拖拽条）
                const target = e.target as HTMLElement;
                const isOnElement = target.closest('[id^="element-"]');
                const isOnResizeHandle = target.closest('[data-element-resize-handle="true"]');
                const isOnPaperResize =
                  target.closest('[data-paper-resize-handle="true"]') ||
                  target.closest('[title*="Drag to resize paper"]') ||
                  target.closest('[title*="Drag to increase paper height"]') ||
                  target.closest('[title*="Drag to increase paper width"]');
                const isOnCanvasArea = canvasRef.current?.contains(target);
                if (isOnCanvasArea && !isOnElement && !isOnResizeHandle && !isOnPaperResize && !dragRef.current && !resizeRef.current) {
                  if (isSpacePressed || e.button === 1) {
                    e.preventDefault();
                    e.stopPropagation();
                    setIsPanning(true);
                    panOffsetStartRef.current = {
                      x: panOffset.x,
                      y: panOffset.y,
                      startX: e.clientX,
                      startY: e.clientY,
                    };
                    panStartRef.current = {
                      x: e.clientX,
                      y: e.clientY,
                      scrollLeft: scrollContainerRef.current?.scrollLeft ?? 0,
                      scrollTop: scrollContainerRef.current?.scrollTop ?? 0,
                    };
                    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
                  }
                }
              }}
              onPointerMove={handlePointerMove}
              onPointerUp={handlePointerUp}
              onKeyDown={handleKeyDown}
            >
            <div
              className="pointer-events-none absolute inset-0 overflow-hidden bg-white"
              style={{
                backgroundImage: showGrid
                  ? `linear-gradient(to right, rgba(0,0,0,0.06) 1px, transparent 1px),
                     linear-gradient(to bottom, rgba(0,0,0,0.06) 1px, transparent 1px)`
                  : undefined,
                backgroundSize: showGrid ? `${GRID_SIZE}px ${GRID_SIZE}px` : undefined,
              }}
              aria-hidden
            />
            <CanvasSafeMarginOverlay baseW={baseW} baseH={baseH} />
            <div
              className="absolute left-0 top-0 overflow-visible"
              style={{
                width: baseW,
                height: baseH,
                ...printContentLayerStyle(printOrientation, baseW, baseH),
              }}
            >
            {/* 选中元素对齐参考线：延伸至画布四边，蓝色虚线 */}
            {selectedElementLive
              ? (() => {
                  const el = selectedElementLive;
                  const lineCls = "pointer-events-none absolute z-[2] border-blue-600";
                  return (
                    <>
                      <div
                        className={cn(lineCls, "left-0 right-0 border-t border-dashed")}
                        style={{ top: el.y }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "left-0 right-0 border-t border-dashed")}
                        style={{ top: el.y + el.height }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "top-0 bottom-0 border-l border-dashed")}
                        style={{ left: el.x }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "top-0 bottom-0 border-l border-dashed")}
                        style={{ left: el.x + el.width }}
                        aria-hidden
                      />
                    </>
                  );
                })()
              : null}
            {template.elements.map((el) => {
              const effectiveEl = mergeLabelElementLivePatch(el, liveElementPatch);
              const isPrintField = isPrintInputElement(el);
              const isSelected = selectedId === el.id;
              return (
              <div
                key={el.id}
                id={`element-${el.id}`}
                className={cn(
                  'absolute box-border cursor-move overflow-visible transition-shadow',
                  el.border === 'line' && 'border border-gray-400',
                  el.border === 'dotted' && 'border border-dotted border-gray-400',
                  isSelected && 'z-10',
                )}
                style={{
                  left: effectiveEl.x,
                  top: effectiveEl.y,
                  width: effectiveEl.width,
                  height: effectiveEl.height,
                }}
                onClick={(e) => {
                  e.stopPropagation();
                  onSelect(el.id);
                }}
                onPointerDown={(e) => handlePointerDown(e, el.id)}
              >
                <div
                  className={cn(
                    'w-full h-full min-h-0 relative overflow-hidden',
                    isPrintField &&
                      !isSelected &&
                      'rounded-sm border-2 border-dashed border-amber-500/85 bg-amber-50/35',
                  )}
                >
                  <ElementContent el={el} isAppPrintField={isPrintField} />
                </div>
              </div>
            );
            })}
            {selectedElementLive ? (
              <ElementSelectionFrame
                el={selectedElementLive}
                templateUnit={template.unit}
                paperWidthPx={baseW}
                paperHeightPx={baseH}
                paperWidthTemplate={template.width}
                paperHeightTemplate={template.height}
                displayUnit={previewRulerUnit}
                printOrientation={printOrientation}
                interactive
                onResizePointerDown={(e, handleId) =>
                  beginElementResize(e, selectedElementLive.id, handleId)
                }
              />
            ) : null}
            </div>
            </div>
            </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/** Preview only: no grid, no rulers, no drag; scale to fit. */
export function LabelPreviewOnly({
  template,
  canvasBorder,
  maxWidth = 480,
  previewAsPrinted = false,
  highlightElementId = null,
  previewRulerUnit = "cm",
  printOrientation = "vertical",
}: {
  template: LabelTemplate;
  canvasBorder?: 'none' | 'line' | 'dotted';
  maxWidth?: number;
  /** 标签新建/编辑：与模板编辑器画布同坐标系，按元素真实字号与框尺寸渲染 */
  previewAsPrinted?: boolean;
  /** 画布当前选中元素 id：在预览中高亮其框架位置 */
  highlightElementId?: string | null;
  /** 选中框尺寸读数单位（与编辑器预览标尺一致） */
  previewRulerUnit?: PreviewRulerDisplayUnit;
  /** 横打时内容整体旋转 90°，纸张尺寸不变；未传时读 template.printOrientation */
  printOrientation?: PrintOrientation;
}) {
  const effectivePrintOrientation =
    printOrientation ?? template.printOrientation ?? 'vertical';
  /** 画布 = 模板 width×height×unit（如 2inch×2inch → 192×192px），控件 x/y/width/height 与编辑区 1:1 */
  const baseW = unitToPx(Number(template.width) || 0, template.unit);
  const baseH = unitToPx(Number(template.height) || 0, template.unit);
  const scaleToFit =
    maxWidth > 0
      ? Math.min(maxWidth / baseW, maxWidth / baseH, previewAsPrinted ? 3 : 2)
      : 1;
  const displayW = baseW * scaleToFit;
  const displayH = baseH * scaleToFit;
  const effectiveCanvasBorder = canvasBorder ?? template.border ?? 'none';
  const previewBorderClass =
    effectiveCanvasBorder === 'line'
      ? 'border border-gray-500'
      : effectiveCanvasBorder === 'dotted'
        ? 'border border-dotted border-gray-500'
        : 'border border-transparent';
  // 与编辑区一致：内层 baseW×baseH，transformOrigin 0 0 缩放，保证位置/样式一致
  return (
    <div
      className="inline-flex items-center justify-center p-4 bg-gray-100 rounded"
      style={{ minWidth: displayW + 32 }}
    >
      <div
        style={{ width: displayW, height: displayH }}
        className={cn("relative overflow-hidden bg-white shadow-lg", previewBorderClass)}
      >
        <div
          className="origin-top-left overflow-hidden"
          style={{
            position: 'absolute',
            left: 0,
            top: 0,
            width: baseW,
            height: baseH,
            transform: `scale(${scaleToFit})`,
            transformOrigin: '0 0',
          }}
        >
          <div
            className="absolute left-0 top-0"
            style={{
              width: baseW,
              height: baseH,
              ...printContentLayerStyle(effectivePrintOrientation, baseW, baseH),
            }}
          >
          {template.elements.map((el) => {
            const isPrintField = isPrintInputElement(el);
            const isHighlighted = highlightElementId === el.id;
            return (
            <div
              key={el.id}
              className={cn(
                'absolute box-border overflow-hidden pointer-events-none',
                !previewAsPrinted && 'flex items-center justify-center text-xs',
                isHighlighted && 'z-[5]',
              )}
              style={{
                left: el.x,
                top: el.y,
                width: el.width,
                height: el.height,
                border:
                  !isHighlighted && el.border === 'line'
                    ? '1px solid #999'
                    : !isHighlighted && el.border === 'dotted'
                      ? '1px dotted #999'
                      : undefined,
              }}
            >
              <div
                className={cn(
                  'relative h-full w-full min-h-0',
                  isPrintField &&
                    !previewAsPrinted &&
                    !isHighlighted &&
                    'rounded-sm border-2 border-dashed border-amber-500/85 bg-amber-50/35',
                )}
              >
                <ElementContent
                  el={el}
                  isAppPrintField={isPrintField}
                  previewAsPrinted={previewAsPrinted}
                  previewSelectionHighlight={isHighlighted}
                />
                {isHighlighted ? (
                  <div
                    className="pointer-events-none absolute inset-0 z-[30]"
                    style={{ backgroundColor: 'rgba(45, 212, 191, 0.42)' }}
                    aria-hidden
                  />
                ) : null}
              </div>
            </div>
            );
          })}
          </div>
        </div>
      </div>
    </div>
  );
}
