<!-- SPEC: PJ2026-010405 云端控制台. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<!-- Responsibility: Generic bounded split workspace layout for tool pages. -->

<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from "vue";

const props = withDefaults(
  defineProps<{
    leftCollapsed?: boolean;
    rightOpen?: boolean;
    rightCollapsed?: boolean;
    leftWidth?: number;
    rightWidth?: number;
    minLeftWidth?: number;
    maxLeftWidth?: number;
    minRightWidth?: number;
    maxRightWidth?: number;
    leftCollapsedWidth?: number;
    rightCollapsedWidth?: number;
    leftResizerTestId?: string;
    rightResizerTestId?: string;
    ariaLabel?: string;
  }>(),
  {
    leftCollapsed: false,
    rightOpen: false,
    rightCollapsed: false,
    leftWidth: 30,
    rightWidth: 50,
    minLeftWidth: 18,
    maxLeftWidth: 36,
    minRightWidth: 30,
    maxRightWidth: 70,
    leftCollapsedWidth: 44,
    rightCollapsedWidth: 44,
    leftResizerTestId: undefined,
    rightResizerTestId: undefined,
    ariaLabel: "工作区布局",
  },
);

const emit = defineEmits<{
  "update:leftWidth": [value: number];
  "update:rightWidth": [value: number];
  "update:leftCollapsed": [value: boolean];
  "update:rightCollapsed": [value: boolean];
}>();

const layoutRef = ref<HTMLElement | null>(null);
const resizing = ref<"left" | "right" | null>(null);
const KEYBOARD_RESIZE_STEP = 2;

const clampedLeftWidth = computed(() =>
  clamp(props.leftWidth, props.minLeftWidth, props.maxLeftWidth),
);
const clampedRightWidth = computed(() =>
  clamp(props.rightWidth, props.minRightWidth, props.maxRightWidth),
);

const layoutStyle = computed(() => {
  const leftCol = props.leftCollapsed
    ? "0px"
    : `minmax(${props.leftCollapsedWidth}px, ${clampedLeftWidth.value}%)`;
  const leftResizerCol = props.leftCollapsed ? "0px" : "6px";
  if (!props.rightOpen) {
    return {
      gridTemplateColumns: `${leftCol} ${leftResizerCol} minmax(0, 1fr)`,
    };
  }
  const mainShare = props.rightCollapsed
    ? "minmax(0, 1fr)"
    : `minmax(0, ${100 - clampedRightWidth.value}fr)`;
  const rightResizeColumn = props.rightCollapsed ? "0" : "6px";
  const rightColumn = props.rightCollapsed
    ? "0px"
    : `minmax(280px, ${clampedRightWidth.value}fr)`;
  return {
    gridTemplateColumns: `${leftCol} ${leftResizerCol} ${mainShare} ${rightResizeColumn} ${rightColumn}`,
  };
});

onBeforeUnmount(() => stopResize());

function startResize(kind: "left" | "right", event: PointerEvent): void {
  if (kind === "left" && props.leftCollapsed) return;
  if (kind === "right" && (!props.rightOpen || props.rightCollapsed)) return;
  event.preventDefault();
  resizing.value = kind;
  window.addEventListener("pointermove", resizePane);
  window.addEventListener("pointerup", stopResize, { once: true });
}

function resizePane(event: PointerEvent): void {
  const rect = layoutRef.value?.getBoundingClientRect();
  if (!rect || rect.width <= 0 || !resizing.value) return;
  if (resizing.value === "left") {
    emit(
      "update:leftWidth",
      Math.round(
        clamp(
          ((event.clientX - rect.left) / rect.width) * 100,
          props.minLeftWidth,
          props.maxLeftWidth,
        ),
      ),
    );
    return;
  }
  const leftPixels = props.leftCollapsed
    ? props.leftCollapsedWidth
    : rect.width * (clampedLeftWidth.value / 100);
  const remaining = Math.max(320, rect.width - leftPixels - 18);
  emit(
    "update:rightWidth",
    Math.round(
      clamp(
        ((rect.right - event.clientX) / remaining) * 100,
        props.minRightWidth,
        props.maxRightWidth,
      ),
    ),
  );
}

function stopResize(): void {
  resizing.value = null;
  window.removeEventListener("pointermove", resizePane);
  window.removeEventListener("pointerup", stopResize);
}

function handleResizeKey(kind: "left" | "right", event: KeyboardEvent): void {
  if (kind === "left" && props.leftCollapsed) return;
  if (kind === "right" && (!props.rightOpen || props.rightCollapsed)) return;
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
  event.preventDefault();
  const direction = event.key === "ArrowRight" ? 1 : -1;
  if (kind === "left") {
    emit(
      "update:leftWidth",
      Math.round(
        clamp(
          clampedLeftWidth.value + direction * KEYBOARD_RESIZE_STEP,
          props.minLeftWidth,
          props.maxLeftWidth,
        ),
      ),
    );
    return;
  }
  emit(
    "update:rightWidth",
    Math.round(
      clamp(
        clampedRightWidth.value - direction * KEYBOARD_RESIZE_STEP,
        props.minRightWidth,
        props.maxRightWidth,
      ),
    ),
  );
}

function clamp(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) return min;
  return Math.min(max, Math.max(min, value));
}
</script>

<template>
  <section
    ref="layoutRef"
    class="split-workspace-layout operations-workspace"
    :style="layoutStyle"
    :aria-label="ariaLabel"
    :data-left-collapsed="leftCollapsed"
    :data-right-open="rightOpen"
    :data-right-collapsed="rightCollapsed"
    :data-resizing="resizing || undefined"
  >
    <aside
      class="split-workspace-pane split-workspace-left"
      :data-collapsed="leftCollapsed"
    >
      <slot name="left" />
    </aside>
    <div
      class="split-workspace-resizer split-workspace-resizer-left"
      :data-testid="leftResizerTestId"
      role="separator"
      :tabindex="leftCollapsed ? -1 : 0"
      aria-orientation="vertical"
      aria-label="调整左侧面板宽度"
      :aria-valuemin="Math.round(minLeftWidth)"
      :aria-valuemax="Math.round(maxLeftWidth)"
      :aria-valuenow="Math.round(clampedLeftWidth)"
      @pointerdown="startResize('left', $event)"
      @keydown="handleResizeKey('left', $event)"
    />
    <main class="split-workspace-main">
      <slot />
    </main>
    <div
      v-if="rightOpen"
      class="split-workspace-resizer split-workspace-resizer-right"
      :data-testid="rightResizerTestId"
      role="separator"
      :tabindex="rightCollapsed ? -1 : 0"
      aria-orientation="vertical"
      aria-label="调整右侧面板宽度"
      :aria-valuemin="Math.round(minRightWidth)"
      :aria-valuemax="Math.round(maxRightWidth)"
      :aria-valuenow="Math.round(clampedRightWidth)"
      @pointerdown="startResize('right', $event)"
      @keydown="handleResizeKey('right', $event)"
    />
    <aside
      v-if="rightOpen"
      class="split-workspace-pane split-workspace-right"
      :data-collapsed="rightCollapsed"
    >
      <slot name="right" />
    </aside>
  </section>
</template>

<style scoped>
.split-workspace-layout {
  display: grid;
  min-width: 0;
  min-height: 0;
  height: 100%;
  overflow: hidden;
  gap: 0;
  background: #fff;
}

.split-workspace-pane,
.split-workspace-main {
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.split-workspace-left {
  border-right: 1px solid #d8e1e7;
  background: #f8fafb;
}

.split-workspace-main {
  background: #fff;
}

.split-workspace-right {
  border-left: 1px solid #d8e1e7;
  background: #f8fafb;
}

.split-workspace-resizer {
  min-width: 0;
  border-inline: 1px solid transparent;
  background: linear-gradient(
    90deg,
    transparent 2px,
    #cbd6dd 3px,
    transparent 4px
  );
  cursor: col-resize;
}

.split-workspace-resizer:focus-visible {
  border-color: #0f766e;
  outline: 2px solid #5eead4;
  outline-offset: -2px;
  background-color: #ecfdf5;
}

.split-workspace-resizer-right {
  opacity: 1;
}

.split-workspace-layout[data-left-collapsed="true"]
  .split-workspace-resizer-left,
.split-workspace-layout[data-right-collapsed="true"]
  .split-workspace-resizer-right {
  display: none;
}

.split-workspace-left[data-collapsed="true"],
.split-workspace-right[data-collapsed="true"] {
  overflow: hidden;
}

.split-workspace-layout[data-resizing] {
  user-select: none;
}

.split-workspace-layout.mdtodo-workspace {
  height: 100%;
  max-height: none;
  align-items: stretch;
  overflow: hidden;
}

.split-workspace-layout.mdtodo-workspace .split-workspace-pane,
.split-workspace-layout.mdtodo-workspace .split-workspace-main {
  align-self: stretch;
}

.split-workspace-layout.mdtodo-workspace .split-workspace-resizer {
  align-self: stretch;
}

@media (max-width: 720px) {
  .split-workspace-layout {
    display: grid;
    height: 100%;
    min-height: 0;
    grid-template-columns: 1fr !important;
    grid-template-rows: minmax(96px, 28%) minmax(0, 1fr) minmax(120px, 34%);
    gap: 10px;
    overflow: hidden;
  }

  .split-workspace-layout:not([data-right-open="true"]) {
    grid-template-rows: minmax(96px, 30%) minmax(0, 1fr);
  }

  .split-workspace-resizer {
    display: none;
  }
}
</style>
