@bongos/core 1.19.96 → 1.19.98
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/.bongos-core.json +11 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +31 -0
- package/public-landing/projects.html +131 -9
- package/src/module-api.js +5 -1
- package/tests/projects_hub.mjs +100 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.98",
|
|
6
|
+
"core_contract": "1.19.98",
|
|
7
|
+
"source_commit": "6616c83d7f0ee7029db21b183e3983fc16568aad",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-13T22:
|
|
9
|
+
"built_at": "2026-08-13T22:25:33.504Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 298,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1549,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "729068034b75cf1c5878e8099c36f949384e07228d6337ea7387c7d586abd68e",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4127,12 +4127,12 @@
|
|
|
4127
4127
|
{
|
|
4128
4128
|
"path": "package-lock.json",
|
|
4129
4129
|
"mode": "0000644",
|
|
4130
|
-
"sha256": "
|
|
4130
|
+
"sha256": "1fac3202890f9d9899a8aaa72a473e4f73b6c27dccc28dd7a064a3c5b1367b85"
|
|
4131
4131
|
},
|
|
4132
4132
|
{
|
|
4133
4133
|
"path": "package.json",
|
|
4134
4134
|
"mode": "0000644",
|
|
4135
|
-
"sha256": "
|
|
4135
|
+
"sha256": "b8338943bf583ab9caa3686836ebeafb43be12df7f156f991763278abcca02dc"
|
|
4136
4136
|
},
|
|
4137
4137
|
{
|
|
4138
4138
|
"path": "public-docs/index.html",
|
|
@@ -4372,7 +4372,7 @@
|
|
|
4372
4372
|
{
|
|
4373
4373
|
"path": "public-landing/assets/cosmos.css",
|
|
4374
4374
|
"mode": "0000644",
|
|
4375
|
-
"sha256": "
|
|
4375
|
+
"sha256": "5f39814ab40306bc3fc5d4160119b85e2134f778dd62a7a6e6183e36a3ed193e"
|
|
4376
4376
|
},
|
|
4377
4377
|
{
|
|
4378
4378
|
"path": "public-landing/buddha.png",
|
|
@@ -4392,7 +4392,7 @@
|
|
|
4392
4392
|
{
|
|
4393
4393
|
"path": "public-landing/projects.html",
|
|
4394
4394
|
"mode": "0000644",
|
|
4395
|
-
"sha256": "
|
|
4395
|
+
"sha256": "5fd5fe20f1cb0186fd4ae69a7124a3305703952c7ac9662421793449b7ba8e26"
|
|
4396
4396
|
},
|
|
4397
4397
|
{
|
|
4398
4398
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5702,7 +5702,7 @@
|
|
|
5702
5702
|
{
|
|
5703
5703
|
"path": "src/module-api.js",
|
|
5704
5704
|
"mode": "0000644",
|
|
5705
|
-
"sha256": "
|
|
5705
|
+
"sha256": "fb7a891d2f7b815e57271245e93c4e4ede5f78bedb76e4648fcbf56b69ad3de3"
|
|
5706
5706
|
},
|
|
5707
5707
|
{
|
|
5708
5708
|
"path": "src/module-loader/loader.js",
|
|
@@ -7212,7 +7212,7 @@
|
|
|
7212
7212
|
{
|
|
7213
7213
|
"path": "tests/projects_hub.mjs",
|
|
7214
7214
|
"mode": "0000644",
|
|
7215
|
-
"sha256": "
|
|
7215
|
+
"sha256": "9495982aa30c4e845ab879a571be1d6753e39c3f1e3b2629ae9b41feeaf471cc"
|
|
7216
7216
|
},
|
|
7217
7217
|
{
|
|
7218
7218
|
"path": "tests/provision.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.98",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.98",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.98",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -533,6 +533,8 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
533
533
|
color:var(--ink-soft);vertical-align:middle;}
|
|
534
534
|
.tbl tr:last-child td{border-bottom:none;}
|
|
535
535
|
.tbl tr:hover td{background:color-mix(in srgb,var(--accent) 5%,transparent);}
|
|
536
|
+
/* the whole row opens the project card, so it must look like it does */
|
|
537
|
+
.tbl tbody tr{cursor:pointer;}
|
|
536
538
|
.tbl .pname{display:flex;align-items:center;gap:11px;}
|
|
537
539
|
.tbl .pdot{width:11px;height:11px;border-radius:50%;background:var(--acc);flex:0 0 auto;
|
|
538
540
|
box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 22%,transparent);}
|
|
@@ -557,6 +559,35 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
557
559
|
.tbl .colName{width:auto;}
|
|
558
560
|
@media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}}
|
|
559
561
|
|
|
562
|
+
/* ── the project card ──
|
|
563
|
+
An overlay INSIDE the map, not a destination: opening a project must not cost
|
|
564
|
+
you the sky you were reading (direction §9). It sits above .shell (z-index 40)
|
|
565
|
+
because a modal under the page chrome is a modal you can click behind.
|
|
566
|
+
|
|
567
|
+
The scrim is a plain rgba black in BOTH modes on purpose, so it needs no dark
|
|
568
|
+
override at all — a token here would have to be ink, and ink is near-white in
|
|
569
|
+
dark, which paints a white sheet over the page instead of dimming it. */
|
|
570
|
+
.cardScrim{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
|
|
571
|
+
padding:24px;background:rgba(0,0,0,.55);backdrop-filter:blur(3px);}
|
|
572
|
+
.projCard{position:relative;width:min(420px,100%);background:var(--bg-card);border:1.5px solid var(--line);
|
|
573
|
+
border-radius:var(--r-card,20px);box-shadow:var(--shadow-card);padding:30px 28px 26px;text-align:center;}
|
|
574
|
+
.projCard .cardDot{display:block;width:54px;height:54px;border-radius:50%;margin:0 auto 16px;
|
|
575
|
+
background:var(--acc,var(--accent));box-shadow:0 0 0 8px color-mix(in srgb,var(--acc,var(--accent)) 18%,transparent);}
|
|
576
|
+
.projCard h3{font-family:var(--font-display);font-weight:700;font-size:21px;color:var(--ink);margin-bottom:6px;}
|
|
577
|
+
.projCard .cardTag{font-size:14px;color:var(--ink-faint);font-weight:600;line-height:1.5;margin-bottom:14px;}
|
|
578
|
+
.projCard .cardRel{display:inline-block;font-family:var(--font-mono);font-size:11px;padding:3px 10px;
|
|
579
|
+
border-radius:999px;background:var(--accent-soft);color:var(--accent-deep);margin-bottom:18px;}
|
|
580
|
+
:root[data-mode="dark"] .projCard .cardRel{color:var(--sun-lit);}
|
|
581
|
+
@media (prefers-color-scheme: dark){:root:not([data-mode="light"]) .projCard .cardRel{color:var(--sun-lit);}}
|
|
582
|
+
.projCard .cardActions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
|
|
583
|
+
.projCard .cardX{position:absolute;top:10px;right:12px;width:30px;height:30px;border-radius:50%;
|
|
584
|
+
border:none;background:none;cursor:pointer;font-size:14px;color:var(--ink-faint);line-height:1;}
|
|
585
|
+
.projCard .cardX:hover{background:var(--accent-soft);color:var(--accent-deep);}
|
|
586
|
+
@media (prefers-reduced-motion:no-preference){
|
|
587
|
+
.cardScrim:not([hidden]) .projCard{animation:cardIn .16s ease-out;}
|
|
588
|
+
@keyframes cardIn{from{opacity:0;transform:translateY(6px) scale(.98);}to{opacity:1;transform:none;}}
|
|
589
|
+
}
|
|
590
|
+
|
|
560
591
|
/* ── your projects: search + a state filter whose pills carry live counts, so
|
|
561
592
|
a state with nothing in it reads 0 before you click it. The filter GROUPS
|
|
562
593
|
the chip outcomes; it never relabels them — "Live" holds both `live` and
|
|
@@ -159,6 +159,22 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
159
159
|
</div>
|
|
160
160
|
<div class="hintbar"><span><kbd>click a project</kbd> open it</span></div>
|
|
161
161
|
</div>
|
|
162
|
+
|
|
163
|
+
<!-- The project card. An OVERLAY inside the map, not a fourth view: opening
|
|
164
|
+
a project must not cost you the sky you were reading (direction §9).
|
|
165
|
+
Real markup, never JS-built (C7) — the same reason every other view
|
|
166
|
+
ships as markup — so its copy is in the document and its ids are stable.
|
|
167
|
+
Filled by openCard(); it carries no project identity of its own. -->
|
|
168
|
+
<div class="cardScrim" id="cardScrim" hidden>
|
|
169
|
+
<div class="projCard" id="projCard" role="dialog" aria-modal="true" aria-labelledby="cardName">
|
|
170
|
+
<button type="button" class="cardX" id="cardClose" aria-label="Close">✕</button>
|
|
171
|
+
<span class="cardDot" id="cardDot"></span>
|
|
172
|
+
<h3 id="cardName"></h3>
|
|
173
|
+
<p class="cardTag" id="cardTag"></p>
|
|
174
|
+
<p class="cardRel" id="cardRel" hidden></p>
|
|
175
|
+
<div class="cardActions" id="cardActions"></div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
162
178
|
</div>
|
|
163
179
|
|
|
164
180
|
<div class="wrap viewWrap" id="sub-table-body" hidden>
|
|
@@ -457,13 +473,30 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
457
473
|
$('markPrefix').textContent = prefix;
|
|
458
474
|
if (REDUCED || prefix !== 'cloud' || word !== 'bongos') return;
|
|
459
475
|
var PREFIXES = ['cloud', 'claude', 'chatgpt', 'gemini'];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
|
|
477
|
+
/* Type, HOLD, erase — the landing hero's cadence (index.html), not a
|
|
478
|
+
symmetric ping-pong. The single reversing interval this replaced turned
|
|
479
|
+
around on the very tick it completed a word, so `claude bongos` finished
|
|
480
|
+
and was already being deleted: the reader never got to see the word the
|
|
481
|
+
whole animation exists to show. A hold is the only part of the cycle that
|
|
482
|
+
carries meaning; the typing is just how it arrives.
|
|
483
|
+
|
|
484
|
+
`cloud` rests longest because it is the real wordmark and the state the
|
|
485
|
+
mark should sit in most — the others are the joke, and the joke lands
|
|
486
|
+
once. Same two numbers the hero uses, so the two surfaces read as one
|
|
487
|
+
system rather than two animations that happen to share a font. */
|
|
488
|
+
var TYPE = 72, ERASE = 38, HOLD = 900, HOLD_HOME = 2100;
|
|
489
|
+
var el = $('markPrefix'), wi = 0;
|
|
490
|
+
(function cycle() {
|
|
491
|
+
var w = PREFIXES[wi++ % PREFIXES.length], i = 0;
|
|
492
|
+
(function type() {
|
|
493
|
+
if (i <= w.length) { el.textContent = w.slice(0, i++); setTimeout(type, TYPE); }
|
|
494
|
+
else setTimeout(function erase() {
|
|
495
|
+
if (el.textContent.length) { el.textContent = el.textContent.slice(0, -1); setTimeout(erase, ERASE); }
|
|
496
|
+
else cycle();
|
|
497
|
+
}, w === 'cloud' ? HOLD_HOME : HOLD);
|
|
498
|
+
})();
|
|
499
|
+
})();
|
|
467
500
|
})();
|
|
468
501
|
|
|
469
502
|
/* ══ THE MAP ═══════════════════════════════════════════════════════════════
|
|
@@ -495,7 +528,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
495
528
|
and the sort comparator as if a row existed. This keeps the self entry a
|
|
496
529
|
view-level concern (the owner's call, 2026-08-13) while still letting the
|
|
497
530
|
search box match it by the same predicate every other project uses. */
|
|
498
|
-
var SELF_PROJECT = { name: productName, tagline: 'this platform' };
|
|
531
|
+
var SELF_PROJECT = { name: productName, tagline: 'this platform', isSelf: true };
|
|
499
532
|
var mapSort = { key: 'name', dir: 1 };
|
|
500
533
|
|
|
501
534
|
/* Catalog origins become hrefs, so they are scheme-checked before they are
|
|
@@ -559,8 +592,13 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
559
592
|
a.setAttribute('data-generated', '');
|
|
560
593
|
a.setAttribute('data-rel', relOf(p));
|
|
561
594
|
var href = safeHttpUrl(p.origin);
|
|
595
|
+
/* The href STAYS, and stays honest, even though a plain click now opens
|
|
596
|
+
the card instead of following it: it is what makes the orb a real link
|
|
597
|
+
— focusable, Enter-activatable, and still ctrl/middle-clickable into a
|
|
598
|
+
new tab. The card is layered over that, not substituted for it. */
|
|
562
599
|
a.href = href || '#';
|
|
563
600
|
if (href) { a.target = '_blank'; a.rel = 'noopener'; }
|
|
601
|
+
a.setAttribute('data-origin', String(p.origin || ''));
|
|
564
602
|
a.title = p.tagline || p.name || '';
|
|
565
603
|
a.style.cssText = '--acc:' + accentOf(p) +
|
|
566
604
|
';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
|
|
@@ -585,7 +623,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
585
623
|
var rel = relOf(p);
|
|
586
624
|
var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
|
|
587
625
|
: rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
|
|
588
|
-
return '<tr data-generated>' +
|
|
626
|
+
return '<tr data-generated data-origin="' + esc(String(p.origin || '')) + '">' +
|
|
589
627
|
'<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
|
|
590
628
|
accentOf(p) + '"></span>' +
|
|
591
629
|
'<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
|
|
@@ -648,6 +686,90 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
648
686
|
$('mapCount').textContent = shown + (shown === 1 ? ' project' : ' projects');
|
|
649
687
|
}
|
|
650
688
|
|
|
689
|
+
/* ══ THE PROJECT CARD ══════════════════════════════════════════════════════
|
|
690
|
+
Every orb and every row opens THIS. Before, each one navigated: catalog
|
|
691
|
+
orbs into a new tab and the platform's own orb — an <a href="/"> — straight
|
|
692
|
+
back to the landing page, so clicking the project you were standing on threw
|
|
693
|
+
away the map to show you a marketing page. Direction §9 called for one
|
|
694
|
+
overlay card serving every project, self included; the owner adopted it
|
|
695
|
+
2026-08-13. The card is layered OVER the links rather than replacing them,
|
|
696
|
+
so ctrl/middle-click still opens a project's site directly.
|
|
697
|
+
|
|
698
|
+
It states only what the public catalog actually carries — name, tagline,
|
|
699
|
+
your relationship, a door — because /projects/featured selects four columns
|
|
700
|
+
and cannot answer anything richer for a catalog row (task 1002744 owns that
|
|
701
|
+
data path). A card that showed builders or liveness here would be inventing
|
|
702
|
+
both, the same C6 lie the table's em dashes exist to avoid. ══ */
|
|
703
|
+
|
|
704
|
+
var cardReturn = null; /* the element focus came from, to hand it back */
|
|
705
|
+
|
|
706
|
+
function projByOrigin(o) {
|
|
707
|
+
for (var i = 0; i < catalog.length; i++) if (String(catalog[i].origin) === o) return catalog[i];
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function openCard(p, rel, returnTo) {
|
|
712
|
+
var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
|
|
713
|
+
$('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
|
|
714
|
+
$('cardName').textContent = p.name || '';
|
|
715
|
+
$('cardTag').textContent = p.tagline || '';
|
|
716
|
+
var label = rel === 'owner' ? 'yours' : rel === 'builder' ? 'you build here' : '';
|
|
717
|
+
$('cardRel').textContent = label;
|
|
718
|
+
$('cardRel').hidden = !label;
|
|
719
|
+
/* the platform's door is same-tab: / is this site, and a new tab for the
|
|
720
|
+
page you are already on is a bug, not a courtesy */
|
|
721
|
+
$('cardActions').innerHTML = href
|
|
722
|
+
? '<a class="btn small" href="' + esc(href) + '"' +
|
|
723
|
+
(p.isSelf ? '' : ' target="_blank" rel="noopener"') + '>' +
|
|
724
|
+
(p.isSelf ? 'Home →' : 'Open site →') + '</a>'
|
|
725
|
+
: '';
|
|
726
|
+
cardReturn = returnTo || null;
|
|
727
|
+
$('cardScrim').hidden = false;
|
|
728
|
+
$('cardClose').focus();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function closeCard() {
|
|
732
|
+
if ($('cardScrim').hidden) return;
|
|
733
|
+
$('cardScrim').hidden = true;
|
|
734
|
+
if (cardReturn && cardReturn.focus) cardReturn.focus();
|
|
735
|
+
cardReturn = null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* Delegated, so orbs and rows rebuilt by every filter keystroke stay wired
|
|
739
|
+
without re-binding. A modified click is left ALONE — ctrl/cmd/shift/middle
|
|
740
|
+
mean "open in a new tab" everywhere else on the web and the orb is a real
|
|
741
|
+
link, so intercepting them would break an expectation the markup makes. */
|
|
742
|
+
function isPlainClick(ev) {
|
|
743
|
+
return !(ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey || ev.button > 0);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
$('orbField').addEventListener('click', function (ev) {
|
|
747
|
+
var a = ev.target.closest && ev.target.closest('.worldOrb');
|
|
748
|
+
if (!a || !isPlainClick(ev)) return;
|
|
749
|
+
ev.preventDefault();
|
|
750
|
+
if (a.id === 'selfOrb') return openCard(SELF_PROJECT, selfRel(), a);
|
|
751
|
+
var p = projByOrigin(a.getAttribute('data-origin') || '');
|
|
752
|
+
if (p) openCard(p, relOf(p), a);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
$('mapRows').addEventListener('click', function (ev) {
|
|
756
|
+
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
757
|
+
if (ev.target.closest && ev.target.closest('a')) return;
|
|
758
|
+
var tr = ev.target.closest && ev.target.closest('tr');
|
|
759
|
+
if (!tr || !isPlainClick(ev)) return;
|
|
760
|
+
if (tr.id === 'selfRow') return openCard(SELF_PROJECT, selfRel(), null);
|
|
761
|
+
var p = projByOrigin(tr.getAttribute('data-origin') || '');
|
|
762
|
+
if (p) openCard(p, relOf(p), null);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
$('cardClose').addEventListener('click', closeCard);
|
|
766
|
+
$('cardScrim').addEventListener('click', function (ev) {
|
|
767
|
+
if (ev.target === $('cardScrim')) closeCard(); /* backdrop only, never the card */
|
|
768
|
+
});
|
|
769
|
+
document.addEventListener('keydown', function (ev) {
|
|
770
|
+
if (ev.key === 'Escape') closeCard();
|
|
771
|
+
});
|
|
772
|
+
|
|
651
773
|
function loadCatalog() {
|
|
652
774
|
fetch(API + '/projects/featured', { credentials: 'same-origin' })
|
|
653
775
|
.then(function (res) { if (!res.ok) throw new Error('catalog'); return res.json(); })
|
package/src/module-api.js
CHANGED
|
@@ -619,8 +619,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
619
619
|
// landed since 1.19.94 with no explicit bump. run 31746921919. (task 1002620)
|
|
620
620
|
// 1.19.96 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
621
621
|
// landed since 1.19.95 with no explicit bump. run 31748829423. (task 1002620)
|
|
622
|
+
// 1.19.97 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
623
|
+
// landed since 1.19.96 with no explicit bump. run 31749223878. (task 1002620)
|
|
624
|
+
// 1.19.98 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
625
|
+
// landed since 1.19.97 with no explicit bump. run 31749841394. (task 1002620)
|
|
622
626
|
// ---------------------------------------------------------------------------
|
|
623
|
-
const CORE_VERSION = '1.19.
|
|
627
|
+
const CORE_VERSION = '1.19.98'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
624
628
|
|
|
625
629
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
626
630
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -238,7 +238,7 @@ test('the renderer preserves the self row exactly as it preserves the self orb',
|
|
|
238
238
|
'a wholesale innerHTML= on #mapRows destroys the static self row');
|
|
239
239
|
assert.match(SCRIPT, /querySelectorAll\('tr\[data-generated\]'\)/,
|
|
240
240
|
'generated rows must be removed selectively, like .worldOrb[data-generated]');
|
|
241
|
-
assert.match(SCRIPT, /<tr data-generated
|
|
241
|
+
assert.match(SCRIPT, /<tr data-generated[ >]/, 'generated rows must be markable');
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
test('"only mine" no longer hides the platform from a signed-in builder', () => {
|
|
@@ -271,3 +271,102 @@ test('the count includes the platform when it is shown', () => {
|
|
|
271
271
|
'the count must not ignore the self entry it is rendering beside');
|
|
272
272
|
assert.match(SCRIPT, /var shown = list\.length \+ \(selfShown \? 1 : 0\)/);
|
|
273
273
|
});
|
|
274
|
+
|
|
275
|
+
/* ── The wordmark's cadence (task 1002765) ──────────────────────────────────
|
|
276
|
+
The mark completed a word and erased it on the same tick, so the word the
|
|
277
|
+
animation exists to show was never legible. Owner-reported, twice. ── */
|
|
278
|
+
|
|
279
|
+
test('the wordmark HOLDS the finished word before erasing it', () => {
|
|
280
|
+
const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
|
|
281
|
+
assert.ok(mark, 'typeMark() must exist');
|
|
282
|
+
// the regression shape: one interval that flips direction the moment it
|
|
283
|
+
// completes a word, so there is no dwell at full length
|
|
284
|
+
assert.doesNotMatch(mark, /setInterval/,
|
|
285
|
+
'a single reversing interval cannot hold — it turns around on the tick it completes');
|
|
286
|
+
assert.match(mark, /HOLD\s*=\s*\d+/, 'the hold must be a named duration, not an accident of timing');
|
|
287
|
+
// and it must actually be a pause worth reading, not a couple of frames
|
|
288
|
+
const hold = Number((mark.match(/HOLD\s*=\s*(\d+)/) || [, 0])[1]);
|
|
289
|
+
assert.ok(hold >= 600, `the hold must be legible; got ${hold}ms`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('the wordmark rests on the real product name, not on a joke', () => {
|
|
293
|
+
const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
|
|
294
|
+
assert.match(mark, /w === 'cloud' \? HOLD_HOME : HOLD/,
|
|
295
|
+
'the canonical prefix should dwell longest — it is the wordmark, the others are the gag');
|
|
296
|
+
const home = Number((mark.match(/HOLD_HOME\s*=\s*(\d+)/) || [, 0])[1]);
|
|
297
|
+
const hold = Number((mark.match(/HOLD\s*=\s*(\d+)/) || [, 0])[1]);
|
|
298
|
+
assert.ok(home > hold, 'the resting state must outlast the alternates');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('the hub and the landing hero share one typing cadence', () => {
|
|
302
|
+
// two surfaces, one system — a divergence here is a visible inconsistency
|
|
303
|
+
const LANDING = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
304
|
+
const heroHolds = [...LANDING.matchAll(/\?\s*(\d+)\s*:\s*(\d+)\)/g)].map(m => [m[1], m[2]]).flat();
|
|
305
|
+
const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
|
|
306
|
+
for (const n of [mark.match(/HOLD_HOME\s*=\s*(\d+)/)[1], mark.match(/HOLD\s*=\s*(\d+)/)[1]]) {
|
|
307
|
+
assert.ok(heroHolds.includes(n), `hold ${n}ms should match the hero's cadence (${heroHolds.join('/')})`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/* ── The project card (task 1002770) ────────────────────────────────────────
|
|
312
|
+
Every orb used to navigate: catalog orbs into a new tab, and the platform's
|
|
313
|
+
own orb straight back to the landing page — so clicking the project you were
|
|
314
|
+
standing on threw away the map. Direction §9's overlay, owner-adopted. ── */
|
|
315
|
+
|
|
316
|
+
test('the card is real markup in the map, not a JS-built node (C7)', () => {
|
|
317
|
+
assert.match(HUB, /id="cardScrim"/);
|
|
318
|
+
assert.match(HUB, /id="projCard"/);
|
|
319
|
+
// it lives inside the map view, not as a fourth destination
|
|
320
|
+
const map = (HUB.match(/<section id="view-map">([\s\S]*?)<\/section>/) || [, ''])[1];
|
|
321
|
+
assert.match(map, /id="cardScrim"/, 'the card is an overlay INSIDE the map');
|
|
322
|
+
assert.doesNotMatch(HUB, /id="view-detail"/, 'and never a view of its own');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('the card is a labelled modal dialog', () => {
|
|
326
|
+
const card = (HUB.match(/<div class="projCard"[^>]*>/) || [''])[0];
|
|
327
|
+
assert.match(card, /role="dialog"/);
|
|
328
|
+
assert.match(card, /aria-modal="true"/);
|
|
329
|
+
assert.match(card, /aria-labelledby="cardName"/);
|
|
330
|
+
assert.match(HUB, /id="cardClose"[^>]*aria-label="Close"/, 'and has a labelled close control');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('the platform orb opens the card instead of leaving for the landing page', () => {
|
|
334
|
+
// the reported bug: <a id="selfOrb" href="/"> navigated away on a plain click
|
|
335
|
+
assert.match(SCRIPT, /a\.id === 'selfOrb'\) return openCard\(SELF_PROJECT/,
|
|
336
|
+
'a plain click on the self orb must open the card');
|
|
337
|
+
// the href stays — it is what keeps the orb a real, focusable, ctrl-clickable link
|
|
338
|
+
assert.match(HUB, /id="selfOrb" href="\/"/, 'the orb stays a real link');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('a modified click is left alone — the orb is still a link', () => {
|
|
342
|
+
assert.match(SCRIPT, /function isPlainClick/);
|
|
343
|
+
const fn = (SCRIPT.match(/function isPlainClick\(ev\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
344
|
+
for (const k of ['metaKey', 'ctrlKey', 'shiftKey']) {
|
|
345
|
+
assert.match(fn, new RegExp(k), `${k} must fall through to the browser`);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('the card closes by Escape, backdrop and the close control', () => {
|
|
350
|
+
assert.match(SCRIPT, /ev\.key === 'Escape'/);
|
|
351
|
+
assert.match(SCRIPT, /ev\.target === \$\('cardScrim'\)/, 'backdrop only — a click on the card must not close it');
|
|
352
|
+
assert.match(SCRIPT, /\$\('cardClose'\)\.addEventListener\('click', closeCard\)/);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('focus enters the card and is handed back on close', () => {
|
|
356
|
+
assert.match(SCRIPT, /\$\('cardClose'\)\.focus\(\)/, 'focus must move into the dialog');
|
|
357
|
+
assert.match(SCRIPT, /cardReturn(\.focus)?/, 'and return to where it came from');
|
|
358
|
+
assert.match(SCRIPT, /cardReturn\.focus\(\)/);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('the card claims nothing the public catalog cannot answer (C6)', () => {
|
|
362
|
+
const card = (HUB.match(/<div class="cardScrim"[\s\S]*?<\/div>\s*<\/div>/) || [''])[0];
|
|
363
|
+
// /projects/featured selects origin, name, tagline, art_url — and nothing else
|
|
364
|
+
for (const invented of [/Builders/i, /\blive\b/i, /uptime/i, /\btags\b/i]) {
|
|
365
|
+
assert.doesNotMatch(card, invented, `the card must not invent ${invented}`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('the row keeps its own Open link working underneath the card', () => {
|
|
370
|
+
assert.match(SCRIPT, /if \(ev\.target\.closest && ev\.target\.closest\('a'\)\) return;/,
|
|
371
|
+
'a click on the row\'s own anchor must not be swallowed by the card handler');
|
|
372
|
+
});
|