@wasm-gaming/engine-specs 0.3.1 → 0.4.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 +107 -3
- package/demo/app.html +59 -19
- package/demo/components/asset-picker.html +33 -14
- package/demo/components/engine-header.html +17 -28
- package/demo/components/site-toolbar.html +31 -0
- package/demo/demo.css +22 -12
- package/demo/demo.sdk.js +104 -322
- package/demo/index.html +17 -0
- package/dist/engine-specs.d.ts +388 -0
- package/dist/engine-specs.d.ts.map +1 -1
- package/dist/engine-specs.js +644 -1
- package/dist/engine-specs.js.map +1 -1
- package/package.json +1 -1
- package/src/engine-specs.ts +977 -0
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ The **engine contract** for the wasm-gaming ecosystem: one shared JS API that ev
|
|
|
4
4
|
WASM game/emulator engine implements and every host drives. Define it once here,
|
|
5
5
|
depend on it everywhere, and engines can't drift.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The types, plus the contract implemented once so twelve engines inherit it instead of
|
|
8
|
+
re-deriving it — [src/engine-specs.ts](src/engine-specs.ts) is the whole package. No
|
|
9
|
+
runtime dependencies, because twelve engines depend on this.
|
|
9
10
|
|
|
10
11
|
## The shape
|
|
11
12
|
|
|
@@ -55,6 +56,104 @@ without asking the engine anything, and without the host knowing a single key na
|
|
|
55
56
|
`restart()`. The descriptor shape is never accepted on write: that asymmetry is what
|
|
56
57
|
keeps a write unambiguous the day a value is itself an object.
|
|
57
58
|
|
|
59
|
+
## Listening
|
|
60
|
+
|
|
61
|
+
`on` and `off` are on the same fluent surface, so they chain wherever the setters do —
|
|
62
|
+
and **registration is synchronous on every carrier**, including the promise `start()`
|
|
63
|
+
returns. The chain is what the return value continues, never what the subscription
|
|
64
|
+
waits for; otherwise `start().on('error', …)` would bind after the session opened and
|
|
65
|
+
miss an error thrown while it was opening.
|
|
66
|
+
|
|
67
|
+
A listener receives an **event**, not the payload: what it carries is `detail`, as
|
|
68
|
+
`CustomEvent` does it. The envelope is what lets an event grow a field without every
|
|
69
|
+
listener signature changing — and what survives `postMessage` intact when the engine
|
|
70
|
+
lives in a worker, which is where this ecosystem is going.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const play = await new Rsdkv4SDK({ on: { error: report } }) // in the constructor
|
|
74
|
+
.on('start', (event) => console.log('session', event.detail))
|
|
75
|
+
.assets({ pack })
|
|
76
|
+
.start();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It is deliberately **not** a DOM event: two members, frozen, no bubbling, no
|
|
80
|
+
`preventDefault()`, no `target`. Half-implementing a familiar interface is worse than
|
|
81
|
+
offering a different one.
|
|
82
|
+
|
|
83
|
+
The factory and the engine it opens are **one channel**, so subscribing before there is
|
|
84
|
+
an engine is the normal way to do it. Every engine emits `start`, `error`, `exit`,
|
|
85
|
+
`pause` and `resume`; an engine that has more of its own declares them in its payload
|
|
86
|
+
bundle and they type-check alongside the core set:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
interface Rsdkv4Payloads extends EnginePayloads {
|
|
90
|
+
// …the four payloads
|
|
91
|
+
events: { levelLoaded: { zone: string } }; // optional; omit it for the core set alone
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`error` is emitted **and** the promise rejects — both, always — so a host that does not
|
|
96
|
+
`await` is not left blind, and one that does is not forced to subscribe.
|
|
97
|
+
|
|
98
|
+
## Writing an engine
|
|
99
|
+
|
|
100
|
+
None of the above has to be implemented by hand. `EnginePlayBase` and `EngineSDKBase`
|
|
101
|
+
carry the state, the descriptors, the cumulative merge, the chainable promise and the
|
|
102
|
+
channel; an engine declares its properties as data and fills in what only it knows.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { EnginePlayBase, EngineSDKBase } from '@wasm-gaming/engine-specs';
|
|
106
|
+
|
|
107
|
+
const spec = {
|
|
108
|
+
config: { region: { default: 'us', enum: ['us', 'eu', 'jp'], live: false } },
|
|
109
|
+
options: { slot: { default: 0, live: true } },
|
|
110
|
+
assets: { pack: { allowedTypes: ['.bin'], live: false } },
|
|
111
|
+
storage: { namespace: { default: 'rsdkv4', live: false } },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
class Rsdkv4Play extends EnginePlayBase {
|
|
115
|
+
constructor(init, events) { super(spec, init, events); }
|
|
116
|
+
async open(options) { /* start or reopen a session */ }
|
|
117
|
+
async close() { /* stop and release */ }
|
|
118
|
+
mounted(target) { /* the picture goes here */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class Rsdkv4SDK extends EngineSDKBase {
|
|
122
|
+
static id = 'rsdkv4';
|
|
123
|
+
static version = '1.0.0';
|
|
124
|
+
createPlay(init, events) { return new Rsdkv4Play(init, events); }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`default` is declared in the spec and **nowhere else** — the base seeds its starting
|
|
129
|
+
state from it, so the initial value and the advertised default cannot drift apart.
|
|
130
|
+
`start`, `exit` and `error` are emitted for you, including the rule that a failing hook
|
|
131
|
+
both rejects the chain and emits `error`; `this.emit(…)` is for the engine's own
|
|
132
|
+
events.
|
|
133
|
+
|
|
134
|
+
Adoption is optional: the contract is structural, so an engine with its own emitter and
|
|
135
|
+
its own state satisfies `EngineSDK` without extending anything. `EventEmitter` and
|
|
136
|
+
`hydratePlay` are exported for engines that want the pieces rather than the whole.
|
|
137
|
+
|
|
138
|
+
## Menus: one usability scheme
|
|
139
|
+
|
|
140
|
+
Engines also ship their own screens (start / load game, pause, options) as an **engine app**, and
|
|
141
|
+
all of them — and the launcher — follow one scheme, described in [UX.md](UX.md). The package carries
|
|
142
|
+
its runtime, so no engine needs another dependency:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { createInput, nextFocus, uiTokensCss } from '@wasm-gaming/engine-specs';
|
|
146
|
+
|
|
147
|
+
const input = createInput(); // keyboard + gamepads → actions, with repeat
|
|
148
|
+
input.on((action) => { // 'up' | 'down' | … | 'accept' | 'back' | 'menu' | 'more'
|
|
149
|
+
if (action === 'down') focus(nextFocus(current, buttons, 'down'));
|
|
150
|
+
});
|
|
151
|
+
input.suspend(); // the game has the controls now
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
An app implements `EngineApp` — `mount(target, host)` — and receives `game`, `storage`, `assets`,
|
|
155
|
+
the shared `input` and `exit()` from the host.
|
|
156
|
+
|
|
58
157
|
## Closing the types
|
|
59
158
|
|
|
60
159
|
One type parameter carries all four payloads, so there is no order to get wrong:
|
|
@@ -85,10 +184,15 @@ make preview # serve site/ on :8020 (PORT= to change it)
|
|
|
85
184
|
Two engines implement the contract in this repo, and both are type-checked in CI:
|
|
86
185
|
[tests/engine.conformance.ts](tests/engine.conformance.ts), the reference engine, and
|
|
87
186
|
[demo/demo.sdk.js](demo/demo.sdk.js), which drives the [demo
|
|
88
|
-
page](https://wasm-gaming.github.io/engine-specs/demo/).
|
|
187
|
+
page](https://wasm-gaming.github.io/engine-specs/demo/). Between them they cover both
|
|
188
|
+
branches of the event map — the reference engine declares one of its own, the demo
|
|
189
|
+
omits `events` and gets the core set alone. The negative cases live in
|
|
89
190
|
[tests/types.test-d.ts](tests/types.test-d.ts), guarded by `@ts-expect-error` — which
|
|
90
191
|
fails if a rejection ever stops happening.
|
|
91
192
|
|
|
193
|
+
The emitter is the one part with behaviour to run rather than compile, so it has
|
|
194
|
+
tests that run: [tests/event-emitter.test.mjs](tests/event-emitter.test.mjs).
|
|
195
|
+
|
|
92
196
|
## License
|
|
93
197
|
|
|
94
198
|
MIT.
|
package/demo/app.html
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
It drives the engine only through the contract, and every panel is painted from
|
|
5
5
|
something the contract handed back:
|
|
6
6
|
|
|
7
|
-
- the
|
|
7
|
+
- the toolbar is the reference's own, so the two pages are one site;
|
|
8
|
+
- the footer under it all is the class's static identity, read before any engine
|
|
9
|
+
exists — and the document title is the same reading;
|
|
8
10
|
- the pickers get their file filter from each asset descriptor's `allowedTypes`;
|
|
9
11
|
- the tables are `config()`, `options()`, `assets()` and `storage()` rendered as
|
|
10
12
|
they arrive — one component, and not one key name written on this side.
|
|
@@ -13,22 +15,16 @@
|
|
|
13
15
|
there is no mount bookkeeping here.
|
|
14
16
|
-->
|
|
15
17
|
|
|
16
|
-
<
|
|
18
|
+
<SiteToolbar :version="packageVersion" />
|
|
17
19
|
|
|
18
20
|
<div class="layout">
|
|
19
21
|
<aside>
|
|
20
|
-
<section>
|
|
22
|
+
<section class="panel">
|
|
21
23
|
<h2>assets</h2>
|
|
22
24
|
<AssetPicker :assets="assets" @select="onSelect" @remove="onRemove" />
|
|
23
25
|
</section>
|
|
24
26
|
|
|
25
|
-
<section>
|
|
26
|
-
<h2>lifecycle</h2>
|
|
27
|
-
<LifecycleBar :running="running" @start="onStart" @restart="onRestart"
|
|
28
|
-
@pause="onPause" @resume="onResume" @destroy="onDestroy" />
|
|
29
|
-
</section>
|
|
30
|
-
|
|
31
|
-
<section>
|
|
27
|
+
<section class="panel">
|
|
32
28
|
<h2>calls</h2>
|
|
33
29
|
<CallLog :entries="calls" />
|
|
34
30
|
</section>
|
|
@@ -37,11 +33,22 @@
|
|
|
37
33
|
<div>
|
|
38
34
|
<!-- The engine is handed this element and appends its own canvas to it. -->
|
|
39
35
|
<div id="stage"></div>
|
|
36
|
+
|
|
37
|
+
<!-- Under the picture and as wide as it: the verbs act on what is above them. -->
|
|
38
|
+
<section class="panel">
|
|
39
|
+
<h2>lifecycle</h2>
|
|
40
|
+
<LifecycleBar :running="running" @start="onStart" @restart="onRestart"
|
|
41
|
+
@pause="onPause" @resume="onResume" @destroy="onDestroy" />
|
|
42
|
+
</section>
|
|
43
|
+
|
|
40
44
|
<PropertyTable :tables="tables" />
|
|
41
45
|
</div>
|
|
42
46
|
</div>
|
|
43
47
|
|
|
48
|
+
<EngineHeader :version="version" :description="description" />
|
|
49
|
+
|
|
44
50
|
<script :setup="{}">
|
|
51
|
+
const SiteToolbar = await import('./components/site-toolbar.html');
|
|
45
52
|
const EngineHeader = await import('./components/engine-header.html');
|
|
46
53
|
const AssetPicker = await import('./components/asset-picker.html');
|
|
47
54
|
const PropertyTable = await import('./components/property-table.html');
|
|
@@ -50,9 +57,19 @@
|
|
|
50
57
|
|
|
51
58
|
const { DemoEngineSDK } = await import('./demo.sdk.js');
|
|
52
59
|
|
|
60
|
+
// The *package* version, for the toolbar — not the engine's. `make site` writes this
|
|
61
|
+
// file next to the demo from package.json, which is the same source TypeDoc reads, so
|
|
62
|
+
// the two bars cannot disagree. A bare `demo/` checkout has no such file, and the bar
|
|
63
|
+
// then shows the package name alone.
|
|
64
|
+
let packageVersion = '';
|
|
65
|
+
try {
|
|
66
|
+
({ version: packageVersion } = await import('./package-version.js'));
|
|
67
|
+
} catch {
|
|
68
|
+
packageVersion = '';
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
// Identity, off the class — no engine constructed to read it.
|
|
54
72
|
const name = DemoEngineSDK.name;
|
|
55
|
-
const id = DemoEngineSDK.id;
|
|
56
73
|
const version = DemoEngineSDK.version;
|
|
57
74
|
const description = DemoEngineSDK.description;
|
|
58
75
|
document.title = `${name} — engine-specs demo`;
|
|
@@ -71,6 +88,27 @@
|
|
|
71
88
|
calls = [line, ...calls].slice(0, 40);
|
|
72
89
|
};
|
|
73
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The only place the factory is built, so the page subscribes exactly once — and
|
|
93
|
+
* does it in the constructor, which is the same call as `.on(type, listener)`.
|
|
94
|
+
*
|
|
95
|
+
* The handlers outlive every engine this factory opens: `start` fires again on each
|
|
96
|
+
* `restart()`, `exit` on `destroy()`, and `pause`/`resume` come from methods the
|
|
97
|
+
* contract does not know about. Nothing below re-subscribes.
|
|
98
|
+
*
|
|
99
|
+
* Each one receives an event, not the payload: what it carries is `detail`.
|
|
100
|
+
*/
|
|
101
|
+
const engine = () =>
|
|
102
|
+
(sdk ??= new DemoEngineSDK({
|
|
103
|
+
on: {
|
|
104
|
+
start: ({ detail }) => say(`▸ start fileName: ${detail.fileName ?? 'unset'}`),
|
|
105
|
+
pause: ({ detail }) => say(`▸ pause owner: ${detail.owner}`),
|
|
106
|
+
resume: ({ detail }) => say(`▸ resume owner: ${detail.owner}`),
|
|
107
|
+
error: ({ detail }) => say(`▸ error ${detail.message}`),
|
|
108
|
+
exit: ({ type }) => say(`▸ ${type}`),
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
|
|
74
112
|
const show = (value) => {
|
|
75
113
|
if (value === undefined) return 'unset';
|
|
76
114
|
if (value instanceof Uint8Array) return `${value.byteLength} bytes`;
|
|
@@ -92,7 +130,7 @@
|
|
|
92
130
|
|
|
93
131
|
/** Reads the live engine — or the factory, when there is not one yet. */
|
|
94
132
|
const refresh = async () => {
|
|
95
|
-
const source = play ?? (
|
|
133
|
+
const source = play ?? engine();
|
|
96
134
|
const [config, options, assetProps, storage] = await Promise.all([
|
|
97
135
|
source.config(),
|
|
98
136
|
source.options(),
|
|
@@ -121,7 +159,7 @@
|
|
|
121
159
|
const onSelect = async ({ key, file }) => {
|
|
122
160
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
123
161
|
picked[key] = file.name;
|
|
124
|
-
|
|
162
|
+
engine();
|
|
125
163
|
say(`sdk.assets({ ${key}: Uint8Array(${bytes.byteLength}) })`);
|
|
126
164
|
sdk.assets({ [key]: bytes });
|
|
127
165
|
if (key === 'rom') {
|
|
@@ -139,7 +177,7 @@
|
|
|
139
177
|
};
|
|
140
178
|
|
|
141
179
|
const onStart = async () => {
|
|
142
|
-
|
|
180
|
+
engine();
|
|
143
181
|
say('play = await sdk.mount(stage).start()');
|
|
144
182
|
// The whole chain in one expression, exactly as the contract advertises it.
|
|
145
183
|
play = await sdk.mount(document.querySelector('#stage')).start();
|
|
@@ -152,15 +190,16 @@
|
|
|
152
190
|
await refresh();
|
|
153
191
|
};
|
|
154
192
|
|
|
155
|
-
//
|
|
193
|
+
// The methods are this engine's own; the `pause`/`resume` they emit are the
|
|
194
|
+
// contract's — which is why the log shows a ▸ line the page never wrote here.
|
|
156
195
|
const onPause = async () => {
|
|
157
|
-
say('play.pause() // engine-specific,
|
|
196
|
+
say('play.pause() // engine-specific method, contract event');
|
|
158
197
|
play.pause();
|
|
159
198
|
await refresh();
|
|
160
199
|
};
|
|
161
200
|
|
|
162
201
|
const onResume = async () => {
|
|
163
|
-
say('play.resume() // engine-specific,
|
|
202
|
+
say('play.resume() // engine-specific method, contract event');
|
|
164
203
|
play.resume();
|
|
165
204
|
await refresh();
|
|
166
205
|
};
|
|
@@ -193,7 +232,8 @@
|
|
|
193
232
|
}
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
aside section
|
|
235
|
+
/* A class, not `aside section`: the lifecycle panel lives in the other column now. */
|
|
236
|
+
.panel {
|
|
197
237
|
background: var(--panel, #fff);
|
|
198
238
|
border: 1px solid var(--border, #e2e8f0);
|
|
199
239
|
border-radius: 4px;
|
|
@@ -201,7 +241,7 @@
|
|
|
201
241
|
margin-bottom: 1rem;
|
|
202
242
|
}
|
|
203
243
|
|
|
204
|
-
|
|
244
|
+
.panel h2 {
|
|
205
245
|
margin: 0 0 0.75rem;
|
|
206
246
|
font-size: 0.78rem;
|
|
207
247
|
font-family: var(--mono, monospace);
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
@dragleave="dragging = null"
|
|
14
14
|
@drop.prevent="onDrop($event, asset)"
|
|
15
15
|
:class.active="dragging === asset.key">
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
<div class="meta">
|
|
17
|
+
<strong>{{ asset.key }}</strong>
|
|
18
|
+
<p class="hint">{{ asset.description }}</p>
|
|
19
|
+
<p :class.file-selected="asset.fileName">{{ asset.fileName || asset.accepts }}</p>
|
|
20
|
+
</div>
|
|
19
21
|
<div class="picker-actions">
|
|
20
22
|
<label class="picker">
|
|
21
23
|
{{ asset.fileName ? 'Change' : 'Pick file' }}
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
<button type="button" :if="asset.fileName" class="remove-btn" @click.stop="$emit('remove', asset)">Remove</button>
|
|
25
27
|
</div>
|
|
26
28
|
</section>
|
|
29
|
+
|
|
27
30
|
</div>
|
|
28
31
|
|
|
29
32
|
<script :setup="{
|
|
@@ -58,14 +61,26 @@
|
|
|
58
61
|
gap: 0.6rem;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/*
|
|
65
|
+
A row, not a card: the key and its hints on the left, the actions on the right. The
|
|
66
|
+
whole box is still the drop target — it is only shorter, so two assets and the call
|
|
67
|
+
log fit the column without scrolling.
|
|
68
|
+
*/
|
|
61
69
|
.drop {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 0.6rem;
|
|
62
74
|
border: 1px dashed var(--border, #e2e8f0);
|
|
63
75
|
border-radius: 4px;
|
|
64
|
-
padding:
|
|
65
|
-
text-align: center;
|
|
76
|
+
padding: 0.6rem 0.75rem;
|
|
66
77
|
transition: border-color 200ms ease, background 200ms ease;
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
.meta {
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
.drop.active {
|
|
70
85
|
border-color: var(--accent, #ff007f);
|
|
71
86
|
background: rgb(255 0 127 / 0.06);
|
|
@@ -85,14 +100,16 @@
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
.drop p {
|
|
88
|
-
margin: 0.
|
|
103
|
+
margin: 0.15rem 0 0;
|
|
89
104
|
color: var(--muted, #64748b);
|
|
90
|
-
font-size: 0.
|
|
91
|
-
|
|
105
|
+
font-size: 0.75rem;
|
|
106
|
+
line-height: 1.35;
|
|
107
|
+
/* `anywhere`, not `break-all`: a long file name still breaks, prose does not. */
|
|
108
|
+
overflow-wrap: anywhere;
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
.drop p.hint {
|
|
95
|
-
font-size: 0.
|
|
112
|
+
font-size: 0.78rem;
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
.drop p.file-selected {
|
|
@@ -102,18 +119,20 @@
|
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
.picker-actions {
|
|
105
|
-
display:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
align-items: stretch;
|
|
125
|
+
gap: 0.3rem;
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
.picker,
|
|
112
129
|
.remove-btn {
|
|
113
130
|
display: inline-flex;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
white-space: nowrap;
|
|
114
133
|
font-family: var(--mono, monospace);
|
|
115
134
|
font-size: 0.75rem;
|
|
116
|
-
padding: 0.
|
|
135
|
+
padding: 0.3rem 0.6rem;
|
|
117
136
|
border: 1px solid var(--border, #e2e8f0);
|
|
118
137
|
border-radius: 3px;
|
|
119
138
|
background: transparent;
|
|
@@ -4,58 +4,51 @@
|
|
|
4
4
|
This is what replaced the manifest: `id`, `version`, `name` and `description` are
|
|
5
5
|
static members of the SDK constructor, so a launcher can list engines without
|
|
6
6
|
booting one.
|
|
7
|
+
|
|
8
|
+
It sits at the foot of the page and says nothing about the site: the toolbar above
|
|
9
|
+
carries that, which is why neither the link back to the reference nor a page-titling
|
|
10
|
+
`h1` is here any more. The name still reaches the reader — as the document title,
|
|
11
|
+
written from the same static member.
|
|
7
12
|
-->
|
|
8
|
-
<header>
|
|
13
|
+
<footer class="engine-header">
|
|
9
14
|
<div>
|
|
10
|
-
<h1>{{ name }} <code>{{ id }}</code></h1>
|
|
11
15
|
<p>{{ description }}</p>
|
|
12
16
|
</div>
|
|
13
17
|
<div class="right">
|
|
14
18
|
<span class="pill">v{{ version }}</span>
|
|
15
|
-
<a class="pill link" :href="referenceHref">API reference</a>
|
|
16
19
|
</div>
|
|
17
|
-
</
|
|
20
|
+
</footer>
|
|
18
21
|
|
|
19
22
|
<script :setup="{
|
|
20
|
-
name,
|
|
21
|
-
id,
|
|
22
23
|
version,
|
|
23
24
|
description = '',
|
|
24
|
-
referenceHref = '../',
|
|
25
25
|
}">
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
28
|
<style>
|
|
29
|
-
|
|
29
|
+
/*
|
|
30
|
+
Scoped to a class, not to the element. A component's styles land in the page's own
|
|
31
|
+
<style>, unscoped — so a bare `header {}` here reached the site toolbar above and
|
|
32
|
+
made a flex container of it, which centred the whole bar. Class selectors keep a
|
|
33
|
+
component's look to the component.
|
|
34
|
+
*/
|
|
35
|
+
.engine-header {
|
|
30
36
|
display: flex;
|
|
31
37
|
flex-wrap: wrap;
|
|
32
38
|
gap: 1rem;
|
|
33
39
|
align-items: center;
|
|
34
40
|
justify-content: space-between;
|
|
35
41
|
padding: 1.25rem 1.5rem;
|
|
36
|
-
border-
|
|
42
|
+
border-top: 1px solid var(--border, #e2e8f0);
|
|
37
43
|
background: var(--panel, #fff);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
.engine-header p {
|
|
41
47
|
margin: 0;
|
|
42
|
-
font-size: 1.25rem;
|
|
43
|
-
letter-spacing: 0.02em;
|
|
44
|
-
color: var(--text, #0f172a);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
code {
|
|
48
|
-
font-family: var(--mono, monospace);
|
|
49
|
-
font-size: 0.75em;
|
|
50
48
|
color: var(--muted, #64748b);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
margin: 0.25rem 0 0;
|
|
55
|
-
color: var(--muted, #64748b);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.right {
|
|
51
|
+
.engine-header .right {
|
|
59
52
|
display: flex;
|
|
60
53
|
gap: 0.5rem;
|
|
61
54
|
}
|
|
@@ -71,8 +64,4 @@
|
|
|
71
64
|
text-transform: uppercase;
|
|
72
65
|
white-space: nowrap;
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
.link {
|
|
76
|
-
text-decoration: none;
|
|
77
|
-
}
|
|
78
67
|
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The reference's own toolbar, reused rather than imitated: the class names are
|
|
3
|
+
TypeDoc's and so is the stylesheet that paints them, which index.html loads from
|
|
4
|
+
`../assets/style.css`. Two pages, one bar, and nothing here to keep in sync by hand.
|
|
5
|
+
|
|
6
|
+
What is missing is `button#tsd-search-trigger`. This page carries no search index and
|
|
7
|
+
does not load TypeDoc's `main.js`, so the button would open nothing — and a control
|
|
8
|
+
that does nothing is worse than a control that isn't there.
|
|
9
|
+
|
|
10
|
+
The version is *given* to this component, never written into it: TypeDoc reads it from
|
|
11
|
+
package.json, and a copy hard-coded here would drift the first time it changed. With
|
|
12
|
+
nothing to show — a bare `demo/` checkout — the bar shows the name alone rather than a
|
|
13
|
+
wrong number.
|
|
14
|
+
-->
|
|
15
|
+
<header class="tsd-page-toolbar">
|
|
16
|
+
<div class="tsd-toolbar-contents container">
|
|
17
|
+
<a class="title" :href="referenceHref">{{ title }}<span :if="version"> - v{{ version }}</span></a>
|
|
18
|
+
<div id="tsd-toolbar-links">
|
|
19
|
+
<a :href="referenceHref">Reference</a>
|
|
20
|
+
<a :href="repositoryHref">GitHub</a>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<script :setup="{
|
|
26
|
+
title = '@wasm-gaming/engine-specs',
|
|
27
|
+
version = '',
|
|
28
|
+
referenceHref = '../',
|
|
29
|
+
repositoryHref = 'https://github.com/wasm-gaming/engine-specs',
|
|
30
|
+
}">
|
|
31
|
+
</script>
|
package/demo/demo.css
CHANGED
|
@@ -7,22 +7,32 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
:root {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/*
|
|
11
|
+
The reference's palette, read off the variables TypeDoc defines, so the theme
|
|
12
|
+
switch in the docs sidebar drives this page too — there is no second switch here
|
|
13
|
+
to disagree with it, and no `prefers-color-scheme` block either: TypeDoc's `os`
|
|
14
|
+
theme already means "follow the system".
|
|
15
|
+
|
|
16
|
+
`--accent` stays the demo's own. It marks what the page does, not what the site
|
|
17
|
+
is, and the reference has no equivalent to borrow.
|
|
18
|
+
*/
|
|
19
|
+
--bg: var(--color-background);
|
|
20
|
+
--panel: var(--color-background-secondary);
|
|
21
|
+
--border: var(--color-accent);
|
|
22
|
+
--text: var(--color-text);
|
|
23
|
+
--muted: var(--color-text-aside);
|
|
15
24
|
--accent: #ff007f;
|
|
16
25
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
/*
|
|
29
|
+
TypeDoc hides the toolbar links below 769px because its menu trigger takes over
|
|
30
|
+
there. This page has no menu, so hiding them would leave no way back to the
|
|
31
|
+
reference. Unlayered, so it wins.
|
|
32
|
+
*/
|
|
33
|
+
@media (max-width: 769px) {
|
|
34
|
+
#tsd-toolbar-links {
|
|
35
|
+
display: flex;
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
|