@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.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 +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
// why the mechanism exists, just not exercised by any class shown here).
|
|
46
46
|
|
|
47
47
|
import { classAncestorChain, SPRITE_REGISTRY } from "../domain/sprite-map.mjs";
|
|
48
|
-
import { resolveSpriteAsset } from "../domain/sprite-templates.mjs";
|
|
48
|
+
import { resolveSpriteAsset, matchConstraints } from "../domain/sprite-templates.mjs";
|
|
49
49
|
import { MATERIAL_PALETTE } from "../domain/sprite-materials.mjs";
|
|
50
50
|
import { spriteFactRows } from "../domain/sprite-facts.mjs";
|
|
51
51
|
import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
|
|
@@ -165,10 +165,10 @@ export const GROUP_OBJECT = "object";
|
|
|
165
165
|
export const GROUP_EMOJI = "emoji";
|
|
166
166
|
|
|
167
167
|
export const CATALOG_GROUPS = Object.freeze([
|
|
168
|
-
Object.freeze({ id: GROUP_ADVENTURE, label: "Ashcombe Hall's own adventure props", note: "the icon tier's
|
|
168
|
+
Object.freeze({ id: GROUP_ADVENTURE, label: "Ashcombe Hall's own adventure props", note: "the icon tier's named cast and furniture. Each has its own 44px sprite." }),
|
|
169
169
|
Object.freeze({ id: GROUP_PERSON, label: "Person roles" }),
|
|
170
170
|
Object.freeze({ id: GROUP_OBJECT, label: "Physical objects, creatures & places" }),
|
|
171
|
-
Object.freeze({ id: GROUP_EMOJI, label: "Emotions & events", note: "abstract concepts with no
|
|
171
|
+
Object.freeze({ id: GROUP_EMOJI, label: "Emotions & events", note: "abstract concepts with no single physical picture, drawn as the familiar emoji instead" }),
|
|
172
172
|
]);
|
|
173
173
|
|
|
174
174
|
/** Which catalog section `cls` belongs in. Pure. `isIconTierClass`/`isEmoji`
|
|
@@ -181,6 +181,33 @@ export function groupForClass(cls, { isIconTierClass, isEmoji }) {
|
|
|
181
181
|
return GROUP_OBJECT;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/** Cluster one presentation group's entries under each class's nearest
|
|
185
|
+
* ILLUSTRATED ancestor — the first term after the class itself, walking its
|
|
186
|
+
* own real chain outward, that carries a sprite template at either tier. A
|
|
187
|
+
* new leaf attached under that ancestor later resolves to the ancestor's
|
|
188
|
+
* own sprite through the same walk, so these headings show exactly where
|
|
189
|
+
* the graph can grow with no new art needed. A cluster needs two members
|
|
190
|
+
* to earn a heading; singletons (and classes whose chain reaches no
|
|
191
|
+
* illustrated ancestor) fold into one trailing `ancestor: null` bucket so
|
|
192
|
+
* the section never shreds into one-card headings. Pure. */
|
|
193
|
+
export function clusterEntriesByAncestor(entries, spritedClasses) {
|
|
194
|
+
const byKey = new Map();
|
|
195
|
+
for (const e of entries || []) {
|
|
196
|
+
const key = (e.chain || []).slice(1).find((t) => spritedClasses.has(t)) || "";
|
|
197
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
198
|
+
byKey.get(key).push(e);
|
|
199
|
+
}
|
|
200
|
+
const clusters = [];
|
|
201
|
+
const rest = [];
|
|
202
|
+
for (const [key, list] of byKey) {
|
|
203
|
+
if (key && list.length >= 2) clusters.push({ ancestor: key, entries: list });
|
|
204
|
+
else rest.push(...list);
|
|
205
|
+
}
|
|
206
|
+
clusters.sort((a, b) => b.entries.length - a.entries.length || a.ancestor.localeCompare(b.ancestor));
|
|
207
|
+
if (rest.length) clusters.push({ ancestor: null, entries: [...rest].sort((a, b) => a.className.localeCompare(b.className)) });
|
|
208
|
+
return clusters;
|
|
209
|
+
}
|
|
210
|
+
|
|
184
211
|
function templatesForClass(cls, templates) {
|
|
185
212
|
return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(cls));
|
|
186
213
|
}
|
|
@@ -220,7 +247,16 @@ export function parameterVariantsFor(template) {
|
|
|
220
247
|
* generic root-fallback shape, not its own silhouette (this module's own
|
|
221
248
|
* header names the 19 large-tier classes this applies to). Every
|
|
222
249
|
* [parameters.*] value and every [match] variant renders through the real
|
|
223
|
-
* resolveSpriteAsset with the exact property
|
|
250
|
+
* resolveSpriteAsset with the exact property facts it declares — and a
|
|
251
|
+
* parameter sitting ON a [match] variant is rendered with BOTH facts, so a
|
|
252
|
+
* facing profile's mood swatch shows the profile wearing that mood and is
|
|
253
|
+
* labeled for the pair ("left + happy"). Rendering it on the parameter fact
|
|
254
|
+
* alone would resolve a different template altogether (the front-facing
|
|
255
|
+
* one) and label it as if it belonged to the profile. A value that a
|
|
256
|
+
* match-free template of the same class already offers is listed once, from
|
|
257
|
+
* that template: the facing pair and the `*-with-emotion.toml` file both
|
|
258
|
+
* carry all six moods, and one swatch per mood is the catalog's unit. A
|
|
259
|
+
* class
|
|
224
260
|
* with templates but no plain among them also gets one extra swatch,
|
|
225
261
|
* labeled `fallback: true`, showing exactly what the real resolver returns
|
|
226
262
|
* for that class with NO taught fact — honest engine behaviour, never
|
|
@@ -237,19 +273,33 @@ export function tierSwatchesFor(cls, templates, registry, tier) {
|
|
|
237
273
|
svg: resolveSpriteAsset(cls, [], [], templates, registry, { instanceKey: `${cls}-${tier}-plain` }),
|
|
238
274
|
});
|
|
239
275
|
}
|
|
276
|
+
const valueKey = (v) => `${v.paramName}:${v.rawValue}`;
|
|
277
|
+
const shownByUnmatchedTemplate = new Set(
|
|
278
|
+
forClass.filter((t) => !t.match).flatMap((t) => parameterVariantsFor(t).map(valueKey)),
|
|
279
|
+
);
|
|
240
280
|
for (const t of forClass) {
|
|
241
|
-
|
|
242
|
-
|
|
281
|
+
// A variant may require several facts at once (sprite-templates.mjs's own
|
|
282
|
+
// matchConstraints), so the swatch is rendered with EVERY fact its
|
|
283
|
+
// [match] asks for and labeled for the whole set — "left + moving" for a
|
|
284
|
+
// combined facing-and-pose file. A one-constraint variant reads exactly
|
|
285
|
+
// as it did before the plural spelling existed.
|
|
286
|
+
const constraints = matchConstraints(t);
|
|
287
|
+
const matchFact = constraints.map((c) => ({ predicate: c.property, object: c.value }));
|
|
288
|
+
const variantLabel = constraints.map((c) => c.value).join(" + ");
|
|
289
|
+
const variantKey = constraints.map((c) => c.value).join("-");
|
|
290
|
+
const matchPrefix = constraints.length ? `${variantLabel} + ` : "";
|
|
291
|
+
if (constraints.length) {
|
|
243
292
|
swatches.push({
|
|
244
|
-
tier, label:
|
|
245
|
-
svg: resolveSpriteAsset(cls, [],
|
|
293
|
+
tier, label: variantLabel, kind: "variant", property: constraints.map((c) => c.property).join(" + "),
|
|
294
|
+
svg: resolveSpriteAsset(cls, [], matchFact, templates, registry, { instanceKey: `${cls}-${tier}-match-${variantKey}` }),
|
|
246
295
|
});
|
|
247
296
|
}
|
|
248
297
|
for (const v of parameterVariantsFor(t)) {
|
|
249
|
-
|
|
298
|
+
if (constraints.length && shownByUnmatchedTemplate.has(valueKey(v))) continue;
|
|
299
|
+
const propertyFacts = [...matchFact, { predicate: v.property, object: v.rawValue }];
|
|
250
300
|
swatches.push({
|
|
251
|
-
tier, label: v.rawValue
|
|
252
|
-
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${v.paramName}-${v.rawValue}` }),
|
|
301
|
+
tier, label: `${matchPrefix}${v.rawValue}`, kind: "material", property: v.property, treatment: v.treatment,
|
|
302
|
+
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${matchPrefix ? `${variantKey}-` : ""}${v.paramName}-${v.rawValue}` }),
|
|
253
303
|
});
|
|
254
304
|
}
|
|
255
305
|
}
|
|
@@ -284,121 +334,95 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
|
|
|
284
334
|
});
|
|
285
335
|
}
|
|
286
336
|
|
|
287
|
-
// ---- scene composer
|
|
337
|
+
// ---- scene composer ----
|
|
288
338
|
//
|
|
289
|
-
// The free-text "there is a..." box (
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// (className -> real swatch labels, read straight off this page's own
|
|
339
|
+
// The free-text "there is a..." box (adventure-viz.mjs's roomSceneObjects/
|
|
340
|
+
// room-frame is the same shape) over THIS page's own already-real classes.
|
|
341
|
+
// The parser itself is scene-compose.mjs, which resolves a typed class name
|
|
342
|
+
// through the real resolver; this page owns only the class index it is matched
|
|
343
|
+
// against (className -> real swatch labels, read straight off this page's own
|
|
294
344
|
// already-rendered `.card`/`.swatch-label` markup at load time — see this
|
|
295
345
|
// module's own header for why that beats re-embedding the same SVG data a
|
|
296
|
-
// second time) is built client-side
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
346
|
+
// second time), which is built client-side, since walking rendered DOM has no
|
|
347
|
+
// meaning in this pure module.
|
|
348
|
+
|
|
349
|
+
export { extractSceneItems } from "../domain/scene-compose.mjs";
|
|
350
|
+
|
|
351
|
+
// ---- the catalog question lane ----
|
|
352
|
+
//
|
|
353
|
+
// There isn't one. Every catalog-shaped question the dock takes — "how many
|
|
354
|
+
// sprite classes are there", "list the sprite classes", "what parameters does
|
|
355
|
+
// a person sprite take", "what emotions does a person sprite accept" — is
|
|
356
|
+
// answered by the ordinary chat engine reading the same sprite-facts rows this
|
|
357
|
+
// page embeds, through the membership, count and property lanes it already runs
|
|
358
|
+
// for every other caller. The predicate spellings sprite-facts.mjs mints
|
|
359
|
+
// (mgx:take-parameter, mgx:accept-emotion, mgx:offer-variant) are what those
|
|
360
|
+
// lanes read, so the page hands the line over and renders what comes back.
|
|
361
|
+
|
|
362
|
+
// ---- the animated swatches (pure) ----
|
|
363
|
+
//
|
|
364
|
+
// Each card's large tier carries three animated swatches, one per axis the
|
|
365
|
+
// sprite tier actually varies on: mood (mgx:feels), facing (mgx:faces) and
|
|
366
|
+
// pose (mgx:pose). The page builds their frames client-side out of its own
|
|
367
|
+
// already-rendered swatches (see the inline script below — the same
|
|
368
|
+
// read-the-DOM posture the scene composer's class index takes), but the
|
|
369
|
+
// frame ORDER and the tempo are real logic worth pinning on their own, so
|
|
370
|
+
// they live here as pure functions the page splices in by `.toString()`.
|
|
371
|
+
// One delay constant serves all three, so the three can't drift apart into
|
|
372
|
+
// three tempos.
|
|
373
|
+
//
|
|
374
|
+
// A `frame` is `{svg, label}`. An empty return means there is nothing worth
|
|
375
|
+
// animating for that class, and the page leaves its static swatch alone.
|
|
376
|
+
|
|
377
|
+
/** The step between animated swatch frames, in milliseconds — the mood
|
|
378
|
+
* cycle's own established tempo, shared by the facing sweep and the pose
|
|
379
|
+
* toggle. */
|
|
380
|
+
export const CYCLE_FRAME_DELAY_MS = 800;
|
|
381
|
+
|
|
382
|
+
/** The turntable order the facing sweep steps through: one continuous
|
|
383
|
+
* rotation from the sprite's own left profile round to its right. `null`
|
|
384
|
+
* is the centre pose — the plain sprite, no mgx:faces fact at all — which
|
|
385
|
+
* sits in the middle of the sweep exactly where the geometry puts it, so
|
|
386
|
+
* the turn reads as a rotation rather than a jump back through front. */
|
|
387
|
+
export const FACING_TURN_ORDER = Object.freeze(["left", "half-left", null, "half-right", "right"]);
|
|
388
|
+
|
|
389
|
+
/** The mood cycle's frames: the class's plain sprite, then every mood it
|
|
390
|
+
* has a rendered swatch for, in the order the templates declare them. Two
|
|
391
|
+
* moods is the floor — one mood alternating with plain is a blink, not a
|
|
392
|
+
* cycle. Pure; self-contained (no outer refs), `.toString()`-splice safe. */
|
|
393
|
+
export function moodFrameSequence(plainFrame, moodFrames) {
|
|
394
|
+
const moods = (moodFrames || []).filter(Boolean);
|
|
395
|
+
if (moods.length < 2) return [];
|
|
396
|
+
return plainFrame ? [plainFrame, ...moods] : moods;
|
|
308
397
|
}
|
|
309
398
|
|
|
310
|
-
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
* first, so the longer name always wins the position it starts at. Pure. */
|
|
325
|
-
export function extractSceneItems(text, classIndex) {
|
|
326
|
-
const index = classIndex || {};
|
|
327
|
-
const candidates = Object.keys(index)
|
|
328
|
-
.map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
|
|
329
|
-
.filter((c) => c.words.length)
|
|
330
|
-
.sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
|
|
331
|
-
const tokens = tokenizeSceneText(text);
|
|
332
|
-
const used = new Array(tokens.length).fill(false);
|
|
333
|
-
const items = [];
|
|
334
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
335
|
-
if (used[i]) continue;
|
|
336
|
-
const hit = candidates.find(({ words }) => {
|
|
337
|
-
if (i + words.length > tokens.length) return false;
|
|
338
|
-
for (let k = 0; k < words.length; k += 1) {
|
|
339
|
-
if (used[i + k] || tokens[i + k].word !== words[k]) return false;
|
|
340
|
-
}
|
|
341
|
-
return true;
|
|
342
|
-
});
|
|
343
|
-
if (!hit) continue;
|
|
344
|
-
let materialLabel = null;
|
|
345
|
-
if (i > 0 && !used[i - 1]) {
|
|
346
|
-
const materials = index[hit.name]?.materials || {};
|
|
347
|
-
const prevWord = tokens[i - 1].word;
|
|
348
|
-
if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
|
|
349
|
-
materialLabel = prevWord;
|
|
350
|
-
used[i - 1] = true;
|
|
351
|
-
}
|
|
399
|
+
/** The facing sweep's frames: `order` (FACING_TURN_ORDER) filtered down to
|
|
400
|
+
* the facings this class actually has a rendered swatch for, with
|
|
401
|
+
* `centreFrame` — the plain, undirected sprite — dropped into the centre
|
|
402
|
+
* slot. `facingFrames` is keyed by each swatch's own label, which is its
|
|
403
|
+
* real mgx:faces value. A class with one facing or none has no sweep.
|
|
404
|
+
* Pure; self-contained, `.toString()`-splice safe. */
|
|
405
|
+
export function turnFrameSequence(order, centreFrame, facingFrames) {
|
|
406
|
+
const byFacing = facingFrames || {};
|
|
407
|
+
const frames = [];
|
|
408
|
+
for (const facing of order || []) {
|
|
409
|
+
if (facing === null) {
|
|
410
|
+
if (centreFrame) frames.push({ svg: centreFrame.svg, label: "centre" });
|
|
411
|
+
} else if (byFacing[facing]) {
|
|
412
|
+
frames.push({ svg: byFacing[facing].svg, label: facing });
|
|
352
413
|
}
|
|
353
|
-
for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
|
|
354
|
-
items.push({ className: hit.name, materialLabel });
|
|
355
414
|
}
|
|
356
|
-
return
|
|
415
|
+
return frames.length >= 2 ? frames : [];
|
|
357
416
|
}
|
|
358
417
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
* including a catalog-shaped question about a class with no rows on record —
|
|
368
|
-
* the caller falls through to the engine, whose refusal is the honest miss.
|
|
369
|
-
* Self-contained (no outer refs), `.toString()`-splice safe. Pure. */
|
|
370
|
-
export function answerSpriteQuestion(text, rows) {
|
|
371
|
-
const all = Array.isArray(rows) ? rows : [];
|
|
372
|
-
const q = String(text ?? "").trim().toLowerCase().replace(/[?.!\s]+$/, "");
|
|
373
|
-
|
|
374
|
-
if (/^what (?:classes|sprites|sprite classes) (?:can you|do you|are there)(?:\s+(?:render|draw|show))?$/.test(q)) {
|
|
375
|
-
const classes = all
|
|
376
|
-
.filter((r) => r.predicate === "rdf:type" && r.object === "sprite class")
|
|
377
|
-
.map((r) => String(r.subject).replace(/ sprite$/, ""));
|
|
378
|
-
if (!classes.length) return null;
|
|
379
|
-
const sample = classes.slice(0, 10).join(", ");
|
|
380
|
-
return {
|
|
381
|
-
answer: `${classes.length} sprite classes are on record — ${sample}, … (the catalog below lists every one).`,
|
|
382
|
-
grounding: classes.length,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const m = q.match(/^(?:what|which) parameters? (?:does|do|can|will) (?:a |an |the )?(.+?)(?: sprite)? (?:take|accept|use)s?$/);
|
|
387
|
-
if (m) {
|
|
388
|
-
const subject = `${m[1]} sprite`;
|
|
389
|
-
const params = all.filter((r) => r.subject === subject && r.predicate === "mgx:take-parameter").map((r) => r.object);
|
|
390
|
-
if (!params.length) return null;
|
|
391
|
-
let grounding = params.length;
|
|
392
|
-
const parts = params.map((p) => {
|
|
393
|
-
const values = all.filter((r) => r.subject === subject && r.predicate === `mgx:accept-${p}`).map((r) => r.object);
|
|
394
|
-
grounding += values.length;
|
|
395
|
-
return values.length ? `${p} (${values.join(", ")})` : p;
|
|
396
|
-
});
|
|
397
|
-
const lead = params.length === 1 ? "one parameter" : `${params.length} parameters`;
|
|
398
|
-
return { answer: `a ${subject} takes ${lead}: ${parts.join("; ")}.`, grounding };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return null;
|
|
418
|
+
/** The pose toggle's frames: the class's plain idle sprite alternating with
|
|
419
|
+
* its mgx:pose = "moving" one. Both have to be there — a class with no
|
|
420
|
+
* moving sprite has no second state to toggle into, and gets nothing
|
|
421
|
+
* rather than a still frame pretending to animate. Pure; self-contained,
|
|
422
|
+
* `.toString()`-splice safe. */
|
|
423
|
+
export function movingFrameSequence(idleFrame, movingFrame) {
|
|
424
|
+
if (!idleFrame || !movingFrame) return [];
|
|
425
|
+
return [{ svg: idleFrame.svg, label: "idle" }, { svg: movingFrame.svg, label: "moving" }];
|
|
402
426
|
}
|
|
403
427
|
|
|
404
428
|
// ---- rendering ----
|
|
@@ -418,7 +442,8 @@ function swatchHtml(s) {
|
|
|
418
442
|
if (s.treatment) parts.push(`<span class="swatch-treat">→ ${escapeHtml(s.treatment)} treatment</span>`);
|
|
419
443
|
const title = s.property ? `${s.property} = ${s.label}` : s.label;
|
|
420
444
|
const cls = ["swatch", s.tier, s.kind].filter(Boolean).join(" ");
|
|
421
|
-
|
|
445
|
+
const property = s.property ? ` data-property="${escapeHtml(s.property)}"` : "";
|
|
446
|
+
return `<div class="${cls}"${property} title="${escapeHtml(title)}"><div class="swatch-img">${s.svg}</div><div class="swatch-caption">${parts.join("")}</div></div>`;
|
|
422
447
|
}
|
|
423
448
|
|
|
424
449
|
function tierRowHtml(tierName, swatches) {
|
|
@@ -438,14 +463,32 @@ function cardHtml(entry) {
|
|
|
438
463
|
</article>`;
|
|
439
464
|
}
|
|
440
465
|
|
|
441
|
-
|
|
466
|
+
/** One ancestor cluster inside a section: a heading that carries the
|
|
467
|
+
* ancestor's OWN resolved sprite (the proof the fallback has art to land
|
|
468
|
+
* on), then that cluster's cards. `ancestorSvg` is null for the trailing
|
|
469
|
+
* no-illustrated-ancestor bucket. */
|
|
470
|
+
function clusterHtml(cluster, ancestorSvg) {
|
|
471
|
+
const name = cluster.ancestor || "everything else";
|
|
472
|
+
const chip = ancestorSvg ? `<span class="cluster-chip">${ancestorSvg}</span>` : "";
|
|
473
|
+
return `<div class="cluster">
|
|
474
|
+
<h3 class="cluster-head">${chip}<span class="cluster-name">${escapeHtml(name)}</span><span class="count">${cluster.entries.length}</span></h3>
|
|
475
|
+
<div class="cards">${cluster.entries.map(cardHtml).join("")}</div>
|
|
476
|
+
</div>`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function sectionHtml(group, entries, { clusterBy = null } = {}) {
|
|
442
480
|
const rows = entries.filter((e) => e.group === group.id);
|
|
443
481
|
if (!rows.length) return "";
|
|
444
482
|
const note = group.note ? `<p class="section-note">${escapeHtml(group.note)}</p>` : "";
|
|
483
|
+
const body = clusterBy
|
|
484
|
+
? clusterEntriesByAncestor(rows, clusterBy.spritedClasses)
|
|
485
|
+
.map((c) => clusterHtml(c, c.ancestor ? clusterBy.resolveChip(c.ancestor) : null))
|
|
486
|
+
.join("")
|
|
487
|
+
: `<div class="cards">${rows.map(cardHtml).join("")}</div>`;
|
|
445
488
|
return `<section class="group" id="g-${group.id}" aria-label="${escapeHtml(group.label)}">
|
|
446
489
|
<h2>${escapeHtml(group.label)} <span class="count">${rows.length}</span></h2>
|
|
447
490
|
${note}
|
|
448
|
-
|
|
491
|
+
${body}
|
|
449
492
|
</section>`;
|
|
450
493
|
}
|
|
451
494
|
|
|
@@ -459,18 +502,29 @@ function sectionHtml(group, entries) {
|
|
|
459
502
|
* ancestor chains.
|
|
460
503
|
*
|
|
461
504
|
* `spritesBundleAvailable: true` (ledger-viz.mjs's own ledgerBundleAvailable
|
|
462
|
-
* idiom) is what adds the
|
|
463
|
-
* sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
|
|
505
|
+
* idiom) is what adds the two interactive panels at all: the page then embeds
|
|
506
|
+
* the sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
|
|
464
507
|
* same two template sets) and references the sibling
|
|
465
508
|
* ./sprites-browser.bundle.js scripts/build-demo-site.mjs builds alongside
|
|
466
|
-
* it.
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
509
|
+
* it. Both panels need that bundle — the dock for its chat session, the scene
|
|
510
|
+
* composer for the resolver its parser asks. Left false, the page is the
|
|
511
|
+
* catalog alone: no dock, no composer, no bundle reference, nothing extra to
|
|
512
|
+
* 404 — including the favicon links, since the CLI's standalone export (also
|
|
513
|
+
* `spritesBundleAvailable: false`) can't carry a dangling relative
|
|
514
|
+
* ./favicon.svg either. */
|
|
470
515
|
export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false } = {}) {
|
|
471
516
|
const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
|
|
472
517
|
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
473
518
|
const pageData = embedJson({ classCount: entries.length, swatchCount: totalSwatches });
|
|
519
|
+
// Ancestor clustering for the two big sections: the heading chip is the
|
|
520
|
+
// ancestor's OWN resolved sprite — visible proof that a new subclass
|
|
521
|
+
// attached there already has fallback art waiting for it.
|
|
522
|
+
const spritedClasses = new Set([...iconTemplates, ...largeTemplates].flatMap((t) => t?.classes || []));
|
|
523
|
+
const clusterBy = {
|
|
524
|
+
spritedClasses,
|
|
525
|
+
resolveChip: (ancestor) =>
|
|
526
|
+
resolveSpriteAsset(ancestor, [], [], largeTemplates, SPRITE_REGISTRY, { instanceKey: `cluster-${ancestor}` }),
|
|
527
|
+
};
|
|
474
528
|
const dockRows = spritesBundleAvailable ? spriteFactRows({ iconTemplates, largeTemplates }) : [];
|
|
475
529
|
const navHtml = CATALOG_GROUPS
|
|
476
530
|
.map((g) => `<a class="jump" href="#g-${g.id}">${escapeHtml(g.label)} <span class="count">${entries.filter((e) => e.group === g.id).length}</span></a>`)
|
|
@@ -478,8 +532,6 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
478
532
|
|
|
479
533
|
const dockCss = !spritesBundleAvailable ? "" : `
|
|
480
534
|
.dockwrap { margin: .2rem 0 1.3rem; }
|
|
481
|
-
.dockwrap .panel { background: var(--card); border: 1px solid var(--line); border-top: 2px solid var(--taught); padding: .75rem .85rem; }
|
|
482
|
-
.dockwrap h2 { font-family: ${SERIF_STACK}; font-variant: small-caps; font-size: .82rem; letter-spacing: .04em; color: var(--muted); font-weight: 600; margin: 0 0 .55rem; }
|
|
483
535
|
.dock-note { color: var(--muted); font-size: .8rem; margin: 0 0 .6rem; max-width: 72ch; }
|
|
484
536
|
.docklog { display: flex; flex-direction: column; gap: .4rem; max-height: 240px; overflow-y: auto; margin-bottom: .5rem; }
|
|
485
537
|
.docklog:empty { display: none; margin-bottom: 0; }
|
|
@@ -490,8 +542,8 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
490
542
|
.docklog .a.grounded { border-left: 2px solid var(--taught); padding-left: .5rem; }
|
|
491
543
|
.dockask { display: flex; align-items: center; gap: .5rem; }
|
|
492
544
|
.dockask .prompt { color: var(--taught); font-size: .78rem; }
|
|
493
|
-
.dockask input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--
|
|
494
|
-
.dockask input:focus-visible { outline: 2px solid var(--
|
|
545
|
+
.dockask input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--ai-panel-hi); color: var(--ink); border: 1px solid var(--ai-edge); border-radius: 3px; padding: .38rem .6rem; min-width: 0; }
|
|
546
|
+
.dockask input:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
|
|
495
547
|
.dockask input:disabled { opacity: .5; }
|
|
496
548
|
.dock-status { font-size: .72rem; color: var(--muted); margin-top: .55rem; }
|
|
497
549
|
`;
|
|
@@ -499,7 +551,7 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
499
551
|
const dockHtml = !spritesBundleAvailable ? "" : `<div class="dockwrap">
|
|
500
552
|
<section class="panel" aria-label="Ask about the sprite catalog">
|
|
501
553
|
<h2>ask the catalog</h2>
|
|
502
|
-
<p class="dock-note">every answer is read from the sprite templates’ own facts
|
|
554
|
+
<p class="dock-note">every answer is read from the sprite templates’ own facts. A question they can’t ground gets a refusal, never a guess.</p>
|
|
503
555
|
<div class="docklog" id="dockLog" aria-live="polite"></div>
|
|
504
556
|
<form class="dockask" id="dockForm">
|
|
505
557
|
<span class="prompt mono">tmct></span>
|
|
@@ -509,22 +561,56 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
509
561
|
</form>
|
|
510
562
|
<div class="pills" id="dockPills" role="group" aria-label="quick questions to ask">
|
|
511
563
|
<button type="button" class="pill" data-q="what parameters does a person sprite take?">person parameters</button>
|
|
512
|
-
<button type="button" class="pill" data-q="what
|
|
513
|
-
<button type="button" class="pill" data-q="what
|
|
564
|
+
<button type="button" class="pill" data-q="what emotions does a person sprite accept?">person emotions</button>
|
|
565
|
+
<button type="button" class="pill" data-q="what materials does a cabinet sprite accept?">cabinet materials</button>
|
|
566
|
+
<button type="button" class="pill" data-q="how many sprite classes are there?">classes on record</button>
|
|
514
567
|
<button type="button" class="pill" data-q="what is a portrait sprite?">about the portrait sprite</button>
|
|
515
568
|
</div>
|
|
516
569
|
<div class="dock-status mono" id="dockStatus">loading the engine…</div>
|
|
517
570
|
</section>
|
|
518
571
|
</div>`;
|
|
519
572
|
|
|
573
|
+
// The bundle carries the scene composer's parser as well as the dock's
|
|
574
|
+
// session, and the catalog script below calls it directly, so it loads ahead
|
|
575
|
+
// of that script rather than beside the dock's own wiring at the end.
|
|
576
|
+
const spriteBundleScript = !spritesBundleAvailable ? "" : `<script src="./sprites-browser.bundle.js"></script>`;
|
|
577
|
+
|
|
578
|
+
// The composer resolves a typed class name through the real resolver, which
|
|
579
|
+
// reaches the page in that bundle. Without it there is nothing to type into,
|
|
580
|
+
// so the panel stays out rather than rendering a box that can't answer.
|
|
581
|
+
const composerHtml = !spritesBundleAvailable ? "" : `<div class="composer">
|
|
582
|
+
<section class="panel compose-panel" aria-label="Describe a scene">
|
|
583
|
+
<h2>describe a scene</h2>
|
|
584
|
+
<form class="composeform" id="composeForm">
|
|
585
|
+
<span class="prompt mono">there is a</span>
|
|
586
|
+
<input id="composeq" type="text" autocomplete="off"
|
|
587
|
+
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
588
|
+
aria-label="Continue the sentence: there is a…">
|
|
589
|
+
</form>
|
|
590
|
+
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
591
|
+
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
592
|
+
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
593
|
+
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
594
|
+
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
595
|
+
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
596
|
+
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
597
|
+
</div>
|
|
598
|
+
</section>
|
|
599
|
+
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
600
|
+
<h2>the scene</h2>
|
|
601
|
+
<div class="scene-frame" id="sceneFrame">
|
|
602
|
+
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
603
|
+
<span class="empty-note" id="sceneEmpty">nothing recognized yet. Try “a doctor with a hat, and a cabinet”.</span>
|
|
604
|
+
</div>
|
|
605
|
+
</section>
|
|
606
|
+
</div>`;
|
|
607
|
+
|
|
520
608
|
const dockScripts = !spritesBundleAvailable ? "" : `<script>
|
|
521
609
|
const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
522
610
|
</script>
|
|
523
|
-
<script src="./sprites-browser.bundle.js"></script>
|
|
524
611
|
<script>
|
|
525
612
|
(function () {
|
|
526
613
|
"use strict";
|
|
527
|
-
const answerSpriteQuestion = ${answerSpriteQuestion.toString()};
|
|
528
614
|
const dockLogEl = document.getElementById("dockLog");
|
|
529
615
|
const dockFormEl = document.getElementById("dockForm");
|
|
530
616
|
const dockqEl = document.getElementById("dockq");
|
|
@@ -573,12 +659,11 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
573
659
|
if (!q) return;
|
|
574
660
|
dockqEl.value = "";
|
|
575
661
|
addDockLine("u", q);
|
|
576
|
-
|
|
577
|
-
if (local) { addDockLine("a grounded", local.answer); return; }
|
|
578
|
-
if (!session) { addDockLine("a miss", "the engine is still loading \\u2014 try again in a moment."); return; }
|
|
662
|
+
if (!session) { addDockLine("a miss", "the engine is still loading. Try again in a moment."); return; }
|
|
579
663
|
withLock(async () => {
|
|
580
664
|
const result = await session.turn(q);
|
|
581
|
-
|
|
665
|
+
const missed = !!(result.record && result.record.miss);
|
|
666
|
+
addDockLine(missed ? "a miss" : "a grounded", result.answer);
|
|
582
667
|
});
|
|
583
668
|
});
|
|
584
669
|
|
|
@@ -594,9 +679,9 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
594
679
|
await tryLoadWink();
|
|
595
680
|
session = await tmctSprites.createSpriteCatalogSession({ factRows: SPRITE_CHAT.rows });
|
|
596
681
|
dockqEl.disabled = false;
|
|
597
|
-
dockStatusEl.textContent = SPRITE_CHAT.rows.length + " sprite facts on record
|
|
682
|
+
dockStatusEl.textContent = SPRITE_CHAT.rows.length + " sprite facts on record. Ask away, or use a quick question.";
|
|
598
683
|
} catch (err) {
|
|
599
|
-
dockStatusEl.textContent = "the chat engine failed to load
|
|
684
|
+
dockStatusEl.textContent = "the chat engine failed to load. The catalog below still works.";
|
|
600
685
|
console.error("tmct sprites: dock boot failed", err);
|
|
601
686
|
}
|
|
602
687
|
})();
|
|
@@ -614,49 +699,67 @@ ${spritesBundleAvailable ? `<link rel="icon" href="./favicon.svg" type="image/sv
|
|
|
614
699
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png">` : ""}
|
|
615
700
|
<style>
|
|
616
701
|
${THEME_TOKENS_CSS}
|
|
617
|
-
html { background: var(--
|
|
618
|
-
|
|
702
|
+
html { background: var(--ai-canvas); }
|
|
703
|
+
/* Creative-app chrome: a dark document bar in BOTH themes (the app-window
|
|
704
|
+
read), panels and a recessed canvas that follow the theme, and a
|
|
705
|
+
transparency checker under every drawn swatch so each sprite proves it
|
|
706
|
+
holds its own boundary with nothing behind it. */
|
|
707
|
+
:root {
|
|
708
|
+
--ai-bar: #26272B; --ai-bar-ink: #E7E5DF;
|
|
709
|
+
--ai-panel: #EDECE8; --ai-panel-hi: #F4F3F0; --ai-canvas: #DFDEDA; --ai-edge: #C8C6C0;
|
|
710
|
+
--checker: rgba(0, 0, 0, .07);
|
|
711
|
+
}
|
|
712
|
+
@media (prefers-color-scheme: dark) { :root { --ai-panel: #26272B; --ai-panel-hi: #2D2E33; --ai-canvas: #1A1B1F; --ai-edge: #34363C; --checker: rgba(255, 255, 255, .055); } }
|
|
713
|
+
:root[data-theme="dark"] { --ai-panel: #26272B; --ai-panel-hi: #2D2E33; --ai-canvas: #1A1B1F; --ai-edge: #34363C; --checker: rgba(255, 255, 255, .055); }
|
|
714
|
+
:root[data-theme="light"] { --ai-panel: #EDECE8; --ai-panel-hi: #F4F3F0; --ai-canvas: #DFDEDA; --ai-edge: #C8C6C0; --checker: rgba(0, 0, 0, .07); }
|
|
715
|
+
body { margin: 0; background: var(--ai-canvas); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
619
716
|
.mono { font-family: ${MONO_STACK}; }
|
|
620
|
-
main { max-width:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
.
|
|
717
|
+
main { max-width: 1240px; margin: 0 auto; padding: 0 1.2rem 3rem; }
|
|
718
|
+
button:focus-visible, input:focus-visible, a:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
|
|
719
|
+
.appbar { display: flex; align-items: flex-end; gap: 1rem; margin: 0 -1.2rem; padding: .6rem 1.2rem 0; background: var(--ai-bar); }
|
|
720
|
+
.appbar h1 { font-family: ${MONO_STACK}; font-size: .84rem; font-weight: 600; letter-spacing: .02em; margin: 0; padding: .32rem .8rem .38rem; background: var(--ai-panel); color: var(--ink); border-radius: 4px 4px 0 0; }
|
|
721
|
+
.appbar .doc-sub { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: color-mix(in srgb, var(--ai-bar-ink) 65%, transparent); padding-bottom: .5rem; }
|
|
722
|
+
.topbar { position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; background: var(--ai-panel); border-bottom: 1px solid var(--ai-edge); margin: 0 -1.2rem 1.4rem; padding: .5rem 1.2rem; }
|
|
723
|
+
.jump { font-family: ${MONO_STACK}; font-size: .7rem; padding: .2rem .6rem; border: 1px solid var(--ai-edge); border-radius: 3px; background: transparent; color: var(--ink); text-decoration: none; }
|
|
724
|
+
.jump:hover { border-color: var(--corpus); color: var(--corpus); }
|
|
725
|
+
.jump .count { color: var(--muted); }
|
|
726
|
+
.filter { margin-left: auto; display: flex; align-items: center; gap: .4rem; }
|
|
727
|
+
.filter input { font-family: ${MONO_STACK}; font-size: .78rem; background: var(--ai-panel-hi); color: var(--ink); border: 1px solid var(--ai-edge); border-radius: 3px; padding: .3rem .6rem; width: 200px; }
|
|
728
|
+
.filter .n { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); white-space: nowrap; }
|
|
729
|
+
.composer { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1.2rem 0 1.3rem; }
|
|
625
730
|
@media (max-width: 700px) { .composer { grid-template-columns: 1fr; } }
|
|
626
|
-
.composer .panel { background: var(--
|
|
627
|
-
.composer h2 { font-family: ${
|
|
731
|
+
.composer .panel, .dockwrap .panel { background: var(--ai-panel); border: 1px solid var(--ai-edge); border-radius: 4px; padding: .75rem .85rem; min-width: 0; box-shadow: 0 1px 2px rgba(0, 0, 0, .08); }
|
|
732
|
+
.composer h2, .dockwrap h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); font-weight: 600; margin: -.75rem -.85rem .6rem; padding: .42rem .85rem; background: var(--ai-panel-hi); border-bottom: 1px solid var(--ai-edge); border-radius: 4px 4px 0 0; }
|
|
628
733
|
.composeform { display: flex; align-items: center; gap: .5rem; }
|
|
629
734
|
.composeform .prompt { color: var(--taught); font-size: .8rem; white-space: nowrap; }
|
|
630
|
-
.composeform input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--
|
|
631
|
-
.composeform input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
735
|
+
.composeform input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--ai-panel-hi); color: var(--ink); border: 1px solid var(--ai-edge); border-radius: 3px; padding: .38rem .6rem; min-width: 0; }
|
|
632
736
|
.pills { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
|
|
633
|
-
.pill { font-family: ${MONO_STACK}; font-size: .7rem; padding: .22rem .55rem; border: 1px solid var(--
|
|
634
|
-
.pill:hover { border-color: var(--
|
|
635
|
-
.pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
737
|
+
.pill { font-family: ${MONO_STACK}; font-size: .7rem; padding: .22rem .55rem; border: 1px solid var(--ai-edge); border-radius: 999px; background: var(--ai-panel-hi); color: var(--ink); cursor: pointer; }
|
|
738
|
+
.pill:hover { border-color: var(--corpus); color: var(--corpus); }
|
|
636
739
|
.scene-frame { min-height: 5.6rem; display: flex; align-items: center; }
|
|
637
740
|
.scene-row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; width: 100%; }
|
|
741
|
+
.scene-row:empty { display: none; }
|
|
638
742
|
.scene-card { display: flex; flex-direction: column; align-items: center; width: 74px; }
|
|
639
|
-
.scene-sprite { width: 60px; height: 60px; border-radius:
|
|
640
|
-
.scene-sprite svg { width: 100%; height: 100%; display: block; }
|
|
743
|
+
.scene-sprite { width: 60px; height: 60px; border-radius: 3px; border: 1px solid var(--ai-edge); display: flex; align-items: center; justify-content: center; padding: 6px; box-sizing: border-box; background-image: linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%), linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%); background-position: 0 0, 5px 5px; background-size: 10px 10px; background-color: var(--card); }
|
|
744
|
+
.scene-sprite svg { width: 100%; height: 100%; display: block; filter: var(--sprite-pop); }
|
|
641
745
|
.scene-label { font-size: .7rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.2; }
|
|
642
746
|
.empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
|
|
643
|
-
.
|
|
644
|
-
.
|
|
645
|
-
.
|
|
646
|
-
.jump .count { color: var(--muted); }
|
|
647
|
-
.filter { margin-left: auto; display: flex; align-items: center; gap: .4rem; }
|
|
648
|
-
.filter input { font-family: ${MONO_STACK}; font-size: .8rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .6rem; width: 200px; }
|
|
649
|
-
.filter .n { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); white-space: nowrap; }
|
|
650
|
-
.group { margin: 2rem 0; content-visibility: auto; contain-intrinsic-size: 800px; }
|
|
651
|
-
.group h2 { font-size: 1.05rem; margin: 0 0 .2rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
652
|
-
.group h2 .count { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); font-weight: 400; }
|
|
747
|
+
.group { margin: 2.2rem 0; content-visibility: auto; contain-intrinsic-size: 800px; }
|
|
748
|
+
.group h2 { font-family: ${MONO_STACK}; font-size: .78rem; text-transform: uppercase; letter-spacing: .09em; margin: 0 0 .2rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
749
|
+
.group h2 .count { font-size: .7rem; color: var(--muted); font-weight: 400; }
|
|
653
750
|
.section-note { color: var(--muted); font-size: .82rem; margin: 0 0 .8rem; max-width: 68ch; }
|
|
751
|
+
.cluster { margin: 1rem 0 1.5rem; }
|
|
752
|
+
.cluster-head { display: flex; align-items: center; gap: .45rem; font-family: ${MONO_STACK}; font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); margin: 0 0 .55rem; }
|
|
753
|
+
.cluster-head .count { font-weight: 400; opacity: .75; }
|
|
754
|
+
.cluster-head::after { content: ""; flex: 1; height: 1px; background: var(--ai-edge); }
|
|
755
|
+
.cluster-chip { width: 26px; height: 26px; display: inline-flex; padding: 2px; box-sizing: border-box; border: 1px solid var(--ai-edge); border-radius: 3px; background-image: linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%), linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%); background-position: 0 0, 4px 4px; background-size: 8px 8px; background-color: var(--card); }
|
|
756
|
+
.cluster-chip svg { width: 100%; height: 100%; display: block; filter: var(--sprite-pop); }
|
|
654
757
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: .7rem; }
|
|
655
|
-
.card { background: var(--card); border: 1px solid var(--
|
|
758
|
+
.card { background: var(--card); border: 1px solid var(--ai-edge); border-radius: 4px; padding: .55rem .7rem .7rem; box-shadow: 0 1px 2px rgba(0, 0, 0, .08); content-visibility: auto; contain-intrinsic-size: 220px; }
|
|
656
759
|
.card[hidden] { display: none; }
|
|
657
|
-
.card-name { font-
|
|
760
|
+
.card-name { font-family: ${MONO_STACK}; font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0 0 .35rem; }
|
|
658
761
|
.chain { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); margin-bottom: .5rem; display: flex; flex-wrap: wrap; align-items: center; gap: .15rem; }
|
|
659
|
-
.chain-link { padding: .04rem .35rem; border: 1px solid var(--
|
|
762
|
+
.chain-link { padding: .04rem .35rem; border: 1px solid var(--ai-edge); border-radius: 99px; }
|
|
660
763
|
.chain-link.own { border-color: var(--taught); color: var(--taught); }
|
|
661
764
|
.chain-arrow { color: var(--muted); opacity: .6; }
|
|
662
765
|
.chain-more { font-style: italic; opacity: .75; }
|
|
@@ -665,47 +768,28 @@ ${THEME_TOKENS_CSS}
|
|
|
665
768
|
.tier-label { font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
666
769
|
.swatches { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .25rem; }
|
|
667
770
|
.swatch { width: 64px; text-align: center; }
|
|
771
|
+
.swatch-img { border: 1px solid var(--ai-edge); border-radius: 3px; box-sizing: border-box; padding: 3px; background-image: linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%), linear-gradient(45deg, var(--checker) 25%, transparent 25% 75%, var(--checker) 75%); background-position: 0 0, 5px 5px; background-size: 10px 10px; background-color: var(--card); }
|
|
668
772
|
.swatch.large .swatch-img { width: 64px; height: 64px; }
|
|
669
|
-
.swatch.icon .swatch-img { width:
|
|
670
|
-
.swatch-img svg { width: 100%; height: 100%; display: block; }
|
|
773
|
+
.swatch.icon .swatch-img { width: 36px; height: 36px; margin: 0 auto; }
|
|
774
|
+
.swatch-img svg { width: 100%; height: 100%; display: block; filter: var(--sprite-pop); }
|
|
671
775
|
.swatch.fallback { opacity: .55; }
|
|
672
|
-
.swatch.fallback .swatch-img {
|
|
776
|
+
.swatch.fallback .swatch-img { border-style: dashed; }
|
|
673
777
|
.swatch-caption { font-family: ${MONO_STACK}; font-size: .58rem; color: var(--muted); line-height: 1.25; margin-top: .15rem; word-break: break-word; }
|
|
674
778
|
.swatch-treat { display: block; opacity: .8; }
|
|
675
|
-
|
|
676
|
-
|
|
779
|
+
.swatch.cycle .swatch-img { border-color: var(--taught); cursor: pointer; display: block; }
|
|
780
|
+
.swatch.cycle .swatch-img:hover { border-color: var(--corpus); }
|
|
781
|
+
.swatch.cycle .swatch-caption { color: var(--taught); }
|
|
782
|
+
footer.page { max-width: 74ch; margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--ai-edge); font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
783
|
+
@media (prefers-reduced-motion: no-preference) { .jump, .swatch, .pill { transition: border-color .12s ease, color .12s ease, opacity .12s ease; } }
|
|
677
784
|
${dockCss}</style>
|
|
678
785
|
</head>
|
|
679
786
|
<body>
|
|
680
787
|
<main>
|
|
681
|
-
<
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
<form class="composeform" id="composeForm">
|
|
687
|
-
<span class="prompt mono">there is a</span>
|
|
688
|
-
<input id="composeq" type="text" autocomplete="off"
|
|
689
|
-
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
690
|
-
aria-label="Continue the sentence: there is a…">
|
|
691
|
-
</form>
|
|
692
|
-
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
693
|
-
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
694
|
-
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
695
|
-
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
696
|
-
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
697
|
-
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
698
|
-
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
699
|
-
</div>
|
|
700
|
-
</section>
|
|
701
|
-
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
702
|
-
<h2>the scene</h2>
|
|
703
|
-
<div class="scene-frame" id="sceneFrame">
|
|
704
|
-
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
705
|
-
<span class="empty-note" id="sceneEmpty">nothing recognized yet — try “a doctor with a hat, and a cabinet”.</span>
|
|
706
|
-
</div>
|
|
707
|
-
</section>
|
|
708
|
-
</div>
|
|
788
|
+
<header class="appbar">
|
|
789
|
+
<h1>Sprites</h1>
|
|
790
|
+
<span class="doc-sub">tmct · the sprite library</span>
|
|
791
|
+
</header>
|
|
792
|
+
${composerHtml}
|
|
709
793
|
${dockHtml}
|
|
710
794
|
<div class="topbar">
|
|
711
795
|
<nav aria-label="Jump to group">${navHtml}</nav>
|
|
@@ -714,12 +798,13 @@ ${dockCss}</style>
|
|
|
714
798
|
<span class="n mono" id="qcount"></span>
|
|
715
799
|
</div>
|
|
716
800
|
</div>
|
|
717
|
-
${CATALOG_GROUPS.map((g) => sectionHtml(g, entries)).join("")}
|
|
801
|
+
${CATALOG_GROUPS.map((g) => sectionHtml(g, entries, { clusterBy: g.id === GROUP_PERSON || g.id === GROUP_OBJECT ? clusterBy : null })).join("")}
|
|
718
802
|
<footer class="page">${entries.length} classes · ${totalSwatches} swatches · icon tier 44px, sprite tier 400px</footer>
|
|
719
803
|
</main>
|
|
720
804
|
<script>
|
|
721
805
|
const SPRITE_CATALOG = ${pageData};
|
|
722
806
|
</script>
|
|
807
|
+
${spriteBundleScript}
|
|
723
808
|
<script>
|
|
724
809
|
(function () {
|
|
725
810
|
"use strict";
|
|
@@ -747,10 +832,10 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
747
832
|
// THIS page's own already-rendered card and swatch-label markup (this
|
|
748
833
|
// module's own header explains why: never a second embedded copy of the
|
|
749
834
|
// same swatch data), so the composed scene below only ever shows a sprite
|
|
750
|
-
// this same page already proved the resolver draws.
|
|
835
|
+
// this same page already proved the resolver draws. Which class a typed
|
|
836
|
+
// word names is the bundle's extractSceneItems, which asks the real
|
|
837
|
+
// resolver — the page never matches a class name itself.
|
|
751
838
|
const esc = ${escapeHtml.toString()};
|
|
752
|
-
const tokenizeSceneText = ${tokenizeSceneText.toString()};
|
|
753
|
-
const extractSceneItems = ${extractSceneItems.toString()};
|
|
754
839
|
|
|
755
840
|
function buildClassIndexFromDom() {
|
|
756
841
|
const index = {};
|
|
@@ -777,42 +862,131 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
777
862
|
return index;
|
|
778
863
|
}
|
|
779
864
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
865
|
+
function wireSceneComposer() {
|
|
866
|
+
const composeqEl = document.getElementById("composeq");
|
|
867
|
+
if (!composeqEl) return;
|
|
868
|
+
const extractSceneItems = tmctSprites.extractSceneItems;
|
|
869
|
+
const composeFormEl = document.getElementById("composeForm");
|
|
870
|
+
const composePillsEl = document.getElementById("composePills");
|
|
871
|
+
const sceneRowEl = document.getElementById("sceneRow");
|
|
872
|
+
const sceneEmptyEl = document.getElementById("sceneEmpty");
|
|
873
|
+
const classIndex = buildClassIndexFromDom();
|
|
874
|
+
|
|
875
|
+
function renderScene(text) {
|
|
876
|
+
const items = extractSceneItems(text, classIndex);
|
|
877
|
+
if (!items.length) {
|
|
878
|
+
sceneRowEl.innerHTML = "";
|
|
879
|
+
sceneEmptyEl.hidden = false;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
sceneEmptyEl.hidden = true;
|
|
883
|
+
sceneRowEl.innerHTML = items.map((item) => {
|
|
884
|
+
const entry = classIndex[item.className];
|
|
885
|
+
if (!entry) return "";
|
|
886
|
+
const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
|
|
887
|
+
const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
|
|
888
|
+
return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
|
|
889
|
+
}).join("");
|
|
793
890
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
891
|
+
|
|
892
|
+
composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
|
|
893
|
+
composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
|
|
894
|
+
composePillsEl.addEventListener("click", (e) => {
|
|
895
|
+
const btn = e.target.closest(".pill");
|
|
896
|
+
if (!btn) return;
|
|
897
|
+
const phrase = btn.dataset.fill || "";
|
|
898
|
+
const current = composeqEl.value.trim();
|
|
899
|
+
composeqEl.value = current ? current + ", a " + phrase : phrase;
|
|
900
|
+
composeqEl.focus();
|
|
901
|
+
renderScene(composeqEl.value);
|
|
902
|
+
});
|
|
903
|
+
renderScene("");
|
|
904
|
+
}
|
|
905
|
+
wireSceneComposer();
|
|
906
|
+
|
|
907
|
+
// ---- the animated swatches — a card's large tier gains one moving
|
|
908
|
+
// swatch per axis its own templates actually vary on: mood (mgx:feels),
|
|
909
|
+
// facing (mgx:faces) and pose (mgx:pose). Frames are the card's own
|
|
910
|
+
// already-rendered swatches (the same read-the-DOM posture classIndex
|
|
911
|
+
// takes above — never a second embedded copy), ordered by the pure
|
|
912
|
+
// *FrameSequence functions spliced in below, and one shared interval
|
|
913
|
+
// steps every cycle in sync at one shared delay. The mood cycle leads the
|
|
914
|
+
// row; the facing sweep and the pose toggle take over the static plain
|
|
915
|
+
// and happy swatches' own places rather than adding cells, so the row
|
|
916
|
+
// keeps its width. Reduced motion disables the auto-step only; each frame
|
|
917
|
+
// is a button, so a click or Enter always advances one frame by hand.
|
|
918
|
+
// Injected AFTER buildClassIndexFromDom ran, so a moving frame never
|
|
919
|
+
// leaks into the composer's material index.
|
|
920
|
+
const CYCLE_FRAME_DELAY_MS = ${CYCLE_FRAME_DELAY_MS};
|
|
921
|
+
const FACING_TURN_ORDER = ${embedJson(FACING_TURN_ORDER)};
|
|
922
|
+
const MOOD_PROPERTY = "mgx:feels";
|
|
923
|
+
const FACING_PROPERTY = "mgx:faces";
|
|
924
|
+
const POSE_PROPERTY = "mgx:pose";
|
|
925
|
+
const moodFrameSequence = ${moodFrameSequence.toString()};
|
|
926
|
+
const turnFrameSequence = ${turnFrameSequence.toString()};
|
|
927
|
+
const movingFrameSequence = ${movingFrameSequence.toString()};
|
|
928
|
+
const cycleSteppers = [];
|
|
929
|
+
|
|
930
|
+
function frameFromSwatch(swatch) {
|
|
931
|
+
const img = swatch && swatch.querySelector(".swatch-img");
|
|
932
|
+
const label = swatch && swatch.querySelector(".swatch-label");
|
|
933
|
+
if (!img || !label) return null;
|
|
934
|
+
return { svg: img.innerHTML, label: label.textContent.trim() };
|
|
802
935
|
}
|
|
803
936
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
937
|
+
function makeCycleSwatch(frames, ariaLabel, kind) {
|
|
938
|
+
const holder = document.createElement("div");
|
|
939
|
+
holder.className = "swatch large cycle cycle-" + kind;
|
|
940
|
+
holder.innerHTML = '<button type="button" class="swatch-img" aria-label="' + esc(ariaLabel)
|
|
941
|
+
+ '"></button><div class="swatch-caption"><span class="swatch-label"></span></div>';
|
|
942
|
+
const frameImgEl = holder.querySelector(".swatch-img");
|
|
943
|
+
const frameLabelEl = holder.querySelector(".swatch-label");
|
|
944
|
+
let frameIndex = 0;
|
|
945
|
+
const showFrame = () => {
|
|
946
|
+
frameImgEl.innerHTML = frames[frameIndex].svg;
|
|
947
|
+
frameLabelEl.textContent = frames[frameIndex].label;
|
|
948
|
+
};
|
|
949
|
+
const stepFrame = () => { frameIndex = (frameIndex + 1) % frames.length; showFrame(); };
|
|
950
|
+
frameImgEl.addEventListener("click", stepFrame);
|
|
951
|
+
showFrame();
|
|
952
|
+
cycleSteppers.push(stepFrame);
|
|
953
|
+
return holder;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
for (const card of cards) {
|
|
957
|
+
const swatchRow = card.querySelector('.tier-row[data-tier="large"] .swatches');
|
|
958
|
+
if (!swatchRow) continue;
|
|
959
|
+
const cls = card.dataset.cls;
|
|
960
|
+
const rows = Array.from(swatchRow.querySelectorAll(".swatch"))
|
|
961
|
+
.map((el) => ({ el, property: el.dataset.property || "", frame: frameFromSwatch(el) }))
|
|
962
|
+
.filter((r) => r.frame);
|
|
963
|
+
const baseSwatch = swatchRow.querySelector(".swatch.plain, .swatch.fallback");
|
|
964
|
+
const baseFrame = frameFromSwatch(baseSwatch);
|
|
965
|
+
|
|
966
|
+
const moodRows = rows.filter((r) => r.property === MOOD_PROPERTY);
|
|
967
|
+
const moodFrames = moodFrameSequence(baseFrame && { svg: baseFrame.svg, label: cls }, moodRows.map((r) => r.frame));
|
|
968
|
+
if (moodFrames.length) {
|
|
969
|
+
swatchRow.insertBefore(makeCycleSwatch(moodFrames, "step through the " + cls + " expressions", "mood"), swatchRow.firstChild);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const facingFrames = {};
|
|
973
|
+
for (const r of rows) { if (r.property === FACING_PROPERTY) facingFrames[r.frame.label] = r.frame; }
|
|
974
|
+
const turnFrames = turnFrameSequence(FACING_TURN_ORDER, baseFrame, facingFrames);
|
|
975
|
+
if (turnFrames.length && baseSwatch) {
|
|
976
|
+
swatchRow.replaceChild(makeCycleSwatch(turnFrames, "watch the " + cls + " turn through every direction", "turn"), baseSwatch);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const movingRow = rows.find((r) => r.property === POSE_PROPERTY && r.frame.label === "moving");
|
|
980
|
+
const movingFrames = movingFrameSequence(baseFrame, movingRow && movingRow.frame);
|
|
981
|
+
if (movingFrames.length) {
|
|
982
|
+
const happyRow = moodRows.find((r) => r.frame.label === "happy");
|
|
983
|
+
swatchRow.replaceChild(makeCycleSwatch(movingFrames, "watch the " + cls + " move", "moving"), (happyRow || movingRow).el);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
987
|
+
if (cycleSteppers.length && !reducedMotion) {
|
|
988
|
+
setInterval(() => { for (const stepFrame of cycleSteppers) stepFrame(); }, CYCLE_FRAME_DELAY_MS);
|
|
989
|
+
}
|
|
816
990
|
})();
|
|
817
991
|
</script>
|
|
818
992
|
${dockScripts}
|