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:
617
apps/viewer/src/components/gm/GmOpsWindow.vue
Normal file
617
apps/viewer/src/components/gm/GmOpsWindow.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
useVueTable,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
createColumnHelper,
|
||||
FlexRender,
|
||||
type SortingState,
|
||||
type ColumnOrderState,
|
||||
type Updater,
|
||||
} from "@tanstack/vue-table";
|
||||
import { storeToRefs } from "pinia";
|
||||
import GmWindow from "./GmWindow.vue";
|
||||
import { useGmStore } from "../../ui/stores/gmStore";
|
||||
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||
import type { ShipSnapshot } from "../../contractsShips";
|
||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||
import type { FactionSnapshot } from "../../contractsFactions";
|
||||
|
||||
// ── Column ordering composable ─────────────────────────────────────────────
|
||||
|
||||
function useColumnOrder(initialIds: string[]) {
|
||||
const columnOrder = ref<ColumnOrderState>([...initialIds]);
|
||||
const draggingId = ref<string | null>(null);
|
||||
const overId = ref<string | null>(null);
|
||||
|
||||
function onColumnOrderChange(updater: Updater<ColumnOrderState>) {
|
||||
columnOrder.value = typeof updater === "function" ? updater(columnOrder.value) : updater;
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent, id: string) {
|
||||
draggingId.value = id;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", id);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent, id: string) {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||
overId.value = id;
|
||||
}
|
||||
|
||||
function onDragLeave() {
|
||||
overId.value = null;
|
||||
}
|
||||
|
||||
function onDrop(targetId: string) {
|
||||
if (!draggingId.value || draggingId.value === targetId) return;
|
||||
const order = [...columnOrder.value];
|
||||
const from = order.indexOf(draggingId.value);
|
||||
const to = order.indexOf(targetId);
|
||||
order.splice(from, 1);
|
||||
order.splice(to, 0, draggingId.value);
|
||||
columnOrder.value = order;
|
||||
draggingId.value = null;
|
||||
overId.value = null;
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
draggingId.value = null;
|
||||
overId.value = null;
|
||||
}
|
||||
|
||||
return { columnOrder, draggingId, overId, onColumnOrderChange, onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd };
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
focus: [id: string, kind: "ship" | "station"];
|
||||
}>();
|
||||
|
||||
type TabId = "ships" | "stations" | "factions";
|
||||
const activeTab = ref<TabId>("ships");
|
||||
|
||||
const gmStore = useGmStore();
|
||||
const selectionStore = useViewerSelectionStore();
|
||||
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||
|
||||
// Faction name lookup
|
||||
const factionMap = computed(() =>
|
||||
new Map(gmStore.factions.map((f) => [f.id, f.label])),
|
||||
);
|
||||
|
||||
// ── Ships table ────────────────────────────────────────────────────────────
|
||||
|
||||
type ShipRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
class: string;
|
||||
faction: string;
|
||||
system: string;
|
||||
state: string;
|
||||
behavior: string;
|
||||
task: string;
|
||||
cargo: number;
|
||||
health: number;
|
||||
};
|
||||
|
||||
const shipRows = computed<ShipRow[]>(() =>
|
||||
gmStore.ships.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
state: s.state,
|
||||
behavior: s.defaultBehaviorKind + (s.behaviorPhase ? ` · ${s.behaviorPhase}` : ""),
|
||||
task: s.controllerTaskKind,
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
health: Math.round(s.health),
|
||||
})),
|
||||
);
|
||||
|
||||
const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||
const shipColumns = [
|
||||
shipColumnHelper.accessor("label", { header: "Name" }),
|
||||
shipColumnHelper.accessor("class", { header: "Class" }),
|
||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
shipColumnHelper.accessor("system", { header: "System" }),
|
||||
shipColumnHelper.accessor("state", { header: "State" }),
|
||||
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||
];
|
||||
|
||||
const shipFilter = ref("");
|
||||
const shipSorting = ref<SortingState>([]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "behavior", "task", "cargo", "health"]);
|
||||
|
||||
const shipTable = useVueTable({
|
||||
get data() { return shipRows.value; },
|
||||
columns: shipColumns,
|
||||
state: {
|
||||
get globalFilter() { return shipFilter.value; },
|
||||
get sorting() { return shipSorting.value; },
|
||||
get columnOrder() { return shipOrder.columnOrder.value; },
|
||||
},
|
||||
onGlobalFilterChange: (v) => { shipFilter.value = String(v); },
|
||||
onSortingChange: (updater) => {
|
||||
shipSorting.value = typeof updater === "function" ? updater(shipSorting.value) : updater;
|
||||
},
|
||||
onColumnOrderChange: shipOrder.onColumnOrderChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
// ── Stations table ─────────────────────────────────────────────────────────
|
||||
|
||||
type StationRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
category: string;
|
||||
faction: string;
|
||||
system: string;
|
||||
docked: string;
|
||||
cargo: number;
|
||||
population: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
const stationRows = computed<StationRow[]>(() =>
|
||||
gmStore.stations.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
category: s.category,
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
docked: `${s.dockedShips} / ${s.dockingPads}`,
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
population: Math.round(s.population),
|
||||
modules: s.installedModules.length,
|
||||
})),
|
||||
);
|
||||
|
||||
const stationColumnHelper = createColumnHelper<StationRow>();
|
||||
const stationColumns = [
|
||||
stationColumnHelper.accessor("label", { header: "Name" }),
|
||||
stationColumnHelper.accessor("category", { header: "Category" }),
|
||||
stationColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
stationColumnHelper.accessor("system", { header: "System" }),
|
||||
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||
stationColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||
stationColumnHelper.accessor("population", { header: "Pop" }),
|
||||
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||
];
|
||||
|
||||
const stationFilter = ref("");
|
||||
const stationSorting = ref<SortingState>([]);
|
||||
const stationOrder = useColumnOrder(["label", "category", "faction", "system", "docked", "cargo", "population", "modules"]);
|
||||
|
||||
const stationTable = useVueTable({
|
||||
get data() { return stationRows.value; },
|
||||
columns: stationColumns,
|
||||
state: {
|
||||
get globalFilter() { return stationFilter.value; },
|
||||
get sorting() { return stationSorting.value; },
|
||||
get columnOrder() { return stationOrder.columnOrder.value; },
|
||||
},
|
||||
onGlobalFilterChange: (v) => { stationFilter.value = String(v); },
|
||||
onSortingChange: (updater) => {
|
||||
stationSorting.value = typeof updater === "function" ? updater(stationSorting.value) : updater;
|
||||
},
|
||||
onColumnOrderChange: stationOrder.onColumnOrderChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
// ── Factions table ─────────────────────────────────────────────────────────
|
||||
|
||||
type FactionRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
credits: number;
|
||||
population: number;
|
||||
military: number;
|
||||
miners: number;
|
||||
transport: number;
|
||||
constructors: number;
|
||||
systems: string;
|
||||
ore: number;
|
||||
shipsBuilt: number;
|
||||
shipsLost: number;
|
||||
};
|
||||
|
||||
const factionRows = computed<FactionRow[]>(() =>
|
||||
gmStore.factions.map((f) => {
|
||||
const gs = f.goapState;
|
||||
return {
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
credits: Math.round(f.credits),
|
||||
population: Math.round(f.populationTotal),
|
||||
military: gs?.militaryShipCount ?? 0,
|
||||
miners: gs?.minerShipCount ?? 0,
|
||||
transport: gs?.transportShipCount ?? 0,
|
||||
constructors: gs?.constructorShipCount ?? 0,
|
||||
systems: gs ? `${gs.controlledSystemCount} / ${gs.targetSystemCount}` : "—",
|
||||
ore: gs ? Math.round(gs.oreStockpile) : 0,
|
||||
shipsBuilt: f.shipsBuilt,
|
||||
shipsLost: f.shipsLost,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||
const factionColumns = [
|
||||
factionColumnHelper.accessor("label", { header: "Faction" }),
|
||||
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
||||
factionColumnHelper.accessor("population", { header: "Pop" }),
|
||||
factionColumnHelper.accessor("military", { header: "Military" }),
|
||||
factionColumnHelper.accessor("miners", { header: "Miners" }),
|
||||
factionColumnHelper.accessor("transport", { header: "Transport" }),
|
||||
factionColumnHelper.accessor("constructors", { header: "Constructors" }),
|
||||
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
||||
factionColumnHelper.accessor("ore", { header: "Ore" }),
|
||||
factionColumnHelper.accessor("shipsBuilt", { header: "Built" }),
|
||||
factionColumnHelper.accessor("shipsLost", { header: "Lost" }),
|
||||
];
|
||||
|
||||
const factionFilter = ref("");
|
||||
const factionSorting = ref<SortingState>([]);
|
||||
const factionOrder = useColumnOrder(["label", "credits", "population", "military", "miners", "transport", "constructors", "systems", "ore", "shipsBuilt", "shipsLost"]);
|
||||
|
||||
const factionTable = useVueTable({
|
||||
get data() { return factionRows.value; },
|
||||
columns: factionColumns,
|
||||
state: {
|
||||
get globalFilter() { return factionFilter.value; },
|
||||
get sorting() { return factionSorting.value; },
|
||||
get columnOrder() { return factionOrder.columnOrder.value; },
|
||||
},
|
||||
onGlobalFilterChange: (v) => { factionFilter.value = String(v); },
|
||||
onSortingChange: (updater) => {
|
||||
factionSorting.value = typeof updater === "function" ? updater(factionSorting.value) : updater;
|
||||
},
|
||||
onColumnOrderChange: factionOrder.onColumnOrderChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
// ── Row counts ─────────────────────────────────────────────────────────────
|
||||
|
||||
const tabs: { id: TabId; label: string }[] = [
|
||||
{ id: "ships", label: "Ships" },
|
||||
{ id: "stations", label: "Stations" },
|
||||
{ id: "factions", label: "Factions" },
|
||||
];
|
||||
|
||||
const activeFilter = computed({
|
||||
get: () => {
|
||||
if (activeTab.value === "ships") return shipFilter.value;
|
||||
if (activeTab.value === "stations") return stationFilter.value;
|
||||
return factionFilter.value;
|
||||
},
|
||||
set: (v: string) => {
|
||||
if (activeTab.value === "ships") shipFilter.value = v;
|
||||
else if (activeTab.value === "stations") stationFilter.value = v;
|
||||
else factionFilter.value = v;
|
||||
},
|
||||
});
|
||||
|
||||
const activeRowCount = computed(() => {
|
||||
if (activeTab.value === "ships") return shipTable.getFilteredRowModel().rows.length;
|
||||
if (activeTab.value === "stations") return stationTable.getFilteredRowModel().rows.length;
|
||||
return factionTable.getFilteredRowModel().rows.length;
|
||||
});
|
||||
|
||||
const activeTotalCount = computed(() => {
|
||||
if (activeTab.value === "ships") return gmStore.ships.length;
|
||||
if (activeTab.value === "stations") return gmStore.stations.length;
|
||||
return gmStore.factions.length;
|
||||
});
|
||||
|
||||
// ── Row interaction ────────────────────────────────────────────────────────
|
||||
|
||||
function onShipClick(row: ShipRow) {
|
||||
selectionStore.selectSelection({ id: row.id, kind: "ship", label: row.label }, "ui");
|
||||
}
|
||||
|
||||
function onShipDblClick(row: ShipRow) {
|
||||
selectionStore.selectSelection({ id: row.id, kind: "ship", label: row.label }, "ui");
|
||||
emit("focus", row.id, "ship");
|
||||
}
|
||||
|
||||
function onStationClick(row: StationRow) {
|
||||
selectionStore.selectSelection({ id: row.id, kind: "station", label: row.label }, "ui");
|
||||
}
|
||||
|
||||
function onStationDblClick(row: StationRow) {
|
||||
selectionStore.selectSelection({ id: row.id, kind: "station", label: row.label }, "ui");
|
||||
emit("focus", row.id, "station");
|
||||
}
|
||||
|
||||
function isShipSelected(id: string) {
|
||||
return selectedEntityKind.value === "ship" && selectedEntityId.value === id;
|
||||
}
|
||||
|
||||
function isStationSelected(id: string) {
|
||||
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GmWindow
|
||||
title="Ships"
|
||||
:initial-width="980"
|
||||
:initial-height="560"
|
||||
:initial-x="80"
|
||||
:initial-y="80"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Tab bar + search -->
|
||||
<div class="gm-toolbar flex shrink-0 items-center gap-3 px-3 py-2">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="gm-tab-btn rounded px-3 py-1 text-xs font-semibold uppercase tracking-widest transition"
|
||||
:class="activeTab === tab.id ? 'gm-tab-btn--active' : ''"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
v-model="activeFilter"
|
||||
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
||||
placeholder="Filter…"
|
||||
type="search"
|
||||
/>
|
||||
<svg class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 opacity-40" width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" />
|
||||
</svg>
|
||||
<button
|
||||
v-if="activeFilter"
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] opacity-40 hover:opacity-80"
|
||||
@click="activeFilter = ''"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="gm-row-count shrink-0 font-mono text-xs tabular-nums opacity-60">
|
||||
{{ activeRowCount }} / {{ activeTotalCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Ships table -->
|
||||
<div
|
||||
v-show="activeTab === 'ships'"
|
||||
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr
|
||||
v-for="headerGroup in shipTable.getHeaderGroups()"
|
||||
:key="headerGroup.id"
|
||||
>
|
||||
<th
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||
:class="[
|
||||
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||
shipOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||
shipOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="shipOrder.onDragStart($event, header.column.id)"
|
||||
@dragover="shipOrder.onDragOver($event, header.column.id)"
|
||||
@dragleave="shipOrder.onDragLeave()"
|
||||
@drop="shipOrder.onDrop(header.column.id)"
|
||||
@dragend="shipOrder.onDragEnd()"
|
||||
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||
>
|
||||
<span class="flex select-none items-center gap-1">
|
||||
<FlexRender
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
<span class="text-[10px] opacity-60">
|
||||
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||
</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in shipTable.getRowModel().rows"
|
||||
:key="row.id"
|
||||
class="gm-tr cursor-pointer"
|
||||
:class="isShipSelected(row.original.id) ? 'gm-tr--selected' : ''"
|
||||
@click="onShipClick(row.original)"
|
||||
@dblclick="onShipDblClick(row.original)"
|
||||
>
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||
>
|
||||
<span
|
||||
v-if="cell.column.id === 'class'"
|
||||
class="gm-badge rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</span>
|
||||
<FlexRender
|
||||
v-else
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="shipTable.getRowModel().rows.length === 0">
|
||||
<td :colspan="shipColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||
No ships match the filter.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Stations table -->
|
||||
<div
|
||||
v-show="activeTab === 'stations'"
|
||||
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr
|
||||
v-for="headerGroup in stationTable.getHeaderGroups()"
|
||||
:key="headerGroup.id"
|
||||
>
|
||||
<th
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||
:class="[
|
||||
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||
stationOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||
stationOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="stationOrder.onDragStart($event, header.column.id)"
|
||||
@dragover="stationOrder.onDragOver($event, header.column.id)"
|
||||
@dragleave="stationOrder.onDragLeave()"
|
||||
@drop="stationOrder.onDrop(header.column.id)"
|
||||
@dragend="stationOrder.onDragEnd()"
|
||||
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||
>
|
||||
<span class="flex select-none items-center gap-1">
|
||||
<FlexRender
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
<span class="text-[10px] opacity-60">
|
||||
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||
</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in stationTable.getRowModel().rows"
|
||||
:key="row.id"
|
||||
class="gm-tr cursor-pointer"
|
||||
:class="isStationSelected(row.original.id) ? 'gm-tr--selected' : ''"
|
||||
@click="onStationClick(row.original)"
|
||||
@dblclick="onStationDblClick(row.original)"
|
||||
>
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||
>
|
||||
<span
|
||||
v-if="cell.column.id === 'category'"
|
||||
class="gm-badge rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</span>
|
||||
<FlexRender
|
||||
v-else
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="stationTable.getRowModel().rows.length === 0">
|
||||
<td :colspan="stationColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||
No stations match the filter.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Factions table -->
|
||||
<div
|
||||
v-show="activeTab === 'factions'"
|
||||
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr
|
||||
v-for="headerGroup in factionTable.getHeaderGroups()"
|
||||
:key="headerGroup.id"
|
||||
>
|
||||
<th
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||
:class="[
|
||||
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||
factionOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||
factionOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="factionOrder.onDragStart($event, header.column.id)"
|
||||
@dragover="factionOrder.onDragOver($event, header.column.id)"
|
||||
@dragleave="factionOrder.onDragLeave()"
|
||||
@drop="factionOrder.onDrop(header.column.id)"
|
||||
@dragend="factionOrder.onDragEnd()"
|
||||
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||
>
|
||||
<span class="flex select-none items-center gap-1">
|
||||
<FlexRender
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
<span class="text-[10px] opacity-60">
|
||||
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||
</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in factionTable.getRowModel().rows"
|
||||
:key="row.id"
|
||||
class="gm-tr"
|
||||
>
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="factionTable.getRowModel().rows.length === 0">
|
||||
<td :colspan="factionColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||
No factions match the filter.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</GmWindow>
|
||||
</template>
|
||||
Reference in New Issue
Block a user