@polycode-projects/the-mechanical-code-talker 2.8.9 → 2.8.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.8.9",
3
+ "version": "2.8.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -283,6 +283,78 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
283
283
  });
284
284
  }
285
285
 
286
+ // ---- scene composer (pure) ----
287
+ //
288
+ // The free-text "there is a..." box (PLAN_GAMES_UPLIFT_V3.md's own precedent:
289
+ // adventure-viz.mjs's roomSceneObjects/room-frame) over THIS page's own
290
+ // already-real classes — never a general NLU pass. extractSceneItems is the
291
+ // one pure, unit-testable piece; the DOM index it's matched against
292
+ // (className -> real swatch labels, read straight off this page's own
293
+ // already-rendered `.card`/`.swatch-label` markup at load time — see this
294
+ // module's own header for why that beats re-embedding the same SVG data a
295
+ // second time) is built client-side in the inline script below, since
296
+ // walking rendered DOM has no meaning in this pure module.
297
+
298
+ /** `text`'s lowercase word runs with their token index, the unit
299
+ * extractSceneItems matches class names against — punctuation never fuses
300
+ * two real words into one token nor splits one real word into two. */
301
+ function tokenizeSceneText(text) {
302
+ const tokens = [];
303
+ const re = /[A-Za-z]+/g;
304
+ let m;
305
+ while ((m = re.exec(String(text ?? "")))) tokens.push({ word: m[0].toLowerCase() });
306
+ return tokens;
307
+ }
308
+
309
+ /** Every real catalog class the free-typed `text` names, in the order each
310
+ * first appears, paired with the real material label (one of that SAME
311
+ * class's own swatch labels — never another class's, never a fabricated
312
+ * one) immediately preceding it, or `null`. `classIndex` is
313
+ * `{className: {materials}}` with `materials` keyed by lowercase label
314
+ * (sprite-catalog-viz's own client-side `buildClassIndexFromDom` output, or
315
+ * an equivalent test fixture) — a class name absent from `classIndex` can
316
+ * never match, and a modifier word that isn't one of ITS matched class's
317
+ * own material labels is silently dropped rather than guessed at, the same
318
+ * honest-miss posture an unrecognized class name gets (an unmatched word,
319
+ * e.g. "red" before a lamp with no red material, is never an error, just
320
+ * silently not drawn). A multi-word class name (e.g. "body of water") is
321
+ * checked, at every token position, before any shorter class that would
322
+ * otherwise claim part of it — candidates are tried longest-word-count
323
+ * first, so the longer name always wins the position it starts at. Pure. */
324
+ export function extractSceneItems(text, classIndex) {
325
+ const index = classIndex || {};
326
+ const candidates = Object.keys(index)
327
+ .map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
328
+ .filter((c) => c.words.length)
329
+ .sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
330
+ const tokens = tokenizeSceneText(text);
331
+ const used = new Array(tokens.length).fill(false);
332
+ const items = [];
333
+ for (let i = 0; i < tokens.length; i += 1) {
334
+ if (used[i]) continue;
335
+ const hit = candidates.find(({ words }) => {
336
+ if (i + words.length > tokens.length) return false;
337
+ for (let k = 0; k < words.length; k += 1) {
338
+ if (used[i + k] || tokens[i + k].word !== words[k]) return false;
339
+ }
340
+ return true;
341
+ });
342
+ if (!hit) continue;
343
+ let materialLabel = null;
344
+ if (i > 0 && !used[i - 1]) {
345
+ const materials = index[hit.name]?.materials || {};
346
+ const prevWord = tokens[i - 1].word;
347
+ if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
348
+ materialLabel = prevWord;
349
+ used[i - 1] = true;
350
+ }
351
+ }
352
+ for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
353
+ items.push({ className: hit.name, materialLabel });
354
+ }
355
+ return items;
356
+ }
357
+
286
358
  // ---- rendering ----
287
359
 
288
360
  function chainHtml(chain) {
@@ -361,7 +433,26 @@ ${THEME_TOKENS_CSS}
361
433
  main { max-width: 1180px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
362
434
  .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
363
435
  h1 { font-size: 1.4rem; margin: .3rem 0 .6rem; text-wrap: balance; }
364
- .intro { max-width: 74ch; color: var(--muted); font-size: .92rem; }
436
+
437
+ .composer { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: .6rem 0 1.3rem; }
438
+ @media (max-width: 700px) { .composer { grid-template-columns: 1fr; } }
439
+ .composer .panel { background: var(--card); border: 1px solid var(--line); border-top: 2px solid var(--taught); padding: .75rem .85rem; min-width: 0; }
440
+ .composer 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; }
441
+ .composeform { display: flex; align-items: center; gap: .5rem; }
442
+ .composeform .prompt { color: var(--taught); font-size: .8rem; white-space: nowrap; }
443
+ .composeform input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: .38rem .6rem; min-width: 0; }
444
+ .composeform input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
445
+ .pills { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
446
+ .pill { font-family: ${MONO_STACK}; font-size: .7rem; padding: .22rem .55rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); color: var(--ink); cursor: pointer; }
447
+ .pill:hover { border-color: var(--taught); }
448
+ .pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
449
+ .scene-frame { min-height: 5.6rem; display: flex; align-items: center; }
450
+ .scene-row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; width: 100%; }
451
+ .scene-card { display: flex; flex-direction: column; align-items: center; width: 74px; }
452
+ .scene-sprite { width: 60px; height: 60px; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; padding: 8px; box-sizing: border-box; }
453
+ .scene-sprite svg { width: 100%; height: 100%; display: block; }
454
+ .scene-label { font-size: .7rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.2; }
455
+ .empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
365
456
  .topbar { position: sticky; top: 0; z-index: 2; background: var(--bg); display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .6rem 0; margin: 1rem 0 1.2rem; }
366
457
  .jump { font-family: ${MONO_STACK}; font-size: .72rem; padding: .18rem .55rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); color: var(--ink); text-decoration: none; }
367
458
  .jump:hover { border-color: var(--taught); }
@@ -395,21 +486,39 @@ ${THEME_TOKENS_CSS}
395
486
  .swatch-caption { font-family: ${MONO_STACK}; font-size: .58rem; color: var(--muted); line-height: 1.25; margin-top: .15rem; word-break: break-word; }
396
487
  .swatch-treat { display: block; opacity: .8; }
397
488
  footer.page { max-width: 74ch; margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
398
- @media (prefers-reduced-motion: no-preference) { .jump, .swatch { transition: border-color .12s ease, opacity .12s ease; } }
489
+ @media (prefers-reduced-motion: no-preference) { .jump, .swatch, .pill { transition: border-color .12s ease, opacity .12s ease; } }
399
490
  </style>
400
491
  </head>
401
492
  <body>
402
493
  <main>
403
494
  <div class="eyebrow">tmct &middot; the sprite library</div>
404
- <h1>Every shape the sprite library can draw, and why</h1>
405
- <p class="intro">Two tiers share one resolver: a 44px icon set for the live games, a 400px
406
- gradient-shaded set for a closer look. Each card below shows a class's real
407
- <span class="mono">rdfs:subClassOf</span> ancestor chain and every swatch the real resolver
408
- (<span class="mono">resolveSpriteAsset</span>) actually returns for it &mdash; a material-bearing
409
- class shows its real taught-material variants, never an invented one. Every class shown here
410
- already carries its own template, so live resolution always stops at the chain's own first
411
- link (marked); the fuller chain is real ancestry on record in this catalog's corpus slice, kept
412
- for context.</p>
495
+ <h1>Sprites</h1>
496
+ <div class="composer">
497
+ <section class="panel compose-panel" aria-label="Describe a scene">
498
+ <h2>describe a scene</h2>
499
+ <form class="composeform" id="composeForm">
500
+ <span class="prompt mono">there is a</span>
501
+ <input id="composeq" type="text" autocomplete="off"
502
+ placeholder="red lamp, a doctor with a hat, and a cabinet"
503
+ aria-label="Continue the sentence: there is a&hellip;">
504
+ </form>
505
+ <div class="pills" id="composePills" role="group" aria-label="quick words to add">
506
+ <button type="button" class="pill" data-fill="doctor">doctor</button>
507
+ <button type="button" class="pill" data-fill="hat">hat</button>
508
+ <button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
509
+ <button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
510
+ <button type="button" class="pill" data-fill="cat">cat</button>
511
+ <button type="button" class="pill" data-fill="garden">garden</button>
512
+ </div>
513
+ </section>
514
+ <section class="panel viewer-panel" aria-label="The composed scene">
515
+ <h2>the scene</h2>
516
+ <div class="scene-frame" id="sceneFrame">
517
+ <div class="scene-row" id="sceneRow" aria-live="polite"></div>
518
+ <span class="empty-note" id="sceneEmpty">nothing recognized yet &mdash; try &ldquo;a doctor with a hat, and a cabinet&rdquo;.</span>
519
+ </div>
520
+ </section>
521
+ </div>
413
522
  <div class="topbar">
414
523
  <nav aria-label="Jump to group">${navHtml}</nav>
415
524
  <div class="filter">
@@ -445,6 +554,77 @@ const SPRITE_CATALOG = ${pageData};
445
554
  }
446
555
  q.addEventListener("input", apply);
447
556
  apply();
557
+
558
+ // ---- the scene composer — reads the class/material index straight off
559
+ // THIS page's own already-rendered card and swatch-label markup (this
560
+ // module's own header explains why: never a second embedded copy of the
561
+ // same swatch data), so the composed scene below only ever shows a sprite
562
+ // this same page already proved the resolver draws.
563
+ const esc = ${escapeHtml.toString()};
564
+ const tokenizeSceneText = ${tokenizeSceneText.toString()};
565
+ const extractSceneItems = ${extractSceneItems.toString()};
566
+
567
+ function buildClassIndexFromDom() {
568
+ const index = {};
569
+ for (const card of cards) {
570
+ const largeRow = card.querySelector('.tier-row[data-tier="large"]');
571
+ if (!largeRow) continue;
572
+ let defaultSvg = null;
573
+ const materials = {};
574
+ for (const swatch of largeRow.querySelectorAll(".swatch")) {
575
+ const labelEl = swatch.querySelector(".swatch-label");
576
+ const svgEl = swatch.querySelector(".swatch-img");
577
+ if (!labelEl || !svgEl) continue;
578
+ const svg = svgEl.innerHTML;
579
+ if (swatch.classList.contains("plain")) defaultSvg = svg;
580
+ else if (swatch.classList.contains("fallback")) { if (!defaultSvg) defaultSvg = svg; }
581
+ else materials[labelEl.textContent.trim().toLowerCase()] = svg;
582
+ }
583
+ if (!defaultSvg) {
584
+ const firstSvg = largeRow.querySelector(".swatch-img");
585
+ if (firstSvg) defaultSvg = firstSvg.innerHTML;
586
+ }
587
+ index[card.dataset.cls] = { defaultSvg, materials };
588
+ }
589
+ return index;
590
+ }
591
+
592
+ const classIndex = buildClassIndexFromDom();
593
+ const composeqEl = document.getElementById("composeq");
594
+ const composeFormEl = document.getElementById("composeForm");
595
+ const composePillsEl = document.getElementById("composePills");
596
+ const sceneRowEl = document.getElementById("sceneRow");
597
+ const sceneEmptyEl = document.getElementById("sceneEmpty");
598
+
599
+ function renderScene(text) {
600
+ const items = extractSceneItems(text, classIndex);
601
+ if (!items.length) {
602
+ sceneRowEl.innerHTML = "";
603
+ sceneEmptyEl.hidden = false;
604
+ return;
605
+ }
606
+ sceneEmptyEl.hidden = true;
607
+ sceneRowEl.innerHTML = items.map((item) => {
608
+ const entry = classIndex[item.className];
609
+ if (!entry) return "";
610
+ const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
611
+ const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
612
+ return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
613
+ }).join("");
614
+ }
615
+
616
+ composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
617
+ composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
618
+ composePillsEl.addEventListener("click", (e) => {
619
+ const btn = e.target.closest(".pill");
620
+ if (!btn) return;
621
+ const phrase = btn.dataset.fill || "";
622
+ const current = composeqEl.value.trim();
623
+ composeqEl.value = current ? current + ", a " + phrase : phrase;
624
+ composeqEl.focus();
625
+ renderScene(composeqEl.value);
626
+ });
627
+ renderScene("");
448
628
  })();
449
629
  </script>
450
630
  </body>