@jgengine/react 0.14.0 → 0.15.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.
- package/CHANGELOG.md +500 -1
- package/dist/accessibility.d.ts +39 -0
- package/dist/accessibility.js +61 -0
- package/dist/achievements.d.ts +50 -0
- package/dist/achievements.js +94 -0
- package/dist/cameraShake.d.ts +52 -0
- package/dist/cameraShake.js +53 -0
- package/dist/coachMarks.d.ts +72 -0
- package/dist/coachMarks.js +136 -0
- package/dist/codex.d.ts +25 -0
- package/dist/codex.js +62 -0
- package/dist/comboMeter.d.ts +63 -0
- package/dist/comboMeter.js +93 -0
- package/dist/components.js +4 -3
- package/dist/creditsScreen.d.ts +19 -0
- package/dist/creditsScreen.js +31 -0
- package/dist/cutscene.d.ts +66 -0
- package/dist/cutscene.js +132 -0
- package/dist/damageDirection.d.ts +51 -0
- package/dist/damageDirection.js +103 -0
- package/dist/dialogueView.d.ts +69 -0
- package/dist/dialogueView.js +111 -0
- package/dist/entityFrames.d.ts +114 -0
- package/dist/entityFrames.js +55 -0
- package/dist/fastTravel.d.ts +36 -0
- package/dist/fastTravel.js +103 -0
- package/dist/floatingText.d.ts +84 -0
- package/dist/floatingText.js +61 -0
- package/dist/gameViewport.js +8 -12
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.js +85 -0
- package/dist/hudTheme.d.ts +104 -7
- package/dist/hudTheme.js +300 -2
- package/dist/i18n.d.ts +29 -0
- package/dist/i18n.js +44 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/interactionPrompt.d.ts +80 -0
- package/dist/interactionPrompt.js +112 -0
- package/dist/keybindingMenu.d.ts +59 -0
- package/dist/keybindingMenu.js +146 -0
- package/dist/killFeed.d.ts +63 -0
- package/dist/killFeed.js +88 -0
- package/dist/map.d.ts +123 -2
- package/dist/map.js +239 -36
- package/dist/menuRouter.d.ts +51 -0
- package/dist/menuRouter.js +49 -0
- package/dist/modals.d.ts +131 -0
- package/dist/modals.js +168 -0
- package/dist/notifications.d.ts +39 -0
- package/dist/notifications.js +111 -0
- package/dist/objectiveBanner.d.ts +62 -0
- package/dist/objectiveBanner.js +129 -0
- package/dist/photoMode.d.ts +21 -0
- package/dist/photoMode.js +39 -0
- package/dist/previewFixtures.d.ts +30 -0
- package/dist/previewFixtures.js +46 -0
- package/dist/questTracker.d.ts +24 -0
- package/dist/questTracker.js +31 -0
- package/dist/quickMenu.d.ts +39 -0
- package/dist/quickMenu.js +79 -0
- package/dist/radialMenu.d.ts +50 -0
- package/dist/radialMenu.js +69 -0
- package/dist/saveSlots.d.ts +81 -0
- package/dist/saveSlots.js +138 -0
- package/dist/scoreboard.d.ts +67 -0
- package/dist/scoreboard.js +71 -0
- package/dist/settings.d.ts +9 -3
- package/dist/settings.js +10 -4
- package/dist/shopGrid.d.ts +70 -0
- package/dist/shopGrid.js +129 -0
- package/dist/statusEffectBar.d.ts +52 -0
- package/dist/statusEffectBar.js +57 -0
- package/dist/streetGeometryPreview.d.ts +7 -0
- package/dist/streetGeometryPreview.js +119 -0
- package/dist/talentTree.d.ts +60 -0
- package/dist/talentTree.js +217 -0
- package/dist/timerReadout.d.ts +90 -0
- package/dist/timerReadout.js +111 -0
- package/dist/useDebouncedCommit.d.ts +54 -0
- package/dist/useDebouncedCommit.js +81 -0
- package/dist/waveHud.d.ts +49 -0
- package/dist/waveHud.js +53 -0
- package/dist/waypointMarkers.d.ts +80 -0
- package/dist/waypointMarkers.js +116 -0
- package/package.json +3 -3
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +5 -3
- package/skills/jgengine-ui/api.md +708 -19
- package/skills/jgengine-ui/capabilities.md +362 -1
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { PhotoModeState, PhotoModeStore } from "@jgengine/core/ui/photoMode";
|
|
3
|
+
/** Subscribe to a photo-mode store's live state. */
|
|
4
|
+
export declare function usePhotoMode(store: PhotoModeStore): PhotoModeState;
|
|
5
|
+
/** Props for {@link PhotoModeControls}. */
|
|
6
|
+
export interface PhotoModeControlsProps {
|
|
7
|
+
store: PhotoModeStore;
|
|
8
|
+
/** Take the shot — wire to shell `captureCanvas` + `downloadImage`. */
|
|
9
|
+
onCapture?: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Photo-mode toolbar — a hide/show-HUD toggle, a capture button, and an exit
|
|
15
|
+
* button, bound to a `createPhotoModeStore`. Presentation-only: the game wires
|
|
16
|
+
* `onCapture` to shell `captureCanvas`/`downloadImage` and reads `hideHud` to
|
|
17
|
+
* drop its gameplay HUD.
|
|
18
|
+
*
|
|
19
|
+
* @capability photo-mode-controls photo-mode toolbar (hide-HUD toggle, capture, exit) bound to a photo-mode store
|
|
20
|
+
*/
|
|
21
|
+
export declare function PhotoModeControls({ store, onCapture, className, style }: PhotoModeControlsProps): ReactNode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
/** Subscribe to a photo-mode store's live state. */
|
|
4
|
+
export function usePhotoMode(store) {
|
|
5
|
+
return useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
6
|
+
}
|
|
7
|
+
const BTN = {
|
|
8
|
+
borderRadius: 8,
|
|
9
|
+
border: "1px solid rgba(148,163,184,0.4)",
|
|
10
|
+
background: "rgba(17,22,30,0.85)",
|
|
11
|
+
color: "#e2e8f0",
|
|
12
|
+
fontSize: 12,
|
|
13
|
+
fontWeight: 600,
|
|
14
|
+
padding: "6px 12px",
|
|
15
|
+
cursor: "pointer",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Photo-mode toolbar — a hide/show-HUD toggle, a capture button, and an exit
|
|
19
|
+
* button, bound to a `createPhotoModeStore`. Presentation-only: the game wires
|
|
20
|
+
* `onCapture` to shell `captureCanvas`/`downloadImage` and reads `hideHud` to
|
|
21
|
+
* drop its gameplay HUD.
|
|
22
|
+
*
|
|
23
|
+
* @capability photo-mode-controls photo-mode toolbar (hide-HUD toggle, capture, exit) bound to a photo-mode store
|
|
24
|
+
*/
|
|
25
|
+
export function PhotoModeControls({ store, onCapture, className, style }) {
|
|
26
|
+
const state = usePhotoMode(store);
|
|
27
|
+
return (_jsxs("div", { className: className, "data-photo-mode-controls": true, style: {
|
|
28
|
+
display: "inline-flex",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
gap: 8,
|
|
31
|
+
padding: 8,
|
|
32
|
+
borderRadius: 12,
|
|
33
|
+
background: "rgba(6,9,14,0.86)",
|
|
34
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.3))",
|
|
35
|
+
color: "#e2e8f0",
|
|
36
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
37
|
+
...style,
|
|
38
|
+
}, children: [_jsx("span", { style: { fontSize: 11, letterSpacing: 1.4, textTransform: "uppercase", color: "rgba(203,213,225,0.75)", padding: "0 4px" }, children: "\uD83D\uDCF7 Photo" }), _jsx("button", { type: "button", "data-photo-hide-hud": true, "aria-pressed": state.hideHud, onClick: () => store.setHideHud(!state.hideHud), style: { ...BTN, ...(state.hideHud ? { borderColor: "rgba(56,189,248,0.6)", color: "#bae6fd" } : {}) }, children: state.hideHud ? "HUD hidden" : "HUD shown" }), onCapture !== undefined ? (_jsx("button", { type: "button", "data-photo-capture": true, onClick: onCapture, style: { ...BTN, borderColor: "rgba(74,222,128,0.6)", color: "#bbf7d0" }, children: "Capture" })) : null, _jsx("button", { type: "button", "data-photo-exit": true, onClick: () => store.exit(), style: BTN, children: "Exit" })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GamePreviewComponent } from "./preview.js";
|
|
2
|
+
/**
|
|
3
|
+
* One entry in the engine preview-fixture registry: a deterministic, engine-level
|
|
4
|
+
* preview component keyed by a stable id.
|
|
5
|
+
*/
|
|
6
|
+
export interface PreviewFixture {
|
|
7
|
+
/** Stable id used on the `?fixture=` capture route and `bun run shoot --fixture <name>`. */
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** One-line description of what the deterministic fixture renders. */
|
|
10
|
+
readonly description: string;
|
|
11
|
+
/** The real exported preview component, rendered from static values (screenshots identically). */
|
|
12
|
+
readonly component: GamePreviewComponent;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Registry of deterministic, engine-level preview fixtures — the *real* exported
|
|
16
|
+
* `@jgengine/react` components (`HudThemePreview`, `BarsPreview`, `IconsPreview`, …) that render
|
|
17
|
+
* identically every time from static values, so they can be screenshotted as regression evidence
|
|
18
|
+
* without booting a game.
|
|
19
|
+
*
|
|
20
|
+
* The dev runner's fixtures route (`?fixture=<name>`) and `bun run shoot --fixture <name>` mount
|
|
21
|
+
* these by name; `bun run shoot --list` (or `--fixture` with no name) prints the registered set.
|
|
22
|
+
* Register any additional deterministic preview component here — the route and `shoot` pick it up
|
|
23
|
+
* with no further wiring. Genre-agnostic: there is no hardcoded fixture set beyond what this record
|
|
24
|
+
* declares.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PREVIEW_FIXTURES: Record<string, PreviewFixture>;
|
|
27
|
+
/** Sorted list of registered fixture names, for discovery/listing. */
|
|
28
|
+
export declare function previewFixtureNames(): string[];
|
|
29
|
+
/** Resolve a fixture by name, or `undefined` when it is not registered. */
|
|
30
|
+
export declare function resolvePreviewFixture(name: string): PreviewFixture | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BarsPreview } from "./barsPreview.js";
|
|
2
|
+
import { HudThemePreview } from "./hudThemePreview.js";
|
|
3
|
+
import { IconsPreview } from "./iconsPreview.js";
|
|
4
|
+
import { StreetGeometryPreview } from "./streetGeometryPreview.js";
|
|
5
|
+
/**
|
|
6
|
+
* Registry of deterministic, engine-level preview fixtures — the *real* exported
|
|
7
|
+
* `@jgengine/react` components (`HudThemePreview`, `BarsPreview`, `IconsPreview`, …) that render
|
|
8
|
+
* identically every time from static values, so they can be screenshotted as regression evidence
|
|
9
|
+
* without booting a game.
|
|
10
|
+
*
|
|
11
|
+
* The dev runner's fixtures route (`?fixture=<name>`) and `bun run shoot --fixture <name>` mount
|
|
12
|
+
* these by name; `bun run shoot --list` (or `--fixture` with no name) prints the registered set.
|
|
13
|
+
* Register any additional deterministic preview component here — the route and `shoot` pick it up
|
|
14
|
+
* with no further wiring. Genre-agnostic: there is no hardcoded fixture set beyond what this record
|
|
15
|
+
* declares.
|
|
16
|
+
*/
|
|
17
|
+
export const PREVIEW_FIXTURES = {
|
|
18
|
+
HudThemePreview: {
|
|
19
|
+
name: "HudThemePreview",
|
|
20
|
+
description: "Every HudTheme genre preset × atomic bars + a themed frame + a slot row.",
|
|
21
|
+
component: HudThemePreview,
|
|
22
|
+
},
|
|
23
|
+
BarsPreview: {
|
|
24
|
+
name: "BarsPreview",
|
|
25
|
+
description: "Every atomic vitals bar rendered twice under different BarTokens blocks.",
|
|
26
|
+
component: BarsPreview,
|
|
27
|
+
},
|
|
28
|
+
IconsPreview: {
|
|
29
|
+
name: "IconsPreview",
|
|
30
|
+
description: "IconTreatment school-gradient row plus a treated-icon hotbar with badges.",
|
|
31
|
+
component: IconsPreview,
|
|
32
|
+
},
|
|
33
|
+
StreetGeometryPreview: {
|
|
34
|
+
name: "StreetGeometryPreview",
|
|
35
|
+
description: "Close-up welded road meshes for turns, unequal crossings, and five-way junctions.",
|
|
36
|
+
component: StreetGeometryPreview,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
/** Sorted list of registered fixture names, for discovery/listing. */
|
|
40
|
+
export function previewFixtureNames() {
|
|
41
|
+
return Object.keys(PREVIEW_FIXTURES).sort();
|
|
42
|
+
}
|
|
43
|
+
/** Resolve a fixture by name, or `undefined` when it is not registered. */
|
|
44
|
+
export function resolvePreviewFixture(name) {
|
|
45
|
+
return PREVIEW_FIXTURES[name];
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { TrackedQuestView } from "@jgengine/core/game/quest";
|
|
3
|
+
/** Props for {@link QuestTracker}. */
|
|
4
|
+
export interface QuestTrackerProps {
|
|
5
|
+
/** Quests to show, e.g. `journal.list(userId).map((q) => describeTrackedQuest(defs[q.questId], q))`. */
|
|
6
|
+
quests: readonly TrackedQuestView[];
|
|
7
|
+
title?: string;
|
|
8
|
+
emptyLabel?: string;
|
|
9
|
+
/** Max objectives shown per quest before a "+N more" line. Default: all. */
|
|
10
|
+
maxObjectives?: number;
|
|
11
|
+
/** Accent color for the completion tick / progress fill. Reads a HudTheme token by default. */
|
|
12
|
+
accent?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Quest / objective tracker HUD — a compact panel of active quests with a title
|
|
18
|
+
* and labelled, progress-barred objectives (checked + struck when complete).
|
|
19
|
+
* Presentation-only over `describeTrackedQuest(def, instance)` views, so it stays
|
|
20
|
+
* decoupled from the `QuestJournal` runtime.
|
|
21
|
+
*
|
|
22
|
+
* @capability quest-tracker compact quest/objective HUD tracker — titles + labelled objective progress from describeTrackedQuest views
|
|
23
|
+
*/
|
|
24
|
+
export declare function QuestTracker({ quests, title, emptyLabel, maxObjectives, accent, className, style, }: QuestTrackerProps): ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function ObjectiveRow({ label, progress, count, complete, accent }) {
|
|
3
|
+
const fraction = count > 0 ? Math.min(1, progress / count) : complete ? 1 : 0;
|
|
4
|
+
return (_jsxs("li", { "data-quest-objective": true, "data-complete": complete, style: { display: "flex", flexDirection: "column", gap: 3, listStyle: "none" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8 }, children: [_jsxs("span", { style: { display: "flex", alignItems: "baseline", gap: 6, opacity: complete ? 0.65 : 1 }, children: [_jsx("span", { "aria-hidden": true, style: { color: complete ? accent : "rgba(148,163,184,0.7)" }, children: complete ? "✓" : "○" }), _jsx("span", { style: { textDecoration: complete ? "line-through" : "none" }, children: label })] }), count > 1 ? (_jsxs("span", { style: { fontVariantNumeric: "tabular-nums", color: "rgba(203,213,225,0.7)", fontSize: "0.9em" }, children: [Math.min(progress, count), "/", count] })) : null] }), count > 1 && !complete ? (_jsx("div", { style: { height: 3, borderRadius: 9999, background: "rgba(148,163,184,0.2)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${Math.round(fraction * 100)}%`, height: "100%", background: accent } }) })) : null] }));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Quest / objective tracker HUD — a compact panel of active quests with a title
|
|
8
|
+
* and labelled, progress-barred objectives (checked + struck when complete).
|
|
9
|
+
* Presentation-only over `describeTrackedQuest(def, instance)` views, so it stays
|
|
10
|
+
* decoupled from the `QuestJournal` runtime.
|
|
11
|
+
*
|
|
12
|
+
* @capability quest-tracker compact quest/objective HUD tracker — titles + labelled objective progress from describeTrackedQuest views
|
|
13
|
+
*/
|
|
14
|
+
export function QuestTracker({ quests, title = "Quests", emptyLabel = "No active quests.", maxObjectives, accent = "var(--jg-accent, #facc15)", className, style, }) {
|
|
15
|
+
return (_jsxs("div", { className: className, "data-quest-tracker": true, style: {
|
|
16
|
+
width: 260,
|
|
17
|
+
borderRadius: 12,
|
|
18
|
+
padding: 12,
|
|
19
|
+
background: "linear-gradient(160deg, rgba(20,24,32,0.92), rgba(11,14,19,0.92))",
|
|
20
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.28))",
|
|
21
|
+
color: "#e2e8f0",
|
|
22
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
23
|
+
fontSize: 12,
|
|
24
|
+
...style,
|
|
25
|
+
}, children: [title !== "" ? (_jsx("div", { style: { fontSize: 10, letterSpacing: 1.4, textTransform: "uppercase", color: "rgba(203,213,225,0.7)", marginBottom: 8 }, children: title })) : null, quests.length === 0 ? (_jsx("p", { style: { margin: 0, color: "rgba(148,163,184,0.8)" }, children: emptyLabel })) : (_jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 10 }, children: quests.map((quest) => {
|
|
26
|
+
const shown = maxObjectives === undefined ? quest.objectives : quest.objectives.slice(0, maxObjectives);
|
|
27
|
+
const hidden = quest.objectives.length - shown.length;
|
|
28
|
+
const done = quest.status === "completed";
|
|
29
|
+
return (_jsxs("li", { "data-quest": quest.id, "data-status": quest.status, style: { listStyle: "none" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6, marginBottom: 4 }, children: [_jsx("span", { style: { fontWeight: 700, color: done ? accent : "#f8fafc" }, children: quest.title }), done ? _jsx("span", { style: { fontSize: 10, color: accent }, children: "DONE" }) : null] }), _jsx("ul", { style: { margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 5 }, children: shown.map((objective) => (_jsx(ObjectiveRow, { label: objective.label, progress: objective.progress, count: objective.count, complete: objective.complete, accent: accent }, objective.id))) }), hidden > 0 ? (_jsxs("div", { style: { marginTop: 4, fontSize: 11, color: "rgba(148,163,184,0.75)" }, children: ["+", hidden, " more"] })) : null] }, quest.id));
|
|
30
|
+
}) }))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { RadialArc } from "@jgengine/core/ui/radialMenu";
|
|
3
|
+
import { type RadialMenuOption } from "./radialMenu.js";
|
|
4
|
+
/** An entry on a {@link QuickMenu}: a leaf action, or (with `children`) a submenu. */
|
|
5
|
+
export interface QuickMenuItem extends RadialMenuOption {
|
|
6
|
+
/** Nested items; selecting this entry drills into them instead of firing `onSelect`. */
|
|
7
|
+
children?: readonly QuickMenuItem[];
|
|
8
|
+
/** Optional group heading (list/grid layouts). */
|
|
9
|
+
section?: string;
|
|
10
|
+
}
|
|
11
|
+
/** The forms a {@link QuickMenu} can take. */
|
|
12
|
+
export type QuickMenuLayout = "radial" | "arc" | "list" | "grid";
|
|
13
|
+
/** Props for {@link QuickMenu}. */
|
|
14
|
+
export interface QuickMenuProps {
|
|
15
|
+
items: readonly QuickMenuItem[];
|
|
16
|
+
layout?: QuickMenuLayout;
|
|
17
|
+
onSelect: (id: string) => void;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Grid columns. Default 3. */
|
|
21
|
+
columns?: number;
|
|
22
|
+
/** Arc span for `layout="arc"`. Default a bottom half. */
|
|
23
|
+
arc?: RadialArc;
|
|
24
|
+
/** Diameter for radial/arc forms. Default 300. */
|
|
25
|
+
radialSize?: number;
|
|
26
|
+
accent?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
style?: CSSProperties;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Versatile quick menu that takes many forms — a `radial` wheel, a partial
|
|
32
|
+
* `arc`, a vertical `list`, or a `grid` — over one item model with icons,
|
|
33
|
+
* labels, hotkeys, count badges, cooldowns, section headers, and nested
|
|
34
|
+
* submenus (drill in / back). Radial and arc forms reuse {@link RadialMenu};
|
|
35
|
+
* list and grid render sectioned rows/tiles. The game supplies content and skin.
|
|
36
|
+
*
|
|
37
|
+
* @capability quick-menu multi-form quick menu (radial / arc / list / grid) over one item model with hotkeys, badges, cooldowns, sections, and nested submenus
|
|
38
|
+
*/
|
|
39
|
+
export declare function QuickMenu({ items, layout, onSelect, onClose, title, columns, arc, radialSize, accent, className, style, }: QuickMenuProps): ReactNode;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { RadialMenu } from "./radialMenu.js";
|
|
4
|
+
const PANEL = {
|
|
5
|
+
borderRadius: 14,
|
|
6
|
+
padding: 12,
|
|
7
|
+
background: "linear-gradient(160deg, rgba(20,24,32,0.94), rgba(11,14,19,0.94))",
|
|
8
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.3))",
|
|
9
|
+
color: "#e2e8f0",
|
|
10
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
11
|
+
};
|
|
12
|
+
function groupBySection(items) {
|
|
13
|
+
const groups = [];
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const last = groups[groups.length - 1];
|
|
16
|
+
if (last !== undefined && last.section === item.section)
|
|
17
|
+
last.items.push(item);
|
|
18
|
+
else
|
|
19
|
+
groups.push({ section: item.section, items: [item] });
|
|
20
|
+
}
|
|
21
|
+
return groups;
|
|
22
|
+
}
|
|
23
|
+
function Badges({ item, accent }) {
|
|
24
|
+
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [item.cooldown !== undefined && item.cooldown > 0 ? (_jsxs("span", { style: { fontSize: 10, color: "rgba(148,163,184,0.85)" }, children: [Math.ceil(item.cooldown * 100), "%"] })) : null, item.badge !== undefined ? (_jsx("span", { style: { fontSize: 11, fontWeight: 700, color: accent, fontVariantNumeric: "tabular-nums" }, children: item.badge })) : null, item.hotkey !== undefined ? (_jsx("kbd", { style: { fontSize: 10, padding: "1px 5px", borderRadius: 4, border: "1px solid rgba(148,163,184,0.4)", color: "rgba(226,232,240,0.85)" }, children: item.hotkey })) : null, item.children !== undefined ? _jsx("span", { style: { color: "rgba(226,232,240,0.7)" }, children: "\u25B8" }) : null] }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Versatile quick menu that takes many forms — a `radial` wheel, a partial
|
|
28
|
+
* `arc`, a vertical `list`, or a `grid` — over one item model with icons,
|
|
29
|
+
* labels, hotkeys, count badges, cooldowns, section headers, and nested
|
|
30
|
+
* submenus (drill in / back). Radial and arc forms reuse {@link RadialMenu};
|
|
31
|
+
* list and grid render sectioned rows/tiles. The game supplies content and skin.
|
|
32
|
+
*
|
|
33
|
+
* @capability quick-menu multi-form quick menu (radial / arc / list / grid) over one item model with hotkeys, badges, cooldowns, sections, and nested submenus
|
|
34
|
+
*/
|
|
35
|
+
export function QuickMenu({ items, layout = "list", onSelect, onClose, title, columns = 3, arc = { startAngle: Math.PI / 2, sweep: Math.PI }, radialSize = 300, accent = "var(--jg-accent, #38bdf8)", className, style, }) {
|
|
36
|
+
const [path, setPath] = useState([]);
|
|
37
|
+
const current = useMemo(() => {
|
|
38
|
+
let level = items;
|
|
39
|
+
for (const id of path) {
|
|
40
|
+
const next = level.find((item) => item.id === id)?.children;
|
|
41
|
+
if (next === undefined)
|
|
42
|
+
return level;
|
|
43
|
+
level = next;
|
|
44
|
+
}
|
|
45
|
+
return level;
|
|
46
|
+
}, [items, path]);
|
|
47
|
+
const activate = (item) => {
|
|
48
|
+
if (item.disabled === true)
|
|
49
|
+
return;
|
|
50
|
+
if (item.children !== undefined && item.children.length > 0)
|
|
51
|
+
setPath((p) => [...p, item.id]);
|
|
52
|
+
else
|
|
53
|
+
onSelect(item.id);
|
|
54
|
+
};
|
|
55
|
+
const back = () => setPath((p) => p.slice(0, -1));
|
|
56
|
+
const header = title !== undefined || path.length > 0 ? (_jsxs("div", { "data-quick-header": true, style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }, children: [path.length > 0 ? (_jsx("button", { type: "button", "data-quick-back": true, onClick: back, style: { border: "1px solid rgba(148,163,184,0.4)", borderRadius: 6, background: "transparent", color: "#e2e8f0", fontSize: 11, padding: "2px 8px", cursor: "pointer" }, children: "\u2039 Back" })) : null, title !== undefined ? (_jsx("span", { style: { fontSize: 11, letterSpacing: 1.2, textTransform: "uppercase", color: "rgba(203,213,225,0.75)" }, children: title })) : null] })) : null;
|
|
57
|
+
if (layout === "radial" || layout === "arc") {
|
|
58
|
+
const options = current.map((item) => ({ ...item, hasSubmenu: item.children !== undefined && item.children.length > 0 }));
|
|
59
|
+
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": layout, style: { position: "relative", ...style }, children: [header, _jsx(RadialMenu, { options: options, size: radialSize, accent: accent, ...(layout === "arc" ? { arc } : {}), onSelect: (id) => {
|
|
60
|
+
const item = current.find((entry) => entry.id === id);
|
|
61
|
+
if (item !== undefined)
|
|
62
|
+
activate(item);
|
|
63
|
+
}, onClose: onClose })] }));
|
|
64
|
+
}
|
|
65
|
+
const groups = groupBySection(current);
|
|
66
|
+
if (layout === "grid") {
|
|
67
|
+
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "grid", style: { ...PANEL, width: columns * 92 + 24, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize: 10, textTransform: "uppercase", letterSpacing: 1.2, color: "rgba(148,163,184,0.75)", margin: "6px 0 4px" }, children: group.section })) : null, _jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: 6 }, children: group.items.map((item) => (_jsxs("button", { type: "button", "data-quick-item": item.id, "data-disabled": item.disabled === true, onClick: () => activate(item), disabled: item.disabled === true, style: {
|
|
68
|
+
position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, padding: "10px 4px",
|
|
69
|
+
borderRadius: 10, border: "1px solid rgba(148,163,184,0.25)", background: "rgba(17,22,30,0.7)", color: "#e2e8f0",
|
|
70
|
+
cursor: item.disabled === true ? "default" : "pointer", opacity: item.disabled === true ? 0.4 : 1,
|
|
71
|
+
}, children: [_jsx("span", { style: { fontSize: 22 }, children: item.icon }), item.label !== undefined ? _jsx("span", { style: { fontSize: 10 }, children: item.label }) : null, item.badge !== undefined ? (_jsx("span", { style: { position: "absolute", top: 3, right: 5, fontSize: 10, fontWeight: 700, color: accent }, children: item.badge })) : null, item.children !== undefined ? _jsx("span", { style: { position: "absolute", bottom: 3, right: 5, fontSize: 10 }, children: "\u25B8" }) : null] }, item.id))) })] }, group.section ?? gi)))] }));
|
|
72
|
+
}
|
|
73
|
+
// list
|
|
74
|
+
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "list", style: { ...PANEL, width: 240, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize: 10, textTransform: "uppercase", letterSpacing: 1.2, color: "rgba(148,163,184,0.75)", margin: "6px 0 4px" }, children: group.section })) : null, _jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 2 }, children: group.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-quick-item": item.id, "data-disabled": item.disabled === true, onClick: () => activate(item), disabled: item.disabled === true, style: {
|
|
75
|
+
width: "100%", display: "flex", alignItems: "center", gap: 8, padding: "7px 9px", borderRadius: 8,
|
|
76
|
+
border: "1px solid transparent", background: "transparent", color: "#e2e8f0", fontSize: 13, textAlign: "left",
|
|
77
|
+
cursor: item.disabled === true ? "default" : "pointer", opacity: item.disabled === true ? 0.45 : 1,
|
|
78
|
+
}, children: [item.icon !== undefined ? _jsx("span", { style: { width: 22, textAlign: "center", fontSize: 17 }, children: item.icon }) : null, _jsx("span", { style: { flex: 1 }, children: item.label ?? item.id }), _jsx(Badges, { item: item, accent: accent })] }) }, item.id))) })] }, group.section ?? gi)))] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type RadialArc } from "@jgengine/core/ui/radialMenu";
|
|
3
|
+
/** One selectable entry on a radial/quick menu (weapon or emote wheel). */
|
|
4
|
+
export interface RadialMenuOption {
|
|
5
|
+
id: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Glyph/icon node the game supplies. */
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Per-slice accent; falls back to the menu `accent`. */
|
|
11
|
+
color?: string;
|
|
12
|
+
/** Keybind hint drawn on the slice (e.g. "1", "Q"). */
|
|
13
|
+
hotkey?: string;
|
|
14
|
+
/** Count/quantity badge (ammo, stack size). */
|
|
15
|
+
badge?: string | number;
|
|
16
|
+
/** Cooldown remaining as `0..1` — dims the slice and shows the fraction. */
|
|
17
|
+
cooldown?: number;
|
|
18
|
+
/** Draw a ► marker to signal the slice opens a submenu. */
|
|
19
|
+
hasSubmenu?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Props for {@link RadialMenu}. */
|
|
22
|
+
export interface RadialMenuProps {
|
|
23
|
+
options: readonly RadialMenuOption[];
|
|
24
|
+
/** Render nothing when false. Default true. */
|
|
25
|
+
open?: boolean;
|
|
26
|
+
onSelect: (id: string) => void;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
/** Diameter in px. Default 300. */
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Neutral inner-hub radius (dead zone) in px. Default 52. */
|
|
31
|
+
innerRadius?: number;
|
|
32
|
+
/** Partial arc instead of a full wheel (e.g. a bottom half or quarter). */
|
|
33
|
+
arc?: RadialArc;
|
|
34
|
+
/** Force a highlighted slice (also the initial hover) — for controllers/tests. */
|
|
35
|
+
highlightIndex?: number | null;
|
|
36
|
+
accent?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
style?: CSSProperties;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Radial / quick menu (weapon or emote wheel) — a ring or partial `arc` of
|
|
42
|
+
* selectable wedges the player points at with the mouse (or a stick). Pointer
|
|
43
|
+
* angle drives the highlight via core `radialIndexFromVector`; click/confirm
|
|
44
|
+
* fires `onSelect`, the neutral hub closes. Options carry optional hotkeys,
|
|
45
|
+
* count badges, cooldowns, and submenu markers. The game supplies icons/labels
|
|
46
|
+
* and skins it.
|
|
47
|
+
*
|
|
48
|
+
* @capability radial-menu pointer/stick-driven radial or arc quick menu (weapon/emote wheel) — angular slice selection with hotkeys, badges, cooldowns, and a neutral hub
|
|
49
|
+
*/
|
|
50
|
+
export declare function RadialMenu({ options, open, onSelect, onClose, size, innerRadius, arc, highlightIndex, accent, className, style, }: RadialMenuProps): ReactNode;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { radialIndexFromVector, radialSlicePosition, radialSlices, } from "@jgengine/core/ui/radialMenu";
|
|
4
|
+
const TWO_PI = Math.PI * 2;
|
|
5
|
+
function wedgePath(cx, cy, innerR, outerR, center, half) {
|
|
6
|
+
const point = (r, a) => [cx + Math.sin(a) * r, cy - Math.cos(a) * r];
|
|
7
|
+
const start = center - half;
|
|
8
|
+
const end = center + half;
|
|
9
|
+
const [x0, y0] = point(outerR, start);
|
|
10
|
+
const [x1, y1] = point(outerR, end);
|
|
11
|
+
const [x2, y2] = point(innerR, end);
|
|
12
|
+
const [x3, y3] = point(innerR, start);
|
|
13
|
+
const large = half * 2 > Math.PI ? 1 : 0;
|
|
14
|
+
return `M${x0},${y0} A${outerR},${outerR} 0 ${large} 1 ${x1},${y1} L${x2},${y2} A${innerR},${innerR} 0 ${large} 0 ${x3},${y3} Z`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Radial / quick menu (weapon or emote wheel) — a ring or partial `arc` of
|
|
18
|
+
* selectable wedges the player points at with the mouse (or a stick). Pointer
|
|
19
|
+
* angle drives the highlight via core `radialIndexFromVector`; click/confirm
|
|
20
|
+
* fires `onSelect`, the neutral hub closes. Options carry optional hotkeys,
|
|
21
|
+
* count badges, cooldowns, and submenu markers. The game supplies icons/labels
|
|
22
|
+
* and skins it.
|
|
23
|
+
*
|
|
24
|
+
* @capability radial-menu pointer/stick-driven radial or arc quick menu (weapon/emote wheel) — angular slice selection with hotkeys, badges, cooldowns, and a neutral hub
|
|
25
|
+
*/
|
|
26
|
+
export function RadialMenu({ options, open = true, onSelect, onClose, size = 300, innerRadius = 52, arc, highlightIndex, accent = "var(--jg-accent, #38bdf8)", className, style, }) {
|
|
27
|
+
const count = options.length;
|
|
28
|
+
const [hovered, setHovered] = useState(highlightIndex ?? null);
|
|
29
|
+
const active = highlightIndex !== undefined ? highlightIndex : hovered;
|
|
30
|
+
const cx = size / 2;
|
|
31
|
+
const cy = size / 2;
|
|
32
|
+
const outerR = size / 2 - 3;
|
|
33
|
+
const labelRadius = (innerRadius + outerR) / 2;
|
|
34
|
+
const sweep = arc?.sweep ?? TWO_PI;
|
|
35
|
+
const half = count > 0 ? (sweep >= TWO_PI - 1e-6 ? Math.PI / count : sweep / count / 2) : 0;
|
|
36
|
+
const centers = useMemo(() => radialSlices(count, arc).map((slice) => slice.centerAngle), [count, arc]);
|
|
37
|
+
if (!open || count === 0)
|
|
38
|
+
return null;
|
|
39
|
+
const commit = () => {
|
|
40
|
+
if (active === null) {
|
|
41
|
+
onClose?.();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const option = options[active];
|
|
45
|
+
if (option !== undefined && option.disabled !== true)
|
|
46
|
+
onSelect(option.id);
|
|
47
|
+
};
|
|
48
|
+
return (_jsx("div", { className: className, "data-radial-menu": true, style: { position: "relative", width: size, height: size, userSelect: "none", ...style }, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, style: { display: "block", cursor: "pointer" }, onPointerMove: (event) => {
|
|
49
|
+
if (highlightIndex !== undefined)
|
|
50
|
+
return;
|
|
51
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
52
|
+
const px = ((event.clientX - rect.left) / rect.width) * size;
|
|
53
|
+
const py = ((event.clientY - rect.top) / rect.height) * size;
|
|
54
|
+
setHovered(radialIndexFromVector(px - cx, py - cy, count, { deadZone: innerRadius, ...arc }));
|
|
55
|
+
}, onClick: commit, children: [centers.map((center, index) => {
|
|
56
|
+
const option = options[index];
|
|
57
|
+
const isActive = index === active;
|
|
58
|
+
const color = option.color ?? accent;
|
|
59
|
+
const onCooldown = option.cooldown !== undefined && option.cooldown > 0;
|
|
60
|
+
return (_jsx("path", { "data-radial-option": option.id, "data-highlighted": isActive, "data-disabled": option.disabled === true, d: wedgePath(cx, cy, innerRadius, outerR, center, half * 0.92), fill: isActive ? color : "rgba(17,22,30,0.82)", fillOpacity: option.disabled === true ? 0.3 : onCooldown ? 0.5 : isActive ? 0.9 : 0.82, stroke: "rgba(148,163,184,0.35)", strokeWidth: 1 }, option.id));
|
|
61
|
+
}), _jsx("circle", { cx: cx, cy: cy, r: innerRadius - 6, fill: "rgba(8,11,16,0.9)", stroke: "rgba(148,163,184,0.3)" }), centers.map((_, index) => {
|
|
62
|
+
const option = options[index];
|
|
63
|
+
const at = radialSlicePosition(index, count, labelRadius, arc);
|
|
64
|
+
const isActive = index === active;
|
|
65
|
+
const x = cx + at.x;
|
|
66
|
+
const y = cy + at.y;
|
|
67
|
+
return (_jsxs("g", { transform: `translate(${x} ${y})`, style: { pointerEvents: "none" }, children: [option.icon !== undefined ? (_jsx("foreignObject", { x: -18, y: -26, width: 36, height: 30, style: { overflow: "visible" }, children: _jsx("div", { style: { display: "flex", justifyContent: "center", fontSize: 20 }, children: option.icon }) })) : null, option.label !== undefined ? (_jsx("text", { y: option.icon !== undefined ? 14 : 4, textAnchor: "middle", fontSize: 11, fontWeight: 700, fill: isActive ? "#f8fafc" : "rgba(226,232,240,0.8)", fontFamily: "ui-sans-serif, system-ui, sans-serif", children: option.label })) : null, option.hotkey !== undefined ? (_jsxs("text", { y: -20, textAnchor: "middle", fontSize: 9, fontWeight: 700, fill: "rgba(148,163,184,0.9)", children: ["[", option.hotkey, "]"] })) : null, option.badge !== undefined ? (_jsx("text", { x: 16, y: -8, textAnchor: "middle", fontSize: 10, fontWeight: 700, fill: accent, children: option.badge })) : null, option.cooldown !== undefined && option.cooldown > 0 ? (_jsxs("text", { y: 28, textAnchor: "middle", fontSize: 9, fill: "rgba(148,163,184,0.85)", children: [Math.ceil(option.cooldown * 100), "%"] })) : null, option.hasSubmenu === true ? (_jsx("text", { x: 22, y: 4, textAnchor: "middle", fontSize: 11, fill: "rgba(226,232,240,0.75)", children: "\u25B8" })) : null] }, option.id));
|
|
68
|
+
}), _jsx("text", { x: cx, y: cy + 4, textAnchor: "middle", fontSize: 12, fontWeight: 700, fill: "rgba(226,232,240,0.9)", fontFamily: "ui-sans-serif, system-ui, sans-serif", style: { pointerEvents: "none" }, children: active !== null && options[active] !== undefined ? options[active].label ?? "" : "" })] }) }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { SaveSlotMeta, SaveSlots } from "@jgengine/core/game/saveSlots";
|
|
3
|
+
import { type GameIconName } from "./gameIcons.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe to a {@link SaveSlots} index and re-render on any change (write,
|
|
6
|
+
* clear, rename, restore). Returns the live list of slots in menu order. Wire it
|
|
7
|
+
* into a save-select screen and mutate the model in event handlers — the menu
|
|
8
|
+
* re-renders itself.
|
|
9
|
+
*
|
|
10
|
+
* @capability use-save-slots React hook binding a save-slots metadata index — re-renders on change and returns the ordered slot list
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSaveSlots(model: SaveSlots): SaveSlotMeta[];
|
|
13
|
+
/** Reskin tokens for {@link SaveSlotMenu}. All optional — each falls back to a HudTheme var then a neutral default. */
|
|
14
|
+
export interface SaveSlotMenuTheme {
|
|
15
|
+
/** Accent for the Continue action, savedAt time, and focus glow. Default reads `--jg-accent`. */
|
|
16
|
+
accent?: string;
|
|
17
|
+
/** Card background. */
|
|
18
|
+
cardBg?: string;
|
|
19
|
+
/** Card border. */
|
|
20
|
+
cardBorder?: string;
|
|
21
|
+
/** Primary text color (slot name). */
|
|
22
|
+
text?: string;
|
|
23
|
+
/** Muted text color (meta chips, empty label). */
|
|
24
|
+
muted?: string;
|
|
25
|
+
/** Meta-chip background. */
|
|
26
|
+
chipBg?: string;
|
|
27
|
+
/** Font family. */
|
|
28
|
+
fontFamily?: string;
|
|
29
|
+
/** Card corner radius (CSS length). */
|
|
30
|
+
radius?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Turn a `savedAt` timestamp into a compact relative label ("just now", "4m ago",
|
|
34
|
+
* "2d ago"). Presentation-only helper — the model stores raw ms.
|
|
35
|
+
*/
|
|
36
|
+
export declare function relativeSavedAt(savedAt: number, nowMs?: number): string;
|
|
37
|
+
/** Props for {@link SaveSlotMenu}. */
|
|
38
|
+
export interface SaveSlotMenuProps {
|
|
39
|
+
/** The metadata index to render. */
|
|
40
|
+
model: SaveSlots;
|
|
41
|
+
/**
|
|
42
|
+
* Load an existing save. Given the filled slot the player chose. Wire this to
|
|
43
|
+
* your `createSaveStore.switchSlot(id)` + scene load.
|
|
44
|
+
*/
|
|
45
|
+
onLoad?: (slot: SaveSlotMeta) => void;
|
|
46
|
+
/** Start a new game in an empty slot. Given the empty slot chosen. */
|
|
47
|
+
onNew?: (slot: SaveSlotMeta) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Delete a save. Given the filled slot. A game typically clears both this index
|
|
50
|
+
* (`model.clear(id)`) and the matching `createSaveStore` slot.
|
|
51
|
+
*/
|
|
52
|
+
onDelete?: (slot: SaveSlotMeta) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Continue the most recent save (the Continue button targets `model.mostRecent()`).
|
|
55
|
+
* Omit to hide the Continue button.
|
|
56
|
+
*/
|
|
57
|
+
onContinue?: (slot: SaveSlotMeta) => void;
|
|
58
|
+
/** Menu heading. Default `"Select Save"`. */
|
|
59
|
+
title?: ReactNode;
|
|
60
|
+
/** Reskin tokens. */
|
|
61
|
+
theme?: SaveSlotMenuTheme;
|
|
62
|
+
/** Wall clock (ms) for relative times — inject in tests/screenshots. Default `Date.now`. */
|
|
63
|
+
now?: () => number;
|
|
64
|
+
/** Optional per-`meta`-key icon, so a game can prefix chips (e.g. `{ level: "star", playtime: "hourglass" }`). */
|
|
65
|
+
metaIcons?: Record<string, GameIconName>;
|
|
66
|
+
className?: string;
|
|
67
|
+
style?: CSSProperties;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A drop-in save-select / profile menu that renders a {@link SaveSlots} index as
|
|
71
|
+
* a column of cards. A filled slot shows its name, the free-string `meta` the
|
|
72
|
+
* game supplied as chips, and a relative "saved" time, with Load and Delete
|
|
73
|
+
* actions; an empty slot shows a "New Game" affordance. A top-level Continue
|
|
74
|
+
* button targets the newest save (`model.mostRecent()`). This is a starting-point
|
|
75
|
+
* game-front-end building block — the game owns final art, wording, and layout;
|
|
76
|
+
* reskin it with {@link SaveSlotMenuTheme} or HudTheme CSS vars. Presentation
|
|
77
|
+
* only: all state lives in the core model, and `meta` keys are never interpreted.
|
|
78
|
+
*
|
|
79
|
+
* @capability save-slot-menu drop-in save-select / profile menu that renders a core save-slots index as New / Continue / Load / Delete cards with free-string meta chips and relative save times, theme-skinnable
|
|
80
|
+
*/
|
|
81
|
+
export declare function SaveSlotMenu({ model, onLoad, onNew, onDelete, onContinue, title, theme, now, metaIcons, className, style, }: SaveSlotMenuProps): ReactNode;
|