feat(viewer): add GM Ops Console window replacing ops strip
Introduces a floating, draggable, resizable Game Master console as the first of a planned series of GM/debug windows. Replaces the horizontal ops-strip card layout with proper data tables using TanStack Table v8. - GmWindow.vue: reusable draggable+resizable floating window base; snapshots offsetWidth/Height on drag start so resize is preserved - GmOpsWindow.vue: Ships / Stations / Factions tabs with global filter, column sorting, and drag-to-reorder columns (useColumnOrder composable) - gmStore.ts: Pinia store fed from ViewerWorldLifecycle.rebuildFactions with raw world arrays (ships, stations, factions) - Removes opsStripEl binding (was stored but never read by controller) - GM Console toggle button replaces the bottom ops strip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
100
apps/viewer/src/components/gm/GmWindow.vue
Normal file
100
apps/viewer/src/components/gm/GmWindow.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<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 w = ref(props.initialWidth);
|
||||
const h = ref(props.initialHeight);
|
||||
const isDragging = ref(false);
|
||||
let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
|
||||
function onTitleMouseDown(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).closest("button")) return;
|
||||
// Snapshot current rendered size so resize isn't lost on drag
|
||||
if (windowEl.value) {
|
||||
w.value = windowEl.value.offsetWidth;
|
||||
h.value = windowEl.value.offsetHeight;
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
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`,
|
||||
width: `${w}px`,
|
||||
height: `${h}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>
|
||||
Reference in New Issue
Block a user