@mpgd/target-config 0.19.0 → 0.21.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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # Target configuration
2
+
3
+ Target-specific feature availability, effective configuration and viewport helpers.
4
+
5
+ ## Responsive game shell
6
+
7
+ `@mpgd/target-config/adaptive-shell` is an opt-in DOM entrypoint over the existing
8
+ viewport composition APIs. Import `@mpgd/target-config/adaptive-shell/base.css`
9
+ before application paint styles; every selector has zero specificity.
10
+
11
+ ```ts
12
+ import { mountAdaptiveGameShell, waitForAdaptiveShellViewport } from '@mpgd/target-config/adaptive-shell';
13
+ const initialMeasurement = await waitForAdaptiveShellViewport(gameRoot);
14
+ const shell = mountAdaptiveGameShell({ gameRoot, runtime,
15
+ initialMeasurement, policy: { gameAspectRatio: 3 / 4, minRailWidth: 160 } });
16
+ const stop = shell.subscribe(({ composition }) => resizeGame(composition.gameBounds));
17
+ // Teardown: stop(); shell.destroy();
18
+ ```
19
+
20
+ The shell applies safe bounds to the stage and optional left/right rails. Aspect
21
+ ratio, rail threshold, panel slots (at most 64), element IDs/classes and all visual
22
+ content remain consumer-owned. Authored slots define a readiness data attribute,
23
+ a CSS variable prefix, min/max width, gutter and minimum rail height; their
24
+ left/top/width variables are written on the owning document root. Inputs are
25
+ snapshotted before observation starts and invalid policy/slot/measurement inputs
26
+ fail before DOM restructuring.
27
+ Each live controller reserves its slot variable prefixes in the owning document.
28
+ Another stage using one of those prefixes fails before either stage is changed;
29
+ destroy releases them, and replacing the same stage transfers ownership. Separate
30
+ documents may reuse the same prefixes.
31
+
32
+ ResizeObserver, window and visual-viewport events coalesce through one animation
33
+ frame. A new controller for the same stage disposes its previous owner. Destroy
34
+ is idempotent, detaches observers/listeners, ignores late callbacks and clears
35
+ slot variables. It leaves the shell/stage/rails and their bounds in the DOM;
36
+ remove those elements separately if the page outlives the game. Embedded stages
37
+ use their own document/window. Subscriber errors do not skip other listeners and
38
+ are rethrown in a microtask for the host's error reporting. Importing the module
39
+ creates no observers, elements or listeners and does not load a platform SDK.
40
+
41
+ Startup waits accept `{ signal }` so teardown can cancel a still-unmeasurable
42
+ surface and detach observation. Fixed shells prefer visual-viewport dimensions
43
+ and follow its resize/scroll offsets; document-root panel variables include those
44
+ offsets. This covers keyboard/zoom changes that leave layout-viewport dimensions
45
+ unchanged ([VisualViewport reference](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport)).
@@ -0,0 +1,46 @@
1
+ /* @mpgd/target-config/adaptive-shell base geometry.
2
+ *
3
+ * Only the layout mechanism lives here: a fixed full-viewport shell, an absolutely positioned
4
+ * stage and rails whose inline bounds are written by `mountAdaptiveGameShell`, and the safe-area
5
+ * tokens it reads back. Every selector is wrapped in :where() so any game rule overrides it
6
+ * without specificity work. Backgrounds, borders, shadows, touch policy and container names
7
+ * are game-owned. */
8
+
9
+ :where(:root) {
10
+ --mpgd-safe-area-top: env(safe-area-inset-top, 0px);
11
+ --mpgd-safe-area-right: env(safe-area-inset-right, 0px);
12
+ --mpgd-safe-area-bottom: env(safe-area-inset-bottom, 0px);
13
+ --mpgd-safe-area-left: env(safe-area-inset-left, 0px);
14
+ }
15
+
16
+ :where([data-game-shell]) {
17
+ position: fixed;
18
+ inset: 0;
19
+ overflow: hidden;
20
+ isolation: isolate;
21
+ }
22
+
23
+ :where([data-game-shell-stage]) {
24
+ position: absolute;
25
+ z-index: 2;
26
+ box-sizing: border-box;
27
+ min-width: 0;
28
+ min-height: 0;
29
+ overflow: hidden;
30
+ isolation: isolate;
31
+ /* Stage-relative cq* units resolve against the stage, not the outer window. */
32
+ container-type: size;
33
+ }
34
+
35
+ :where([data-game-shell-stage] canvas) {
36
+ display: block;
37
+ width: 100%;
38
+ height: 100%;
39
+ }
40
+
41
+ :where([data-game-shell-rail]) {
42
+ position: absolute;
43
+ z-index: 1;
44
+ box-sizing: border-box;
45
+ overflow: hidden;
46
+ }
@@ -0,0 +1,2 @@
1
+ export { assertAdaptiveShellPolicy, assertAdaptiveShellRailSlot, assertAdaptiveShellRailSlots, resolveAdaptiveShellComposition, resolveAdaptiveShellRailBounds, resolveAdaptiveShellRailSlot, type AdaptiveShellPolicy, type AdaptiveShellRailSide, type AdaptiveShellRailSlot, type AdaptiveShellRailSlotPlacement, type AdaptiveShellReservedNames, } from './policy.js';
2
+ export { adaptiveShellAttributes, adaptiveShellReservedVariablePrefixes, defaultAdaptiveShellId, mountAdaptiveGameShell, waitForAdaptiveShellViewport, type AdaptiveGameShellController, type AdaptiveGameShellState, type AdaptiveShellElementNames, type MountAdaptiveGameShellInput, } from './shell.js';
@@ -0,0 +1,2 @@
1
+ export { assertAdaptiveShellPolicy, assertAdaptiveShellRailSlot, assertAdaptiveShellRailSlots, resolveAdaptiveShellComposition, resolveAdaptiveShellRailBounds, resolveAdaptiveShellRailSlot, } from './policy.js';
2
+ export { adaptiveShellAttributes, adaptiveShellReservedVariablePrefixes, defaultAdaptiveShellId, mountAdaptiveGameShell, waitForAdaptiveShellViewport, } from './shell.js';
@@ -0,0 +1,60 @@
1
+ import { type TargetViewportBounds, type TargetViewportComposition, type TargetViewportOrientationPolicy, type TargetViewportSnapshot } from '../viewport.js';
2
+ /**
3
+ * Game-owned stage policy. The shell owns the mechanism; each game adapter owns these numbers.
4
+ */
5
+ export interface AdaptiveShellPolicy {
6
+ /** Stage width divided by height when expanded viewports render side rails, e.g. `3 / 4`. */
7
+ readonly gameAspectRatio: number;
8
+ /** Narrowest rail that still justifies side rails; narrower viewports use the full bounds. */
9
+ readonly minRailWidth: number;
10
+ /** Orientation policy passed to the viewport snapshot. Defaults to `responsive`. */
11
+ readonly orientationPolicy?: TargetViewportOrientationPolicy;
12
+ }
13
+ export type AdaptiveShellRailSide = 'left' | 'right';
14
+ /**
15
+ * A centered panel slot inside one rail, such as a tutorial popover or an idle-game upgrade list.
16
+ * The slot is ready only when the rail can hold `minWidth` plus the inline gutter and is at least
17
+ * `minRailHeight` tall; its width then grows with the rail up to `maxWidth`.
18
+ */
19
+ export interface AdaptiveShellRailSlot {
20
+ readonly rail: AdaptiveShellRailSide;
21
+ /** Shell attribute set to `"true"` while the slot is placed, e.g. `data-tutorial-rail-ready`. */
22
+ readonly readyAttribute: `data-${string}`;
23
+ /** Root custom-property prefix; the shell writes `<prefix>-left`, `-top` and `-width`. */
24
+ readonly cssVariablePrefix: `--${string}`;
25
+ readonly minWidth: number;
26
+ readonly maxWidth: number;
27
+ /** Total horizontal space kept free around the slot (split evenly on both sides). */
28
+ readonly inlineGutter: number;
29
+ readonly minRailHeight: number;
30
+ }
31
+ /** Viewport-pixel placement of a ready rail slot: centered on `left + width / 2` and on `top`. */
32
+ export interface AdaptiveShellRailSlotPlacement {
33
+ readonly left: number;
34
+ readonly top: number;
35
+ readonly width: number;
36
+ }
37
+ /**
38
+ * Reject a policy the shared composition would refuse, using the same rules as
39
+ * `@mpgd/target-config`, so mount can fail before it restructures the DOM.
40
+ */
41
+ export declare function assertAdaptiveShellPolicy(policy: Pick<AdaptiveShellPolicy, 'gameAspectRatio' | 'minRailWidth'>): void;
42
+ /** Apply a game's stage aspect and rail policy to shared target viewport geometry. */
43
+ export declare function resolveAdaptiveShellComposition(viewport: TargetViewportSnapshot, policy: Pick<AdaptiveShellPolicy, 'gameAspectRatio' | 'minRailWidth'>): TargetViewportComposition;
44
+ /** Rail bounds for one side, or `undefined` when the composition renders no side rails. */
45
+ export declare function resolveAdaptiveShellRailBounds(composition: TargetViewportComposition, side: AdaptiveShellRailSide): TargetViewportBounds | undefined;
46
+ /** Fit a centered slot into rail bounds, or return `undefined` when the rail is too small. */
47
+ export declare function resolveAdaptiveShellRailSlot(railBounds: TargetViewportBounds | undefined, slot: Pick<AdaptiveShellRailSlot, 'minWidth' | 'maxWidth' | 'inlineGutter' | 'minRailHeight'>): AdaptiveShellRailSlotPlacement | undefined;
48
+ /** Reject slot definitions that could never place, or that would write unscoped names. */
49
+ export declare function assertAdaptiveShellRailSlot(slot: AdaptiveShellRailSlot): void;
50
+ /** Names a slot must not take because the shell itself writes or reads them. */
51
+ export interface AdaptiveShellReservedNames {
52
+ readonly attributes?: readonly string[];
53
+ readonly variablePrefixes?: readonly string[];
54
+ }
55
+ /**
56
+ * Validate a slot set. Slots share the shell element and the document root, so a repeated ready
57
+ * attribute or variable prefix, or one the shell reserves, would overwrite or clear another
58
+ * slot's placement or the shell's own state.
59
+ */
60
+ export declare function assertAdaptiveShellRailSlots(slots: readonly AdaptiveShellRailSlot[], reserved?: AdaptiveShellReservedNames): void;
@@ -0,0 +1,86 @@
1
+ import { resolveTargetViewportComposition, } from '../viewport.js';
2
+ /**
3
+ * Reject a policy the shared composition would refuse, using the same rules as
4
+ * `@mpgd/target-config`, so mount can fail before it restructures the DOM.
5
+ */
6
+ export function assertAdaptiveShellPolicy(policy) {
7
+ if (!Number.isFinite(policy.gameAspectRatio) || policy.gameAspectRatio <= 0) {
8
+ throw new RangeError('Adaptive shell gameAspectRatio must be a positive finite number.');
9
+ }
10
+ if (!Number.isFinite(policy.minRailWidth) || policy.minRailWidth < 0) {
11
+ throw new RangeError('Adaptive shell minRailWidth must be a non-negative finite number.');
12
+ }
13
+ }
14
+ /** Apply a game's stage aspect and rail policy to shared target viewport geometry. */
15
+ export function resolveAdaptiveShellComposition(viewport, policy) {
16
+ return resolveTargetViewportComposition({
17
+ viewport,
18
+ expandedLayout: 'side-rails',
19
+ gameAspectRatio: policy.gameAspectRatio,
20
+ minRailWidth: policy.minRailWidth,
21
+ });
22
+ }
23
+ /** Rail bounds for one side, or `undefined` when the composition renders no side rails. */
24
+ export function resolveAdaptiveShellRailBounds(composition, side) {
25
+ if (composition.mode !== 'side-rails') {
26
+ return undefined;
27
+ }
28
+ return side === 'left' ? composition.leftRailBounds : composition.rightRailBounds;
29
+ }
30
+ /** Fit a centered slot into rail bounds, or return `undefined` when the rail is too small. */
31
+ export function resolveAdaptiveShellRailSlot(railBounds, slot) {
32
+ if (railBounds === undefined
33
+ || railBounds.width < slot.minWidth + slot.inlineGutter
34
+ || railBounds.height < slot.minRailHeight) {
35
+ return undefined;
36
+ }
37
+ const width = Math.min(slot.maxWidth, railBounds.width - slot.inlineGutter);
38
+ return {
39
+ left: railBounds.x + (railBounds.width - width) / 2,
40
+ top: railBounds.y + railBounds.height / 2,
41
+ width,
42
+ };
43
+ }
44
+ /** Reject slot definitions that could never place, or that would write unscoped names. */
45
+ export function assertAdaptiveShellRailSlot(slot) {
46
+ if (slot.rail !== 'left' && slot.rail !== 'right') {
47
+ throw new TypeError(`Rail slot ${slot.readyAttribute} rail must be 'left' or 'right': ${String(slot.rail)}`);
48
+ }
49
+ for (const key of ['minWidth', 'maxWidth', 'inlineGutter', 'minRailHeight']) {
50
+ if (!Number.isFinite(slot[key]) || slot[key] < 0) {
51
+ throw new RangeError(`Rail slot ${slot.readyAttribute} ${key} must be a finite non-negative number.`);
52
+ }
53
+ }
54
+ if (slot.maxWidth < slot.minWidth) {
55
+ throw new RangeError(`Rail slot ${slot.readyAttribute} maxWidth must not be less than minWidth.`);
56
+ }
57
+ if (!/^data-[a-z][a-z0-9-]*$/u.test(slot.readyAttribute)) {
58
+ throw new TypeError(`Rail slot readyAttribute must be a lower-case data-* name: ${slot.readyAttribute}`);
59
+ }
60
+ if (!/^--[a-zA-Z][\w-]*$/u.test(slot.cssVariablePrefix)) {
61
+ throw new TypeError(`Rail slot cssVariablePrefix must be a custom-property name: ${slot.cssVariablePrefix}`);
62
+ }
63
+ }
64
+ /**
65
+ * Validate a slot set. Slots share the shell element and the document root, so a repeated ready
66
+ * attribute or variable prefix, or one the shell reserves, would overwrite or clear another
67
+ * slot's placement or the shell's own state.
68
+ */
69
+ export function assertAdaptiveShellRailSlots(slots, reserved = {}) {
70
+ if (slots.length > 64) {
71
+ throw new RangeError('Adaptive shell supports at most 64 rail slots.');
72
+ }
73
+ const readyAttributes = new Set(reserved.attributes);
74
+ const variablePrefixes = new Set(reserved.variablePrefixes);
75
+ for (const slot of slots) {
76
+ assertAdaptiveShellRailSlot(slot);
77
+ if (readyAttributes.has(slot.readyAttribute)) {
78
+ throw new TypeError(`Rail slot readyAttribute is already in use: ${slot.readyAttribute}`);
79
+ }
80
+ if (variablePrefixes.has(slot.cssVariablePrefix)) {
81
+ throw new TypeError(`Rail slot cssVariablePrefix is already in use: ${slot.cssVariablePrefix}`);
82
+ }
83
+ readyAttributes.add(slot.readyAttribute);
84
+ variablePrefixes.add(slot.cssVariablePrefix);
85
+ }
86
+ }
@@ -0,0 +1,71 @@
1
+ import type { TargetRuntimeKind } from '../runtime.js';
2
+ import { type TargetViewportComposition, type TargetViewportMeasurement, type TargetViewportSnapshot } from '../viewport.js';
3
+ import { type AdaptiveShellPolicy, type AdaptiveShellRailSlot } from './policy.js';
4
+ /**
5
+ * DOM attributes the shell owns. `base.css` selects the marker attributes; game stylesheets may
6
+ * select the state attributes. Element ids and class names stay game-owned.
7
+ */
8
+ export declare const adaptiveShellAttributes: {
9
+ /** Marker on the outer full-viewport shell element. */
10
+ readonly shell: 'data-game-shell';
11
+ /** Marker on the game mount (the stage). */
12
+ readonly stage: 'data-game-shell-stage';
13
+ /** `left` or `right` on each rail element. */
14
+ readonly rail: 'data-game-shell-rail';
15
+ /** Shell: `side-rails`, `bottom-controls` or `compact-portrait`. */
16
+ readonly compositionMode: 'data-composition-mode';
17
+ /** Shell: where primary controls belong for the current composition. */
18
+ readonly primaryControls: 'data-primary-controls';
19
+ /** Stage: the same composition mode, for selectors scoped under the game mount. */
20
+ readonly layoutMode: 'data-layout-mode';
21
+ /** Stage: `portrait` or `landscape` from the measured viewport layout. */
22
+ readonly viewportOrientation: 'data-viewport-orientation';
23
+ };
24
+ /** Element naming for a shell the package creates. Defaults to `#game-shell` with no classes. */
25
+ export interface AdaptiveShellElementNames {
26
+ readonly shellId?: string;
27
+ readonly shellClassName?: string;
28
+ readonly railClassName?: string;
29
+ }
30
+ export declare const defaultAdaptiveShellId = "game-shell";
31
+ /**
32
+ * Root custom-property prefixes the shell reads back (`--mpgd-safe-area-top`, ...). A rail slot
33
+ * using one would shadow the safe-area tokens and skew every composition.
34
+ */
35
+ export declare const adaptiveShellReservedVariablePrefixes: readonly ['--mpgd-safe-area'];
36
+ export interface AdaptiveGameShellState {
37
+ readonly composition: TargetViewportComposition;
38
+ readonly viewport: TargetViewportSnapshot;
39
+ }
40
+ export interface AdaptiveGameShellController {
41
+ readonly shell: HTMLElement;
42
+ readonly stage: HTMLElement;
43
+ readonly leftRail: HTMLElement;
44
+ readonly rightRail: HTMLElement;
45
+ readonly state: AdaptiveGameShellState;
46
+ readonly subscribe: (listener: (state: AdaptiveGameShellState) => void) => () => void;
47
+ /**
48
+ * Stop observing and clear rail slots. The shell, rails, attributes and inline bounds stay in
49
+ * the DOM; the game owns unmounting them, usually by unloading the page.
50
+ */
51
+ readonly destroy: () => void;
52
+ }
53
+ export interface MountAdaptiveGameShellInput {
54
+ /** The game mount. It becomes the stage between the two rails. */
55
+ readonly gameRoot: HTMLElement;
56
+ readonly runtime: TargetRuntimeKind;
57
+ readonly policy: AdaptiveShellPolicy;
58
+ /** Measurement already obtained from `waitForAdaptiveShellViewport`. */
59
+ readonly initialMeasurement?: TargetViewportMeasurement | undefined;
60
+ readonly elements?: AdaptiveShellElementNames;
61
+ readonly railSlots?: readonly AdaptiveShellRailSlot[];
62
+ }
63
+ /** Wait for host layout instead of booting the engine with a zero-sized viewport. */
64
+ export declare function waitForAdaptiveShellViewport(container: HTMLElement, options?: {
65
+ readonly signal?: AbortSignal;
66
+ }): Promise<TargetViewportMeasurement>;
67
+ /**
68
+ * Mount the full-viewport shell around `gameRoot` and keep stage/rail bounds in sync.
69
+ * An existing ancestor with the shell id is reused when it already contains both rails.
70
+ */
71
+ export declare function mountAdaptiveGameShell(input: MountAdaptiveGameShellInput): AdaptiveGameShellController;