Files
space-game/apps/viewer/src/components/gm/GmWindow.vue

98 lines
2.6 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const props = withDefaults(defineProps<{
title: string;
initialX?: number;
initialY?: number;
initialWidth?: number;
initialHeight?: number;
}>(), {
initialX: 80,
initialY: 80,
initialWidth: 960,
initialHeight: 580,
});
const emit = defineEmits<{
close: [];
}>();
const windowEl = ref<HTMLDivElement | null>(null);
const x = ref(props.initialX);
const y = ref(props.initialY);
const isDragging = ref(false);
let dragOffsetX = 0;
let dragOffsetY = 0;
function onTitleMouseDown(e: MouseEvent) {
if ((e.target as HTMLElement).closest("button")) return;
isDragging.value = true;
dragOffsetX = e.clientX - x.value;
dragOffsetY = e.clientY - y.value;
e.preventDefault();
}
function onMouseMove(e: MouseEvent) {
if (!isDragging.value) return;
x.value = e.clientX - dragOffsetX;
y.value = e.clientY - dragOffsetY;
}
function onMouseUp() {
isDragging.value = false;
}
onMounted(() => {
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
// Set initial size imperatively so Vue's reactive style binding never
// touches width/height — the browser's CSS resize handle owns them.
if (windowEl.value) {
windowEl.value.style.width = `${props.initialWidth}px`;
windowEl.value.style.height = `${props.initialHeight}px`;
}
});
onUnmounted(() => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
});
</script>
<template>
<div
ref="windowEl"
class="gm-window pointer-events-auto fixed flex flex-col overflow-hidden rounded-xl border"
:style="{
left: `${x}px`,
top: `${y}px`,
cursor: isDragging ? 'grabbing' : 'default',
zIndex: 200,
}"
>
<!-- Title bar -->
<div
class="gm-window-titlebar flex shrink-0 cursor-grab select-none items-center gap-2 px-4 py-2.5"
:style="{ cursor: isDragging ? 'grabbing' : 'grab' }"
@mousedown="onTitleMouseDown"
>
<span class="gm-window-title-badge mr-1 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-widest">GM</span>
<h2 class="flex-1 font-[Space_Grotesk] text-sm font-semibold tracking-wide">{{ title }}</h2>
<button
type="button"
class="gm-window-close-btn flex h-6 w-6 items-center justify-center rounded text-xs opacity-60 transition hover:opacity-100"
aria-label="Close window"
@click="emit('close')"
>
</button>
</div>
<!-- Content -->
<div class="min-h-0 flex-1 overflow-hidden">
<slot />
</div>
</div>
</template>