@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,39 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type AccessibilityState, type AccessibilityStore } from "@jgengine/core/ui/accessibility";
|
|
3
|
+
interface AccessibilityContextValue {
|
|
4
|
+
state: AccessibilityState;
|
|
5
|
+
store: AccessibilityStore;
|
|
6
|
+
}
|
|
7
|
+
/** Read the live accessibility state + its store; throws without an {@link AccessibilityProvider} above. */
|
|
8
|
+
export declare function useAccessibility(): AccessibilityContextValue;
|
|
9
|
+
/**
|
|
10
|
+
* Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that
|
|
11
|
+
* `AccessibilityProvider` references by `filter: url(#jg-cb-<mode>)`. Rendered
|
|
12
|
+
* automatically inside the provider; export standalone for custom roots.
|
|
13
|
+
*
|
|
14
|
+
* @capability colorblind-filters SVG feColorMatrix defs (protanopia/deuteranopia/tritanopia/grayscale) referenced by the accessibility colorblind filter
|
|
15
|
+
*/
|
|
16
|
+
export declare function ColorblindFilters(): ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for
|
|
19
|
+
* text to scale off, sets `data-reduced-motion` / `data-high-contrast` /
|
|
20
|
+
* `data-colorblind` / `data-captions` for CSS to respond to, and wraps the tree
|
|
21
|
+
* in the selected colorblind `feColorMatrix` filter. Reads a
|
|
22
|
+
* `createAccessibilityStore` and re-renders on change; provides the state to
|
|
23
|
+
* `useAccessibility`.
|
|
24
|
+
*
|
|
25
|
+
* @capability accessibility-provider apply accessibility preferences to a subtree — text-scale CSS var, reduced-motion/high-contrast/colorblind/captions data attrs, and the colorblind color-matrix filter
|
|
26
|
+
*/
|
|
27
|
+
export declare function AccessibilityProvider({ store, children, className, style, }: {
|
|
28
|
+
store: AccessibilityStore;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
}): ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* OS "reduce motion" preference (`prefers-reduced-motion: reduce`) as a live
|
|
35
|
+
* boolean — SSR-safe (defaults false). Use it to seed an accessibility store's
|
|
36
|
+
* `reducedMotion` default.
|
|
37
|
+
*/
|
|
38
|
+
export declare function usePrefersReducedMotion(): boolean;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
3
|
+
import { COLORBLIND_MATRICES, } from "@jgengine/core/ui/accessibility";
|
|
4
|
+
const AccessibilityContext = createContext(null);
|
|
5
|
+
const FILTER_MODES = [
|
|
6
|
+
"protanopia",
|
|
7
|
+
"deuteranopia",
|
|
8
|
+
"tritanopia",
|
|
9
|
+
"grayscale",
|
|
10
|
+
];
|
|
11
|
+
/** Read the live accessibility state + its store; throws without an {@link AccessibilityProvider} above. */
|
|
12
|
+
export function useAccessibility() {
|
|
13
|
+
const value = useContext(AccessibilityContext);
|
|
14
|
+
if (value === null)
|
|
15
|
+
throw new Error("useAccessibility must be used within an <AccessibilityProvider>");
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that
|
|
20
|
+
* `AccessibilityProvider` references by `filter: url(#jg-cb-<mode>)`. Rendered
|
|
21
|
+
* automatically inside the provider; export standalone for custom roots.
|
|
22
|
+
*
|
|
23
|
+
* @capability colorblind-filters SVG feColorMatrix defs (protanopia/deuteranopia/tritanopia/grayscale) referenced by the accessibility colorblind filter
|
|
24
|
+
*/
|
|
25
|
+
export function ColorblindFilters() {
|
|
26
|
+
return (_jsx("svg", { "aria-hidden": true, width: 0, height: 0, style: { position: "absolute" }, "data-colorblind-filters": true, children: _jsx("defs", { children: FILTER_MODES.map((mode) => (_jsx("filter", { id: `jg-cb-${mode}`, colorInterpolationFilters: "sRGB", children: _jsx("feColorMatrix", { type: "matrix", values: COLORBLIND_MATRICES[mode] ?? "" }) }, mode))) }) }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for
|
|
30
|
+
* text to scale off, sets `data-reduced-motion` / `data-high-contrast` /
|
|
31
|
+
* `data-colorblind` / `data-captions` for CSS to respond to, and wraps the tree
|
|
32
|
+
* in the selected colorblind `feColorMatrix` filter. Reads a
|
|
33
|
+
* `createAccessibilityStore` and re-renders on change; provides the state to
|
|
34
|
+
* `useAccessibility`.
|
|
35
|
+
*
|
|
36
|
+
* @capability accessibility-provider apply accessibility preferences to a subtree — text-scale CSS var, reduced-motion/high-contrast/colorblind/captions data attrs, and the colorblind color-matrix filter
|
|
37
|
+
*/
|
|
38
|
+
export function AccessibilityProvider({ store, children, className, style, }) {
|
|
39
|
+
const state = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
40
|
+
const rootStyle = { ...style };
|
|
41
|
+
rootStyle["--jg-text-scale"] = String(state.textScale);
|
|
42
|
+
if (state.colorblind !== "none")
|
|
43
|
+
rootStyle.filter = `url(#jg-cb-${state.colorblind})`;
|
|
44
|
+
return (_jsx(AccessibilityContext.Provider, { value: { state, store }, children: _jsxs("div", { className: className, "data-accessibility": true, "data-reduced-motion": state.reducedMotion, "data-high-contrast": state.highContrast, "data-colorblind": state.colorblind, "data-captions": state.captions, style: rootStyle, children: [_jsx(ColorblindFilters, {}), children] }) }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* OS "reduce motion" preference (`prefers-reduced-motion: reduce`) as a live
|
|
48
|
+
* boolean — SSR-safe (defaults false). Use it to seed an accessibility store's
|
|
49
|
+
* `reducedMotion` default.
|
|
50
|
+
*/
|
|
51
|
+
export function usePrefersReducedMotion() {
|
|
52
|
+
return useSyncExternalStore((listener) => {
|
|
53
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
54
|
+
return () => undefined;
|
|
55
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
56
|
+
query.addEventListener("change", listener);
|
|
57
|
+
return () => query.removeEventListener("change", listener);
|
|
58
|
+
}, () => typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
59
|
+
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
60
|
+
: false, () => false);
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { AchievementTracker, AchievementView } from "@jgengine/core/game/achievements";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe a component to an achievement tracker's live view list. The list
|
|
5
|
+
* keeps a stable identity between changes, so this re-renders only on unlock or
|
|
6
|
+
* progress — not every frame.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useAchievements(tracker: AchievementTracker): readonly AchievementView[];
|
|
9
|
+
/** Props for {@link AchievementToast}. */
|
|
10
|
+
export interface AchievementToastProps {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Icon/glyph node the game supplies; the engine never picks art. */
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
points?: number;
|
|
16
|
+
/** Small heading above the name. Default `"Achievement Unlocked"`. */
|
|
17
|
+
heading?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The unlock-moment banner — icon, "Achievement Unlocked" heading, name, and
|
|
23
|
+
* optional points. Purely presentational; pair the game's `onUnlock` seam with
|
|
24
|
+
* a toast queue and render one of these per entry.
|
|
25
|
+
*
|
|
26
|
+
* @capability achievement-toast unlock-moment banner (icon, heading, name, points) for an achievement toast queue
|
|
27
|
+
*/
|
|
28
|
+
export declare function AchievementToast({ name, description, icon, points, heading, className, style, }: AchievementToastProps): ReactNode;
|
|
29
|
+
/** Props for {@link AchievementGallery}. */
|
|
30
|
+
export interface AchievementGalleryProps {
|
|
31
|
+
achievements: readonly AchievementView[];
|
|
32
|
+
title?: string;
|
|
33
|
+
/** Render secret + still-locked achievements as masked "???". Default true. */
|
|
34
|
+
maskSecrets?: boolean;
|
|
35
|
+
/** Optional per-achievement icon; falls back to the def `icon` glyph then a lock/trophy. */
|
|
36
|
+
renderIcon?: (achievement: AchievementView) => ReactNode;
|
|
37
|
+
columns?: number;
|
|
38
|
+
emptyLabel?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
style?: CSSProperties;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Achievement/trophy gallery — a responsive grid of cards showing unlocked vs.
|
|
44
|
+
* locked state, a progress bar for counter achievements, and a header summary
|
|
45
|
+
* of completion and score. Secret+locked entries mask their name/description.
|
|
46
|
+
* Feed it `useAchievements(tracker)`.
|
|
47
|
+
*
|
|
48
|
+
* @capability achievement-gallery responsive achievement/trophy grid with unlocked/locked state, counter progress bars, secret masking, and a completion/score header
|
|
49
|
+
*/
|
|
50
|
+
export declare function AchievementGallery({ achievements, title, maskSecrets, renderIcon, columns, emptyLabel, className, style, }: AchievementGalleryProps): ReactNode;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe a component to an achievement tracker's live view list. The list
|
|
5
|
+
* keeps a stable identity between changes, so this re-renders only on unlock or
|
|
6
|
+
* progress — not every frame.
|
|
7
|
+
*/
|
|
8
|
+
export function useAchievements(tracker) {
|
|
9
|
+
return useSyncExternalStore(tracker.subscribe, tracker.list, tracker.list);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The unlock-moment banner — icon, "Achievement Unlocked" heading, name, and
|
|
13
|
+
* optional points. Purely presentational; pair the game's `onUnlock` seam with
|
|
14
|
+
* a toast queue and render one of these per entry.
|
|
15
|
+
*
|
|
16
|
+
* @capability achievement-toast unlock-moment banner (icon, heading, name, points) for an achievement toast queue
|
|
17
|
+
*/
|
|
18
|
+
export function AchievementToast({ name, description, icon, points, heading = "Achievement Unlocked", className, style, }) {
|
|
19
|
+
return (_jsxs("div", { className: className, "data-achievement-toast": true, style: {
|
|
20
|
+
display: "flex",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
gap: 12,
|
|
23
|
+
minWidth: 240,
|
|
24
|
+
padding: "10px 14px",
|
|
25
|
+
borderRadius: 12,
|
|
26
|
+
background: "linear-gradient(160deg, rgba(30,26,14,0.96), rgba(16,13,8,0.96))",
|
|
27
|
+
border: "1px solid var(--jg-rarity-legendary, rgba(245,158,11,0.55))",
|
|
28
|
+
boxShadow: "0 12px 34px rgba(0,0,0,0.5)",
|
|
29
|
+
color: "#f8fafc",
|
|
30
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
31
|
+
...style,
|
|
32
|
+
}, children: [_jsx("div", { "data-achievement-toast-icon": true, style: {
|
|
33
|
+
display: "flex",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center",
|
|
36
|
+
width: 40,
|
|
37
|
+
height: 40,
|
|
38
|
+
borderRadius: 10,
|
|
39
|
+
background: "rgba(245,158,11,0.16)",
|
|
40
|
+
border: "1px solid rgba(245,158,11,0.4)",
|
|
41
|
+
fontSize: 20,
|
|
42
|
+
flexShrink: 0,
|
|
43
|
+
}, children: icon ?? "🏆" }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 }, children: [_jsx("span", { style: { fontSize: 9, letterSpacing: 1.6, textTransform: "uppercase", color: "rgba(245,158,11,0.85)" }, children: heading }), _jsx("span", { style: { fontSize: 14, fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: name }), description !== undefined ? (_jsx("span", { style: { fontSize: 11, color: "rgba(226,232,240,0.7)" }, children: description })) : null] }), points !== undefined ? (_jsx("span", { style: { marginLeft: "auto", fontSize: 13, fontWeight: 700, color: "var(--jg-rarity-legendary, #f59e0b)", fontVariantNumeric: "tabular-nums" }, children: points })) : null] }));
|
|
44
|
+
}
|
|
45
|
+
const MASK = "???";
|
|
46
|
+
/**
|
|
47
|
+
* Achievement/trophy gallery — a responsive grid of cards showing unlocked vs.
|
|
48
|
+
* locked state, a progress bar for counter achievements, and a header summary
|
|
49
|
+
* of completion and score. Secret+locked entries mask their name/description.
|
|
50
|
+
* Feed it `useAchievements(tracker)`.
|
|
51
|
+
*
|
|
52
|
+
* @capability achievement-gallery responsive achievement/trophy grid with unlocked/locked state, counter progress bars, secret masking, and a completion/score header
|
|
53
|
+
*/
|
|
54
|
+
export function AchievementGallery({ achievements, title = "Achievements", maskSecrets = true, renderIcon, columns = 2, emptyLabel = "No achievements yet.", className, style, }) {
|
|
55
|
+
const unlockedCount = achievements.filter((a) => a.unlocked).length;
|
|
56
|
+
const score = achievements.reduce((sum, a) => sum + (a.unlocked ? a.points ?? 0 : 0), 0);
|
|
57
|
+
const hasPoints = achievements.some((a) => a.points !== undefined);
|
|
58
|
+
return (_jsxs("div", { className: className, "data-achievement-gallery": true, style: {
|
|
59
|
+
borderRadius: 14,
|
|
60
|
+
padding: 14,
|
|
61
|
+
background: "linear-gradient(160deg, rgba(20,24,32,0.97), rgba(11,14,19,0.97))",
|
|
62
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.3))",
|
|
63
|
+
color: "#e2e8f0",
|
|
64
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
65
|
+
...style,
|
|
66
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 10 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 700, letterSpacing: 1.4, textTransform: "uppercase" }, children: title }), _jsxs("span", { style: { fontSize: 11, color: "rgba(203,213,225,0.75)", fontVariantNumeric: "tabular-nums" }, children: [unlockedCount, "/", achievements.length, hasPoints ? ` · ${score} pts` : ""] })] }), achievements.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 12, color: "rgba(148,163,184,0.8)" }, children: emptyLabel })) : (_jsx("ul", { style: {
|
|
67
|
+
listStyle: "none",
|
|
68
|
+
margin: 0,
|
|
69
|
+
padding: 0,
|
|
70
|
+
display: "grid",
|
|
71
|
+
gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`,
|
|
72
|
+
gap: 8,
|
|
73
|
+
}, children: achievements.map((achievement) => {
|
|
74
|
+
const masked = maskSecrets && achievement.secret === true && !achievement.unlocked;
|
|
75
|
+
const name = masked ? MASK : achievement.name;
|
|
76
|
+
const description = masked ? "Hidden achievement" : achievement.description;
|
|
77
|
+
const showProgress = !achievement.unlocked && achievement.target !== undefined && achievement.target > 0;
|
|
78
|
+
const icon = renderIcon?.(achievement) ?? (masked ? "🔒" : achievement.icon ?? (achievement.unlocked ? "🏆" : "◻"));
|
|
79
|
+
return (_jsxs("li", { "data-achievement": achievement.id, "data-unlocked": achievement.unlocked, style: {
|
|
80
|
+
display: "flex",
|
|
81
|
+
gap: 10,
|
|
82
|
+
padding: 10,
|
|
83
|
+
borderRadius: 10,
|
|
84
|
+
background: achievement.unlocked ? "rgba(245,158,11,0.10)" : "rgba(148,163,184,0.06)",
|
|
85
|
+
border: `1px solid ${achievement.unlocked ? "rgba(245,158,11,0.4)" : "rgba(148,163,184,0.18)"}`,
|
|
86
|
+
opacity: achievement.unlocked ? 1 : 0.75,
|
|
87
|
+
}, children: [_jsx("div", { style: { fontSize: 20, width: 26, textAlign: "center", flexShrink: 0 }, children: icon }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 2, minWidth: 0, flex: 1 }, children: [_jsxs("span", { style: { fontSize: 13, fontWeight: 700, color: achievement.unlocked ? "#f8fafc" : "rgba(226,232,240,0.85)" }, children: [name, achievement.points !== undefined && !masked ? (_jsx("span", { style: { marginLeft: 6, fontSize: 10, color: "rgba(245,158,11,0.85)", fontWeight: 600 }, children: achievement.points })) : null] }), description !== undefined ? (_jsx("span", { style: { fontSize: 11, color: "rgba(203,213,225,0.7)" }, children: description })) : null, showProgress ? (_jsx("div", { "data-achievement-progress": true, style: { marginTop: 3, height: 5, borderRadius: 9999, background: "rgba(148,163,184,0.2)", overflow: "hidden" }, children: _jsx("div", { style: {
|
|
88
|
+
width: `${Math.round(achievement.fraction * 100)}%`,
|
|
89
|
+
height: "100%",
|
|
90
|
+
borderRadius: 9999,
|
|
91
|
+
background: "var(--jg-accent, #f59e0b)",
|
|
92
|
+
} }) })) : null] })] }, achievement.id));
|
|
93
|
+
}) }))] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { CameraShakeController } from "@jgengine/core/vfx/cameraShake";
|
|
3
|
+
import { type HudFrameVariation } from "./hudFrame.js";
|
|
4
|
+
/** A live read of a camera-shake controller: current trauma `0..1` and the last game-owned `kind`. */
|
|
5
|
+
export interface CameraShakeReadout {
|
|
6
|
+
/** Current trauma in `[0, 1]`. */
|
|
7
|
+
trauma: number;
|
|
8
|
+
/** The last free-string `kind` handed to the controller, or `undefined`. */
|
|
9
|
+
kind: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to a camera-shake controller and re-render on every change (add /
|
|
13
|
+
* decay / clear). Optionally drives `update()` on an animation frame so the trauma
|
|
14
|
+
* meter bleeds down live even when no game loop ticks the controller. Returns the
|
|
15
|
+
* current trauma + kind for a HUD readout.
|
|
16
|
+
*
|
|
17
|
+
* @capability use-camera-shake React hook binding a camera-shake controller — subscribes for live trauma/kind and can self-drive its decay on a frame loop
|
|
18
|
+
*/
|
|
19
|
+
export declare function useCameraShake(controller: CameraShakeController, animate?: boolean): CameraShakeReadout;
|
|
20
|
+
/** Props for {@link CameraShakeMeter}. */
|
|
21
|
+
export interface CameraShakeMeterProps {
|
|
22
|
+
/** The controller to read. */
|
|
23
|
+
controller: CameraShakeController;
|
|
24
|
+
/**
|
|
25
|
+
* Drive the controller's decay on an animation frame so the meter bleeds down
|
|
26
|
+
* live. Default `false` — leave off when a shell consumer already ticks
|
|
27
|
+
* `update()` each frame (the usual case), on to make the panel self-sufficient.
|
|
28
|
+
*/
|
|
29
|
+
animate?: boolean;
|
|
30
|
+
/** Header label. Default `"CAMERA SHAKE"`. */
|
|
31
|
+
title?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Human labels per game-owned `kind`, so the readout can show "Explosion" for a
|
|
34
|
+
* `"explosion"` impact. The model never interprets `kind`; this is pure display.
|
|
35
|
+
*/
|
|
36
|
+
kindLabels?: Record<string, string>;
|
|
37
|
+
/** Frame skin. Default `"glass"`. */
|
|
38
|
+
variation?: HudFrameVariation;
|
|
39
|
+
className?: string;
|
|
40
|
+
style?: CSSProperties;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A drop-in HUD panel that visualizes a core {@link CameraShakeController}: a
|
|
44
|
+
* trauma meter (reusing the shared {@link StaminaBar} building block) plus a live
|
|
45
|
+
* percentage and the current impact `kind` label. Presentation only — it reads the
|
|
46
|
+
* controller and never interprets `kind`, mapping it through `kindLabels` for
|
|
47
|
+
* display. A shell component applies the same controller's `offset()` to the
|
|
48
|
+
* camera; this panel is the on-screen readout of how much shake is in flight.
|
|
49
|
+
*
|
|
50
|
+
* @capability camera-shake-meter HUD readout for a core camera-shake controller — a trauma meter plus current impact-kind label, reskinnable and kind-labelled
|
|
51
|
+
*/
|
|
52
|
+
export declare function CameraShakeMeter({ controller, animate, title, kindLabels, variation, className, style, }: CameraShakeMeterProps): ReactNode;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer, useRef } from "react";
|
|
3
|
+
import { StaminaBar } from "./bars.js";
|
|
4
|
+
import { HudFrame } from "./hudFrame.js";
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to a camera-shake controller and re-render on every change (add /
|
|
7
|
+
* decay / clear). Optionally drives `update()` on an animation frame so the trauma
|
|
8
|
+
* meter bleeds down live even when no game loop ticks the controller. Returns the
|
|
9
|
+
* current trauma + kind for a HUD readout.
|
|
10
|
+
*
|
|
11
|
+
* @capability use-camera-shake React hook binding a camera-shake controller — subscribes for live trauma/kind and can self-drive its decay on a frame loop
|
|
12
|
+
*/
|
|
13
|
+
export function useCameraShake(controller, animate = true) {
|
|
14
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
15
|
+
const rafRef = useRef(0);
|
|
16
|
+
useEffect(() => controller.subscribe(bump), [controller]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!animate)
|
|
19
|
+
return;
|
|
20
|
+
let running = true;
|
|
21
|
+
let last = performance.now();
|
|
22
|
+
const loop = () => {
|
|
23
|
+
if (!running)
|
|
24
|
+
return;
|
|
25
|
+
const nowMs = performance.now();
|
|
26
|
+
controller.update((nowMs - last) / 1000);
|
|
27
|
+
last = nowMs;
|
|
28
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
29
|
+
};
|
|
30
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
31
|
+
return () => {
|
|
32
|
+
running = false;
|
|
33
|
+
cancelAnimationFrame(rafRef.current);
|
|
34
|
+
};
|
|
35
|
+
}, [controller, animate]);
|
|
36
|
+
return { trauma: controller.trauma(), kind: controller.kind() };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A drop-in HUD panel that visualizes a core {@link CameraShakeController}: a
|
|
40
|
+
* trauma meter (reusing the shared {@link StaminaBar} building block) plus a live
|
|
41
|
+
* percentage and the current impact `kind` label. Presentation only — it reads the
|
|
42
|
+
* controller and never interprets `kind`, mapping it through `kindLabels` for
|
|
43
|
+
* display. A shell component applies the same controller's `offset()` to the
|
|
44
|
+
* camera; this panel is the on-screen readout of how much shake is in flight.
|
|
45
|
+
*
|
|
46
|
+
* @capability camera-shake-meter HUD readout for a core camera-shake controller — a trauma meter plus current impact-kind label, reskinnable and kind-labelled
|
|
47
|
+
*/
|
|
48
|
+
export function CameraShakeMeter({ controller, animate = false, title = "CAMERA SHAKE", kindLabels, variation = "glass", className, style, }) {
|
|
49
|
+
const { trauma, kind } = useCameraShake(controller, animate);
|
|
50
|
+
const pct = Math.round(trauma * 100);
|
|
51
|
+
const kindLabel = kind === undefined ? "—" : kindLabels?.[kind] ?? kind;
|
|
52
|
+
return (_jsx(HudFrame, { className: className, variation: variation, title: title, aside: _jsxs("span", { style: { fontVariantNumeric: "tabular-nums", opacity: 0.85 }, children: [pct, "%"] }), padding: 12, width: 240, style: style, children: _jsxs("div", { "data-camera-shake-meter": "", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsx(StaminaBar, { value: pct, max: 100, label: "TRAUMA", fill: "#f97316", showValue: false, width: "100%" }), _jsxs("div", { "data-camera-shake-kind": kind ?? "", style: { display: "flex", justifyContent: "space-between", fontSize: 12, opacity: 0.85 }, children: [_jsx("span", { style: { textTransform: "uppercase", letterSpacing: 0.4, opacity: 0.7 }, children: "Last impact" }), _jsx("span", { style: { fontWeight: 700 }, children: kindLabel })] })] }) }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { CoachMarkSequence, CoachMarkView } from "@jgengine/core/ui/coachMarks";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to a coach-mark sequence and re-render on every change, returning the
|
|
5
|
+
* current step view (or `null` when the tour is complete or waiting on a gate).
|
|
6
|
+
*
|
|
7
|
+
* @capability use-coach-marks React hook binding a coach-mark sequence to a component — re-renders on advance/skip/gate changes
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCoachMarks(sequence: CoachMarkSequence): CoachMarkView | null;
|
|
10
|
+
/** Reskin tokens for {@link CoachMarkHost} / {@link CoachMark}. */
|
|
11
|
+
export interface CoachMarkTheme {
|
|
12
|
+
/** Accent for the counter, Next button, and pointer. Default reads `--jg-accent`. */
|
|
13
|
+
accent?: string;
|
|
14
|
+
/** Callout background. */
|
|
15
|
+
background?: string;
|
|
16
|
+
/** Callout text color. */
|
|
17
|
+
text?: string;
|
|
18
|
+
/** Dim color for the backdrop / spotlight. */
|
|
19
|
+
backdrop?: string;
|
|
20
|
+
/** Callout corner radius (px). */
|
|
21
|
+
radius?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Props for {@link CoachMark}. */
|
|
24
|
+
export interface CoachMarkProps {
|
|
25
|
+
/** The step view to render (from `sequence.current()` / {@link useCoachMarks}). */
|
|
26
|
+
view: CoachMarkView;
|
|
27
|
+
/** Absolute-position style computed for the callout (see {@link CoachMarkHost}). */
|
|
28
|
+
positionStyle?: CSSProperties;
|
|
29
|
+
onNext: () => void;
|
|
30
|
+
onSkip: () => void;
|
|
31
|
+
nextLabel?: string;
|
|
32
|
+
doneLabel?: string;
|
|
33
|
+
skipLabel?: string;
|
|
34
|
+
theme?: CoachMarkTheme;
|
|
35
|
+
className?: string;
|
|
36
|
+
style?: CSSProperties;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A single coach-mark callout — title, body, an "N of M" counter, a Next/Got-it
|
|
40
|
+
* button, and a Skip-tour link. Presentation-only; position it with
|
|
41
|
+
* `positionStyle` or drop it into {@link CoachMarkHost}. Reskin via {@link CoachMarkTheme}.
|
|
42
|
+
*
|
|
43
|
+
* @capability coach-mark single onboarding callout — title/body, N-of-M counter, Next + Skip controls, theme-reskinnable
|
|
44
|
+
*/
|
|
45
|
+
export declare function CoachMark({ view, positionStyle, onNext, onSkip, nextLabel, doneLabel, skipLabel, theme, className, style, }: CoachMarkProps): ReactNode;
|
|
46
|
+
/** Props for {@link CoachMarkHost}. */
|
|
47
|
+
export interface CoachMarkHostProps {
|
|
48
|
+
/** The sequence to drive. */
|
|
49
|
+
sequence: CoachMarkSequence;
|
|
50
|
+
/** Resolve a step's `anchor` string to a DOM element. Default: `document.querySelector`. */
|
|
51
|
+
resolveAnchor?: (anchor: string) => Element | null;
|
|
52
|
+
/** Dim the rest of the screen and spotlight the anchor. Default `true`. */
|
|
53
|
+
showBackdrop?: boolean;
|
|
54
|
+
/** Fired the first time the tour reports complete (no eligible step). */
|
|
55
|
+
onComplete?: () => void;
|
|
56
|
+
nextLabel?: string;
|
|
57
|
+
doneLabel?: string;
|
|
58
|
+
skipLabel?: string;
|
|
59
|
+
theme?: CoachMarkTheme;
|
|
60
|
+
className?: string;
|
|
61
|
+
style?: CSSProperties;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Full-screen coach-mark host: watches a sequence, positions the current step's
|
|
65
|
+
* callout beside its anchored element (or centered when unanchored) via
|
|
66
|
+
* `getBoundingClientRect`, and draws an optional dimmed backdrop with a spotlight
|
|
67
|
+
* cutout around the anchor. Next advances, Skip ends the tour — both persist so
|
|
68
|
+
* hints never re-show. Reskin with {@link CoachMarkTheme}.
|
|
69
|
+
*
|
|
70
|
+
* @capability coach-mark-host full-screen onboarding coach-mark host — anchored/centered callout, dimmed spotlight backdrop, Next/Skip wired to the sequence
|
|
71
|
+
*/
|
|
72
|
+
export declare function CoachMarkHost({ sequence, resolveAnchor, showBackdrop, onComplete, nextLabel, doneLabel, skipLabel, theme, className, style, }: CoachMarkHostProps): ReactNode;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useReducer, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to a coach-mark sequence and re-render on every change, returning the
|
|
5
|
+
* current step view (or `null` when the tour is complete or waiting on a gate).
|
|
6
|
+
*
|
|
7
|
+
* @capability use-coach-marks React hook binding a coach-mark sequence to a component — re-renders on advance/skip/gate changes
|
|
8
|
+
*/
|
|
9
|
+
export function useCoachMarks(sequence) {
|
|
10
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
11
|
+
useEffect(() => sequence.subscribe(bump), [sequence]);
|
|
12
|
+
return sequence.current();
|
|
13
|
+
}
|
|
14
|
+
function resolveTheme(theme) {
|
|
15
|
+
return {
|
|
16
|
+
accent: theme?.accent ?? "var(--jg-accent, #38bdf8)",
|
|
17
|
+
background: theme?.background ?? "linear-gradient(160deg, rgba(20,24,32,0.98), rgba(11,14,19,0.98))",
|
|
18
|
+
text: theme?.text ?? "#e2e8f0",
|
|
19
|
+
backdrop: theme?.backdrop ?? "rgba(4,7,12,0.62)",
|
|
20
|
+
radius: theme?.radius ?? 14,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const CALLOUT_WIDTH = 288;
|
|
24
|
+
const GAP = 14;
|
|
25
|
+
function calloutStyle(rect, placement) {
|
|
26
|
+
if (rect === null || placement === "center") {
|
|
27
|
+
return { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
|
|
28
|
+
}
|
|
29
|
+
switch (placement) {
|
|
30
|
+
case "top":
|
|
31
|
+
return { top: rect.top - GAP, left: rect.left + rect.width / 2, transform: "translate(-50%, -100%)" };
|
|
32
|
+
case "left":
|
|
33
|
+
return { top: rect.top + rect.height / 2, left: rect.left - GAP, transform: "translate(-100%, -50%)" };
|
|
34
|
+
case "right":
|
|
35
|
+
return { top: rect.top + rect.height / 2, left: rect.right + GAP, transform: "translate(0, -50%)" };
|
|
36
|
+
case "bottom":
|
|
37
|
+
default:
|
|
38
|
+
return { top: rect.bottom + GAP, left: rect.left + rect.width / 2, transform: "translate(-50%, 0)" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A single coach-mark callout — title, body, an "N of M" counter, a Next/Got-it
|
|
43
|
+
* button, and a Skip-tour link. Presentation-only; position it with
|
|
44
|
+
* `positionStyle` or drop it into {@link CoachMarkHost}. Reskin via {@link CoachMarkTheme}.
|
|
45
|
+
*
|
|
46
|
+
* @capability coach-mark single onboarding callout — title/body, N-of-M counter, Next + Skip controls, theme-reskinnable
|
|
47
|
+
*/
|
|
48
|
+
export function CoachMark({ view, positionStyle, onNext, onSkip, nextLabel = "Next", doneLabel = "Got it", skipLabel = "Skip tour", theme, className, style, }) {
|
|
49
|
+
const t = resolveTheme(theme);
|
|
50
|
+
const last = view.remaining <= 1;
|
|
51
|
+
return (_jsxs("div", { className: className, "data-coach-mark": view.step.id, style: {
|
|
52
|
+
position: "fixed",
|
|
53
|
+
width: CALLOUT_WIDTH,
|
|
54
|
+
maxWidth: "calc(100vw - 24px)",
|
|
55
|
+
boxSizing: "border-box",
|
|
56
|
+
padding: 16,
|
|
57
|
+
borderRadius: t.radius,
|
|
58
|
+
background: t.background,
|
|
59
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.32))",
|
|
60
|
+
color: t.text,
|
|
61
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
62
|
+
boxShadow: "0 20px 50px rgba(0,0,0,0.55)",
|
|
63
|
+
pointerEvents: "auto",
|
|
64
|
+
...positionStyle,
|
|
65
|
+
...style,
|
|
66
|
+
}, children: [_jsxs("div", { "data-coach-mark-counter": true, style: { fontSize: 10, letterSpacing: 1.4, textTransform: "uppercase", fontWeight: 700, color: t.accent }, children: [view.index + 1, " of ", view.total] }), _jsx("div", { "data-coach-mark-title": true, style: { marginTop: 6, fontSize: 15, fontWeight: 700 }, children: view.step.title }), view.step.body !== undefined ? (_jsx("p", { "data-coach-mark-body": true, style: { margin: "6px 0 0", fontSize: 12.5, lineHeight: 1.5, color: "rgba(203,213,225,0.85)" }, children: view.step.body })) : null, _jsxs("div", { style: { marginTop: 14, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10 }, children: [_jsx("button", { type: "button", "data-coach-mark-skip": true, onClick: onSkip, style: {
|
|
67
|
+
border: "none",
|
|
68
|
+
background: "transparent",
|
|
69
|
+
color: "rgba(148,163,184,0.85)",
|
|
70
|
+
fontSize: 11.5,
|
|
71
|
+
cursor: "pointer",
|
|
72
|
+
padding: 0,
|
|
73
|
+
textDecoration: "underline",
|
|
74
|
+
}, children: skipLabel }), _jsx("button", { type: "button", "data-coach-mark-next": true, onClick: onNext, style: {
|
|
75
|
+
border: "1px solid transparent",
|
|
76
|
+
borderRadius: 8,
|
|
77
|
+
background: t.accent,
|
|
78
|
+
color: "#06121b",
|
|
79
|
+
fontSize: 12.5,
|
|
80
|
+
fontWeight: 700,
|
|
81
|
+
padding: "6px 14px",
|
|
82
|
+
cursor: "pointer",
|
|
83
|
+
}, children: last ? doneLabel : nextLabel })] })] }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Full-screen coach-mark host: watches a sequence, positions the current step's
|
|
87
|
+
* callout beside its anchored element (or centered when unanchored) via
|
|
88
|
+
* `getBoundingClientRect`, and draws an optional dimmed backdrop with a spotlight
|
|
89
|
+
* cutout around the anchor. Next advances, Skip ends the tour — both persist so
|
|
90
|
+
* hints never re-show. Reskin with {@link CoachMarkTheme}.
|
|
91
|
+
*
|
|
92
|
+
* @capability coach-mark-host full-screen onboarding coach-mark host — anchored/centered callout, dimmed spotlight backdrop, Next/Skip wired to the sequence
|
|
93
|
+
*/
|
|
94
|
+
export function CoachMarkHost({ sequence, resolveAnchor, showBackdrop = true, onComplete, nextLabel, doneLabel, skipLabel, theme, className, style, }) {
|
|
95
|
+
const view = useCoachMarks(sequence);
|
|
96
|
+
const [rect, setRect] = useState(null);
|
|
97
|
+
const anchor = view?.step.anchor;
|
|
98
|
+
const stepId = view?.step.id;
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
if (view === null && onComplete !== undefined && sequence.isComplete())
|
|
101
|
+
onComplete();
|
|
102
|
+
}, [view, onComplete, sequence]);
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
if (anchor === undefined || typeof document === "undefined") {
|
|
105
|
+
setRect(null);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const resolve = resolveAnchor ?? ((selector) => document.querySelector(selector));
|
|
109
|
+
const measure = () => {
|
|
110
|
+
const el = resolve(anchor);
|
|
111
|
+
setRect(el !== null ? el.getBoundingClientRect() : null);
|
|
112
|
+
};
|
|
113
|
+
measure();
|
|
114
|
+
window.addEventListener("resize", measure);
|
|
115
|
+
window.addEventListener("scroll", measure, true);
|
|
116
|
+
return () => {
|
|
117
|
+
window.removeEventListener("resize", measure);
|
|
118
|
+
window.removeEventListener("scroll", measure, true);
|
|
119
|
+
};
|
|
120
|
+
}, [anchor, stepId, resolveAnchor]);
|
|
121
|
+
if (view === null)
|
|
122
|
+
return null;
|
|
123
|
+
const t = resolveTheme(theme);
|
|
124
|
+
const placement = view.step.placement ?? (view.step.anchor !== undefined ? "bottom" : "center");
|
|
125
|
+
const spotlight = showBackdrop && rect !== null;
|
|
126
|
+
return (_jsxs("div", { className: className, "data-coach-mark-host": true, style: { position: "fixed", inset: 0, zIndex: 60, pointerEvents: "none", ...style }, children: [showBackdrop ? (spotlight ? (_jsx("div", { "data-coach-mark-spotlight": true, style: {
|
|
127
|
+
position: "fixed",
|
|
128
|
+
top: rect.top - 6,
|
|
129
|
+
left: rect.left - 6,
|
|
130
|
+
width: rect.width + 12,
|
|
131
|
+
height: rect.height + 12,
|
|
132
|
+
borderRadius: 10,
|
|
133
|
+
boxShadow: `0 0 0 9999px ${t.backdrop}`,
|
|
134
|
+
pointerEvents: "auto",
|
|
135
|
+
} })) : (_jsx("div", { "data-coach-mark-backdrop": true, style: { position: "fixed", inset: 0, background: t.backdrop, pointerEvents: "auto" } }))) : null, _jsx(CoachMark, { view: view, positionStyle: calloutStyle(rect, placement), onNext: () => sequence.advance(), onSkip: () => sequence.skipAll(), nextLabel: nextLabel, doneLabel: doneLabel, skipLabel: skipLabel, theme: theme })] }));
|
|
136
|
+
}
|
package/dist/codex.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { CodexEntryView } from "@jgengine/core/game/codex";
|
|
3
|
+
/** Props for {@link Codex}. */
|
|
4
|
+
export interface CodexProps {
|
|
5
|
+
entries: readonly CodexEntryView[];
|
|
6
|
+
title?: string;
|
|
7
|
+
columns?: number;
|
|
8
|
+
/** Mask name/description of secret + undiscovered entries as "???". Default true. */
|
|
9
|
+
maskSecrets?: boolean;
|
|
10
|
+
/** Optional per-entry icon; falls back to the def `icon` glyph, a lock, or a silhouette dot. */
|
|
11
|
+
renderIcon?: (entry: CodexEntryView) => ReactNode;
|
|
12
|
+
onSelect?: (id: string) => void;
|
|
13
|
+
emptyLabel?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Codex / bestiary gallery — category tabs, a responsive grid of entry cards
|
|
19
|
+
* (discovered vs. locked, secret entries masked until found), and a header
|
|
20
|
+
* summarizing completion. Feed it `codex.list()`; wire `onSelect` to a detail
|
|
21
|
+
* pane.
|
|
22
|
+
*
|
|
23
|
+
* @capability codex-gallery codex/bestiary gallery with category tabs, discovered/locked cards, secret masking, and a completion header
|
|
24
|
+
*/
|
|
25
|
+
export declare function Codex({ entries, title, columns, maskSecrets, renderIcon, onSelect, emptyLabel, className, style, }: CodexProps): ReactNode;
|