<script setup>
import { Icon } from "@iconify/vue";
import { useEventListener } from "@vueuse/core";
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { Button } from "../ui/button";
import { CommandGroup, CommandItem } from "../ui/command";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuTrigger
} from "../ui/context-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupCombobox,
  InputGroupNumberField
} from "../ui/input-group";
import { Field, FieldLabel } from "../ui/field";
import useInfinitePlane from "../../composables/useInfinitePlane";
import { DEFAULT_GAP, findFreePlacement } from "../../share/layout";
defineOptions({ name: "ShwfedBlockLayoutEditor" });
const slots = defineSlots();
const layouts = defineModel({ type: Array, ...{ required: true } });
const activeIndex = defineModel("activeIndex", { type: Number, ...{ default: 0 } });
const selectedItemIds = defineModel("selectedItemIds", { type: Array, ...{ default: () => [] } });
const selectedSet = computed(() => new Set(selectedItemIds.value));
function isSelected(id) {
  return selectedSet.value.has(id);
}
function setSelection(ids) {
  const seen = /* @__PURE__ */ new Set();
  const out = [];
  for (const id of ids) {
    if (seen.has(id)) continue;
    seen.add(id);
    out.push(id);
  }
  selectedItemIds.value = out;
}
function addToSelection(ids) {
  setSelection([...selectedItemIds.value, ...ids]);
}
function onItemClick(itemId, e) {
  if (dragJustEnded) return;
  if (e.shiftKey) addToSelection([itemId]);
  else setSelection([itemId]);
}
function onItemContextMenu(itemId) {
  if (!isSelected(itemId)) setSelection([itemId]);
}
const props = defineProps({
  items: { type: Array, required: true },
  findEntry: { type: Function, required: true },
  getItemLabel: { type: Function, required: false },
  itemNoun: { type: String, required: false },
  pickerEntries: { type: Array, required: false },
  canDelete: { type: Boolean, required: false }
});
const emit = defineEmits(["add-new", "drill-down", "delete-item", "delete-items", "copy-items", "paste"]);
let pendingAddSilent = false;
function captureAddModifiers(e) {
  pendingAddSilent = e.metaKey || e.ctrlKey;
}
function emitAddNew(entry) {
  const silent = pendingAddSilent || isFullscreen.value;
  pendingAddSilent = false;
  const before = isFullscreen.value ? new Set(props.items.map((it) => it.id)) : null;
  emit("add-new", entry, { silent });
  if (before) {
    nextTick(() => {
      const added = props.items.find((it) => !before.has(it.id));
      if (added) setSelection([added.id]);
    });
  }
}
const itemNoun = computed(() => props.itemNoun ?? "\u5757");
const CANVAS_STEP_PX = 24;
const INLINE_PANE_MIN_PX = 480;
const inlinePaneWidth = ref(720);
const activeLayout = computed(() => layouts.value[activeIndex.value]);
const editorGaps = ref(/* @__PURE__ */ new Map());
function getGap(i) {
  const cached = editorGaps.value.get(i);
  if (cached !== void 0) return cached;
  return layouts.value[i]?.layout.gap ?? DEFAULT_GAP;
}
const activeGap = computed(() => getGap(activeIndex.value));
const stepPx = computed(() => CANVAS_STEP_PX);
function setGap(value) {
  const n = typeof value === "number" ? value : Number(value);
  if (!Number.isFinite(n) || n < 0) return;
  const next = new Map(editorGaps.value);
  next.set(activeIndex.value, Math.floor(n));
  editorGaps.value = next;
  writeTracks(activeIndex.value);
}
function frameSize(ls) {
  let cols = 0;
  let rows = 0;
  for (const p of Object.values(ls.layout.placements)) {
    const [, [x2, y2]] = p.area;
    if (x2 - 1 > cols) cols = x2 - 1;
    if (y2 - 1 > rows) rows = y2 - 1;
  }
  return { cols, rows };
}
const activeFrame = computed(() => activeLayout.value ? frameSize(activeLayout.value) : { cols: 0, rows: 0 });
function setLayout(i, updater) {
  const ls = layouts.value[i];
  if (!ls) return;
  const next = [...layouts.value];
  next[i] = updater(ls);
  layouts.value = next;
}
function writeTracks(i, nextPlacements) {
  const gap = getGap(i);
  const current = layouts.value[i];
  if (!current) return;
  const source = nextPlacements ?? current.layout.placements;
  let minX = Infinity;
  let minY = Infinity;
  for (const p of Object.values(source)) {
    if (p.area[0][0] < minX) minX = p.area[0][0];
    if (p.area[0][1] < minY) minY = p.area[0][1];
  }
  const sx = Number.isFinite(minX) && minX !== 1 ? 1 - minX : 0;
  const sy = Number.isFinite(minY) && minY !== 1 ? 1 - minY : 0;
  const placements = sx || sy ? Object.fromEntries(
    Object.entries(source).map(([id, p]) => {
      const [[ax, ay], [bx, by]] = p.area;
      return [id, { ...p, area: [[ax + sx, ay + sy], [bx + sx, by + sy]] }];
    })
  ) : source;
  if (sx || sy) {
    coordinate.value = {
      x: coordinate.value.x - sx * stepPx.value,
      y: coordinate.value.y - sy * stepPx.value
    };
  }
  setLayout(i, (ls) => {
    let cols = 0;
    let rows = 0;
    for (const p of Object.values(placements)) {
      const [, [x2, y2]] = p.area;
      if (x2 - 1 > cols) cols = x2 - 1;
      if (y2 - 1 > rows) rows = y2 - 1;
    }
    return {
      ...ls,
      layout: {
        ...ls.layout,
        placements,
        columns: Math.max(1, cols),
        rows: Math.max(1, rows),
        gap
      }
    };
  });
}
const isSpaceHeld = ref(false);
function isTextInput(el) {
  if (!(el instanceof HTMLElement)) return false;
  const tag = el.tagName;
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
  if (el.isContentEditable) return true;
  return false;
}
useEventListener("keydown", (e) => {
  if (e.code !== "Space") return;
  if (isTextInput(e.target)) return;
  isSpaceHeld.value = true;
  e.preventDefault();
});
useEventListener("keyup", (e) => {
  if (e.code !== "Space") return;
  isSpaceHeld.value = false;
});
useEventListener(() => window, "blur", () => {
  isSpaceHeld.value = false;
});
const { planeRef, coordinate, bgStyle, isDragging: isPanning } = useInfinitePlane({
  gap: CANVAS_STEP_PX,
  enabled: isSpaceHeld
});
onMounted(() => recenterSoon());
function recenter() {
  const el = planeRef.value;
  if (!el) return;
  const rect = el.getBoundingClientRect();
  const frame = activeFrame.value;
  const frameW = frame.cols * stepPx.value;
  const frameH = frame.rows * stepPx.value;
  coordinate.value = {
    x: Math.round((rect.width - frameW) / 2),
    y: Math.round((rect.height - frameH) / 2)
  };
}
function recenterSoon() {
  requestAnimationFrame(() => recenter());
}
watch(activeIndex, () => recenterSoon());
const placementEntries = computed(() => {
  const ls = activeLayout.value;
  if (!ls) return [];
  return Object.entries(ls.layout.placements).map(([itemId, placement]) => ({ itemId, placement }));
});
const MARQUEE_THRESHOLD_PX = 3;
const marquee = ref(null);
function placementPxRect(p) {
  const s = stepPx.value;
  const inset = s * BLOCK_INSET_UNITS;
  const [[x1, y1], [x2, y2]] = p.area;
  return {
    x: (x1 - 1) * s + inset + coordinate.value.x,
    y: (y1 - 1) * s + inset + coordinate.value.y,
    w: Math.max(0, (x2 - x1) * s - 2 * inset),
    h: Math.max(0, (y2 - y1) * s - 2 * inset)
  };
}
function marqueeCanvasBox(m) {
  const r = canvasRef.value?.getBoundingClientRect();
  if (!r) return null;
  const ax = Math.min(m.origin.cx, m.current.cx) - r.left;
  const ay = Math.min(m.origin.cy, m.current.cy) - r.top;
  const bx = Math.max(m.origin.cx, m.current.cx) - r.left;
  const by = Math.max(m.origin.cy, m.current.cy) - r.top;
  return { x: ax, y: ay, w: bx - ax, h: by - ay };
}
const marqueeStyle = computed(() => {
  const m = marquee.value;
  if (!m || !m.active) return null;
  const b = marqueeCanvasBox(m);
  if (!b) return null;
  return {
    top: "0",
    left: "0",
    width: `${b.w}px`,
    height: `${b.h}px`,
    transform: `translate3d(${b.x}px, ${b.y}px, 0)`
  };
});
const pendingPasteCell = ref(null);
function onPlaneContextMenu(e) {
  const rect = canvasRef.value?.getBoundingClientRect();
  if (!rect) {
    pendingPasteCell.value = null;
    return;
  }
  const s = stepPx.value;
  const x = Math.floor((e.clientX - rect.left - coordinate.value.x) / s) + 1;
  const y = Math.floor((e.clientY - rect.top - coordinate.value.y) / s) + 1;
  pendingPasteCell.value = { x, y };
}
function onPlaneMouseDown(e) {
  if (e.button !== 0) return;
  if (isSpaceHeld.value) return;
  marquee.value = {
    origin: { cx: e.clientX, cy: e.clientY },
    current: { cx: e.clientX, cy: e.clientY },
    shiftKey: e.shiftKey,
    addBase: e.shiftKey ? [...selectedItemIds.value] : [],
    active: false
  };
}
useEventListener("mousemove", (e) => {
  const m = marquee.value;
  if (!m) return;
  m.current = { cx: e.clientX, cy: e.clientY };
  if (!m.active) {
    const dx = Math.abs(m.current.cx - m.origin.cx);
    const dy = Math.abs(m.current.cy - m.origin.cy);
    if (dx >= MARQUEE_THRESHOLD_PX || dy >= MARQUEE_THRESHOLD_PX) m.active = true;
  }
});
useEventListener("mouseup", () => {
  const m = marquee.value;
  if (!m) return;
  marquee.value = null;
  if (!m.active) {
    if (!m.shiftKey) setSelection([]);
    return;
  }
  const ls = activeLayout.value;
  if (!ls) return;
  const box = marqueeCanvasBox(m);
  if (!box) return;
  const hits = [];
  for (const [id, p] of Object.entries(ls.layout.placements)) {
    const pr = placementPxRect(p);
    if (pr.x < box.x + box.w && pr.x + pr.w > box.x && pr.y < box.y + box.h && pr.y + pr.h > box.y) {
      hits.push(id);
    }
  }
  setSelection(m.shiftKey ? [...m.addBase, ...hits] : hits);
});
function getItem(itemId) {
  return props.items.find((it) => it.id === itemId);
}
function resolveItemLabel(itemId) {
  const it = getItem(itemId);
  if (!it) return `\u672A\u77E5${itemNoun.value}`;
  if (props.getItemLabel) return props.getItemLabel(it);
  const fa = it;
  if (typeof fa.displayName === "string" && fa.displayName.length > 0) return fa.displayName;
  if (typeof fa.name === "string" && fa.name.length > 0) return fa.name;
  return `\u672A\u547D\u540D${itemNoun.value}`;
}
function getItemIcon(itemId) {
  const it = getItem(itemId);
  if (!it) return "fluent:square-20-regular";
  const entry = props.findEntry(it.type, it.compatibilityDate);
  return entry?.metadata.icon ?? "fluent:square-20-regular";
}
const BLOCK_INSET_UNITS = 0.25;
const drag = ref(null);
let dragJustEnded = false;
const UNBOUNDED = { minW: 1, maxW: Infinity, minH: 1, maxH: Infinity };
function resolveBounds(itemId, start) {
  const it = getItem(itemId);
  if (!it) return UNBOUNDED;
  const entry = props.findEntry(it.type, it.compatibilityDate);
  if (!entry) return UNBOUNDED;
  const { w, h } = entry.metadata;
  const [[x1, y1], [x2, y2]] = start.area;
  const spanW = x2 - x1;
  const spanH = y2 - y1;
  return {
    minW: Math.min(w.min, spanW),
    maxW: Math.max(w.max, spanW),
    minH: Math.min(h.min, spanH),
    maxH: Math.max(h.max, spanH)
  };
}
function blockStyle(entry) {
  const s = stepPx.value;
  const inset = s * BLOCK_INSET_UNITS;
  const d = drag.value;
  const placement = d && d.starts[entry.itemId] ? applyDelta(d, entry.itemId) : entry.placement;
  const [[x1, y1], [x2, y2]] = placement.area;
  const cellX = (x1 - 1) * s + inset + coordinate.value.x;
  const cellY = (y1 - 1) * s + inset + coordinate.value.y;
  const cellW = Math.max(0, (x2 - x1) * s - 2 * inset);
  const cellH = Math.max(0, (y2 - y1) * s - 2 * inset);
  return {
    top: "0",
    left: "0",
    width: `${cellW}px`,
    height: `${cellH}px`,
    transform: `translate3d(${cellX}px, ${cellY}px, 0)`
  };
}
function blockInnerStyle(entry) {
  const d = drag.value;
  const placement = d && d.starts[entry.itemId] ? applyDelta(d, entry.itemId) : entry.placement;
  const toFlex = (a) => a === "start" ? "flex-start" : a === "end" ? "flex-end" : "center";
  return `justify-content: ${toFlex(placement.h)}; align-items: ${toFlex(placement.v)}`;
}
function blockPillStyle(entry) {
  const d = drag.value;
  const placement = d && d.starts[entry.itemId] ? applyDelta(d, entry.itemId) : entry.placement;
  const parts = [];
  if (!placement.h || placement.h === "stretch") parts.push("width: 100%");
  if (!placement.v || placement.v === "stretch") parts.push("height: 100%");
  return parts.join("; ");
}
function areasOverlap(a, b) {
  return a[0][0] < b[1][0] && a[1][0] > b[0][0] && a[0][1] < b[1][1] && a[1][1] > b[0][1];
}
function hideItem(itemId) {
  const i = activeIndex.value;
  const ls = layouts.value[i];
  if (!ls) return;
  if (!ls.layout.placements[itemId]) return;
  const { [itemId]: _removed, ...rest } = ls.layout.placements;
  writeTracks(i, rest);
}
function emitDeleteMany(itemIds) {
  emit("delete-items", [...itemIds]);
}
function hideItems(itemIds) {
  const i = activeIndex.value;
  const ls = layouts.value[i];
  if (!ls) return;
  const drop = new Set(itemIds);
  const rest = {};
  let changed = false;
  for (const [id, p] of Object.entries(ls.layout.placements)) {
    if (drop.has(id)) {
      changed = true;
      continue;
    }
    rest[id] = p;
  }
  if (!changed) return;
  writeTracks(i, rest);
}
const alignItemClass = "inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-zinc-600 outline-none transition-colors hover:bg-zinc-100 hover:text-zinc-900 data-[state=checked]:bg-[color-mix(in_srgb,var(--primary)_12%,white)] data-[state=checked]:text-(--primary) [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
function setAlignment(itemId, patch) {
  setAlignmentMany([itemId], patch);
}
function setAlignmentMany(itemIds, patch) {
  if (itemIds.length === 0) return;
  setLayout(activeIndex.value, (ls) => {
    const placements = { ...ls.layout.placements };
    let changed = false;
    for (const itemId of itemIds) {
      const prev = placements[itemId];
      if (!prev) continue;
      const next = { ...prev, ...patch };
      if (next.h === "stretch") delete next.h;
      if (next.v === "stretch") delete next.v;
      placements[itemId] = next;
      changed = true;
    }
    if (!changed) return ls;
    return { ...ls, layout: { ...ls.layout, placements } };
  });
}
function sharedAlignment(axis) {
  const ls = activeLayout.value;
  if (!ls) return void 0;
  let value;
  let initialized = false;
  for (const id of selectedItemIds.value) {
    const p = ls.layout.placements[id];
    if (!p) continue;
    const v = p[axis] ?? "stretch";
    if (!initialized) {
      value = v;
      initialized = true;
    } else if (value !== v) return void 0;
  }
  return initialized ? value : void 0;
}
function beginDrag(kind, itemId, e) {
  if (e.button !== 0) return;
  const entry = placementEntries.value.find((p) => p.itemId === itemId);
  if (!entry) return;
  e.preventDefault();
  e.stopPropagation();
  let itemIds;
  if (kind === "move" && isSelected(itemId) && selectedItemIds.value.length > 1) {
    itemIds = [...selectedItemIds.value];
  } else {
    itemIds = [itemId];
    if (kind === "move") setSelection([itemId]);
  }
  const starts = {};
  for (const id of itemIds) {
    const p = placementEntries.value.find((x) => x.itemId === id);
    if (p) starts[id] = p.placement;
  }
  drag.value = {
    kind,
    itemIds,
    anchorId: itemId,
    origin: { x: e.clientX, y: e.clientY },
    starts,
    delta: { x: 0, y: 0, w: 0, h: 0 },
    bounds: resolveBounds(itemId, entry.placement)
  };
}
function applyDelta(d, itemId) {
  const start = d.starts[itemId] ?? d.starts[d.anchorId];
  const [[x1, y1], [x2, y2]] = start.area;
  const spanW = x2 - x1;
  const spanH = y2 - y1;
  const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
  const { minW, maxW, minH, maxH } = d.bounds;
  let area;
  switch (d.kind) {
    case "move": {
      const nx1 = x1 + d.delta.x;
      const ny1 = y1 + d.delta.y;
      area = [[nx1, ny1], [nx1 + spanW, ny1 + spanH]];
      break;
    }
    case "resize-e": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      area = [[x1, y1], [x1 + w, y2]];
      break;
    }
    case "resize-s": {
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x1, y1], [x2, y1 + h]];
      break;
    }
    case "resize-w": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      area = [[x2 - w, y1], [x2, y2]];
      break;
    }
    case "resize-n": {
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x1, y2 - h], [x2, y2]];
      break;
    }
    case "resize-se": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x1, y1], [x1 + w, y1 + h]];
      break;
    }
    case "resize-sw": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x2 - w, y1], [x2, y1 + h]];
      break;
    }
    case "resize-ne": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x1, y2 - h], [x1 + w, y2]];
      break;
    }
    case "resize-nw": {
      const w = clamp(spanW + d.delta.w, minW, maxW);
      const h = clamp(spanH + d.delta.h, minH, maxH);
      area = [[x2 - w, y2 - h], [x2, y2]];
      break;
    }
  }
  return { ...start, area };
}
useEventListener("mousemove", (e) => {
  const d = drag.value;
  if (!d) return;
  const s = stepPx.value;
  const dx = Math.round((e.clientX - d.origin.x) / s);
  const dy = Math.round((e.clientY - d.origin.y) / s);
  switch (d.kind) {
    case "move":
      d.delta = { x: dx, y: dy, w: 0, h: 0 };
      break;
    case "resize-e":
      d.delta = { x: 0, y: 0, w: dx, h: 0 };
      break;
    case "resize-s":
      d.delta = { x: 0, y: 0, w: 0, h: dy };
      break;
    case "resize-w":
      d.delta = { x: 0, y: 0, w: -dx, h: 0 };
      break;
    case "resize-n":
      d.delta = { x: 0, y: 0, w: 0, h: -dy };
      break;
    case "resize-se":
      d.delta = { x: 0, y: 0, w: dx, h: dy };
      break;
    case "resize-sw":
      d.delta = { x: 0, y: 0, w: -dx, h: dy };
      break;
    case "resize-ne":
      d.delta = { x: 0, y: 0, w: dx, h: -dy };
      break;
    case "resize-nw":
      d.delta = { x: 0, y: 0, w: -dx, h: -dy };
      break;
  }
});
const dragInvalid = computed(() => {
  const d = drag.value;
  if (!d) return false;
  const ls = activeLayout.value;
  if (!ls) return false;
  const moving = new Set(d.itemIds);
  for (const id of d.itemIds) {
    const cand = applyDelta(d, id).area;
    for (const [otherId, p] of Object.entries(ls.layout.placements)) {
      if (moving.has(otherId)) continue;
      if (areasOverlap(cand, p.area)) return true;
    }
  }
  return false;
});
const frameStyle = computed(() => {
  const ls = activeLayout.value;
  if (!ls) return null;
  const placements = { ...ls.layout.placements };
  const d = drag.value;
  if (d) {
    for (const id of d.itemIds) {
      if (placements[id]) placements[id] = applyDelta(d, id);
    }
  }
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (const p of Object.values(placements)) {
    if (p.area[0][0] < minX) minX = p.area[0][0];
    if (p.area[0][1] < minY) minY = p.area[0][1];
    if (p.area[1][0] > maxX) maxX = p.area[1][0];
    if (p.area[1][1] > maxY) maxY = p.area[1][1];
  }
  if (!Number.isFinite(minX)) return null;
  const s = stepPx.value;
  const tx = (minX - 1) * s + coordinate.value.x;
  const ty = (minY - 1) * s + coordinate.value.y;
  const w = (maxX - minX) * s;
  const h = (maxY - minY) * s;
  return {
    top: "0",
    left: "0",
    width: `${w}px`,
    height: `${h}px`,
    transform: `translate3d(${tx}px, ${ty}px, 0)`
  };
});
const frameOfOthers = computed(() => {
  const ls = activeLayout.value;
  if (!ls) return null;
  const skip = new Set(drag.value?.itemIds ?? []);
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (const [id, p] of Object.entries(ls.layout.placements)) {
    if (skip.has(id)) continue;
    if (p.area[0][0] < minX) minX = p.area[0][0];
    if (p.area[0][1] < minY) minY = p.area[0][1];
    if (p.area[1][0] > maxX) maxX = p.area[1][0];
    if (p.area[1][1] > maxY) maxY = p.area[1][1];
  }
  if (!Number.isFinite(minX)) return null;
  return { minX, minY, maxX, maxY };
});
const distanceIndicators = computed(() => {
  const d = drag.value;
  if (!d || d.kind !== "move") return [];
  const ls = activeLayout.value;
  if (!ls) return [];
  let mx1 = Infinity;
  let my1 = Infinity;
  let mx2 = -Infinity;
  let my2 = -Infinity;
  for (const id of d.itemIds) {
    if (!d.starts[id]) continue;
    const [[ax, ay], [bx, by]] = applyDelta(d, id).area;
    if (ax < mx1) mx1 = ax;
    if (ay < my1) my1 = ay;
    if (bx > mx2) mx2 = bx;
    if (by > my2) my2 = by;
  }
  if (!Number.isFinite(mx1)) return [];
  const moving = new Set(d.itemIds);
  const others = [];
  for (const [id, p] of Object.entries(ls.layout.placements)) {
    if (moving.has(id)) continue;
    others.push(p.area);
  }
  const fo = frameOfOthers.value;
  if (others.length === 0 && !fo) return [];
  const s = stepPx.value;
  const cx = coordinate.value.x;
  const cy = coordinate.value.y;
  const out = [];
  const pushVertical = (dir) => {
    let bestUnits = Infinity;
    let overlap = null;
    let occupied = false;
    for (const a of others) {
      const [[ox1, oy1], [ox2, oy2]] = a;
      if (ox2 <= mx1 || ox1 >= mx2) continue;
      const u = dir === "top" ? my1 - oy2 : oy1 - my2;
      if (u < 0) {
        occupied = true;
        continue;
      }
      if (u >= bestUnits) continue;
      bestUnits = u;
      overlap = { lo: Math.max(ox1, mx1), hi: Math.min(ox2, mx2) };
    }
    let units;
    let midX;
    if (Number.isFinite(bestUnits)) {
      units = bestUnits;
      midX = (overlap.lo + overlap.hi) / 2;
    } else if (occupied || !fo) return;
    else {
      units = dir === "top" ? my1 - fo.minY : fo.maxY - my2;
      midX = (mx1 + mx2) / 2;
    }
    if (units <= 1) return;
    const pxX = (midX - 1) * s + cx;
    const pxY = ((dir === "top" ? my1 - units : my2) - 1) * s + cy;
    out.push({ dir, pxX, pxY, length: units * s, units });
  };
  const pushHorizontal = (dir) => {
    let bestUnits = Infinity;
    let overlap = null;
    let occupied = false;
    for (const a of others) {
      const [[ox1, oy1], [ox2, oy2]] = a;
      if (oy2 <= my1 || oy1 >= my2) continue;
      const u = dir === "left" ? mx1 - ox2 : ox1 - mx2;
      if (u < 0) {
        occupied = true;
        continue;
      }
      if (u >= bestUnits) continue;
      bestUnits = u;
      overlap = { lo: Math.max(oy1, my1), hi: Math.min(oy2, my2) };
    }
    let units;
    let midY;
    if (Number.isFinite(bestUnits)) {
      units = bestUnits;
      midY = (overlap.lo + overlap.hi) / 2;
    } else if (occupied || !fo) return;
    else {
      units = dir === "left" ? mx1 - fo.minX : fo.maxX - mx2;
      midY = (my1 + my2) / 2;
    }
    if (units <= 1) return;
    const pxX = ((dir === "left" ? mx1 - units : mx2) - 1) * s + cx;
    const pxY = (midY - 1) * s + cy;
    out.push({ dir, pxX, pxY, length: units * s, units });
  };
  pushVertical("top");
  pushVertical("bottom");
  pushHorizontal("left");
  pushHorizontal("right");
  return out;
});
function indicatorStyle(ind) {
  const isVertical = ind.dir === "top" || ind.dir === "bottom";
  const w = isVertical ? 0 : ind.length;
  const h = isVertical ? ind.length : 0;
  return `top: 0; left: 0; width: ${w}px; height: ${h}px; transform: translate3d(${ind.pxX}px, ${ind.pxY}px, 0)`;
}
useEventListener("mouseup", () => {
  const d = drag.value;
  if (!d) return;
  const moved = d.delta.x !== 0 || d.delta.y !== 0 || d.delta.w !== 0 || d.delta.h !== 0;
  if (moved) {
    dragJustEnded = true;
    queueMicrotask(() => {
      dragJustEnded = false;
    });
  }
  if (dragInvalid.value) {
    drag.value = null;
    return;
  }
  if (!moved) {
    drag.value = null;
    return;
  }
  const ids = d.itemIds;
  const next = { ...activeLayout.value.layout.placements };
  for (const id of ids) {
    if (!next[id]) continue;
    next[id] = applyDelta(d, id);
  }
  drag.value = null;
  writeTracks(activeIndex.value, next);
});
const unplacedItems = computed(() => {
  const ls = activeLayout.value;
  if (!ls) return [];
  const placed = new Set(Object.keys(ls.layout.placements));
  const out = [];
  for (const it of props.items) {
    if (placed.has(it.id)) continue;
    const entry = props.findEntry(it.type, it.compatibilityDate);
    if (!entry) continue;
    out.push({
      item: it,
      label: resolveItemLabel(it.id),
      icon: entry.metadata.icon,
      initialW: entry.metadata.w.initial,
      initialH: entry.metadata.h.initial
    });
  }
  return out;
});
function placeUnplacedItem(entry) {
  const i = activeIndex.value;
  const ls = layouts.value[i];
  if (!ls) return;
  const placement = findFreePlacement(ls.layout.placements, entry.initialW, entry.initialH);
  writeTracks(i, { ...ls.layout.placements, [entry.item.id]: placement });
}
const showPickerNewGroup = computed(
  () => !!props.pickerEntries && props.pickerEntries.length > 0
);
const canvasRef = ref(null);
const isFullscreen = ref(false);
function toggleFullscreen() {
  isFullscreen.value = !isFullscreen.value;
  recenterSoon();
}
function supportsInlineConfig(itemId) {
  const it = getItem(itemId);
  if (!it) return false;
  const entry = props.findEntry(it.type, it.compatibilityDate);
  return entry?.metadata.inlineConfig !== false;
}
const inlineItemId = computed(() => {
  if (!isFullscreen.value || !slots["inline-config"]) return null;
  if (selectedItemIds.value.length !== 1) return null;
  const id = selectedItemIds.value[0];
  if (!getItem(id) || !supportsInlineConfig(id)) return null;
  return id;
});
const inlineItem = computed(
  () => inlineItemId.value ? getItem(inlineItemId.value) ?? null : null
);
function requestDrill(itemId) {
  if (isFullscreen.value && !!slots["inline-config"] && supportsInlineConfig(itemId)) {
    setSelection([itemId]);
    return;
  }
  emit("drill-down", itemId);
}
function closeInline() {
  setSelection([]);
}
let paneResize = null;
function beginPaneResize(e) {
  if (e.button !== 0) return;
  e.preventDefault();
  e.stopPropagation();
  paneResize = { startX: e.clientX, startW: inlinePaneWidth.value };
}
useEventListener("mousemove", (e) => {
  if (!paneResize) return;
  const delta = paneResize.startX - e.clientX;
  const canvasW = canvasRef.value?.clientWidth ?? Infinity;
  const max = Math.max(INLINE_PANE_MIN_PX, canvasW - 320);
  inlinePaneWidth.value = Math.max(INLINE_PANE_MIN_PX, Math.min(max, paneResize.startW + delta));
});
useEventListener("mouseup", () => {
  if (!paneResize) return;
  paneResize = null;
});
</script>

<template>
  <div
    ref="canvasRef"
    class="overflow-hidden border border-zinc-200 bg-zinc-50 select-none"
    :class="isFullscreen ? 'fixed inset-0 z-40 h-screen w-screen' : 'relative h-full rounded'"
  >
    <ContextMenu>
      <ContextMenuTrigger as-child>
        <div
          ref="planeRef"
          class="absolute inset-0"
          :class="[isPanning ? 'cursor-grabbing' : isSpaceHeld ? 'cursor-grab' : '']"
          :style="bgStyle"
          @mousedown="onPlaneMouseDown"
          @contextmenu="onPlaneContextMenu"
        />
      </ContextMenuTrigger>
      <ContextMenuContent
        class="min-w-auto p-1.5"
      >
        <ContextMenuItem @select="emit('paste', pendingPasteCell)">
          <Icon icon="fluent:clipboard-paste-20-regular" />
          <span>粘贴{{ itemNoun }}</span>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>

    <div
      v-if="marqueeStyle"
      class="absolute z-50 pointer-events-none rounded-sm border border-(--primary) bg-(--primary)/10"
      :style="marqueeStyle"
    />

    <div
      class="absolute top-3 right-3 z-[70] transition-[right]"
      :style="inlineItem ? { right: `${inlinePaneWidth + 12}px` } : void 0"
      @mousedown.stop
    >
      <Button
        variant="default"
        size="sm"
        :title="isFullscreen ? '\u9000\u51FA\u5168\u5C4F' : '\u5168\u5C4F'"
        @click="toggleFullscreen"
      >
        <Icon :icon="isFullscreen ? 'fluent:full-screen-minimize-20-regular' : 'fluent:full-screen-maximize-20-regular'" />
      </Button>
    </div>

    <div
      v-if="frameStyle"
      class="frame-outline absolute pointer-events-none rounded-md"
      :style="frameStyle"
    />

    <ContextMenu
      v-for="entry in placementEntries"
      :key="entry.itemId"
    >
      <ContextMenuTrigger as-child>
        <div
          class="field-block rounded-md"
          :class="[
  drag && drag.itemIds.includes(entry.itemId) ? ['shadow-sm backdrop-blur-sm z-50', dragInvalid ? 'bg-red-200/50' : 'bg-white/50'] : ['shadow-xs bg-white'],
  isSelected(entry.itemId) ? 'is-selected' : ''
]"
          :style="blockStyle(entry)"
          @mousedown="beginDrag('move', entry.itemId, $event)"
          @click="onItemClick(entry.itemId, $event)"
          @contextmenu="onItemContextMenu(entry.itemId)"
          @dblclick.stop="requestDrill(entry.itemId)"
        >
          <div
            class="flex h-full p-1 pointer-events-none"
            :style="blockInnerStyle(entry)"
          >
            <div
              class="flex items-center justify-center gap-1.5 rounded-sm bg-zinc-100 px-1.5 py-0.5 text-sm text-zinc-700"
              :style="blockPillStyle(entry)"
            >
              <Icon
                :icon="getItemIcon(entry.itemId)"
                class="size-4 shrink-0 text-zinc-400"
              />
              <span class="truncate">{{ resolveItemLabel(entry.itemId) }}</span>
            </div>
          </div>

          <button
            type="button"
            class="drill-button"
            :title="`\u7F16\u8F91${itemNoun}`"
            @mousedown.stop
            @click.stop="requestDrill(entry.itemId)"
          >
            <Icon
              icon="fluent:arrow-up-right-20-regular"
              class="size-3.5"
            />
          </button>

          <template v-if="selectedItemIds.length === 1 && isSelected(entry.itemId)">
            <div
              class="resize-handle handle-n"
              @mousedown="beginDrag('resize-n', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-e"
              @mousedown="beginDrag('resize-e', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-s"
              @mousedown="beginDrag('resize-s', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-w"
              @mousedown="beginDrag('resize-w', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-nw"
              @mousedown="beginDrag('resize-nw', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-ne"
              @mousedown="beginDrag('resize-ne', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-sw"
              @mousedown="beginDrag('resize-sw', entry.itemId, $event)"
            />
            <div
              class="resize-handle handle-se"
              @mousedown="beginDrag('resize-se', entry.itemId, $event)"
            />
          </template>
        </div>
      </ContextMenuTrigger>
      <ContextMenuContent
        class="min-w-auto p-1.5"
      >
        <template v-if="selectedItemIds.length > 1 && isSelected(entry.itemId)">
          <ContextMenuItem @select="emit('copy-items', [...selectedItemIds])">
            <Icon icon="fluent:copy-20-regular" />
            <span>复制 ({{ selectedItemIds.length }})</span>
          </ContextMenuItem>
          <ContextMenuSeparator />
          <ContextMenuRadioGroup
            :model-value="sharedAlignment('h') ?? ''"
            class="my-1 flex items-center justify-center gap-0.5"
            @update:model-value="(v) => v && setAlignmentMany(selectedItemIds, { h: v })"
          >
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="start"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-left-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                左对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="center"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-center-horizontal-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                水平居中
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="end"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-right-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                右对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="stretch"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:arrow-autofit-width-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                水平撑满
              </TooltipContent>
            </Tooltip>
          </ContextMenuRadioGroup>
          <ContextMenuRadioGroup
            :model-value="sharedAlignment('v') ?? ''"
            class="my-1 flex items-center justify-center gap-0.5"
            @update:model-value="(v) => v && setAlignmentMany(selectedItemIds, { v })"
          >
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="start"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-top-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                顶部对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="center"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-center-vertical-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                垂直居中
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="end"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-bottom-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                底部对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="stretch"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:arrow-autofit-height-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                垂直撑满
              </TooltipContent>
            </Tooltip>
          </ContextMenuRadioGroup>
          <ContextMenuSeparator />
          <ContextMenuItem @select="hideItems(selectedItemIds)">
            <Icon icon="fluent:eye-off-20-regular" />
            <span>从当前布局移除 ({{ selectedItemIds.length }})</span>
          </ContextMenuItem>
          <ContextMenuItem
            v-if="props.canDelete"
            class="text-red-500 focus:text-red-600 hover:text-red-600 focus:bg-red-50 hover:bg-red-50 [&_svg:not([class*='text-'])]:text-red-400"
            @select="emitDeleteMany(selectedItemIds)"
          >
            <Icon icon="fluent:delete-20-regular" />
            <span>彻底删除 ({{ selectedItemIds.length }})</span>
          </ContextMenuItem>
        </template>
        <template v-else>
          <ContextMenuItem @select="emit('copy-items', [entry.itemId])">
            <Icon icon="fluent:copy-20-regular" />
            <span>复制</span>
          </ContextMenuItem>
          <ContextMenuSeparator />
          <ContextMenuRadioGroup
            :model-value="entry.placement.h ?? 'stretch'"
            class="my-1 flex items-center justify-center gap-0.5"
            @update:model-value="(v) => v && setAlignment(entry.itemId, { h: v })"
          >
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="start"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-left-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                左对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="center"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-center-horizontal-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                水平居中
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="end"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-right-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                右对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="stretch"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:arrow-autofit-width-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                水平撑满
              </TooltipContent>
            </Tooltip>
          </ContextMenuRadioGroup>
          <ContextMenuRadioGroup
            :model-value="entry.placement.v ?? 'stretch'"
            class="my-1 flex items-center justify-center gap-0.5"
            @update:model-value="(v) => v && setAlignment(entry.itemId, { v })"
          >
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="start"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-top-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                顶部对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="center"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-center-vertical-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                垂直居中
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="end"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:align-bottom-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                底部对齐
              </TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger as-child>
                <ContextMenuRadioItem
                  value="stretch"
                  hide-indicator
                  :class="alignItemClass"
                  @select.prevent
                >
                  <Icon icon="fluent:arrow-autofit-height-20-regular" />
                </ContextMenuRadioItem>
              </TooltipTrigger>
              <TooltipContent>
                垂直撑满
              </TooltipContent>
            </Tooltip>
          </ContextMenuRadioGroup>
          <ContextMenuSeparator />
          <ContextMenuItem @select="hideItem(entry.itemId)">
            <Icon icon="fluent:eye-off-20-regular" />
            <span>从当前布局移除</span>
          </ContextMenuItem>
          <ContextMenuItem
            v-if="props.canDelete"
            class="text-red-500 focus:text-red-600 hover:text-red-600 focus:bg-red-50 hover:bg-red-50 [&_svg:not([class*='text-'])]:text-red-400"
            @select="emit('delete-item', entry.itemId)"
          >
            <Icon icon="fluent:delete-20-regular" />
            <span>彻底删除</span>
          </ContextMenuItem>
        </template>
      </ContextMenuContent>
    </ContextMenu>

    <template v-if="drag?.kind === 'move'">
      <div
        v-for="ind in distanceIndicators"
        :key="ind.dir"
        class="dist-indicator absolute pointer-events-none"
        :class="`dist-${ind.dir}`"
        :style="indicatorStyle(ind)"
      >
        <span class="dist-line" />
        <span class="dist-label">{{ ind.units }}</span>
        <span class="dist-line" />
      </div>
    </template>

    <div
      v-if="activeLayout"
      class="absolute bottom-3 left-3 flex items-center gap-3 rounded-md bg-transparent px-3 py-2 font-mono text-sm opacity-50 transition-[opacity,background-color,backdrop-filter] hover:bg-white/95 hover:opacity-100 hover:backdrop-blur focus-within:bg-white/95 focus-within:opacity-100 focus-within:backdrop-blur"
      @mousedown.stop
    >
      <Field
        orientation="horizontal"
        class="items-center gap-2"
      >
        <FieldLabel class="text-sm text-zinc-500">
          间距
        </FieldLabel>
        <div class="gap-input w-20">
          <InputGroup class="h-7">
            <InputGroupNumberField
              :model-value="activeGap"
              :min="0"
              :step="1"
              class="font-mono"
              @update:model-value="setGap"
            />
          </InputGroup>
        </div>
      </Field>
    </div>

    <div
      v-if="activeLayout && (showPickerNewGroup || unplacedItems.length > 0)"
      class="absolute bottom-3 right-3 w-60 rounded-md bg-white/95 backdrop-blur transition-[right]"
      :style="inlineItem ? { right: `${inlinePaneWidth + 12}px` } : void 0"
      @mousedown.stop
      @click.capture="captureAddModifiers"
      @keydown.capture="captureAddModifiers"
    >
      <InputGroup>
        <InputGroupAddon align="inline-start">
          <Icon icon="fluent:add-20-regular" />
        </InputGroupAddon>
        <InputGroupCombobox
          :placeholder="`\u6DFB\u52A0${itemNoun}`"
        >
          <CommandGroup
            v-if="unplacedItems.length > 0"
            heading="未放置"
          >
            <CommandItem
              v-for="entry in unplacedItems"
              :key="`existing:${entry.item.id}`"
              :value="`existing:${entry.item.id}`"
              @select="placeUnplacedItem(entry)"
            >
              <Icon
                :icon="entry.icon"
                class="size-4 shrink-0 text-zinc-400"
              />
              <span class="flex-1 truncate">{{ entry.label }}</span>
              <span class="ml-auto text-[10px] tabular-nums text-zinc-400">
                {{ entry.initialW }} × {{ entry.initialH }}
              </span>
            </CommandItem>
          </CommandGroup>
          <CommandGroup
            v-if="showPickerNewGroup"
            heading="组件"
          >
            <CommandItem
              v-for="entry in props.pickerEntries"
              :key="`new:${entry.type}@${entry.compatibilityDate}`"
              :value="`new:${entry.type}@${entry.compatibilityDate}`"
              @select="emitAddNew(entry)"
            >
              <Icon
                :icon="entry.icon"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ entry.name }}</span>
              <span class="ml-auto text-[10px] tabular-nums text-zinc-400">{{ entry.compatibilityDate }}</span>
            </CommandItem>
          </CommandGroup>
        </InputGroupCombobox>
      </InputGroup>
    </div>

    <!-- Inline config pane — docked to the right edge inside the fullscreen
         element so opening it doesn't unmount the canvas (which would exit
         fullscreen). The host supplies the body via the inline-config slot. -->
    <div
      v-if="inlineItem"
      class="inline-config-pane"
      :style="{ width: `${inlinePaneWidth}px` }"
      @mousedown.stop
      @dblclick.stop
      @contextmenu.stop
    >
      <div
        class="inline-config-resizer"
        title="拖动调整宽度"
        @mousedown="beginPaneResize"
      />
      <div class="inline-config-header">
        <Icon
          :icon="getItemIcon(inlineItemId)"
          class="size-4 shrink-0 text-zinc-400"
        />
        <span class="flex-1 truncate text-sm font-medium text-zinc-700">
          {{ resolveItemLabel(inlineItemId) }}
        </span>
        <button
          type="button"
          class="inline-config-close"
          title="关闭"
          @click="closeInline"
        >
          <Icon
            icon="fluent:dismiss-20-regular"
            class="size-4"
          />
        </button>
      </div>
      <div class="inline-config-body">
        <slot
          name="inline-config"
          :item-id="inlineItemId"
          :item="inlineItem"
          :close="closeInline"
        />
      </div>
    </div>
  </div>
</template>

<style scoped>
.field-block{border:1px solid #e4e4e7;cursor:move;position:absolute}.field-block.is-selected{border-color:var(--primary);box-shadow:0 0 0 1px var(--primary)}.frame-outline{border:1px dashed hsla(240,5%,84%,.7)}.resize-handle{background:transparent;position:absolute}.handle-n{top:-3px}.handle-n,.handle-s{cursor:ns-resize;height:6px;left:12px;right:12px}.handle-s{bottom:-3px}.handle-e{right:-3px}.handle-e,.handle-w{bottom:12px;cursor:ew-resize;top:12px;width:6px}.handle-w{left:-3px}.handle-nw{cursor:nwse-resize;left:-3px}.handle-ne,.handle-nw{height:10px;top:-3px;width:10px}.handle-ne{cursor:nesw-resize;right:-3px}.handle-sw{cursor:nesw-resize;left:-3px}.handle-se,.handle-sw{bottom:-3px;height:10px;width:10px}.handle-se{cursor:nwse-resize;right:-3px}.drill-button{align-items:center;background:transparent;border-radius:4px;color:#71717a;cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;position:absolute;right:4px;top:4px;transition:opacity .12s ease,background-color .12s ease,color .12s ease;width:22px;z-index:5}.drill-button:focus-visible,.field-block:hover .drill-button{opacity:1}.drill-button:hover{background:#f4f4f5;color:var(--primary)}.gap-input :deep(input){text-align:center}.inline-config-pane{background:#fff;border-left:1px solid #e4e4e7;bottom:0;box-shadow:-4px 0 16px -8px rgba(0,0,0,.15);display:flex;flex-direction:column;position:absolute;right:0;top:0;-webkit-user-select:text;-moz-user-select:text;user-select:text;z-index:60}.inline-config-resizer{bottom:0;cursor:ew-resize;left:-3px;position:absolute;top:0;width:7px;z-index:1}.inline-config-resizer:after{background:transparent;bottom:0;content:"";left:3px;position:absolute;top:0;transition:background-color .12s ease;width:1px}.inline-config-resizer:hover:after{background:var(--primary)}.inline-config-header{align-items:center;border-bottom:1px solid #f4f4f5;display:flex;flex-shrink:0;gap:.5rem;padding:.5rem .5rem .5rem .75rem}.inline-config-close{align-items:center;border-radius:4px;color:#71717a;cursor:pointer;display:inline-flex;height:28px;justify-content:center;transition:background-color .12s ease,color .12s ease;width:28px}.inline-config-close:hover{background:#f4f4f5;color:#18181b}.inline-config-body{flex:1 1 0;min-height:0;overflow:auto;padding:1rem}.dist-indicator{align-items:center;color:#f87171;display:flex;justify-content:center;z-index:40}.dist-bottom,.dist-top{flex-direction:column}.dist-left,.dist-right{flex-direction:row}.dist-line{background:currentColor;flex:1 1 0}.dist-bottom .dist-line,.dist-top .dist-line{width:1px}.dist-left .dist-line,.dist-right .dist-line{height:1px}.dist-label{background:#fafafa;border-radius:2px;color:#f87171;font-size:10px;font-variant-numeric:tabular-nums;line-height:1;padding:1px 4px}
</style>
