@nano133/nanoterra 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ // The panel's tab bar: Overview, Cosmetics, Orders, Players and Settings. Real tabs (role tablist, tab and tabpanel): a press or the arrow keys, Home and End
2
+ // choose one, and only the chosen tab is in the Tab order. Every panel is made once and only hidden, so a draft, a search or a switch keeps its state when
3
+ // the creator goes to another tab and back. The choice lives in this page's memory only: the panel never writes the page's address (the host page owns it).
4
+
5
+ import { h } from "./dom.js";
6
+ import { icon, type IconName } from "./icons.js";
7
+
8
+ export type TabId = "overview" | "cosmetics" | "orders" | "players" | "settings";
9
+
10
+ export const TABS: readonly { id: TabId; label: string; icon: IconName }[] = [
11
+ { id: "overview", label: "Overview", icon: "overview" },
12
+ { id: "cosmetics", label: "Cosmetics", icon: "shirt" },
13
+ { id: "orders", label: "Orders", icon: "receipt" },
14
+ { id: "players", label: "Players", icon: "players" },
15
+ { id: "settings", label: "Settings", icon: "settings" },
16
+ ];
17
+
18
+ export type TabBar = {
19
+ /** The bar (role tablist). */
20
+ bar: HTMLElement;
21
+ /** The panel of each tab (role tabpanel), empty: the caller fills it. */
22
+ panels: Record<TabId, HTMLElement>;
23
+ /**
24
+ * Shows a tab. `focus` moves the keyboard to it (an arrow key does; a link from another tab does not). When the page was scrolled into the panels, the top of
25
+ * the new panel comes up under the bar (a short tab after a long one must not open at its end).
26
+ */
27
+ select(id: TabId, focus?: boolean): void;
28
+ /** The tab that shows now. */
29
+ readonly current: TabId;
30
+ /** The small number on a tab ("Orders 25"); null takes it away. */
31
+ setCount(id: TabId, n: number | null): void;
32
+ };
33
+
34
+ export function tabBar(first: TabId = "overview"): TabBar {
35
+ let current: TabId = first;
36
+ const bar = h("div", { class: "tabbar", role: "tablist", "aria-label": "Admin sections" });
37
+ const buttons = {} as Record<TabId, HTMLButtonElement>;
38
+ const counts = {} as Record<TabId, HTMLElement>;
39
+ const panels = {} as Record<TabId, HTMLElement>;
40
+
41
+ for (const t of TABS) {
42
+ counts[t.id] = h("span", { class: "tab-count" });
43
+ counts[t.id].hidden = true;
44
+ const b = h(
45
+ "button",
46
+ { class: "tabbar-tab", type: "button", role: "tab", id: `n133-tab-${t.id}`, "aria-controls": `n133-panel-${t.id}` },
47
+ icon(t.icon, 18),
48
+ h("span", { class: "tab-label" }, t.label),
49
+ counts[t.id],
50
+ );
51
+ b.addEventListener("click", () => select(t.id));
52
+ b.addEventListener("keydown", (e: KeyboardEvent) => {
53
+ const i = TABS.findIndex((x) => x.id === t.id);
54
+ const next = e.key === "ArrowRight" ? i + 1 : e.key === "ArrowLeft" ? i - 1 : e.key === "Home" ? 0 : e.key === "End" ? TABS.length - 1 : null;
55
+ if (next === null) return;
56
+ e.preventDefault();
57
+ select(TABS[(next + TABS.length) % TABS.length].id, true);
58
+ });
59
+ buttons[t.id] = b;
60
+ bar.append(b);
61
+ panels[t.id] = h("section", { class: `tabpanel tabpanel-${t.id}`, role: "tabpanel", id: `n133-panel-${t.id}`, "aria-labelledby": `n133-tab-${t.id}`, tabindex: 0 });
62
+ }
63
+
64
+ function select(id: TabId, focus = false) {
65
+ current = id;
66
+ for (const t of TABS) {
67
+ const on = t.id === id;
68
+ buttons[t.id].setAttribute("aria-selected", on ? "true" : "false");
69
+ buttons[t.id].tabIndex = on ? 0 : -1;
70
+ buttons[t.id].classList.toggle("is-on", on);
71
+ panels[t.id].hidden = !on;
72
+ }
73
+ if (focus) buttons[id].focus({ preventScroll: true });
74
+ // Keep the chosen tab in view in the bar, when the bar scrolls sideways (only the bar moves).
75
+ const b = buttons[id];
76
+ if (bar.scrollWidth > bar.clientWidth) bar.scrollLeft = Math.max(0, b.offsetLeft - (bar.clientWidth - b.offsetWidth) / 2);
77
+ const panel = panels[id];
78
+ if (panel.isConnected && panel.getBoundingClientRect().top < bar.getBoundingClientRect().bottom) panel.scrollIntoView?.({ block: "start" });
79
+ }
80
+
81
+ select(first);
82
+ return {
83
+ bar,
84
+ panels,
85
+ select,
86
+ get current() {
87
+ return current;
88
+ },
89
+ setCount(id, n) {
90
+ counts[id].hidden = n === null;
91
+ counts[id].textContent = n === null ? "" : n.toLocaleString("en-US");
92
+ },
93
+ };
94
+ }
@@ -45,9 +45,9 @@ export function mergeSettings(stored: { character?: unknown; signin?: unknown },
45
45
  export type SwitchId = "character" | "signin";
46
46
  export const SWITCHES: Record<SwitchId, { title: string; on: string; off: string }> = {
47
47
  character: {
48
- title: "Players use their Nanoterra character",
49
- on: "On: a player who signs in from Nanoterra plays as the character they picked there. Anyone else plays as the island's first character.",
50
- off: "Off: every player plays as this game's own character.",
48
+ title: "Players can play as their Nanoterra character",
49
+ on: "On: a player who signs in from Nanoterra plays as the character they picked there. Anyone else plays as the island's first character. The game may also let a player switch to its own character.",
50
+ off: "Off: every player plays as this game's own character, with no choice.",
51
51
  },
52
52
  signin: {
53
53
  title: "Players can sign in with Nanoterra",
@@ -62,7 +62,7 @@ export const SWITCHES: Record<SwitchId, { title: string; on: string; off: string
62
62
  * to do (the starter's src/client/ui/player.ts draws no Nanoterra character while sign-in is Off); the card only says it.
63
63
  */
64
64
  export const CHARACTER_NEEDS_SIGNIN = "This needs “Players can sign in with Nanoterra” to be On: only a player who signs in from Nanoterra brings a character.";
65
- export const CHARACTER_SIGNIN_OFF = "Sign-in from Nanoterra is Off, so every player plays as the island's first character, a player who signed in from Nanoterra before included.";
65
+ export const CHARACTER_SIGNIN_OFF = "Sign-in from Nanoterra is Off, so every player starts as the island's first character, a player who signed in from Nanoterra before included. The game may also let a player switch to its own character.";
66
66
 
67
67
  /** What to tell the creator under the sign-in switch about Nanoterra's review. A game cannot see that review: it says so. */
68
68
  export const REVIEW_NOTE =