@bongos/core 1.19.97 → 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 +107 -2
- package/src/module-api.js +3 -1
- package/tests/projects_hub.mjs +64 -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>
|
|
@@ -512,7 +528,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
512
528
|
and the sort comparator as if a row existed. This keeps the self entry a
|
|
513
529
|
view-level concern (the owner's call, 2026-08-13) while still letting the
|
|
514
530
|
search box match it by the same predicate every other project uses. */
|
|
515
|
-
var SELF_PROJECT = { name: productName, tagline: 'this platform' };
|
|
531
|
+
var SELF_PROJECT = { name: productName, tagline: 'this platform', isSelf: true };
|
|
516
532
|
var mapSort = { key: 'name', dir: 1 };
|
|
517
533
|
|
|
518
534
|
/* Catalog origins become hrefs, so they are scheme-checked before they are
|
|
@@ -576,8 +592,13 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
576
592
|
a.setAttribute('data-generated', '');
|
|
577
593
|
a.setAttribute('data-rel', relOf(p));
|
|
578
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. */
|
|
579
599
|
a.href = href || '#';
|
|
580
600
|
if (href) { a.target = '_blank'; a.rel = 'noopener'; }
|
|
601
|
+
a.setAttribute('data-origin', String(p.origin || ''));
|
|
581
602
|
a.title = p.tagline || p.name || '';
|
|
582
603
|
a.style.cssText = '--acc:' + accentOf(p) +
|
|
583
604
|
';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
|
|
@@ -602,7 +623,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
602
623
|
var rel = relOf(p);
|
|
603
624
|
var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
|
|
604
625
|
: rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
|
|
605
|
-
return '<tr data-generated>' +
|
|
626
|
+
return '<tr data-generated data-origin="' + esc(String(p.origin || '')) + '">' +
|
|
606
627
|
'<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
|
|
607
628
|
accentOf(p) + '"></span>' +
|
|
608
629
|
'<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
|
|
@@ -665,6 +686,90 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
665
686
|
$('mapCount').textContent = shown + (shown === 1 ? ' project' : ' projects');
|
|
666
687
|
}
|
|
667
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
|
+
|
|
668
773
|
function loadCatalog() {
|
|
669
774
|
fetch(API + '/projects/featured', { credentials: 'same-origin' })
|
|
670
775
|
.then(function (res) { if (!res.ok) throw new Error('catalog'); return res.json(); })
|
package/src/module-api.js
CHANGED
|
@@ -621,8 +621,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
621
621
|
// landed since 1.19.95 with no explicit bump. run 31748829423. (task 1002620)
|
|
622
622
|
// 1.19.97 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
623
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)
|
|
624
626
|
// ---------------------------------------------------------------------------
|
|
625
|
-
const CORE_VERSION = '1.19.
|
|
627
|
+
const CORE_VERSION = '1.19.98'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
626
628
|
|
|
627
629
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
628
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', () => {
|
|
@@ -307,3 +307,66 @@ test('the hub and the landing hero share one typing cadence', () => {
|
|
|
307
307
|
assert.ok(heroHolds.includes(n), `hold ${n}ms should match the hero's cadence (${heroHolds.join('/')})`);
|
|
308
308
|
}
|
|
309
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
|
+
});
|