@jgengine/react 0.9.0 → 0.11.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 +34 -3
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/actionHud.d.ts +174 -0
- package/dist/actionHud.js +292 -0
- package/dist/cards.d.ts +59 -0
- package/dist/cards.js +116 -0
- package/dist/chat.d.ts +6 -4
- package/dist/chat.js +16 -7
- package/dist/chatBubbles.js +2 -1
- package/dist/components.d.ts +20 -0
- package/dist/components.js +29 -1
- package/dist/display.d.ts +1 -0
- package/dist/display.js +1 -0
- package/dist/engineStore.d.ts +3 -0
- package/dist/engineStore.js +3 -0
- package/dist/fogOverlay.d.ts +3 -0
- package/dist/fogOverlay.js +3 -0
- package/dist/gameViewport.d.ts +54 -0
- package/dist/gameViewport.js +356 -0
- package/dist/hooks.d.ts +22 -0
- package/dist/hooks.js +58 -12
- package/dist/hud.d.ts +87 -0
- package/dist/hud.js +144 -0
- package/dist/hudLayout.d.ts +39 -4
- package/dist/hudLayout.js +152 -11
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/keyHint.d.ts +30 -0
- package/dist/keyHint.js +31 -0
- package/dist/map.d.ts +69 -1
- package/dist/map.js +54 -1
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -0
- package/dist/rotateDevice.d.ts +24 -0
- package/dist/rotateDevice.js +83 -0
- package/dist/save.d.ts +25 -0
- package/dist/save.js +37 -0
- package/dist/selectSnapshot.d.ts +2 -0
- package/dist/selectSnapshot.js +2 -0
- package/dist/selectionHud.d.ts +133 -0
- package/dist/selectionHud.js +182 -0
- package/dist/settings.d.ts +3 -0
- package/dist/skillCheckPaint.d.ts +2 -0
- package/dist/skillCheckPaint.js +2 -0
- package/dist/social.d.ts +3 -3
- package/dist/social.js +31 -10
- package/dist/startScreen.d.ts +62 -0
- package/dist/startScreen.js +62 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +9 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/llms.txt +0 -1633
package/dist/hud.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { groundSpeed } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
import { useCurrency, useEntityStat, useGameClock, useGameStore, useInventory, localPlayerEntity } from "./hooks.js";
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in HUD components — good-looking with zero styling, and fully opt-in: a game imports only the
|
|
7
|
+
* pieces it wants (`StatBar`, `Hotbar`, `Speedometer`, `Clock`, `WaveBanner`, `Coins`, `Crosshair`)
|
|
8
|
+
* and places them itself. The engine imposes none of these; there is no forced HUD. Styling is
|
|
9
|
+
* self-contained inline CSS (no Tailwind `@source` needed) with a shared dark-glass look; pass
|
|
10
|
+
* `style`/`className` to override. Anything reading the player defaults to the local player entity.
|
|
11
|
+
*
|
|
12
|
+
* @capability hud-components opt-in drop-in health/hotbar/speed/clock/wave/currency HUD widgets
|
|
13
|
+
*/
|
|
14
|
+
const PANEL = {
|
|
15
|
+
background: "rgba(10,12,16,0.62)",
|
|
16
|
+
backdropFilter: "blur(6px)",
|
|
17
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
18
|
+
border: "1px solid rgba(255,255,255,0.10)",
|
|
19
|
+
borderRadius: 10,
|
|
20
|
+
color: "#f4f6fb",
|
|
21
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
22
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
|
|
23
|
+
};
|
|
24
|
+
/** Named color ramps for {@link StatBar} tones. `fn` maps fill fraction (0..1) → gradient stops. */
|
|
25
|
+
const TONES = {
|
|
26
|
+
health: (t) => (t > 0.5 ? ["#22c55e", "#4ade80"] : t > 0.25 ? ["#f59e0b", "#fbbf24"] : ["#dc2626", "#f87171"]),
|
|
27
|
+
mana: () => ["#2563eb", "#60a5fa"],
|
|
28
|
+
stamina: () => ["#65a30d", "#a3e635"],
|
|
29
|
+
shield: () => ["#0891b2", "#22d3ee"],
|
|
30
|
+
xp: () => ["#7c3aed", "#a78bfa"],
|
|
31
|
+
neutral: () => ["#475569", "#94a3b8"],
|
|
32
|
+
};
|
|
33
|
+
function useLocalPlayerId(entityId) {
|
|
34
|
+
return useGameStore((ctx) => entityId ?? localPlayerEntity(ctx)?.id ?? null);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
|
|
38
|
+
* fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
|
|
39
|
+
* Renders nothing until the stat exists.
|
|
40
|
+
*/
|
|
41
|
+
export function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }) {
|
|
42
|
+
const id = useLocalPlayerId(entityId);
|
|
43
|
+
const stat = useEntityStat(id ?? "", statId);
|
|
44
|
+
if (id === null || stat === null)
|
|
45
|
+
return null;
|
|
46
|
+
const range = stat.max - stat.min;
|
|
47
|
+
const fraction = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
48
|
+
const [from, to] = (TONES[tone] ?? TONES.neutral)(fraction);
|
|
49
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 8px", width, ...style }, "data-stat": statId, children: [(label !== undefined || icon !== undefined || showValue) && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 4, fontSize: 11, fontWeight: 600, letterSpacing: 0.3 }, children: [icon, _jsx("span", { style: { textTransform: "uppercase", opacity: 0.75 }, children: label ?? statId }), showValue ? (_jsxs("span", { style: { marginLeft: "auto", fontVariantNumeric: "tabular-nums", opacity: 0.9 }, children: [Math.round(stat.current), _jsxs("span", { style: { opacity: 0.5 }, children: [" / ", Math.round(stat.max)] })] })) : null] })), _jsx("div", { style: { height: 8, borderRadius: 999, background: "rgba(255,255,255,0.10)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${fraction * 100}%`, height: "100%", borderRadius: 999, background: `linear-gradient(90deg, ${from}, ${to})`, transition: "width 160ms ease" } }) })] }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A numbered hotbar bound to an inventory — glassy slots, an active-slot highlight, item id + count,
|
|
53
|
+
* and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one.
|
|
54
|
+
*/
|
|
55
|
+
export function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, style, className, }) {
|
|
56
|
+
const slots = useInventory(inventoryId);
|
|
57
|
+
return (_jsx("div", { className: className, style: { display: "flex", gap: 6, ...style }, "data-inventory": inventoryId, children: slots.map((slot, index) => {
|
|
58
|
+
const active = index === activeSlot;
|
|
59
|
+
return (_jsxs("div", { "data-slot": index, style: {
|
|
60
|
+
...PANEL,
|
|
61
|
+
width: slotSize,
|
|
62
|
+
height: slotSize,
|
|
63
|
+
position: "relative",
|
|
64
|
+
display: "flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
justifyContent: "center",
|
|
67
|
+
borderColor: active ? "rgba(56,189,248,0.9)" : "rgba(255,255,255,0.10)",
|
|
68
|
+
boxShadow: active ? "0 0 0 1px rgba(56,189,248,0.7), 0 4px 16px rgba(0,0,0,0.35)" : PANEL.boxShadow,
|
|
69
|
+
}, children: [_jsx("span", { style: { position: "absolute", top: 2, left: 4, fontSize: 9, fontWeight: 700, opacity: 0.55 }, children: keys?.[index] ?? index + 1 }), slot !== null ? (_jsxs("span", { style: { fontSize: 10, textAlign: "center", lineHeight: 1.1, padding: "0 2px", wordBreak: "break-word" }, children: [slot.itemId, slot.count > 1 ? _jsxs("span", { style: { display: "block", opacity: 0.6 }, children: ["\u00D7", slot.count] }) : null] })) : null] }, index));
|
|
70
|
+
}) }));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A speedometer for an entity (defaults to the local player) — an SVG arc gauge + a digital readout.
|
|
74
|
+
* `scale` converts world units/second to your display unit (3.6 → km/h, 2.237 → mph); `max` sets the
|
|
75
|
+
* gauge's top of scale.
|
|
76
|
+
*/
|
|
77
|
+
export function Speedometer({ entityId, scale = 3.6, unit = "km/h", max = 60, size = 96, style, className, }) {
|
|
78
|
+
const speed = useGameStore((ctx) => {
|
|
79
|
+
const entity = entityId === undefined ? localPlayerEntity(ctx) : ctx.scene.entity.get(entityId);
|
|
80
|
+
return entity === null ? 0 : groundSpeed(entity) * scale;
|
|
81
|
+
});
|
|
82
|
+
const fraction = Math.max(0, Math.min(1, speed / max));
|
|
83
|
+
// 240° sweep from -210° to +30°.
|
|
84
|
+
const startDeg = -210;
|
|
85
|
+
const sweep = 240;
|
|
86
|
+
const radius = size / 2 - 8;
|
|
87
|
+
const cx = size / 2;
|
|
88
|
+
const cy = size / 2;
|
|
89
|
+
const arc = (fromFrac, toFrac) => {
|
|
90
|
+
const a0 = ((startDeg + sweep * fromFrac) * Math.PI) / 180;
|
|
91
|
+
const a1 = ((startDeg + sweep * toFrac) * Math.PI) / 180;
|
|
92
|
+
const large = sweep * (toFrac - fromFrac) > 180 ? 1 : 0;
|
|
93
|
+
return `M ${cx + radius * Math.cos(a0)} ${cy + radius * Math.sin(a0)} A ${radius} ${radius} 0 ${large} 1 ${cx + radius * Math.cos(a1)} ${cy + radius * Math.sin(a1)}`;
|
|
94
|
+
};
|
|
95
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, width: size, padding: 6, textAlign: "center", ...style }, children: [_jsxs("svg", { width: size - 12, height: (size - 12) * 0.72, viewBox: `0 0 ${size} ${size * 0.72}`, style: { display: "block", margin: "0 auto" }, children: [_jsx("path", { d: arc(0, 1), fill: "none", stroke: "rgba(255,255,255,0.12)", strokeWidth: 7, strokeLinecap: "round" }), _jsx("path", { d: arc(0, fraction), fill: "none", stroke: fraction > 0.85 ? "#f87171" : "#38bdf8", strokeWidth: 7, strokeLinecap: "round" })] }), _jsx("div", { style: { marginTop: -8, fontVariantNumeric: "tabular-nums", fontWeight: 700, fontSize: 18, lineHeight: 1 }, children: Math.round(speed) }), _jsx("div", { style: { fontSize: 9, opacity: 0.6, letterSpacing: 0.4, textTransform: "uppercase" }, children: unit })] }));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
|
|
99
|
+
* pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
|
|
100
|
+
* a game opts into letting the player scrub time.
|
|
101
|
+
*/
|
|
102
|
+
export function Clock({ format = "24h", showDay = true, controls = false, style, className, }) {
|
|
103
|
+
const clock = useGameClock();
|
|
104
|
+
const { hour, minute, day } = clock.calendar;
|
|
105
|
+
const label = useMemo(() => {
|
|
106
|
+
const mm = minute.toString().padStart(2, "0");
|
|
107
|
+
if (format === "12h") {
|
|
108
|
+
const h12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
109
|
+
return `${h12}:${mm} ${hour < 12 ? "AM" : "PM"}`;
|
|
110
|
+
}
|
|
111
|
+
return `${hour.toString().padStart(2, "0")}:${mm}`;
|
|
112
|
+
}, [hour, minute, format]);
|
|
113
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "5px 10px", display: "inline-flex", alignItems: "center", gap: 8, fontVariantNumeric: "tabular-nums", ...style }, children: [showDay ? _jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: ["Day ", day + 1] }) : null, _jsx("span", { style: { fontWeight: 700, fontSize: 14 }, children: label }), controls ? (_jsxs("span", { style: { display: "inline-flex", gap: 3, marginLeft: 4 }, children: [_jsx(ClockPill, { label: clock.paused ? "▶" : "❚❚", active: false, onClick: () => (clock.paused ? clock.controls.play() : clock.controls.pause()) }), clock.speeds.map((s) => (_jsx(ClockPill, { label: `${s}×`, active: !clock.paused && clock.speed === s, onClick: () => clock.controls.setSpeed(s) }, s)))] })) : null] }));
|
|
114
|
+
}
|
|
115
|
+
function ClockPill({ label, active, onClick }) {
|
|
116
|
+
return (_jsx("button", { type: "button", onClick: onClick, style: {
|
|
117
|
+
pointerEvents: "auto",
|
|
118
|
+
cursor: "pointer",
|
|
119
|
+
fontSize: 10,
|
|
120
|
+
fontWeight: 600,
|
|
121
|
+
padding: "1px 6px",
|
|
122
|
+
borderRadius: 6,
|
|
123
|
+
border: "none",
|
|
124
|
+
color: active ? "#0a0c10" : "#cbd5e1",
|
|
125
|
+
background: active ? "#38bdf8" : "rgba(255,255,255,0.08)",
|
|
126
|
+
}, children: label }));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A wave / round banner — a bold centered pill for "WAVE 3" style callouts, with an optional subtitle
|
|
130
|
+
* (enemies remaining, timer). Pure display: pass the current `wave` and whatever subtitle you track.
|
|
131
|
+
*/
|
|
132
|
+
export function WaveBanner({ wave, label = "Wave", subtitle, style, className, }) {
|
|
133
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 16px", textAlign: "center", display: "inline-block", ...style }, children: [_jsx("div", { style: { fontSize: 10, letterSpacing: 2, textTransform: "uppercase", opacity: 0.65 }, children: label }), _jsx("div", { style: { fontSize: 24, fontWeight: 800, lineHeight: 1.1 }, children: wave }), subtitle !== undefined ? _jsx("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 2 }, children: subtitle }) : null] }));
|
|
134
|
+
}
|
|
135
|
+
/** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. */
|
|
136
|
+
export function Coins({ currencyId, icon = "🪙", style, className, }) {
|
|
137
|
+
const amount = useCurrency(currencyId);
|
|
138
|
+
return (_jsxs("span", { className: className, style: { ...PANEL, padding: "3px 10px", display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 700, fontVariantNumeric: "tabular-nums", ...style }, "data-currency": currencyId, children: [_jsx("span", { "aria-hidden": true, children: icon }), amount] }));
|
|
139
|
+
}
|
|
140
|
+
/** A minimal center crosshair reticle — four ticks around a gap. Purely presentational. */
|
|
141
|
+
export function Crosshair({ size = 18, gap = 5, thickness = 2, color = "rgba(255,255,255,0.85)", style, className, }) {
|
|
142
|
+
const tick = (extra) => ({ position: "absolute", background: color, borderRadius: thickness, ...extra });
|
|
143
|
+
return (_jsx("div", { className: className, style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", ...style }, children: _jsxs("div", { style: { position: "relative", width: size, height: size }, children: [_jsx("div", { style: tick({ left: "50%", top: 0, width: thickness, height: size / 2 - gap, transform: "translateX(-50%)" }) }), _jsx("div", { style: tick({ left: "50%", bottom: 0, width: thickness, height: size / 2 - gap, transform: "translateX(-50%)" }) }), _jsx("div", { style: tick({ top: "50%", left: 0, height: thickness, width: size / 2 - gap, transform: "translateY(-50%)" }) }), _jsx("div", { style: tick({ top: "50%", right: 0, height: thickness, width: size / 2 - gap, transform: "translateY(-50%)" }) })] }) }));
|
|
144
|
+
}
|
package/dist/hudLayout.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { type HudAnchor, type HudLayoutStore } from "@jgengine/core/ui/hudLayout";
|
|
3
|
+
import { type EditorUiDocument } from "@jgengine/core/ui/hudDocument";
|
|
4
|
+
import { type HudPriority, type MobileHudBehavior } from "@jgengine/core/ui/gameLayout";
|
|
5
|
+
import type { GamePhase } from "@jgengine/core/game/gamePhase";
|
|
6
|
+
/** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
|
|
7
|
+
export declare function hudVisibleInPhase(showDuring: readonly GamePhase[] | undefined, phase: GamePhase): boolean;
|
|
3
8
|
/**
|
|
4
9
|
* How a panel behaves on compact (phone-scale) displays. `keep` stays visible
|
|
5
10
|
* at the global compact scale, `chip` collapses to a small tap-to-expand
|
|
@@ -14,6 +19,13 @@ export declare function useHudLayout(options?: {
|
|
|
14
19
|
storageKey?: string;
|
|
15
20
|
snap?: number;
|
|
16
21
|
locked?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Scene-document `ui` section — source of truth for panel placement/size.
|
|
24
|
+
* When provided, hydrates the layout store (and wins over legacy localStorage).
|
|
25
|
+
*/
|
|
26
|
+
documentUi?: EditorUiDocument;
|
|
27
|
+
/** When true (default), canvas moves/resizes write undoable `setUiPanel` patches to a live editor host. */
|
|
28
|
+
documentPatches?: boolean;
|
|
17
29
|
}): HudLayoutStore;
|
|
18
30
|
/**
|
|
19
31
|
* Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine
|
|
@@ -22,15 +34,17 @@ export declare function useHudLayout(options?: {
|
|
|
22
34
|
* (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
|
|
23
35
|
* whole surface scales down and each panel applies its `compact` behavior.
|
|
24
36
|
*/
|
|
25
|
-
export declare function HudCanvas({ layout, editChord, compactScale, className, style, children, }: {
|
|
37
|
+
export declare function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: {
|
|
26
38
|
layout: HudLayoutStore;
|
|
27
39
|
editChord?: HudEditChord | false;
|
|
28
40
|
/** Zoom applied to the whole HUD on compact displays. Default 0.85. */
|
|
29
41
|
compactScale?: number;
|
|
42
|
+
/** Opt-in play-phase gate: render the HUD only during these phases (e.g. `["playing"]`). Omit for always-visible (default). */
|
|
43
|
+
showDuring?: readonly GamePhase[];
|
|
30
44
|
className?: string;
|
|
31
45
|
style?: CSSProperties;
|
|
32
46
|
children?: ReactNode;
|
|
33
|
-
}): import("react").JSX.Element;
|
|
47
|
+
}): import("react").JSX.Element | null;
|
|
34
48
|
/**
|
|
35
49
|
* A HUD block that lives in one of the nine anchor regions. Panels sharing a
|
|
36
50
|
* region stack outward from the screen edge in ascending `order`. On fine
|
|
@@ -38,7 +52,7 @@ export declare function HudCanvas({ layout, editChord, compactScale, className,
|
|
|
38
52
|
* leaves the flow and keeps its custom placement. On compact displays custom
|
|
39
53
|
* placements are ignored and the `compact` behavior applies.
|
|
40
54
|
*/
|
|
41
|
-
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, className, style, children, }: {
|
|
55
|
+
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
|
|
42
56
|
id: string;
|
|
43
57
|
anchor?: HudAnchor;
|
|
44
58
|
/** Stack position within the region, ascending outward from the screen edge. Default 0. */
|
|
@@ -49,12 +63,33 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
|
|
|
49
63
|
chip?: string;
|
|
50
64
|
/** `false` lets pointer events pass through to the game (read-only panels). Default true. */
|
|
51
65
|
interactive?: boolean;
|
|
52
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Fallback pixel inset from the anchor when the scene document has no `ui.panels[id]`.
|
|
68
|
+
* Document layout is the source of truth once authored; TSX props are fallback-only.
|
|
69
|
+
*/
|
|
53
70
|
inset?: {
|
|
54
71
|
x: number;
|
|
55
72
|
y: number;
|
|
56
73
|
};
|
|
57
74
|
locked?: boolean;
|
|
75
|
+
/** Opt-in play-phase gate: render this panel only during these phases. Omit for always-visible (default). */
|
|
76
|
+
showDuring?: readonly GamePhase[];
|
|
77
|
+
/** Gameplay-importance tier, surfaced to layout tooling. `critical` stays visible; `tertiary` is the first to fold away. */
|
|
78
|
+
priority?: HudPriority;
|
|
79
|
+
/** How the panel adapts on mobile. `"hidden"` unmounts on phones; `"transient"` softens its collision policy; others tag the element for game CSS. */
|
|
80
|
+
mobileBehavior?: MobileHudBehavior;
|
|
81
|
+
/** Region ids this panel may overlap without a forbidden-collision report. */
|
|
82
|
+
allowOverlapWith?: readonly string[];
|
|
83
|
+
/** Regions sharing a non-empty group never collide with each other. */
|
|
84
|
+
collisionGroup?: string;
|
|
85
|
+
/** Register this panel as a collision-tracked layout region. Default true. */
|
|
86
|
+
region?: boolean;
|
|
87
|
+
/** Fallback width when the document has not authored a size. */
|
|
88
|
+
width?: number;
|
|
89
|
+
/** Fallback height when the document has not authored a size. */
|
|
90
|
+
height?: number;
|
|
91
|
+
/** Panel type id ({@link registerHudPanelType}) — drives resize axes + ParamSchema. */
|
|
92
|
+
type?: string;
|
|
58
93
|
className?: string;
|
|
59
94
|
style?: CSSProperties;
|
|
60
95
|
children?: ReactNode;
|
package/dist/hudLayout.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, } from "@jgengine/core/ui/hudLayout";
|
|
4
|
+
import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, registerActiveHudLayout, } from "@jgengine/core/ui/hudLayout";
|
|
5
|
+
import { resizePanelSize, resolvePanelResize, } from "@jgengine/core/ui/hudDocument";
|
|
5
6
|
import { hudScaleForViewport, overflowingPanels, resolveHudFit } from "@jgengine/core/ui/hudScale";
|
|
7
|
+
import { isMobileMode, } from "@jgengine/core/ui/gameLayout";
|
|
6
8
|
import { useDisplayProfile } from "./display.js";
|
|
7
9
|
import { useEngineState } from "./engineStore.js";
|
|
10
|
+
import { useGameLayoutMode, useRegisterLayoutRegion } from "./gameViewport.js";
|
|
11
|
+
import { useOptionalGamePhase } from "./hooks.js";
|
|
8
12
|
import { useHudViewport } from "./hudViewport.js";
|
|
13
|
+
/** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
|
|
14
|
+
export function hudVisibleInPhase(showDuring, phase) {
|
|
15
|
+
return showDuring === undefined || showDuring.includes(phase);
|
|
16
|
+
}
|
|
9
17
|
const STORAGE_PREFIX = "jg:hud:";
|
|
10
18
|
const DRAG_THRESHOLD_PX = 4;
|
|
11
19
|
const PERSIST_DELAY_MS = 200;
|
|
@@ -13,18 +21,73 @@ const EDIT_BAR_Z = 100000;
|
|
|
13
21
|
const REGION_GAP = 10;
|
|
14
22
|
const COMPACT_HUD_SCALE = 0.85;
|
|
15
23
|
const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
|
|
24
|
+
function patchDocumentUi(id, panel) {
|
|
25
|
+
if (typeof window === "undefined")
|
|
26
|
+
return;
|
|
27
|
+
const host = window.__jgengineEditorHost;
|
|
28
|
+
if (host === undefined)
|
|
29
|
+
return;
|
|
30
|
+
host.handle({
|
|
31
|
+
method: "dispatch",
|
|
32
|
+
command: { type: "setUiPanel", id, patch: panel },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function sizeStyle(panel) {
|
|
36
|
+
if (panel === undefined)
|
|
37
|
+
return {};
|
|
38
|
+
return {
|
|
39
|
+
...(panel.width === undefined ? {} : { width: panel.width }),
|
|
40
|
+
...(panel.height === undefined ? {} : { height: panel.height }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function ResizeHandles({ axes, onResizeStart, }) {
|
|
44
|
+
if (axes === "none")
|
|
45
|
+
return null;
|
|
46
|
+
const handle = (edge, cursor, style) => (_jsx("div", { "data-hud-resize": edge, onPointerDown: (event) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
onResizeStart(edge, event);
|
|
49
|
+
}, style: {
|
|
50
|
+
position: "absolute",
|
|
51
|
+
zIndex: 3,
|
|
52
|
+
pointerEvents: "auto",
|
|
53
|
+
background: "rgba(255, 255, 255, 0.85)",
|
|
54
|
+
cursor,
|
|
55
|
+
...style,
|
|
56
|
+
} }, edge));
|
|
57
|
+
return (_jsxs(_Fragment, { children: [axes === "x" || axes === "both"
|
|
58
|
+
? handle("e", "ew-resize", { top: 4, bottom: 4, right: -3, width: 6 })
|
|
59
|
+
: null, axes === "y" || axes === "both"
|
|
60
|
+
? handle("s", "ns-resize", { left: 4, right: 4, bottom: -3, height: 6 })
|
|
61
|
+
: null, axes === "both" ? handle("se", "nwse-resize", { right: -4, bottom: -4, width: 10, height: 10 }) : null] }));
|
|
62
|
+
}
|
|
16
63
|
export function useHudLayout(options) {
|
|
17
64
|
const storageKey = options?.storageKey;
|
|
18
65
|
const snap = options?.snap;
|
|
19
66
|
const locked = options?.locked;
|
|
67
|
+
const documentUi = options?.documentUi;
|
|
68
|
+
const documentPatches = options?.documentPatches !== false;
|
|
20
69
|
const layout = useMemo(() => {
|
|
21
|
-
const store = createHudLayout({
|
|
22
|
-
|
|
70
|
+
const store = createHudLayout({
|
|
71
|
+
snap,
|
|
72
|
+
locked,
|
|
73
|
+
onDocumentPatch: documentPatches ? patchDocumentUi : undefined,
|
|
74
|
+
});
|
|
75
|
+
if (documentUi !== undefined) {
|
|
76
|
+
store.applyDocumentUi(documentUi);
|
|
77
|
+
}
|
|
78
|
+
else if (storageKey !== undefined && typeof localStorage !== "undefined") {
|
|
23
79
|
store.hydrate(localStorage.getItem(STORAGE_PREFIX + storageKey));
|
|
24
80
|
}
|
|
25
81
|
return store;
|
|
26
|
-
}, [storageKey, snap, locked]);
|
|
82
|
+
}, [storageKey, snap, locked, documentPatches]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (documentUi !== undefined) {
|
|
85
|
+
layout.applyDocumentUi(documentUi);
|
|
86
|
+
}
|
|
87
|
+
}, [layout, documentUi]);
|
|
27
88
|
useEffect(() => {
|
|
89
|
+
if (documentUi !== undefined)
|
|
90
|
+
return;
|
|
28
91
|
if (storageKey === undefined || typeof localStorage === "undefined")
|
|
29
92
|
return;
|
|
30
93
|
let timer = null;
|
|
@@ -43,7 +106,7 @@ export function useHudLayout(options) {
|
|
|
43
106
|
localStorage.setItem(STORAGE_PREFIX + storageKey, layout.serialize());
|
|
44
107
|
}
|
|
45
108
|
};
|
|
46
|
-
}, [layout, storageKey]);
|
|
109
|
+
}, [layout, storageKey, documentUi]);
|
|
47
110
|
return layout;
|
|
48
111
|
}
|
|
49
112
|
const HudCanvasContext = createContext(null);
|
|
@@ -125,8 +188,9 @@ function edgePad(envInset, extra, basePx, scale) {
|
|
|
125
188
|
* (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
|
|
126
189
|
* whole surface scales down and each panel applies its `compact` behavior.
|
|
127
190
|
*/
|
|
128
|
-
export function HudCanvas({ layout, editChord, compactScale, className, style, children, }) {
|
|
191
|
+
export function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }) {
|
|
129
192
|
const canvasRef = useRef(null);
|
|
193
|
+
const phase = useOptionalGamePhase();
|
|
130
194
|
const { compact } = useDisplayProfile();
|
|
131
195
|
const hudViewport = useHudViewport();
|
|
132
196
|
const fitEnabled = hudViewport?.fitEnabled === true;
|
|
@@ -152,6 +216,7 @@ export function HudCanvas({ layout, editChord, compactScale, className, style, c
|
|
|
152
216
|
}, []);
|
|
153
217
|
const value = useMemo(() => ({ layout, canvasRef, regions, compact }), [layout, regions, compact]);
|
|
154
218
|
const editing = useEngineState(layout).editing;
|
|
219
|
+
useEffect(() => registerActiveHudLayout(layout), [layout]);
|
|
155
220
|
const chordEnabled = editChord !== false;
|
|
156
221
|
const hold = editChord === false ? undefined : (editChord?.hold ?? "F2");
|
|
157
222
|
const press = editChord === false ? undefined : (editChord?.press ?? "KeyC");
|
|
@@ -286,6 +351,8 @@ export function HudCanvas({ layout, editChord, compactScale, className, style, c
|
|
|
286
351
|
"--jg-hud-pad-right": edgePad("env(safe-area-inset-right, 0px)", null, basePad, scale),
|
|
287
352
|
...style,
|
|
288
353
|
};
|
|
354
|
+
if (!hudVisibleInPhase(showDuring, phase))
|
|
355
|
+
return null;
|
|
289
356
|
return (_jsx(HudCanvasContext.Provider, { value: value, children: _jsxs("div", { ref: canvasRef, "data-hud-canvas": "", "data-hud-editing": editing ? "" : undefined, className: className, style: canvasStyle, children: [HUD_ANCHORS.map((anchor) => (_jsx("div", { ref: regionRefs[anchor], "data-hud-region": anchor, style: {
|
|
290
357
|
position: "absolute",
|
|
291
358
|
display: "flex",
|
|
@@ -334,29 +401,54 @@ function HudChip({ label, anchor, children, }) {
|
|
|
334
401
|
* leaves the flow and keeps its custom placement. On compact displays custom
|
|
335
402
|
* placements are ignored and the `compact` behavior applies.
|
|
336
403
|
*/
|
|
337
|
-
export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, className, style, children, }) {
|
|
404
|
+
export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }) {
|
|
338
405
|
const ctx = useContext(HudCanvasContext);
|
|
406
|
+
const phase = useOptionalGamePhase();
|
|
407
|
+
const layoutMode = useGameLayoutMode();
|
|
339
408
|
if (ctx === null)
|
|
340
409
|
throw new Error("HudPanel must be rendered inside a HudCanvas");
|
|
341
410
|
const { layout, canvasRef, regions, compact } = ctx;
|
|
411
|
+
const onMobile = isMobileMode(layoutMode);
|
|
342
412
|
const registeredOnRef = useRef(null);
|
|
343
413
|
if (registeredOnRef.current !== layout) {
|
|
344
414
|
registeredOnRef.current = layout;
|
|
345
|
-
layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 })
|
|
415
|
+
layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 }), {
|
|
416
|
+
width,
|
|
417
|
+
height,
|
|
418
|
+
type,
|
|
419
|
+
});
|
|
346
420
|
}
|
|
347
421
|
const layoutState = useEngineState(layout);
|
|
348
422
|
const panel = layoutState.panels[id];
|
|
349
423
|
const draggable = !compact && locked !== true && isPanelDraggable(layoutState, id);
|
|
350
424
|
const editing = layoutState.editing && draggable;
|
|
425
|
+
const resizeSpec = resolvePanelResize(panel?.type ?? type);
|
|
426
|
+
const resizable = editing && resizeSpec.axes !== "none";
|
|
351
427
|
const rootRef = useRef(null);
|
|
352
428
|
const dragRef = useRef(null);
|
|
353
429
|
const suppressClickRef = useRef(false);
|
|
354
430
|
const detachRef = useRef(null);
|
|
355
431
|
const [dragging, setDragging] = useState(false);
|
|
356
432
|
useEffect(() => () => detachRef.current?.(), []);
|
|
433
|
+
const regionHidden = panel === undefined ||
|
|
434
|
+
panel.visible === false ||
|
|
435
|
+
(compact && compactMode === "hide") ||
|
|
436
|
+
(onMobile && mobileBehavior === "hidden") ||
|
|
437
|
+
!hudVisibleInPhase(showDuring, phase);
|
|
438
|
+
const regionSpec = useMemo(() => ({
|
|
439
|
+
id,
|
|
440
|
+
kind: "hud",
|
|
441
|
+
collisionPolicy: (mobileBehavior === "transient" ? "warn" : "forbid"),
|
|
442
|
+
priority,
|
|
443
|
+
allowOverlapWith,
|
|
444
|
+
collisionGroup,
|
|
445
|
+
}), [id, mobileBehavior, priority, allowOverlapWith, collisionGroup]);
|
|
446
|
+
useRegisterLayoutRegion(regionSpec, rootRef, region && !regionHidden);
|
|
357
447
|
const onPointerDown = (event) => {
|
|
358
448
|
if (!draggable || event.button !== 0)
|
|
359
449
|
return;
|
|
450
|
+
if (event.target?.closest?.("[data-hud-resize]") != null)
|
|
451
|
+
return;
|
|
360
452
|
const root = rootRef.current;
|
|
361
453
|
const canvas = canvasRef.current;
|
|
362
454
|
if (root === null || canvas === null)
|
|
@@ -413,6 +505,44 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
413
505
|
window.addEventListener("pointerup", onEnd);
|
|
414
506
|
window.addEventListener("pointercancel", onEnd);
|
|
415
507
|
};
|
|
508
|
+
const onResizeStart = (edge, event) => {
|
|
509
|
+
if (!resizable || event.button !== 0)
|
|
510
|
+
return;
|
|
511
|
+
const root = rootRef.current;
|
|
512
|
+
if (root === null)
|
|
513
|
+
return;
|
|
514
|
+
const startWidth = panel?.width ?? root.getBoundingClientRect().width;
|
|
515
|
+
const startHeight = panel?.height ?? root.getBoundingClientRect().height;
|
|
516
|
+
const pointerId = event.pointerId;
|
|
517
|
+
const startX = event.clientX;
|
|
518
|
+
const startY = event.clientY;
|
|
519
|
+
suppressClickRef.current = true;
|
|
520
|
+
layout.bringToFront(id);
|
|
521
|
+
const onMove = (moveEvent) => {
|
|
522
|
+
if (moveEvent.pointerId !== pointerId)
|
|
523
|
+
return;
|
|
524
|
+
const dw = edge === "s" ? 0 : moveEvent.clientX - startX;
|
|
525
|
+
const dh = edge === "e" ? 0 : moveEvent.clientY - startY;
|
|
526
|
+
const next = resizePanelSize({ width: startWidth, height: startHeight }, { dw, dh }, resizeSpec.axes, resizeSpec);
|
|
527
|
+
layout.resize(id, next);
|
|
528
|
+
};
|
|
529
|
+
const onEnd = (endEvent) => {
|
|
530
|
+
if (endEvent.pointerId !== pointerId)
|
|
531
|
+
return;
|
|
532
|
+
detachRef.current?.();
|
|
533
|
+
};
|
|
534
|
+
detachRef.current?.();
|
|
535
|
+
const detach = () => {
|
|
536
|
+
window.removeEventListener("pointermove", onMove);
|
|
537
|
+
window.removeEventListener("pointerup", onEnd);
|
|
538
|
+
window.removeEventListener("pointercancel", onEnd);
|
|
539
|
+
detachRef.current = null;
|
|
540
|
+
};
|
|
541
|
+
detachRef.current = detach;
|
|
542
|
+
window.addEventListener("pointermove", onMove);
|
|
543
|
+
window.addEventListener("pointerup", onEnd);
|
|
544
|
+
window.addEventListener("pointercancel", onEnd);
|
|
545
|
+
};
|
|
416
546
|
const onClickCapture = (event) => {
|
|
417
547
|
if (!suppressClickRef.current)
|
|
418
548
|
return;
|
|
@@ -432,11 +562,18 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
432
562
|
color: "rgba(255, 255, 255, 0.9)",
|
|
433
563
|
font: "10px/1.5 system-ui, sans-serif",
|
|
434
564
|
whiteSpace: "nowrap",
|
|
435
|
-
}, children: id })] })) : null;
|
|
565
|
+
}, children: id }), resizable ? _jsx(ResizeHandles, { axes: resizeSpec.axes, onResizeStart: onResizeStart }) : null] })) : null;
|
|
436
566
|
if (panel === undefined)
|
|
437
567
|
return null;
|
|
568
|
+
if (panel.visible === false)
|
|
569
|
+
return null;
|
|
438
570
|
if (compact && compactMode === "hide")
|
|
439
571
|
return null;
|
|
572
|
+
if (onMobile && mobileBehavior === "hidden")
|
|
573
|
+
return null;
|
|
574
|
+
if (!hudVisibleInPhase(showDuring, phase))
|
|
575
|
+
return null;
|
|
576
|
+
const authoredSize = sizeStyle(panel);
|
|
440
577
|
const flow = compact || !panel.moved;
|
|
441
578
|
if (flow) {
|
|
442
579
|
const regionEl = regions[compact ? anchor : panel.placement.anchor];
|
|
@@ -444,7 +581,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
444
581
|
return null;
|
|
445
582
|
const chipped = compact && compactMode === "chip";
|
|
446
583
|
const passThrough = interactive === false && !editing && !chipped;
|
|
447
|
-
return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
584
|
+
return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
448
585
|
position: "relative",
|
|
449
586
|
order: order ?? 0,
|
|
450
587
|
pointerEvents: passThrough ? "none" : "auto",
|
|
@@ -453,6 +590,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
453
590
|
cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
|
|
454
591
|
outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
|
|
455
592
|
outlineOffset: editing ? 2 : undefined,
|
|
593
|
+
boxSizing: "border-box",
|
|
594
|
+
...authoredSize,
|
|
456
595
|
...style,
|
|
457
596
|
}, children: [chipped ? (_jsx(HudChip, { label: chip ?? id, anchor: anchor, children: children })) : (children), editingChrome] }), regionEl);
|
|
458
597
|
}
|
|
@@ -473,7 +612,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
473
612
|
if (fx === 0.5 || fy === 0.5) {
|
|
474
613
|
position.transform = `translate(${fx === 0.5 ? "-50%" : "0"}, ${fy === 0.5 ? "-50%" : "0"})`;
|
|
475
614
|
}
|
|
476
|
-
return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
615
|
+
return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
477
616
|
position: "absolute",
|
|
478
617
|
...position,
|
|
479
618
|
zIndex: panel.z,
|
|
@@ -483,6 +622,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
483
622
|
cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
|
|
484
623
|
outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
|
|
485
624
|
outlineOffset: editing ? 2 : undefined,
|
|
625
|
+
boxSizing: "border-box",
|
|
626
|
+
...authoredSize,
|
|
486
627
|
...style,
|
|
487
628
|
}, children: [children, editingChrome] }));
|
|
488
629
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,12 +5,22 @@ export * from "./chatBubbles.js";
|
|
|
5
5
|
export * from "./voice.js";
|
|
6
6
|
export * from "./social.js";
|
|
7
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./store.js";
|
|
8
9
|
export * from "./settings.js";
|
|
10
|
+
export * from "./save.js";
|
|
9
11
|
export * from "./components.js";
|
|
12
|
+
export * from "./hud.js";
|
|
13
|
+
export * from "./keyHint.js";
|
|
14
|
+
export * from "./startScreen.js";
|
|
10
15
|
export * from "./engineStore.js";
|
|
11
16
|
export * from "./dragLayer.js";
|
|
17
|
+
export * from "./cards.js";
|
|
18
|
+
export * from "./actionHud.js";
|
|
19
|
+
export * from "./selectionHud.js";
|
|
12
20
|
export * from "./hudLayout.js";
|
|
13
21
|
export * from "./hudViewport.js";
|
|
22
|
+
export * from "./gameViewport.js";
|
|
23
|
+
export * from "./rotateDevice.js";
|
|
14
24
|
export * from "./display.js";
|
|
15
25
|
export * from "./map.js";
|
|
16
26
|
export * from "./preview.js";
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,22 @@ export * from "./chatBubbles.js";
|
|
|
5
5
|
export * from "./voice.js";
|
|
6
6
|
export * from "./social.js";
|
|
7
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./store.js";
|
|
8
9
|
export * from "./settings.js";
|
|
10
|
+
export * from "./save.js";
|
|
9
11
|
export * from "./components.js";
|
|
12
|
+
export * from "./hud.js";
|
|
13
|
+
export * from "./keyHint.js";
|
|
14
|
+
export * from "./startScreen.js";
|
|
10
15
|
export * from "./engineStore.js";
|
|
11
16
|
export * from "./dragLayer.js";
|
|
17
|
+
export * from "./cards.js";
|
|
18
|
+
export * from "./actionHud.js";
|
|
19
|
+
export * from "./selectionHud.js";
|
|
12
20
|
export * from "./hudLayout.js";
|
|
13
21
|
export * from "./hudViewport.js";
|
|
22
|
+
export * from "./gameViewport.js";
|
|
23
|
+
export * from "./rotateDevice.js";
|
|
14
24
|
export * from "./display.js";
|
|
15
25
|
export * from "./map.js";
|
|
16
26
|
export * from "./preview.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A single keyboard/mouse key rendered as a cap. Tagged `data-jg-kbd` so tooling
|
|
4
|
+
* can find it; on its own it stays visible (hiding a lone cap would strand its
|
|
5
|
+
* label). Wrap keys + their meaning in `KeyHint` to make the whole hint vanish
|
|
6
|
+
* on touch.
|
|
7
|
+
*
|
|
8
|
+
* @capability keycap styled keyboard/mouse key cap for control hints
|
|
9
|
+
*/
|
|
10
|
+
export declare function Keycap({ children, className, style, }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing
|
|
17
|
+
* on coarse-pointer devices — a touchscreen has no keyboard, so the hint is
|
|
18
|
+
* noise and only fights the on-screen controls for space. The `data-jg-kbd-hint`
|
|
19
|
+
* marker is also hidden by an engine stylesheet as a hydration-safe backstop, so
|
|
20
|
+
* a hint stays hidden on touch even before this component's effect runs. Put the
|
|
21
|
+
* touch equivalent (a `TouchControls` dock, a tappable button) on the coarse
|
|
22
|
+
* branch instead.
|
|
23
|
+
*
|
|
24
|
+
* @capability key-hint keyboard/mouse control hint that hides itself on touch
|
|
25
|
+
*/
|
|
26
|
+
export declare function KeyHint({ children, className, style, }: {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
}): import("react").JSX.Element | null;
|
package/dist/keyHint.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { useDisplayProfile } from "./display.js";
|
|
4
|
+
/**
|
|
5
|
+
* A single keyboard/mouse key rendered as a cap. Tagged `data-jg-kbd` so tooling
|
|
6
|
+
* can find it; on its own it stays visible (hiding a lone cap would strand its
|
|
7
|
+
* label). Wrap keys + their meaning in `KeyHint` to make the whole hint vanish
|
|
8
|
+
* on touch.
|
|
9
|
+
*
|
|
10
|
+
* @capability keycap styled keyboard/mouse key cap for control hints
|
|
11
|
+
*/
|
|
12
|
+
export function Keycap({ children, className, style, }) {
|
|
13
|
+
return (_jsx("kbd", { "data-jg-kbd": "", className: className, style: style, children: children }));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing
|
|
17
|
+
* on coarse-pointer devices — a touchscreen has no keyboard, so the hint is
|
|
18
|
+
* noise and only fights the on-screen controls for space. The `data-jg-kbd-hint`
|
|
19
|
+
* marker is also hidden by an engine stylesheet as a hydration-safe backstop, so
|
|
20
|
+
* a hint stays hidden on touch even before this component's effect runs. Put the
|
|
21
|
+
* touch equivalent (a `TouchControls` dock, a tappable button) on the coarse
|
|
22
|
+
* branch instead.
|
|
23
|
+
*
|
|
24
|
+
* @capability key-hint keyboard/mouse control hint that hides itself on touch
|
|
25
|
+
*/
|
|
26
|
+
export function KeyHint({ children, className, style, }) {
|
|
27
|
+
const { coarsePointer } = useDisplayProfile();
|
|
28
|
+
if (coarsePointer)
|
|
29
|
+
return null;
|
|
30
|
+
return (_jsx("span", { "data-jg-kbd-hint": "", className: className, style: style, children: children }));
|
|
31
|
+
}
|