98 lines
2.6 KiB
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>
|