@jgengine/react 0.24.0 → 0.26.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 +92 -0
- package/dist/engineStyles.d.ts +6 -3
- package/dist/engineStyles.js +13 -7
- package/dist/hudChromePreview.js +2 -2
- package/dist/hudLayout.js +3 -1
- package/dist/hudPanel.d.ts +15 -29
- package/dist/hudPanel.js +5 -30
- package/dist/hudSurface.d.ts +38 -0
- package/dist/hudSurface.js +40 -0
- package/dist/modals.d.ts +2 -2
- package/dist/modals.js +5 -5
- package/dist/notices.d.ts +18 -3
- package/dist/notices.js +65 -11
- package/dist/panels.js +2 -2
- package/dist/styles.css +7 -4
- package/package.json +2 -2
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-ui/SKILL.md +3 -3
- package/skills/jgengine-ui/api.md +98 -31
- package/skills/jgengine-ui/capabilities.md +15 -1
- package/skills/jgengine-ui/reference-primitives.md +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,98 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.26.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.26.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.23.0`; `@jgengine/github` is `0.13.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createLogBus` (`@jgengine/core/devtools/logBus`): a fixed-capacity ring of log lines tagged by owning system, with a separate ring for warnings and errors so a crash line is never pushed out by info traffic. `devtools.logs` now writes through it (`write(level, system, message)`, `snapshot()`, `counts()`, `dropped()`); `DevtoolsLogEntry` gained `system`.
|
|
38
|
+
- `devtools.probes.registerSystem(id, { counters, timers, state })`: a system reports its own counters, named timings and one state line in a few lines, with no shell wiring. Readings surface as `systems` in `devtools.snapshot()`, in `debug_snapshot`, and in every perf record.
|
|
39
|
+
- `PerfReport` (`@jgengine/core/devtools/report`): the one perf record shape shared by `bun run perf` and the in-page report.
|
|
40
|
+
- The shell samples frame telemetry in-page in dev builds: `window.__JG.report()` returns that record, and the page pushes it about once a second to the dev server's `GET/POST /__jgengine/report` route. `bun run debug <game>` prints the record of a game already open and exits non-zero on a budget breach in `scripts/perf-budget.json`.
|
|
41
|
+
- `createNeedSet` (`@jgengine/core/gameplay`) drains named needs on game time and reads them by index — decaying hunger, energy, fun with snapshot/restore and per-need rate retuning.
|
|
42
|
+
- `createUtilityBrain` (`@jgengine/core/world`) picks an action from decaying-need appeal: linear, quadratic, logistic, exponential and threshold response curves, candidate cost (travel, queues, price), a switch margin plus minimum hold (hysteresis), declared interrupts, optional seeded jitter, and a devtools probe string.
|
|
43
|
+
- `createNavRuntime` (`@jgengine/core/world`) consumes an editor-baked nav mesh: reads the `nav` bake off a scene document (`navMeshFromBake`), routes with the mesh query, resolves authored doors, ladders and jumps (`AuthoredNavLink`, priced by kind) onto the current surface, and swaps in a new bake after an edit while matching polygon identity by geometry — `applyMesh` returns a `remap` so only routes over changed polygons need re-planning.
|
|
44
|
+
- `createNavCrowd` (`@jgengine/core/world`) moves many agents across that mesh: plans a corridor, follows the funnel-smoothed waypoints, keeps agents apart with reciprocal velocity obstacle avoidance, bounds path searches per tick by count and milliseconds, fans converging agents out with `spread`, and round-trips through `snapshot`/`restore`.
|
|
45
|
+
- `createRvoCrowd` (`@jgengine/core/world`) is the avoidance solver on its own: RVO/ORCA half-plane solve over a uniform-grid neighbour search capped at `maxNeighbors`, deterministic tie-breaking, overlap relief, retunable `timeHorizon`/`neighborDist`/`maxNeighbors`/`sideBias`/`overlapRelief`, and `snapshot`/`restore` for saves and replays.
|
|
46
|
+
- `resolveRoutine` (`@jgengine/core/gameplay`) resolves an authored day into real arrival and departure times against the world clock. Travel time is injected per stop or as a callback, so a stop departs early enough to walk to the next one and an arrival that runs late slips instead of teleporting; `phaseAt(time)` returns the held place, the walking leg and its progress, and `nextChangeAfter(time)` drives a scheduler. A day whose walks cannot fit its cycle throws at construction.
|
|
47
|
+
- `createOffScreenWorld` (`@jgengine/core/gameplay`) keeps a whole population without simulating it: residents beyond the injected radius are cheap state (position, routine phase, needs) that only moves when the clock crosses a routine boundary, and they spawn into the game's own near simulation through a `NearAgents` interface when they come close. The radius, an optional per-agent cost model, a near budget and a per-tick switch budget are all injected; `snapshot`/`restore` round-trip the whole population, and crossing the boundary preserves identity, schedule progress, need values and position.
|
|
48
|
+
- `createNearAgents` (`@jgengine/core/gameplay`) is the default near half of that seam: a nav crowd with a need set and utility brain per agent, goal-tracking as routines move residents between places, and a decision counter for tests and probes.
|
|
49
|
+
- `createPerception` shares squad knowledge: `squadMates(observerId)` names an observer's roster, and a sighting one member makes is written into every mate's memory with `sourceId` (who saw it) and `sourceTick` (the observe pass it was recorded on). A mate's own sighting at the same instant replaces the hand-off. `maxSharesPerTick` (default 64, per observer pass) bounds a large roster and reports elided shares.
|
|
50
|
+
- Perception memory is auditable without guessing: `memoryOf(observerId, targetId)` returns the one remembered target with its confidence right now or `null` once it has faded, and `stats()` counts pair checks, range prunes, cone tests, line-of-sight calls, sightings, heard stimuli, shares and dropped shares. The check order is range → cone → line of sight, so occlusion work only sees the pruned set. `resetStats()` opens a measurement window; `memories` are pruned per observer so a 200-observer pass does not rescan every memory.
|
|
51
|
+
- `devtoolsName` registers a probe showing observers, remembered targets and the work counters, and `dispose()` removes it.
|
|
52
|
+
- `createCoverField` and `coverPointsFromSolids` (`@jgengine/core/combat`) derive stand-able cover from authored solid boxes instead of a coordinate list: one row of points per vertical face, clear of the corners, filtered by an injected `walkable` test, rebuilt when the world is edited, and queried through a bounded `near(position, radius, limit)`. `CoverSource` is the seam, so a game with its own spatial index can supply cover without the box generator.
|
|
53
|
+
- `selectCover` / `scoreCoverPoint` choose cover by threat: `hidden` is the confidence-weighted share of remembered threats whose line of sight the point (and a body-sized footprint at it) blocks, scored against the role's preferred fighting range, flank angle around the threat, distance, a squadmate's claim on the point, and whether the walk to it is a detour. Fading memories stop steering at `minConfidence`, and a point that hides from only one of two shooters is not cover.
|
|
54
|
+
- `createSquadCombat` (`@jgengine/core/combat`) drives a squad over existing seams: role-driven cover selection, fire discipline (hold, lean out, shoot), suppression from `underFire()`, retreat below a role's health threshold, and relocation under sustained fire. Threat comes only from `threatsFromMemory`, i.e. the perception memory the game already keeps; movement goes through `NavCrowd` with a tighter arrival tolerance so a body actually reaches the stand point. Serializable through `snapshot()`/`restore()` (member decisions, crowd, tuning), retunable, and a `devtoolsName` probe reports pins, peeks, shots and the worst exposure.
|
|
55
|
+
- `assignSquadRoles` and `threatsFromMemory` (`@jgengine/core/combat`): deterministic role assignment over a caller-authored `SquadComposition` (role ids, counts, ranges, flank angles, retreat thresholds and cover weights are all content), and a bounded, confidence-ranked threat list from one observer's memory.
|
|
56
|
+
- A 2v4 arena soak proves the behavior: 90 simulated seconds, 2700 ticks, 4 squad members against 2 scripted raiders, worst exposure 2.33 s (bar 3 s), 0 of 38 exposure spells over 3 s, per-tick cost p50 0.05 ms / p95 0.19 ms.
|
|
57
|
+
- `createDamageRuntime` (`@jgengine/core/combat`) composes a damage table, hit zones, the armor curve and carried statuses into one stateful model: `hit()` returns every stage of the number (`base × zone multiplier × type multiplier × (1 − armor mitigation)`), `tick()` runs status effects through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones or status definitions without dropping either; a `devtoolsName` probe reports hits, crits, ticks and the last hit.
|
|
58
|
+
- A damage table (`combat/damageTable`) of damage type × resistance class, with `armor / (armor + halfValue)` mitigation — armor equal to the half value halves the hit — a per-type `bypassArmor`, and `readDamageTable` / `formatDamageTable` to read every row at given armor values. An unknown damage type throws instead of silently dealing 1×.
|
|
59
|
+
- Hit zones and crits (`combat/hitZones`): a zone carries a normal multiplier and an optional crit multiplier, `resolveHitZone` rolls once and returns one multiplier, and a crit replaces the zone multiplier rather than compounding with it. `hitZoneMultiplier` quotes the same number without a roll.
|
|
60
|
+
- `createStatusRuntime` (`combat/statusRuntime`): data-defined statuses with the existing `StatusStackPolicy` (`refresh`, stack to `max`, `replace`, `ignore`) as the only thing that changes stacks, per-tick effects of `{ kind: "damage", channel }` or `{ kind: "stat", stat }`, damage routed through an injected `applyDamage` so resistances and armor still apply, bounded ticks per call, and plain-data state out and back.
|
|
61
|
+
- Numbers from the tests: 25 base kinetic against a 150 HP target, 0.15 s between shots — 8 shots / 1.05 s unarmored, 20 shots / 2.85 s at 60 armor, 36 shots / 5.25 s at 200 armor; two stacked burns of 4 per tick deal 8, not 16; a 1.5× head zone with a 2.5× crit multiplier deals 2.5×, not 3.75×.
|
|
62
|
+
- `createWeaponSystem` (`@jgengine/core/combat`) runs one `WeaponDefinition`: single, automatic or burst fire, hitscan or ballistic projectiles, recoil that moves the aim the shot flies down, spread bloom, interrupted/resumed reloads, a readable ammo economy (`ammo()`, `stats()`), `snapshot()`/`restore()` and a devtools probe. Every hit, hitscan or projectile, resolves through `createDamageRuntime`. Pellet work is bounded per tick (`maxShotsPerTick`, default 4) and per weapon (`maxInFlight`, default 32).
|
|
63
|
+
- `WeaponDefinition` (`@jgengine/core/combat/weaponDefinition`) as plain JSON — rate, fire mode, delivery, magazine, handling, damage, falloff — with `assertWeaponDefinition`, `weaponIntervalMs`, `weaponDamageAt` and `weaponMaxInFlight`. A generated gun is the same shape.
|
|
64
|
+
- `Magazine.pauseReload()` / `resumeReload()` / `isPaused()` suspend a reload and keep its progress; a paused reload is not `isReloading()`, does not advance in `tick`, and round-trips through `snapshot()`/`restore()`.
|
|
65
|
+
- `environment.grading` on the scene document authors a per-game colour grade: shadow/midtone/highlight swatches plus tint, saturation and contrast bake into a small display-space LUT (`lutCubeFromGrading`, `colorGradeFromDocument` in `@jgengine/core/editor/environment`), edited in the Lighting workspace. The shell applies it from `editorLayers`, and a graded game needs no LUT asset or hardcoded pass.
|
|
66
|
+
- Clamped auto-exposure: `postProcessing.autoExposure: { min, max, adaptationSeconds } | false` meters the pre-tone-map frame every four frames toward 18% grey and stays inside the authored range. `WHITE_POINT` and `MID_GREY` in `@jgengine/core/render/colorPipeline` are the single documented calibration the pipeline uses.
|
|
67
|
+
- `GradeConfig.lut` also accepts a baked `{ cube: { size, data } }` LUT, sampled trilinearly after the numeric grade.
|
|
68
|
+
- `MATERIAL_LIBRARY` (`@jgengine/assets/catalogs/materialLibrary`) names 56 PBR surfaces — wet asphalt, painted metal, stucco, brick, wood, foliage with cut-out coverage and sheen, glass, water, sand, concrete and more — each mapping a name to an ambientCG source, a world tile size in metres and a physical response. `libraryMaterialAsset("asphalt/wet")` returns the editable `MaterialAsset` those names resolve to, and `filterLibraryMaterials` backs pickers. `assets pull library` pulls every pinned map set.
|
|
69
|
+
- `TriplanarAssetMaterial` / `useTriplanarAssetMaterial` / `applyTriplanarProjection` (`@jgengine/shell/render/triplanarMaterial`) render a material asset from world position, so world geometry without a `uv` attribute still gets colour, normal, roughness and AO maps. Tile size is world metres; instanced meshes project in instanced space.
|
|
70
|
+
- The editor's Materials workspace browses the library by name and writes the chosen entry into the document's material assets.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- `createPerception` (`@jgengine/core/world`, also `@jgengine/core/sensor/perception`) takes `isSegmentClear` instead of `occluded`: the game supplies collision as a "nothing blocks this segment" test, and it is only called for pairs already inside range and cone. **Migration:** flip the predicate — a `occluded: (from, to) => blocked(from, to)` becomes `isSegmentClear: (from, to) => !blocked(from, to)`.
|
|
75
|
+
- `MagazineSnapshot` gains an optional `paused: true` field, present only while a reload is suspended, so existing snapshots and idle shapes are unchanged.
|
|
76
|
+
- AgX is the pipeline's tone map: `postProcessing.toneMapping` defaults to `"agx"`, and the `cinematic`, `photoreal`, `comic`, `retro` and `STUDIO_STAGE_POST` configs now pick it too. `"toon"` keeps `"linear"` on purpose. Games that authored `"aces"` explicitly are unchanged.
|
|
77
|
+
- The grade pass samples LUT tiles at texel centres and blends the blue axis by hand, so a supplied strip LUT no longer bleeds across slices.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- `bun run debug <game>` now finds the server `bun dev` started. The website dev server publishes the same checkout marker the runner does (`scripts/devServerMarker.ts`, written at listen, removed on shutdown, dev builds only), and the marker records the host that answers, since Vite's default `localhost` can be IPv6-only where `127.0.0.1` hangs.
|
|
82
|
+
- The website dev server serves `GET/POST /__jgengine/report` too, so a game opened at `/games/<id>` reports from the `/play` runner it embeds.
|
|
83
|
+
- The in-page sampler installs when the dev report route answers, not on a build-time `PROD` check: the `/play` runner is a production build, so the old check silently disabled the report on `bun dev`.
|
|
84
|
+
- A report taken inside the sampler's warmup is no longer judged on link/framebuffer counts, which only mean something after the warmup baseline exists.
|
|
85
|
+
- `extractMaterialMaps` and `buildMaterialCatalog` now recognise ambientCG's `_Opacity` map, so cut-out foliage and decal scans keep their coverage instead of rendering as solid cards.
|
|
86
|
+
- Capture runs no longer leak a ~150 MB temp Chrome profile each. `shoot`, `drive` and `perf` delete their profile on every exit path — success, throw, watchdog and SIGINT — `bun run shoot` reclaims profiles a killed run left untouched for three hours, the capture guard sweeps the profile of a run it force-kills, and the generated `browser.mjs` harness for game repos removes its own. A capture also refuses to start when the temp drive has less free space than it needs.
|
|
87
|
+
- The post-processing chain, graphics settings and textured terrain material keep a stable identity across renders. Before, every render rebuilt the effect composer and recompiled shaders, which dropped games to single-digit FPS.
|
|
88
|
+
|
|
89
|
+
### Numbers
|
|
90
|
+
|
|
91
|
+
- Test fixture at 600 rpm, 30-round magazine: 2.9 s to empty; 17.0° pitch and 3.1° yaw climb over the magazine; back to half climb in 0.37 s and within 5% in 0.6 s; bloom 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot, settling to base 0.63 s after the last shot.
|
|
92
|
+
- A hitscan and a projectile contact at the same distance return the same damage (20 body, 30 on a 1.5x head zone) through one damage model; a 60 m/s projectile crosses 20 m in 0.33 s and drops 2.45 m in 0.5 s at 9.81 m/s².
|
|
93
|
+
|
|
94
|
+
## 0.25.0
|
|
95
|
+
|
|
96
|
+
### Migrate
|
|
97
|
+
|
|
98
|
+
- **Bump lockstep SDK packages to `^0.25.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.22.0`; `@jgengine/github` is `0.12.0`.
|
|
99
|
+
- The frame panel from `@jgengine/react/hudPanel` is now `HudSurface` in `@jgengine/react/hudSurface` (`HudSurfaceProps`, `HudSurfaceElevation`). `HudPanel`, `HudPanelProps` and `HudPanelElevation` from `@jgengine/react/hudPanel` stay as deprecated aliases for this release. The `HudCanvas` layout slot keeps the name `HudPanel`.
|
|
100
|
+
- Engine HUD control sounds (button click/hover, panel and window open/close) no longer turn off with `notices: false` or `notices.cues: false`. Set `audio.hudCues: false` to silence them; `notices.cues` now covers notice sounds only.
|
|
101
|
+
- The notice stack now defaults to z-index 85 (was 45), above `ModalHost`. Pass `notices.zIndex` to put it back under a game overlay.
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- `HudSurface` takes `as` (`section`, `aside`, `nav`, ...) to render as a landmark.
|
|
106
|
+
- `notices.offset`, `notices.avoidHud` and `notices.zIndex`, with matching `NoticeStack` props. The offset adds to the device safe-area inset.
|
|
107
|
+
- `ctx.game.notices.push({ text, silent: true })` shows a notice without its sound.
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
|
|
111
|
+
- The engine stylesheet (`@jgengine/react/styles.css` and the injected copy) sits in the `components.jgengine` cascade layer, after a `theme, base, components, utilities` layer order, so plain game CSS and Tailwind utilities override engine buttons and panels without `!important`.
|
|
112
|
+
- `HudSurface` padding comes from `--jg-panel-padding` (default 12px); the `padding` prop sets that variable instead of an inline `padding`, so a game class or media query can override it.
|
|
113
|
+
- `HudButton` labels wrap (`white-space: normal`, line height 1.2, 4px vertical padding) instead of overflowing on one line.
|
|
114
|
+
- The shell renders the notice stack inside the HUD theme root, so notices use the game's `hudTheme` colours and font.
|
|
115
|
+
- A top or bottom notice stack moves clear of `HudCanvas` panels and touch controls registered in its column (Harbor Heat's courier card, action bars).
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- Flowing `HudCanvas` panels now register their layout region once their anchor region mounts; before, most never registered, so layout collision checks and region consumers missed them.
|
|
120
|
+
|
|
29
121
|
## 0.24.0
|
|
30
122
|
|
|
31
123
|
### Migrate
|
package/dist/engineStyles.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate
|
|
3
|
-
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link
|
|
3
|
+
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link HudSurface}, the
|
|
4
4
|
* quiet hover/press/focus defaults for plain buttons inside the HUD root, and reduced-motion
|
|
5
5
|
* handling (`prefers-reduced-motion` or `[data-reduced-motion="true"]` zero `--jg-motion-scale`).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Every rule sits in the `components.jgengine` cascade layer, so unlayered game CSS, a game's own
|
|
7
|
+
* `components` rules and Tailwind utilities win without `!important`. It first declares
|
|
8
|
+
* `theme, base, components, utilities` so Tailwind v4's preflight stays below engine controls even
|
|
9
|
+
* when the engine sheet loads first. The shell injects it; games that mount outside the shell import
|
|
10
|
+
* `@jgengine/react/styles.css`, which holds the same text.
|
|
8
11
|
*/
|
|
9
12
|
export declare const ENGINE_STYLESHEET: string;
|
|
10
13
|
/** Injects {@link ENGINE_STYLESHEET} into `doc` once. Safe to call repeatedly and during SSR (no-op). */
|
package/dist/engineStyles.js
CHANGED
|
@@ -16,13 +16,18 @@ const ENABLED = ':not(:disabled, [aria-disabled="true"])';
|
|
|
16
16
|
const KILL_MOTION = "animation-duration: 1ms !important; animation-delay: 0ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important;";
|
|
17
17
|
/**
|
|
18
18
|
* The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate
|
|
19
|
-
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link
|
|
19
|
+
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link HudSurface}, the
|
|
20
20
|
* quiet hover/press/focus defaults for plain buttons inside the HUD root, and reduced-motion
|
|
21
21
|
* handling (`prefers-reduced-motion` or `[data-reduced-motion="true"]` zero `--jg-motion-scale`).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Every rule sits in the `components.jgengine` cascade layer, so unlayered game CSS, a game's own
|
|
23
|
+
* `components` rules and Tailwind utilities win without `!important`. It first declares
|
|
24
|
+
* `theme, base, components, utilities` so Tailwind v4's preflight stays below engine controls even
|
|
25
|
+
* when the engine sheet loads first. The shell injects it; games that mount outside the shell import
|
|
26
|
+
* `@jgengine/react/styles.css`, which holds the same text.
|
|
24
27
|
*/
|
|
25
28
|
export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
29
|
+
@layer theme, base, components, utilities;
|
|
30
|
+
@layer components.jgengine {
|
|
26
31
|
:root { --jg-motion-scale: 1; }
|
|
27
32
|
@media (prefers-reduced-motion: reduce) { :root { --jg-motion-scale: 0; } }
|
|
28
33
|
[data-reduced-motion="true"] { --jg-motion-scale: 0; }
|
|
@@ -56,7 +61,7 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
56
61
|
}
|
|
57
62
|
:where([data-jg-hud]) :where(button, [role="button"]):where(${ENABLED}):active { scale: var(--jg-press-scale, 0.97); }
|
|
58
63
|
|
|
59
|
-
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding:
|
|
64
|
+
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding: var(--jg-space-xs, 4px) var(--jg-space-lg, 16px); margin: 0; font-family: var(--jg-font-body, ${HUD_FONT_STACK}); font-size: var(--jg-type-md, 14px); font-weight: 600; line-height: 1.2; letter-spacing: 0.01em; text-align: center; white-space: normal; color: var(--jg-text, #f5f7fa); background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03)), var(--jg-slot-bg, rgba(12,15,21,0.78)); border: var(--jg-slot-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-slot-radius, 8px); box-shadow: ${HIGHLIGHT}, ${RAISED}; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; transition: translate ${FAST} ${EASE}, scale ${FAST} ${SPRING}, box-shadow ${FAST} ${EASE}, border-color ${FAST} ${EASE}, background-color ${FAST} ${EASE}; }
|
|
60
65
|
[data-jg-button]::after { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--jg-hover-overlay, rgba(255,255,255,0.07)); opacity: 0; pointer-events: none; transition: opacity ${FAST} ${EASE}; }
|
|
61
66
|
@media (hover: hover) {
|
|
62
67
|
[data-jg-button]${ENABLED}:hover { translate: 0 var(--jg-hover-lift, -1px); box-shadow: ${HIGHLIGHT}, ${OVERLAY}; }
|
|
@@ -70,12 +75,12 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
70
75
|
[data-jg-button][data-variant="danger"] { color: #fff; background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 60%), var(--jg-danger, #e5484d); border: 1px solid rgba(255,255,255,0.22); }
|
|
71
76
|
[data-jg-button][data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; }
|
|
72
77
|
[data-jg-button][data-variant="ghost"]${ENABLED}:hover { box-shadow: none; }
|
|
73
|
-
[data-jg-button][data-size="sm"] { min-height: 28px; padding:
|
|
74
|
-
[data-jg-button][data-size="lg"] { min-height: 44px; padding:
|
|
78
|
+
[data-jg-button][data-size="sm"] { min-height: 28px; padding-inline: var(--jg-space-md, 12px); font-size: var(--jg-type-sm, 13px); }
|
|
79
|
+
[data-jg-button][data-size="lg"] { min-height: 44px; padding-inline: var(--jg-space-xl, 24px); font-size: var(--jg-type-lg, 16px); }
|
|
75
80
|
[data-jg-button][data-size="icon"] { min-height: 0; width: 28px; height: 28px; padding: 0; font-size: var(--jg-type-sm, 13px); }
|
|
76
81
|
[data-jg-button][data-block] { display: flex; width: 100%; }
|
|
77
82
|
|
|
78
|
-
[data-jg-panel] { box-sizing: border-box; color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, ${HUD_FONT_STACK}); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}, ${RAISED}); }
|
|
83
|
+
[data-jg-panel] { box-sizing: border-box; padding: var(--jg-panel-padding, 12px); color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, ${HUD_FONT_STACK}); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}, ${RAISED}); }
|
|
79
84
|
[data-jg-panel][data-elevation="overlay"] { box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}), ${OVERLAY}; }
|
|
80
85
|
[data-jg-panel][data-elevation="modal"] { box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}), ${MODAL}; }
|
|
81
86
|
[data-jg-panel][data-state="open"] { animation: jg-panel-in ${BASE} ${EASE_OUT} both; }
|
|
@@ -94,6 +99,7 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
94
99
|
@media (hover: hover) { [data-action-id][data-enabled]:hover { translate: 0 var(--jg-hover-lift, -1px); } }
|
|
95
100
|
[data-action-id][data-enabled]:active { translate: 0 0; scale: var(--jg-press-scale, 0.97); }
|
|
96
101
|
[data-action-id]:focus-visible { outline: none; box-shadow: ${FOCUS}; }
|
|
102
|
+
}
|
|
97
103
|
`;
|
|
98
104
|
const STYLE_ID = "jg-engine-styles";
|
|
99
105
|
/** Injects {@link ENGINE_STYLESHEET} into `doc` once. Safe to call repeatedly and during SSR (no-op). */
|
package/dist/hudChromePreview.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ActionButton } from "./actionHud.js";
|
|
|
4
4
|
import { HealthBar, ManaBar } from "./bars.js";
|
|
5
5
|
import { HudButton } from "./hudButton.js";
|
|
6
6
|
import { HudFrame } from "./hudFrame.js";
|
|
7
|
-
import {
|
|
7
|
+
import { HudSurface } from "./hudSurface.js";
|
|
8
8
|
import { HudThemeRoot } from "./hudThemeRoot.js";
|
|
9
9
|
import { ConfirmDialog, PauseMenu } from "./modals.js";
|
|
10
10
|
import { Window } from "./panels.js";
|
|
@@ -23,5 +23,5 @@ const noop = () => undefined;
|
|
|
23
23
|
* variants. Screenshots identically; the before/after evidence for theme and primitive changes.
|
|
24
24
|
*/
|
|
25
25
|
export function HudChromePreview({ className }) {
|
|
26
|
-
return (_jsx("div", { className: className, style: { position: "relative", width: "100%", height: "100%", minHeight: 720, overflow: "hidden", background: "radial-gradient(ellipse at 30% 20%, #3b5a46, #1b2a24 55%, #0f1714)" }, children: _jsxs(HudThemeRoot, { children: [_jsx("div", { style: { position: "absolute", left: 24, top: 24, width: 280 }, children: _jsx(HudFrame, { title: "Warden \u00B7 Lv 12", aside: _jsx(TweenNumber, { value: 12480 }), padding: 12, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ManaBar, { value: 50, max: 80, label: "MP", width: "100%" })] }) }) }), _jsx("div", { style: { position: "absolute", left: 24, bottom: 24, display: "flex", gap: 8 }, children: ACTIONS.map((action, index) => (_jsx(ActionButton, { action: action, focused: index === 0 }, action.id))) }), _jsx(Window, { title: "Inventory", x: 340, y: 24, width: 260, closable: true, onClose: noop, children: _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 6 }, children: Array.from({ length: 10 }, (_, index) => (_jsx("div", { style: { aspectRatio: "1", background: "var(--jg-slot-bg)", border: "var(--jg-slot-border)", borderRadius: "var(--jg-slot-radius)" } }, index))) }) }), _jsx("div", { style: { position: "absolute", left: 640, top: 24 }, children: _jsx(ConfirmDialog, { title: "Abandon the expedition?", body: "Your haul stays in the wagon, but the crew returns home.", confirmLabel: "Abandon", danger: true, onConfirm: noop, onCancel: noop }) }), _jsx("div", { style: { position: "absolute", left: 1040, top: 24 }, children: _jsx(PauseMenu, { onResume: noop, items: [{ id: "settings", label: "Settings", onSelect: noop }, { id: "quit", label: "Quit to title", onSelect: noop, danger: true }] }) }), _jsx("div", { style: { position: "absolute", left: 340, top: 300 }, children: _jsx(
|
|
26
|
+
return (_jsx("div", { className: className, style: { position: "relative", width: "100%", height: "100%", minHeight: 720, overflow: "hidden", background: "radial-gradient(ellipse at 30% 20%, #3b5a46, #1b2a24 55%, #0f1714)" }, children: _jsxs(HudThemeRoot, { children: [_jsx("div", { style: { position: "absolute", left: 24, top: 24, width: 280 }, children: _jsx(HudFrame, { title: "Warden \u00B7 Lv 12", aside: _jsx(TweenNumber, { value: 12480 }), padding: 12, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ManaBar, { value: 50, max: 80, label: "MP", width: "100%" })] }) }) }), _jsx("div", { style: { position: "absolute", left: 24, bottom: 24, display: "flex", gap: 8 }, children: ACTIONS.map((action, index) => (_jsx(ActionButton, { action: action, focused: index === 0 }, action.id))) }), _jsx(Window, { title: "Inventory", x: 340, y: 24, width: 260, closable: true, onClose: noop, children: _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 6 }, children: Array.from({ length: 10 }, (_, index) => (_jsx("div", { style: { aspectRatio: "1", background: "var(--jg-slot-bg)", border: "var(--jg-slot-border)", borderRadius: "var(--jg-slot-radius)" } }, index))) }) }), _jsx("div", { style: { position: "absolute", left: 640, top: 24 }, children: _jsx(ConfirmDialog, { title: "Abandon the expedition?", body: "Your haul stays in the wagon, but the crew returns home.", confirmLabel: "Abandon", danger: true, onConfirm: noop, onCancel: noop }) }), _jsx("div", { style: { position: "absolute", left: 1040, top: 24 }, children: _jsx(PauseMenu, { onResume: noop, items: [{ id: "settings", label: "Settings", onSelect: noop }, { id: "quit", label: "Quit to title", onSelect: noop, danger: true }] }) }), _jsx("div", { style: { position: "absolute", left: 340, top: 300 }, children: _jsx(HudSurface, { title: "Buttons", padding: 14, children: _jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", width: 420 }, children: [_jsx(HudButton, { variant: "primary", children: "Deploy" }), _jsx(HudButton, { children: "Inspect" }), _jsx(HudButton, { variant: "ghost", children: "Skip" }), _jsx(HudButton, { variant: "danger", children: "Scrap" }), _jsx(HudButton, { disabled: true, children: "Locked" }), _jsx(HudButton, { size: "sm", children: "Small" })] }) }) })] }) }));
|
|
27
27
|
}
|
package/dist/hudLayout.js
CHANGED
|
@@ -579,7 +579,9 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
579
579
|
allowOverlapWith,
|
|
580
580
|
collisionGroup,
|
|
581
581
|
}), [id, mobileBehavior, priority, allowOverlapWith, collisionGroup]);
|
|
582
|
-
|
|
582
|
+
// A flowing panel portals into its anchor region, which HudCanvas only provides after its first commit.
|
|
583
|
+
const awaitingRegion = panel !== undefined && (compact || !panel.moved) && regions[compact ? anchor : panel.placement.anchor] === undefined;
|
|
584
|
+
useRegisterLayoutRegion(regionSpec, rootRef, region && !regionHidden && !awaitingRegion);
|
|
583
585
|
const onPointerDown = (event) => {
|
|
584
586
|
if (!draggable || event.button !== 0)
|
|
585
587
|
return;
|
package/dist/hudPanel.d.ts
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type HudInteractListener } from "./hudInteract.js";
|
|
3
|
-
/** Shadow height of a {@link HudPanel}. */
|
|
4
|
-
export type HudPanelElevation = "raised" | "overlay" | "modal";
|
|
5
|
-
/** Props for {@link HudPanel}. */
|
|
6
|
-
export interface HudPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
|
-
/** Show or hide with an enter/exit transition. Unset renders a static panel with no transition. */
|
|
8
|
-
open?: boolean;
|
|
9
|
-
/** Uppercase header label at the top-left. */
|
|
10
|
-
title?: ReactNode;
|
|
11
|
-
/** Header content at the top-right, opposite `title`. */
|
|
12
|
-
aside?: ReactNode;
|
|
13
|
-
/** Shadow height. Default `raised`. */
|
|
14
|
-
elevation?: HudPanelElevation;
|
|
15
|
-
padding?: number | string;
|
|
16
|
-
width?: number | string;
|
|
17
|
-
/** Accept pointer events. Default `true`. */
|
|
18
|
-
interactive?: boolean;
|
|
19
|
-
/** Feedback cue name, rendered as `data-jg-cue` and passed to `onInteract`. */
|
|
20
|
-
cue?: string;
|
|
21
|
-
/** Called with `open` and `close` as the panel shows and hides. */
|
|
22
|
-
onInteract?: HudInteractListener;
|
|
23
|
-
}
|
|
1
|
+
import { type HudSurfaceElevation, type HudSurfaceProps } from "./hudSurface.js";
|
|
24
2
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (`data-state="open" | "closed"`); reduced motion skips both. `className` and `style` restyle it.
|
|
28
|
-
*
|
|
29
|
-
* @capability hud-panel themed panel frame with elevation, header slots, and open/close transitions
|
|
3
|
+
* Old name of {@link HudSurfaceElevation} (`@jgengine/react/hudSurface`).
|
|
4
|
+
* @deprecated Use `HudSurfaceElevation`; this alias goes in the next release.
|
|
30
5
|
*/
|
|
31
|
-
export
|
|
6
|
+
export type HudPanelElevation = HudSurfaceElevation;
|
|
7
|
+
/**
|
|
8
|
+
* Old name of {@link HudSurfaceProps} (`@jgengine/react/hudSurface`).
|
|
9
|
+
* @deprecated Use `HudSurfaceProps`; this alias goes in the next release.
|
|
10
|
+
*/
|
|
11
|
+
export type HudPanelProps = HudSurfaceProps;
|
|
12
|
+
/**
|
|
13
|
+
* Old name of the themed frame {@link HudSurface} (`@jgengine/react/hudSurface`), renamed so it no longer
|
|
14
|
+
* shares a name with the `HudCanvas` layout slot `HudPanel`.
|
|
15
|
+
* @deprecated Use `HudSurface`; this alias goes in the next release.
|
|
16
|
+
*/
|
|
17
|
+
export declare const HudPanel: import("react").ForwardRefExoticComponent<HudSurfaceProps & import("react").RefAttributes<HTMLElement>>;
|
package/dist/hudPanel.js
CHANGED
|
@@ -1,32 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
-
import { useReducedMotion } from "./accessibility.js";
|
|
4
|
-
import { useEngineStyles } from "./engineStyles.js";
|
|
5
|
-
import { useHudInteract } from "./hudInteract.js";
|
|
6
|
-
import { usePresence } from "./presence.js";
|
|
7
|
-
const EXIT_MS = 140;
|
|
1
|
+
import { HudSurface } from "./hudSurface.js";
|
|
8
2
|
/**
|
|
9
|
-
*
|
|
10
|
-
* a
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @capability hud-panel themed panel frame with elevation, header slots, and open/close transitions
|
|
3
|
+
* Old name of the themed frame {@link HudSurface} (`@jgengine/react/hudSurface`), renamed so it no longer
|
|
4
|
+
* shares a name with the `HudCanvas` layout slot `HudPanel`.
|
|
5
|
+
* @deprecated Use `HudSurface`; this alias goes in the next release.
|
|
14
6
|
*/
|
|
15
|
-
export const HudPanel =
|
|
16
|
-
useEngineStyles();
|
|
17
|
-
const reduced = useReducedMotion();
|
|
18
|
-
const shown = open ?? true;
|
|
19
|
-
const presence = usePresence(shown, reduced ? 0 : EXIT_MS);
|
|
20
|
-
const report = useHudInteract("panel", cue, onInteract);
|
|
21
|
-
const wasOpen = useRef(shown);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (wasOpen.current === shown)
|
|
24
|
-
return;
|
|
25
|
-
wasOpen.current = shown;
|
|
26
|
-
report(shown ? "open" : "close");
|
|
27
|
-
}, [shown, report]);
|
|
28
|
-
if (presence === "unmounted")
|
|
29
|
-
return null;
|
|
30
|
-
const hasHeader = title !== undefined || aside !== undefined;
|
|
31
|
-
return (_jsxs("div", { ...rest, ref: ref, "data-jg-panel": "", "data-state": open === undefined ? undefined : presence, "data-elevation": elevation, "data-jg-cue": cue, style: { padding, width, pointerEvents: interactive ? "auto" : undefined, ...style }, children: [hasHeader ? (_jsxs("div", { "data-jg-panel-header": "", style: { marginBottom: 8 }, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
|
|
32
|
-
});
|
|
7
|
+
export const HudPanel = HudSurface;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { type HudInteractListener } from "./hudInteract.js";
|
|
3
|
+
/** Shadow height of a {@link HudSurface}. */
|
|
4
|
+
export type HudSurfaceElevation = "raised" | "overlay" | "modal";
|
|
5
|
+
/** Block elements a {@link HudSurface} can render as, so it can be a landmark. */
|
|
6
|
+
export type HudSurfaceElement = "div" | "section" | "aside" | "nav" | "header" | "footer" | "article" | "main" | "form" | "dialog";
|
|
7
|
+
/** Props for {@link HudSurface}. */
|
|
8
|
+
export interface HudSurfaceProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
9
|
+
/** Element to render. Default `div`; `section`, `aside`, `nav` and the like make it a landmark. */
|
|
10
|
+
as?: HudSurfaceElement;
|
|
11
|
+
/** Show or hide with an enter/exit transition. Unset renders a static panel with no transition. */
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/** Uppercase header label at the top-left. */
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
/** Header content at the top-right, opposite `title`. */
|
|
16
|
+
aside?: ReactNode;
|
|
17
|
+
/** Shadow height. Default `raised`. */
|
|
18
|
+
elevation?: HudSurfaceElevation;
|
|
19
|
+
/** Sets `--jg-panel-padding` (default 12px from the stylesheet), so a game class or media query still overrides it. */
|
|
20
|
+
padding?: number | string;
|
|
21
|
+
width?: number | string;
|
|
22
|
+
/** Accept pointer events. Default `true`. */
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
/** Feedback cue name, rendered as `data-jg-cue` and passed to `onInteract`. */
|
|
25
|
+
cue?: string;
|
|
26
|
+
/** Called with `open` and `close` as the panel shows and hides. */
|
|
27
|
+
onInteract?: HudInteractListener;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Themed panel/window frame over the `HudTheme` frame, elevation, and motion tokens. It enters with
|
|
31
|
+
* a short rise-and-fade and stays mounted through its exit when `open` turns false
|
|
32
|
+
* (`data-state="open" | "closed"`); reduced motion skips both. Padding comes from
|
|
33
|
+
* `--jg-panel-padding`, and the engine rules sit in a cascade layer, so a game's `className`
|
|
34
|
+
* restyles it without `!important`. Not the `HudCanvas` layout slot (`HudPanel` in `@jgengine/react/hudLayout`).
|
|
35
|
+
*
|
|
36
|
+
* @capability hud-surface themed panel frame with elevation, header slots, landmark element, and open/close transitions
|
|
37
|
+
*/
|
|
38
|
+
export declare const HudSurface: import("react").ForwardRefExoticComponent<HudSurfaceProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
+
import { useReducedMotion } from "./accessibility.js";
|
|
4
|
+
import { useEngineStyles } from "./engineStyles.js";
|
|
5
|
+
import { useHudInteract } from "./hudInteract.js";
|
|
6
|
+
import { usePresence } from "./presence.js";
|
|
7
|
+
const EXIT_MS = 140;
|
|
8
|
+
/**
|
|
9
|
+
* Themed panel/window frame over the `HudTheme` frame, elevation, and motion tokens. It enters with
|
|
10
|
+
* a short rise-and-fade and stays mounted through its exit when `open` turns false
|
|
11
|
+
* (`data-state="open" | "closed"`); reduced motion skips both. Padding comes from
|
|
12
|
+
* `--jg-panel-padding`, and the engine rules sit in a cascade layer, so a game's `className`
|
|
13
|
+
* restyles it without `!important`. Not the `HudCanvas` layout slot (`HudPanel` in `@jgengine/react/hudLayout`).
|
|
14
|
+
*
|
|
15
|
+
* @capability hud-surface themed panel frame with elevation, header slots, landmark element, and open/close transitions
|
|
16
|
+
*/
|
|
17
|
+
export const HudSurface = forwardRef(function HudSurface({ as: Element = "div", open, title, aside, elevation = "raised", padding, width, interactive = true, cue, onInteract, style, children, ...rest }, ref) {
|
|
18
|
+
useEngineStyles();
|
|
19
|
+
const reduced = useReducedMotion();
|
|
20
|
+
const shown = open ?? true;
|
|
21
|
+
const presence = usePresence(shown, reduced ? 0 : EXIT_MS);
|
|
22
|
+
const report = useHudInteract("panel", cue, onInteract);
|
|
23
|
+
const wasOpen = useRef(shown);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (wasOpen.current === shown)
|
|
26
|
+
return;
|
|
27
|
+
wasOpen.current = shown;
|
|
28
|
+
report(shown ? "open" : "close");
|
|
29
|
+
}, [shown, report]);
|
|
30
|
+
if (presence === "unmounted")
|
|
31
|
+
return null;
|
|
32
|
+
const hasHeader = title !== undefined || aside !== undefined;
|
|
33
|
+
const frameStyle = {
|
|
34
|
+
width,
|
|
35
|
+
pointerEvents: interactive ? "auto" : undefined,
|
|
36
|
+
...(padding === undefined ? {} : { "--jg-panel-padding": typeof padding === "number" ? `${padding}px` : padding }),
|
|
37
|
+
...style,
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs(Element, { ...rest, ref: ref, "data-jg-panel": "", "data-state": open === undefined ? undefined : presence, "data-elevation": elevation, "data-jg-cue": cue, style: frameStyle, children: [hasHeader ? (_jsxs("div", { "data-jg-panel-header": "", style: { marginBottom: 8 }, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
|
|
40
|
+
});
|
package/dist/modals.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface ConfirmDialogProps {
|
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
90
|
* A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over
|
|
91
|
-
* a `HudTheme`-token-driven {@link
|
|
91
|
+
* a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a
|
|
92
92
|
* modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via
|
|
93
93
|
* `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies
|
|
94
94
|
* the copy.
|
|
@@ -124,7 +124,7 @@ export interface PauseMenuProps {
|
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot
|
|
127
|
-
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
127
|
+
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships
|
|
128
128
|
* the common shape every game's pause screen needs and nothing genre-specific — the game supplies the
|
|
129
129
|
* title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into
|
|
130
130
|
* a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
package/dist/modals.js
CHANGED
|
@@ -6,7 +6,7 @@ import { useDialogBehavior } from "./dialogBehavior.js";
|
|
|
6
6
|
import { useEngineStyles } from "./engineStyles.js";
|
|
7
7
|
import { HudButton } from "./hudButton.js";
|
|
8
8
|
import { useHudInteract } from "./hudInteract.js";
|
|
9
|
-
import {
|
|
9
|
+
import { HudSurface } from "./hudSurface.js";
|
|
10
10
|
import { hudType } from "./hudTokens.js";
|
|
11
11
|
import { usePresence } from "./presence.js";
|
|
12
12
|
/**
|
|
@@ -79,7 +79,7 @@ const DIALOG_BODY = { margin: "10px 0 0", fontSize: hudType("md"), lineHeight: 1
|
|
|
79
79
|
const BUTTON_ROW = { display: "flex", gap: 10, marginTop: 20, justifyContent: "flex-end" };
|
|
80
80
|
/**
|
|
81
81
|
* A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over
|
|
82
|
-
* a `HudTheme`-token-driven {@link
|
|
82
|
+
* a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a
|
|
83
83
|
* modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via
|
|
84
84
|
* `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies
|
|
85
85
|
* the copy.
|
|
@@ -87,7 +87,7 @@ const BUTTON_ROW = { display: "flex", gap: 10, marginTop: 20, justifyContent: "f
|
|
|
87
87
|
* @capability confirm-dialog generic themeable confirm/cancel dialog — title, body, two buttons, danger styling
|
|
88
88
|
*/
|
|
89
89
|
export function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }) {
|
|
90
|
-
return (_jsxs(
|
|
90
|
+
return (_jsxs(HudSurface, { elevation: "modal", width: width, padding: 20, className: className, style: style, children: [_jsx("h2", { "data-confirm-title": "", style: DIALOG_TITLE, children: title }), body !== undefined ? _jsx("p", { "data-confirm-body": "", style: DIALOG_BODY, children: body }) : null, _jsxs("div", { style: BUTTON_ROW, children: [_jsx(HudButton, { "data-confirm-cancel": "", cue: "cancel", onClick: onCancel, children: cancelLabel }), _jsx(HudButton, { "data-confirm-accept": "", cue: "confirm", variant: danger ? "danger" : "primary", onClick: onConfirm, children: confirmLabel })] })] }));
|
|
91
91
|
}
|
|
92
92
|
const PAUSE_TITLE = {
|
|
93
93
|
margin: "0 0 14px",
|
|
@@ -102,7 +102,7 @@ const PAUSE_ITEM = { marginTop: 8 };
|
|
|
102
102
|
const PAUSE_DANGER_ITEM = { ...PAUSE_ITEM, color: "var(--jg-danger, #e5484d)", borderColor: "rgba(229,72,77,0.45)" };
|
|
103
103
|
/**
|
|
104
104
|
* A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot
|
|
105
|
-
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
105
|
+
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships
|
|
106
106
|
* the common shape every game's pause screen needs and nothing genre-specific — the game supplies the
|
|
107
107
|
* title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into
|
|
108
108
|
* a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
|
@@ -110,5 +110,5 @@ const PAUSE_DANGER_ITEM = { ...PAUSE_ITEM, color: "var(--jg-danger, #e5484d)", b
|
|
|
110
110
|
* @capability pause-menu reskinnable pause-menu building block — Resume plus a game-filled Settings/Quit slot list, HudTheme-driven
|
|
111
111
|
*/
|
|
112
112
|
export function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }) {
|
|
113
|
-
return (_jsxs(
|
|
113
|
+
return (_jsxs(HudSurface, { elevation: "modal", width: width, padding: 20, className: className, style: style, children: [_jsx("div", { "data-pause-title": "", style: PAUSE_TITLE, children: title }), _jsx(HudButton, { "data-pause-resume": "", cue: "resume", variant: "primary", size: "lg", block: true, onClick: onResume, children: resumeLabel }), items.map((item) => (_jsx(HudButton, { "data-pause-item": item.id, cue: `pause:${item.id}`, size: "lg", block: true, onClick: item.onSelect, disabled: item.disabled, style: item.danger ? PAUSE_DANGER_ITEM : PAUSE_ITEM, children: item.label }, item.id)))] }));
|
|
114
114
|
}
|
package/dist/notices.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { Notice, NoticeFeed } from "@jgengine/core/game/notices";
|
|
3
|
+
import type { LayoutRect, LayoutRegion } from "@jgengine/core/ui/gameLayout";
|
|
3
4
|
export type { Notice, NoticeTone } from "@jgengine/core/game/notices";
|
|
4
5
|
/** Where {@link NoticeStack} sits: below top-edge HUD bars, over the play area, or above bottom action bars. */
|
|
5
6
|
export type NoticePlacement = "top" | "center" | "bottom";
|
|
7
|
+
/**
|
|
8
|
+
* How far a top or bottom notice stack must move toward the screen middle to clear the registered
|
|
9
|
+
* layout regions in its column. `stack` is its current rect, already moved by `shift`; regions
|
|
10
|
+
* reaching past the container's middle (full-height sidebars, screens) are ignored. @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function noticeAvoidShift(placement: NoticePlacement, stack: LayoutRect, shift: number, container: LayoutRect, regions: readonly Pick<LayoutRegion, "kind" | "rect">[], gap?: number): number;
|
|
6
13
|
type Shown = {
|
|
7
14
|
notice: Notice;
|
|
8
15
|
leaving: boolean;
|
|
@@ -13,14 +20,22 @@ export declare function reconcileShown(previous: readonly Shown[], live: readonl
|
|
|
13
20
|
export declare function useNoticeList(feed: NoticeFeed | null): readonly Notice[];
|
|
14
21
|
/**
|
|
15
22
|
* Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for
|
|
16
|
-
* `ctx.game.notices
|
|
23
|
+
* `ctx.game.notices` inside the HUD theme root. Uses `--jg-*` theme vars with fallbacks, 14px text, and
|
|
24
|
+
* never takes pointer input. A top or bottom stack keeps clear of HUD panels registered in its column
|
|
25
|
+
* and sits above modal overlays.
|
|
17
26
|
*
|
|
18
|
-
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
27
|
+
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
19
28
|
*/
|
|
20
|
-
export declare function NoticeStack({ feed, placement, scale, renderNotice, }: {
|
|
29
|
+
export declare function NoticeStack({ feed, placement, offset, avoidHud, zIndex, scale, renderNotice, }: {
|
|
21
30
|
/** Defaults to the surrounding game's `ctx.game.notices`. */
|
|
22
31
|
feed?: NoticeFeed;
|
|
23
32
|
placement?: NoticePlacement;
|
|
33
|
+
/** Distance from the band's screen edge (px or CSS length), added to the safe-area inset. */
|
|
34
|
+
offset?: number | string;
|
|
35
|
+
/** Move a top/bottom stack clear of registered HUD panels and touch controls in its column. Default true. */
|
|
36
|
+
avoidHud?: boolean;
|
|
37
|
+
/** Default 85: above `ModalHost` (80) and `PanelHost` (40). */
|
|
38
|
+
zIndex?: number;
|
|
24
39
|
/** Multiplies text and spacing (the shell passes the player's UI scale). */
|
|
25
40
|
scale?: number;
|
|
26
41
|
renderNotice?: (notice: Notice) => ReactNode;
|