209f8508cd
Adds a sortable battles list as a new entity under the existing `view → table` submenu (entity slug `battles`), replacing the standalone top-level `battle log` shortcut which always opened a "battle not found" placeholder. The single-battle viewer stays put and is reached only by clicking a row (or a battle marker on the map), identical to the existing `section-battles.svelte` flow. Columns are planet (via the shared `planetLabel` helper) and shots (the per-battle action count carried by `BattleSummary`), sortable both ways with shots-desc default. No backend / FBS / map changes: the wire payload is unchanged. Participants / observers / total mass require the full BattleReport and were intentionally dropped to avoid N round trips per menu open. The top-level `battle log` item is removed from `header/view-menu` and `sidebar/bottom-tabs` (and their stale comment blocks updated); the now-orphan `game.view.battle` i18n key is dropped from both locales.
272 lines
6.6 KiB
Svelte
272 lines
6.6 KiB
Svelte
<!--
|
|
Active-view picker that drives navigation between every in-game view.
|
|
The trigger swaps icon between desktop dropdown (▾) and mobile
|
|
hamburger (☰) at the 1024 px breakpoint via CSS only; the surface
|
|
itself is identical. The same component is reused for the mobile
|
|
"More" drawer entry of `bottom-tabs.svelte`.
|
|
|
|
Lists the IA destinations: map, tables (sub-list of seven entities
|
|
including the battles roll-up that replaces the standalone battle-log
|
|
shortcut), report, mail, science designer. Each entry mutates
|
|
`activeView` (the single-URL app-shell has no per-view routes) and
|
|
closes the menu. Closes on Escape, on outside click, and after a
|
|
selection. Phase 26 introduces the history-mode entry; microcopy is
|
|
refined in a later polish pass.
|
|
-->
|
|
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { activeView, type GameView } from "$lib/app-nav.svelte";
|
|
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
|
import { mailStore } from "$lib/mail-store.svelte";
|
|
import { restoreFocus } from "$lib/a11y/restore-focus";
|
|
|
|
const mailUnread = $derived(mailStore.unreadCount);
|
|
|
|
let open = $state(false);
|
|
let rootEl: HTMLDivElement | null = $state(null);
|
|
|
|
const tableEntities: ReadonlyArray<{ slug: string; key: TranslationKey }> = [
|
|
{ slug: "planets", key: "game.view.table.planets" },
|
|
{ slug: "ship-classes", key: "game.view.table.ship_classes" },
|
|
{ slug: "ship-groups", key: "game.view.table.ship_groups" },
|
|
{ slug: "fleets", key: "game.view.table.fleets" },
|
|
{ slug: "sciences", key: "game.view.table.sciences" },
|
|
{ slug: "races", key: "game.view.table.races" },
|
|
{ slug: "battles", key: "game.view.table.battles" },
|
|
];
|
|
|
|
function toggleOpen(): void {
|
|
open = !open;
|
|
}
|
|
|
|
function select(
|
|
view: GameView,
|
|
params: { tableEntity?: string } = {},
|
|
): void {
|
|
open = false;
|
|
activeView.select(view, params);
|
|
}
|
|
|
|
function onKeyDown(event: KeyboardEvent): void {
|
|
if (event.key === "Escape" && open) {
|
|
open = false;
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
const handleClick = (event: MouseEvent): void => {
|
|
if (!open || rootEl === null) return;
|
|
const target = event.target;
|
|
if (target instanceof Node && rootEl.contains(target)) return;
|
|
open = false;
|
|
};
|
|
document.addEventListener("click", handleClick, true);
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => {
|
|
document.removeEventListener("click", handleClick, true);
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<div class="view-menu" bind:this={rootEl}>
|
|
<button
|
|
type="button"
|
|
class="trigger"
|
|
data-testid="view-menu-trigger"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
aria-label={open
|
|
? i18n.t("game.shell.menu.close_views")
|
|
: i18n.t("game.shell.menu.open_views")}
|
|
onclick={toggleOpen}
|
|
>
|
|
<span class="icon-dropdown" aria-hidden="true">▾</span>
|
|
<span class="icon-hamburger" aria-hidden="true">☰</span>
|
|
</button>
|
|
{#if open}
|
|
<div
|
|
class="surface"
|
|
role="menu"
|
|
data-testid="view-menu-list"
|
|
use:restoreFocus
|
|
>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
data-testid="view-menu-item-map"
|
|
onclick={() => select("map")}
|
|
>
|
|
{i18n.t("game.view.map")}
|
|
</button>
|
|
<details data-testid="view-menu-tables">
|
|
<summary>{i18n.t("game.view.table")}</summary>
|
|
<div class="sub">
|
|
{#each tableEntities as entry (entry.slug)}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
data-testid="view-menu-item-table-{entry.slug}"
|
|
onclick={() => select("table", { tableEntity: entry.slug })}
|
|
>
|
|
{i18n.t(entry.key)}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</details>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
data-testid="view-menu-item-report"
|
|
onclick={() => select("report")}
|
|
>
|
|
{i18n.t("game.view.report")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
data-testid="view-menu-item-mail"
|
|
class="with-badge"
|
|
onclick={() => select("mail")}
|
|
>
|
|
<span>{i18n.t("game.view.mail")}</span>
|
|
{#if mailUnread > 0}
|
|
<span class="badge" data-testid="view-menu-item-mail-badge">
|
|
{i18n.t("game.view.mail.badge", { count: String(mailUnread) })}
|
|
</span>
|
|
{/if}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
data-testid="view-menu-item-designer-science"
|
|
onclick={() => select("designer-science")}
|
|
>
|
|
{i18n.t("game.view.designer.science")}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.view-menu {
|
|
position: relative;
|
|
}
|
|
.trigger {
|
|
font: inherit;
|
|
font-size: 1.1rem;
|
|
padding: 0.25rem 0.6rem;
|
|
background: transparent;
|
|
color: inherit;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.trigger:hover {
|
|
background: var(--color-surface-hover);
|
|
}
|
|
.icon-dropdown {
|
|
display: inline;
|
|
}
|
|
.icon-hamburger {
|
|
display: none;
|
|
}
|
|
@media (max-width: 767.98px) {
|
|
.icon-dropdown {
|
|
display: none;
|
|
}
|
|
.icon-hamburger {
|
|
display: inline;
|
|
}
|
|
}
|
|
.surface {
|
|
position: absolute;
|
|
top: calc(100% + 0.25rem);
|
|
right: 0;
|
|
min-width: 14rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--color-surface-overlay);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 6px;
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 50;
|
|
}
|
|
.surface > button,
|
|
.surface > details > summary {
|
|
text-align: left;
|
|
font: inherit;
|
|
padding: 0.45rem 0.75rem;
|
|
background: transparent;
|
|
color: inherit;
|
|
border: 0;
|
|
cursor: pointer;
|
|
}
|
|
.surface > button.with-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
}
|
|
.surface > button.with-badge .badge {
|
|
min-width: 1.5rem;
|
|
padding: 0 0.4rem;
|
|
text-align: center;
|
|
font-size: 0.75rem;
|
|
border-radius: 999px;
|
|
background: var(--color-accent);
|
|
color: var(--color-accent-contrast);
|
|
}
|
|
.surface > button:hover,
|
|
.surface > details > summary:hover {
|
|
background: var(--color-surface-hover);
|
|
}
|
|
.surface > details > summary {
|
|
list-style: none;
|
|
}
|
|
.surface > details > summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
.surface > details > summary::after {
|
|
content: "▸";
|
|
float: right;
|
|
opacity: 0.7;
|
|
}
|
|
.surface > details[open] > summary::after {
|
|
content: "▾";
|
|
}
|
|
.sub {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-left: 0.5rem;
|
|
border-left: 1px solid var(--color-border);
|
|
margin: 0 0.5rem 0.25rem;
|
|
}
|
|
.sub > button {
|
|
text-align: left;
|
|
font: inherit;
|
|
padding: 0.35rem 0.5rem;
|
|
background: transparent;
|
|
color: inherit;
|
|
border: 0;
|
|
cursor: pointer;
|
|
}
|
|
.sub > button:hover {
|
|
background: var(--color-surface-hover);
|
|
}
|
|
@media (max-width: 767.98px) {
|
|
.surface {
|
|
position: fixed;
|
|
top: 3rem;
|
|
right: 0;
|
|
left: 0;
|
|
min-width: 0;
|
|
border-radius: 0;
|
|
border-left: 0;
|
|
border-right: 0;
|
|
max-height: calc(100vh - 3rem);
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
</style>
|