@bongos/core 1.19.440 → 1.19.441
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 +19 -19
- package/docs/copy-inventory.md +175 -172
- package/docs/copy-registry.json +244 -217
- package/docs/design/mocks/hub/mine.html +89 -1
- package/docs/design/projects-hub-direction-v2.md +36 -0
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +81 -65
- package/public-landing/projects.html +100 -68
- package/public-landing/projects.probes.json +10 -2
- package/public-landing/projects.states.json +2 -1
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +87 -62
- package/tests/projects_hub_pre_uat.mjs +11 -9
|
@@ -492,37 +492,32 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
492
492
|
<h2 class="tblTitle">every project</h2>
|
|
493
493
|
<span class="mastObj" aria-hidden="true"></span>
|
|
494
494
|
</div>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
</tr>
|
|
522
|
-
<!--apex-only:end-->
|
|
523
|
-
</tbody>
|
|
524
|
-
</table>
|
|
525
|
-
<p class="finePrint" style="text-align:center">Sort by Project or Builders.</p>
|
|
495
|
+
<!-- The sort left the column head with the header row (task 1003356): a
|
|
496
|
+
ledger row has no columns to head, so the two orders are hairline
|
|
497
|
+
pills in the mine view's own grammar — real buttons, aria-pressed, the
|
|
498
|
+
live order stated under the list where a screen reader hears it. -->
|
|
499
|
+
<div class="tblSort" role="group" aria-label="Sort the projects">
|
|
500
|
+
<span class="sortLbl">sort</span>
|
|
501
|
+
<button type="button" class="sortPill" data-sort="name" aria-pressed="true">name <span class="dir">a to z</span></button>
|
|
502
|
+
<button type="button" class="sortPill" data-sort="builders" aria-pressed="false">builders</button>
|
|
503
|
+
</div>
|
|
504
|
+
<ul class="tbl" id="mapRows">
|
|
505
|
+
<!-- The platform's own row. Static and apex-only for exactly the reason
|
|
506
|
+
the orb above is (stripApexOnly() is a string transform that cannot
|
|
507
|
+
reach anything a script renders later), and preserved across
|
|
508
|
+
re-renders by the same data-generated contract renderOrbs uses. -->
|
|
509
|
+
<!--apex-only:start-->
|
|
510
|
+
<li class="tblRow" id="selfRow">
|
|
511
|
+
<span class="worldOrb" data-shape="pair" data-style="chrome" data-template="pair-chrome" style="--acc:var(--accent);--d:64px" aria-hidden="true"><span class="disc"></span></span>
|
|
512
|
+
<div class="tWho"><span class="ptitle" data-brand-product-inline>Cloud Bongos</span><span id="selfPill"></span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
|
|
513
|
+
<div class="facts"><span class="fact" id="selfBuilders">builders not known</span><span class="fact" id="selfShipped" hidden></span></div>
|
|
514
|
+
<p class="ptag">this platform</p>
|
|
515
|
+
<div class="runs"><span class="rLbl">runs</span><span id="selfTags">—</span></div>
|
|
516
|
+
<div class="rowDoors"><a class="rowGo" href="/">view</a><a class="rowHall" id="selfHall" href="{{buildersOrigin}}" aria-label="Builders' hall" title="Builders' hall" hidden><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg></a></div>
|
|
517
|
+
</li>
|
|
518
|
+
<!--apex-only:end-->
|
|
519
|
+
</ul>
|
|
520
|
+
<p class="finePrint" id="mapSorted" role="status" aria-live="polite">Sorted by name, A to Z.</p>
|
|
526
521
|
</div>
|
|
527
522
|
|
|
528
523
|
<!-- The project card. An OVERLAY inside the map, not a fourth view: opening
|
|
@@ -1471,7 +1466,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1471
1466
|
}
|
|
1472
1467
|
function typeHtml(t) {
|
|
1473
1468
|
var l = typeLabel(t);
|
|
1474
|
-
|
|
1469
|
+
/* a ledger row's fact is a self-describing line, so "not declared" is said
|
|
1470
|
+
in words where the table's Type cell used to print a dash (task 1003356) */
|
|
1471
|
+
return l ? '<span class="typeTag">' + l + '</span>' : 'type not declared';
|
|
1475
1472
|
}
|
|
1476
1473
|
function matchesMapQuery(p, q) {
|
|
1477
1474
|
if (!q) return true;
|
|
@@ -1545,44 +1542,63 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1545
1542
|
});
|
|
1546
1543
|
}
|
|
1547
1544
|
|
|
1545
|
+
/* ── the row's facts, the ledger grammar (task 1003356) ──
|
|
1546
|
+
Each fact is one self-describing label-face line, the shape the Your-projects
|
|
1547
|
+
row uses, so the two views read as one ledger. The distinction the columns
|
|
1548
|
+
drew survives the move: "we could not ask" (null) and "we asked, the answer
|
|
1549
|
+
is none" (0) are two different sentences, not a dash and a zero. A figure
|
|
1550
|
+
that was never a column — shipped in 30 days, the Listed date — takes no
|
|
1551
|
+
space when it has no answer, the poster card's own rule. */
|
|
1552
|
+
function factLines(p) {
|
|
1553
|
+
var out = ['<span class="fact">' + typeHtml(p.type) + '</span>'];
|
|
1554
|
+
out.push('<span class="fact">' + (p.builders == null ? 'builders not known'
|
|
1555
|
+
: esc(String(p.builders)) + (p.builders === 1 ? ' builder' : ' builders')) + '</span>');
|
|
1556
|
+
var shipped = shippedFigure(p);
|
|
1557
|
+
if (shipped) out.push('<span class="fact">' + esc(shipped) + ' shipped in 30 days</span>');
|
|
1558
|
+
if (p.created_at) out.push('<span class="fact">listed ' + esc(listedDate(p.created_at)) + '</span>');
|
|
1559
|
+
return out.join('');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1548
1562
|
function renderRows(list) {
|
|
1549
1563
|
var body = $('mapRows');
|
|
1550
1564
|
/* replace only the generated rows — the platform's own row is static
|
|
1551
1565
|
apex-only markup and must survive every re-render, for the same reason
|
|
1552
1566
|
#selfOrb does in the sky. It stays pinned first: it is "this platform",
|
|
1553
1567
|
not a peer in the catalog's sort order. */
|
|
1554
|
-
body.querySelectorAll('
|
|
1568
|
+
body.querySelectorAll('li[data-generated]').forEach(function (n) { n.remove(); });
|
|
1555
1569
|
body.insertAdjacentHTML('beforeend', list.map(function (p) {
|
|
1556
1570
|
var rel = relOf(p);
|
|
1557
|
-
var pill = rel === 'owner' ? '
|
|
1558
|
-
: rel === 'builder' ? '
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1571
|
+
var pill = rel === 'owner' ? '<span class="pill">yours</span>'
|
|
1572
|
+
: rel === 'builder' ? '<span class="pill">you build here</span>' : '';
|
|
1573
|
+
/* the project's own planet, the object the sky and the Your-projects row
|
|
1574
|
+
already draw — the owner's chosen template and colour first (task
|
|
1575
|
+
1003347), the slug walk otherwise. Decorative here: the name beside it
|
|
1576
|
+
is the row's accessible door. */
|
|
1577
|
+
var planet = '<span class="worldOrb"' + planetAttrs({ tpl: templateOf(p) }) +
|
|
1578
|
+
' style="--acc:' + accentOf(p) + ';--d:64px" aria-hidden="true"><span class="disc"></span></span>';
|
|
1579
|
+
return '<li class="tblRow" data-generated ' + (p._inst ? 'data-manage="' + esc(String(p._inst.id)) + '" ' : '') +
|
|
1580
|
+
'data-origin="' + esc(String(p.origin || '')) + '">' + planet +
|
|
1581
|
+
'<div class="tWho">' + (p._inst
|
|
1564
1582
|
? '<a class="ptitle" data-manage="' + esc(String(p._inst.id)) + '" href="?view=manage&id=' + esc(String(p._inst.id)) + '">' + esc(p.name) + '</a>'
|
|
1565
|
-
: '<span class="ptitle">' + esc(p.name) + '</span>') + pill +
|
|
1566
|
-
'<span class="ptag">' + esc(p.tagline || '') + '</span></span></span></td>' +
|
|
1567
|
-
/* Builders and Runs come from the project's OWN public API, enriched
|
|
1568
|
-
server-side (task 1002771). null is unknown — the project was
|
|
1569
|
-
unreachable — and an em dash says exactly that. 0 is a real answer and
|
|
1570
|
-
renders as 0, because "nobody has joined yet" is a different fact from
|
|
1571
|
-
"we could not ask". */
|
|
1572
|
-
'<td class="colType">' + typeHtml(p.type) + '</td>' +
|
|
1573
|
-
'<td class="right num">' + (p.builders == null ? '—' : esc(String(p.builders))) + '</td>' +
|
|
1574
|
-
'<td class="colTags">' + tagsHtml(p.tags) + '</td>' +
|
|
1583
|
+
: '<span class="ptitle">' + esc(p.name) + '</span>') + pill +
|
|
1575
1584
|
/* liveness on a CATALOG row is NOT probed by this surface — a chip
|
|
1576
1585
|
saying "live" there would claim more than anything verified (C6).
|
|
1577
1586
|
An OWNED row is different: the own-scoped feed answers for it, so
|
|
1578
1587
|
it carries chipFor()'s truth, the same chip its orb and its manage
|
|
1579
1588
|
card show (task 1002744). */
|
|
1580
|
-
|
|
1581
|
-
: '<span class="chip calm"><span class="cdot"></span
|
|
1582
|
-
'<
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1589
|
+
(p._inst ? chipFor(p._inst)
|
|
1590
|
+
: '<span class="chip calm"><span class="cdot"></span>not checked</span>') + '</div>' +
|
|
1591
|
+
'<div class="facts">' + factLines(p) + '</div>' +
|
|
1592
|
+
'<p class="ptag">' + esc(p.tagline || '') + '</p>' +
|
|
1593
|
+
/* Runs stays the module list here, where the poster card's Runs is the
|
|
1594
|
+
address (direction §7): null is "we could not ask", [] is "core" */
|
|
1595
|
+
'<div class="runs"><span class="rLbl">runs</span>' + tagsHtml(p.tags) + '</div>' +
|
|
1596
|
+
/* at most two doors (task 1003356): the site, and the hall where the
|
|
1597
|
+
viewer builds there and the hall answers at a different address */
|
|
1598
|
+
'<div class="rowDoors">' + (safeHttpUrl(p.origin)
|
|
1599
|
+
? '<a class="rowGo" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">view</a>'
|
|
1600
|
+
: '') + hallGlyphHtml(p, rel) + '</div>' +
|
|
1601
|
+
'</li>';
|
|
1586
1602
|
}).join(''));
|
|
1587
1603
|
}
|
|
1588
1604
|
|
|
@@ -1665,7 +1681,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1665
1681
|
}
|
|
1666
1682
|
if (row) {
|
|
1667
1683
|
row.hidden = !selfShown;
|
|
1668
|
-
$('selfPill').innerHTML = badge ? '
|
|
1684
|
+
$('selfPill').innerHTML = badge ? '<span class="pill">' + badge + '</span>' : '';
|
|
1669
1685
|
/* the hall glyph: member-gated (the badge says so) and address-gated */
|
|
1670
1686
|
var sh = $('selfHall');
|
|
1671
1687
|
if (sh) sh.hidden = !badge || !hallUrlOf(SELF_PROJECT);
|
|
@@ -1911,7 +1927,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1911
1927
|
}
|
|
1912
1928
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
1913
1929
|
if (ev.target.closest && ev.target.closest('a')) return;
|
|
1914
|
-
var tr = ev.target.closest && ev.target.closest('
|
|
1930
|
+
var tr = ev.target.closest && ev.target.closest('li');
|
|
1915
1931
|
if (!tr || !isPlainClick(ev)) return;
|
|
1916
1932
|
if (tr.id === 'selfRow') return openCard(SELF_PROJECT, selfRel(), null);
|
|
1917
1933
|
/* an owned row goes to MANAGE, exactly like its orb — two views, one job */
|
|
@@ -2030,7 +2046,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2030
2046
|
var rows = (d && d.leaderboard) || [];
|
|
2031
2047
|
if (!rows.length) return;
|
|
2032
2048
|
var real = rows.filter(function (b) { return !/-system$/.test(String(b.github_login || '')); });
|
|
2033
|
-
$('selfBuilders').textContent =
|
|
2049
|
+
$('selfBuilders').textContent = real.length === 1 ? '1 builder' : real.length + ' builders';
|
|
2034
2050
|
/* the card reads these too: the same payload answers the platform's
|
|
2035
2051
|
builder count and its owner — the archon row — by the same rule the
|
|
2036
2052
|
server-side enrichment applies to every other project */
|
|
@@ -2056,6 +2072,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2056
2072
|
var n = d.tasks.filter(function (t) { return Date.parse(t && t.shipped_at) >= since; }).length;
|
|
2057
2073
|
SELF_PROJECT.shipped_30d = n;
|
|
2058
2074
|
SELF_PROJECT.shipped_30d_capped = d.tasks.length >= 200 && n === d.tasks.length;
|
|
2075
|
+
/* the row's own fact, by the same rule every other row's is written */
|
|
2076
|
+
var sf = $('selfShipped');
|
|
2077
|
+
sf.textContent = shippedFigure(SELF_PROJECT) + ' shipped in 30 days';
|
|
2078
|
+
sf.hidden = false;
|
|
2059
2079
|
})
|
|
2060
2080
|
.catch(function () { /* unknown stays unknown: the figure hides */ });
|
|
2061
2081
|
}
|
|
@@ -2113,21 +2133,33 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2113
2133
|
|
|
2114
2134
|
$('onlyMine').addEventListener('change', applyMapFilter);
|
|
2115
2135
|
$('mapQ').addEventListener('input', applyMapFilter);
|
|
2116
|
-
/* the sort
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2136
|
+
/* ── the sort, off the column head (task 1003356) ──
|
|
2137
|
+
A ledger row has no columns to head, so aria-sort has no host: the two
|
|
2138
|
+
orders are toggle buttons in a group, pressed = the live order, and the
|
|
2139
|
+
sentence under the list states it for a screen reader that never saw the
|
|
2140
|
+
press. Pressing the pressed one reverses, exactly as the header did. */
|
|
2141
|
+
var SORT_WORDS = { name: ['a to z', 'z to a'], builders: ['fewest first', 'most first'] };
|
|
2142
|
+
function paintSort() {
|
|
2143
|
+
document.querySelectorAll('.tblSort button[data-sort]').forEach(function (b) {
|
|
2144
|
+
var on = b.getAttribute('data-sort') === mapSort.key;
|
|
2145
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
2146
|
+
var dir = b.querySelector('.dir');
|
|
2147
|
+
if (on && !dir) { dir = document.createElement('span'); dir.className = 'dir'; b.appendChild(document.createTextNode(' ')); b.appendChild(dir); }
|
|
2148
|
+
if (dir) { dir.hidden = !on; dir.textContent = on ? SORT_WORDS[mapSort.key][mapSort.dir === 1 ? 0 : 1] : ''; }
|
|
2149
|
+
});
|
|
2150
|
+
$('mapSorted').textContent = 'Sorted by ' + mapSort.key + ', ' +
|
|
2151
|
+
SORT_WORDS[mapSort.key][mapSort.dir === 1 ? 0 : 1] + '.';
|
|
2152
|
+
}
|
|
2153
|
+
document.querySelectorAll('.tblSort button[data-sort]').forEach(function (btn) {
|
|
2122
2154
|
btn.addEventListener('click', function () {
|
|
2123
|
-
var k =
|
|
2155
|
+
var k = btn.getAttribute('data-sort');
|
|
2124
2156
|
mapSort.dir = (mapSort.key === k) ? -mapSort.dir : 1;
|
|
2125
2157
|
mapSort.key = k;
|
|
2126
|
-
|
|
2127
|
-
th.setAttribute('aria-sort', mapSort.dir === 1 ? 'ascending' : 'descending');
|
|
2158
|
+
paintSort();
|
|
2128
2159
|
applyMapFilter();
|
|
2129
2160
|
});
|
|
2130
2161
|
});
|
|
2162
|
+
paintSort();
|
|
2131
2163
|
|
|
2132
2164
|
/* ══ THE LIVING SKY (task 1002762) ═════════════════════════════════════════
|
|
2133
2165
|
The retired gate's cluster, rewritten for the hub. Physics is the gate's
|
|
@@ -75,8 +75,16 @@
|
|
|
75
75
|
"text": [["#cardRel", "yours"]], "href": [["#cardOpen", "/"]] } },
|
|
76
76
|
{ "name": "the table: the hall glyph rides the door cell on the one member row whose hall is elsewhere, and on the platform's row", "continue": true,
|
|
77
77
|
"steps": [["press", "Escape"], ["wait", 150], ["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"table\"]"], ["wait", 300]],
|
|
78
|
-
"expect": { "dialog": false, "visible": ["#selfHall"], "count": [["#mapRows
|
|
79
|
-
"href": [["#mapRows
|
|
78
|
+
"expect": { "dialog": false, "visible": ["#selfHall"], "count": [["#mapRows li[data-generated] .rowHall", 1], ["#mapRows li[data-generated] .facts", 7], ["#mapRows li[data-generated] .runs", 7], ["#mapRows li[data-generated] .rowDoors", 7]],
|
|
79
|
+
"href": [["#mapRows li[data-generated] .rowHall", "https://builders.amazonprimea.com/"]] } },
|
|
80
|
+
{ "name": "the table: the sort left the column head — the pills carry the order, and the sentence under the list states it (task 1003356)", "continue": true,
|
|
81
|
+
"steps": [["click", ".tblSort button[data-sort=\"builders\"]"], ["wait", 200]],
|
|
82
|
+
"expect": { "attr": [[".tblSort button[data-sort=\"builders\"]", "aria-pressed", "true"], [".tblSort button[data-sort=\"name\"]", "aria-pressed", "false"]],
|
|
83
|
+
"text": [["#mapSorted", "Sorted by builders, fewest first."]] } },
|
|
84
|
+
{ "name": "the table: pressing the pressed sort reverses it, exactly as the column header did", "continue": true,
|
|
85
|
+
"steps": [["click", ".tblSort button[data-sort=\"builders\"]"], ["wait", 200]],
|
|
86
|
+
"expect": { "attr": [[".tblSort button[data-sort=\"builders\"]", "aria-pressed", "true"]],
|
|
87
|
+
"text": [["#mapSorted", "Sorted by builders, most first."]] } },
|
|
80
88
|
{ "name": "back to the sky for the rest of the sequence", "continue": true,
|
|
81
89
|
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"sky\"]"], ["wait", 300]],
|
|
82
90
|
"expect": { "hidden": ["#sub-table-body"] } },
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
|
16
16
|
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
17
17
|
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
18
|
-
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows
|
|
18
|
+
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows li[data-generated] .rowHall", 1], ["#mapRows .tblRow .facts", 7], ["#mapRows .tblRow .rowDoors", 7]], "text": [["#mapSorted", "Sorted by name, a to z."]] } },
|
|
19
|
+
"in-table-by-builders": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400], ["click", ".tblSort button[data-sort=\"builders\"]"], ["wait", 200]], "expect": { "visible": ["#sub-table-body"], "attr": [[".tblSort button[data-sort=\"builders\"]", "aria-pressed", "true"], [".tblSort button[data-sort=\"name\"]", "aria-pressed", "false"]], "text": [["#mapSorted", "Sorted by builders, fewest first."]] } },
|
|
19
20
|
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList .proj .actions a", 7], ["#projectsList .proj .actions button", 0]] } },
|
|
20
21
|
"in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs", "#mAddress"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
|
|
21
22
|
"in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.441'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -384,9 +384,9 @@ test('the platform has a self entry in the TABLE, not only in the sky', () => {
|
|
|
384
384
|
// reason: stripApexOnly() is a string transform and cannot reach JS output
|
|
385
385
|
assert.match(HUB, /id="selfOrb"/, 'the sky keeps its self orb');
|
|
386
386
|
assert.match(HUB, /id="selfRow"/, 'the table needs one too — this is the bug');
|
|
387
|
-
// and the row must sit INSIDE the
|
|
388
|
-
const
|
|
389
|
-
assert.match(
|
|
387
|
+
// and the row must sit INSIDE the list the renderer owns
|
|
388
|
+
const rows = (HUB.match(/<ul class="tbl" id="mapRows">([\s\S]*?)<\/ul>/) || [, ''])[1];
|
|
389
|
+
assert.match(rows, /id="selfRow"/, 'the self row belongs in #mapRows');
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
test('both self entries are apex-only — a child instance has no "this platform"', () => {
|
|
@@ -404,9 +404,9 @@ test('the renderer preserves the self row exactly as it preserves the self orb',
|
|
|
404
404
|
// delete a static row on the first catalog paint
|
|
405
405
|
assert.doesNotMatch(SCRIPT, /\$\('mapRows'\)\.innerHTML\s*=/,
|
|
406
406
|
'a wholesale innerHTML= on #mapRows destroys the static self row');
|
|
407
|
-
assert.match(SCRIPT, /querySelectorAll\('
|
|
407
|
+
assert.match(SCRIPT, /querySelectorAll\('li\[data-generated\]'\)/,
|
|
408
408
|
'generated rows must be removed selectively, like .worldOrb[data-generated]');
|
|
409
|
-
assert.match(SCRIPT, /<
|
|
409
|
+
assert.match(SCRIPT, /<li class="tblRow" data-generated[ >]/, 'generated rows must be markable');
|
|
410
410
|
});
|
|
411
411
|
|
|
412
412
|
test('"only mine" no longer hides the platform from a signed-in builder', () => {
|
|
@@ -573,11 +573,11 @@ test('exactly one hall-URL derivation: the DOM for the hub, the feed for a proje
|
|
|
573
573
|
assert.equal((SCRIPT.match(/isMember\(rel\) \? hallUrlOf\(p\) : ''/g) || []).length, 2, 'member-gated on the card AND on the row, one rule');
|
|
574
574
|
assert.match(SCRIPT, /'<a class="cardHall" id="cardHall" href="' \+ esc\(hall\)/, 'the card\'s hall door is escaped');
|
|
575
575
|
assert.match(SCRIPT, /'<a class="rowHall" href="' \+ esc\(hall\) \+ '" target="_blank" rel="noopener"/, 'the row\'s hall glyph is escaped and opens a new tab');
|
|
576
|
-
assert.match(SCRIPT, /: ''\) \+ hallGlyphHtml\(p, rel\) \+ '<\/
|
|
576
|
+
assert.match(SCRIPT, /: ''\) \+ hallGlyphHtml\(p, rel\) \+ '<\/div>'/, 'the glyph is the row\'s SECOND door, inside the one door slot — never a third');
|
|
577
577
|
});
|
|
578
578
|
|
|
579
579
|
test('the platform row carries a stamped, hidden hall glyph the session unhides — same tab, no target', () => {
|
|
580
|
-
const selfRow = (HUB.match(/<
|
|
580
|
+
const selfRow = (HUB.match(/<li class="tblRow" id="selfRow">([\s\S]*?)\r?\n\s*<\/li>/) || [, ''])[1];
|
|
581
581
|
assert.match(selfRow, /<a class="rowHall" id="selfHall" href="\{\{buildersOrigin\}\}"[^>]*hidden>/, 'server-stamped and hidden until the session says member');
|
|
582
582
|
assert.match(SCRIPT, /if \(sh\) sh\.hidden = !badge \|\| !hallUrlOf\(SELF_PROJECT\);/, 'member-gated and address-gated, and null-safe: the stamp drops the anchor whole when a pack names no hall');
|
|
583
583
|
assert.match(SCRIPT, /if \(p\.isSelf\) \{\r?\n\s*var a = \$\('selfHall'\);/);
|
|
@@ -614,7 +614,7 @@ test('the poster is in the world: 560 wide, one hairline, no shadow, the disc su
|
|
|
614
614
|
assert.match(COSMOS, /\.projCard \.cardMeta dd\{[^}]*text-align:right[^}]*border-top:1px solid var\(--rule-soft\)/, 'the value right and on the same hairline');
|
|
615
615
|
assert.match(COSMOS, /\.projCard \.cardRel\{[^}]*font-family:var\(--label-face\)/, 'the relationship is a mono label, not an accent pill');
|
|
616
616
|
assert.doesNotMatch(COSMOS, /\.projCard \.cardMeta \.tags/, 'the card no longer renders the module list');
|
|
617
|
-
assert.match(COSMOS, /\.rowHall\{[^}]*width:
|
|
617
|
+
assert.match(COSMOS, /\.rowHall\{[^}]*width:44px;height:40px/, 'the row\'s second door is a door, not a glyph: the same 40px the "view" pill stands at');
|
|
618
618
|
});
|
|
619
619
|
|
|
620
620
|
test('the row keeps its own Open link working underneath the card', () => {
|
|
@@ -631,14 +631,16 @@ test('the row keeps its own Open link working underneath the card', () => {
|
|
|
631
631
|
task 1002772 when a project began DECLARING it at creation — a nullable
|
|
632
632
|
declared field, so an em dash now means "not declared", never a guess. ── */
|
|
633
633
|
|
|
634
|
-
test('every
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
assert.
|
|
639
|
-
assert.match(
|
|
640
|
-
assert.match(
|
|
641
|
-
assert.match(
|
|
634
|
+
test('every fact the row states has a source — Type is declared, never guessed', () => {
|
|
635
|
+
// the header row left with the table (task 1003356): a fact is a line now, so
|
|
636
|
+
// what once had to be a column with a source is a line with a source
|
|
637
|
+
const facts = (SCRIPT.match(/function factLines\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
638
|
+
assert.ok(facts, 'factLines() must exist');
|
|
639
|
+
assert.match(facts, /typeHtml\(p\.type\)/, 'the Type line must read the declared field through the one formatter');
|
|
640
|
+
assert.match(facts, /p\.builders/, 'the Builders line must read real data');
|
|
641
|
+
assert.match(facts, /shippedFigure\(p\)/, 'shipped-in-30-days is the feed\'s own figure, capped mark and all');
|
|
642
|
+
assert.match(facts, /listedDate\(p\.created_at\)/, 'the Listed date is the catalog\'s');
|
|
643
|
+
assert.match(SCRIPT, /tagsHtml\(p\.tags\)/, 'the Runs line must read real data');
|
|
642
644
|
// the wizard files the declaration with the create — the source end of the pipe
|
|
643
645
|
assert.match(SCRIPT, /type: state\.projType/, 'the create request carries the declared type');
|
|
644
646
|
});
|
|
@@ -712,13 +714,20 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
|
|
|
712
714
|
assert.equal(count, 8, 'gate + six numbered steps + done — bump DRAFT_KEY when this changes');
|
|
713
715
|
});
|
|
714
716
|
|
|
715
|
-
test('
|
|
716
|
-
// a stray <td>
|
|
717
|
-
|
|
718
|
-
const selfRow = (HUB.match(/<
|
|
719
|
-
const generated = (SCRIPT.match(/return '<
|
|
720
|
-
assert.
|
|
721
|
-
|
|
717
|
+
test('every row lays the same five parts — the ledger has no ragged row', () => {
|
|
718
|
+
// a stray <td> used to shift every cell after it; the ledger's version of that
|
|
719
|
+
// bug is a row that skips one grid area, which silently collapses its track
|
|
720
|
+
const selfRow = (HUB.match(/<li class="tblRow" id="selfRow">([\s\S]*?)\r?\n\s*<\/li>/) || [, ''])[1];
|
|
721
|
+
const generated = (SCRIPT.match(/return '<li class="tblRow" data-generated([\s\S]*?)'<\/li>';/) || [, ''])[1];
|
|
722
|
+
assert.ok(selfRow && generated, 'both row templates must keep their extractable shape');
|
|
723
|
+
// the generated row builds its planet into a local first, so the parts test
|
|
724
|
+
// reads that expression too — otherwise dropping the planet would go unseen
|
|
725
|
+
const planet = (SCRIPT.match(/var planet = '<span class="worldOrb"[\s\S]*?;\r?\n/) || [''])[0];
|
|
726
|
+
for (const part of ['worldOrb', 'tWho', 'facts', 'ptag', 'runs', 'rowDoors']) {
|
|
727
|
+
assert.match(selfRow, new RegExp('class="[^"]*\\b' + part + '\\b'), `the self row is missing its ${part}`);
|
|
728
|
+
assert.match(generated + planet, new RegExp('class="[^"]*\\b' + part + '\\b'), `a generated row is missing its ${part}`);
|
|
729
|
+
}
|
|
730
|
+
assert.match(generated, /' \+ planet \+/, 'the generated row plants the planet it built');
|
|
722
731
|
});
|
|
723
732
|
|
|
724
733
|
test('no sort control is offered for a column that cannot sort', () => {
|
|
@@ -735,22 +744,25 @@ test('no sort control is offered for a column that cannot sort', () => {
|
|
|
735
744
|
test('the table is in the world: a masthead, then ledger rows — no panel, no shadow, the accent only on the row\'s door (task 1003332)', () => {
|
|
736
745
|
// the owner's pick from three rendered treatments: the view title in the display
|
|
737
746
|
// voice and one chrome object; the title is two words ("every project", the owner cut ", as rows")
|
|
738
|
-
assert.match(HUB, /<div class="tblMast">\s*<h2 class="tblTitle">every project<\/h2>\s*<span class="mastObj" aria-hidden="true"><\/span>\s*<\/div
|
|
747
|
+
assert.match(HUB, /<div class="tblMast">\s*<h2 class="tblTitle">every project<\/h2>\s*<span class="mastObj" aria-hidden="true"><\/span>\s*<\/div>/, 'one masthead ahead of the rows');
|
|
739
748
|
assert.doesNotMatch(HUB, /tblMast--t[123]|data-tbl|mastCallouts|mastField/, 'the render round\'s switch and the unpicked mastheads left with the pick');
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
assert.
|
|
743
|
-
assert.match(COSMOS, /\.tbl
|
|
744
|
-
assert.match(
|
|
745
|
-
assert.match(
|
|
749
|
+
// the table itself left with task 1003356: a list of ledger rows, no header
|
|
750
|
+
// row, no cell borders, no <table> wearing a role it no longer has
|
|
751
|
+
assert.doesNotMatch(HUB, /<table class="tbl">|<thead>|<th class="col/, 'the table markup, its header row and its cells are gone');
|
|
752
|
+
assert.match(COSMOS, /\.tbl\{list-style:none;margin:0;padding:0;\}/, 'the rows sit on the page ground — no card, no table');
|
|
753
|
+
assert.doesNotMatch((COSMOS.match(/\.tbl\{[^}]*\}/) || [''])[0], /border-radius|box-shadow/, 'no radius, no shadow on the list');
|
|
754
|
+
assert.match(COSMOS, /\.tbl \.tblRow:last-child\{border-bottom:1px solid var\(--rule-soft\);\}/, 'ledger rows: hairlines, and the last one closes');
|
|
755
|
+
assert.match(COSMOS, /\.tbl \.tblRow:hover\{border-top-color:var\(--ink-faint\);\}/, 'hover is the hairline taking ink, not an accent tint');
|
|
756
|
+
assert.match(COSMOS, /\.rowGo\{[^}]*border:1px solid var\(--rule\);border-radius:var\(--radius-pill\);/, 'the row\'s door is the mine row\'s hairline pill');
|
|
757
|
+
assert.match(HUB, /<a class="rowGo" href="\/">view<\/a>/, 'the self row\'s door');
|
|
746
758
|
assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(safeHttpUrl\(p\.origin\)\)/, 'the generated rows\' door');
|
|
747
759
|
assert.match(COSMOS, /\.mastObj\{display:block;--dia:clamp\(96px,10vw,132px\);[^}]*background-image:var\(--plate-set-cut\);/, 'the object is the set\'s small bead by token, in both modes');
|
|
748
760
|
assert.doesNotMatch(COSMOS, /mix-blend-mode:var\(--mast/, 'the cutout needs no blend — the view wrap is a stacking context');
|
|
749
761
|
});
|
|
750
762
|
|
|
751
|
-
test('every live
|
|
752
|
-
for (const live of ['.tbl .
|
|
753
|
-
assert.ok(COSMOS.includes(live), `${live} styles a
|
|
763
|
+
test('every live part of the row keeps its styling — .typeTag included since task 1002772', () => {
|
|
764
|
+
for (const live of ['.tbl .facts', '.tbl .runs', '.tbl .tags', '.tbl .ptag', '.typeTag']) {
|
|
765
|
+
assert.ok(COSMOS.includes(live), `${live} styles a part of the row that renders real data`);
|
|
754
766
|
}
|
|
755
767
|
// the type tag is its own vocabulary: not the relationship .pill, not the
|
|
756
768
|
// liveness .chip — two different facts must not wear the same chrome
|
|
@@ -766,8 +778,11 @@ test('every live column keeps its styling — .colType included since task 10027
|
|
|
766
778
|
test('unknown and zero are rendered as different things', () => {
|
|
767
779
|
// 0 builders is a real answer — "nobody has joined yet". null is not an
|
|
768
780
|
// answer at all. Collapsing them would restate the lie 1002764 removed.
|
|
769
|
-
|
|
770
|
-
|
|
781
|
+
// since task 1003356 the fact is a line, so the two answers are two sentences
|
|
782
|
+
assert.match(SCRIPT, /p\.builders == null \? 'builders not known'/,
|
|
783
|
+
'only null says "not known"');
|
|
784
|
+
assert.match(SCRIPT, /esc\(String\(p\.builders\)\) \+ \(p\.builders === 1 \? ' builder' : ' builders'\)/,
|
|
785
|
+
'0 must render as "0 builders" — a real answer, not an absence');
|
|
771
786
|
});
|
|
772
787
|
|
|
773
788
|
test('a project that cannot be reached degrades to a dash, not to a wrong number', () => {
|
|
@@ -796,7 +811,7 @@ test('each card fact is wired to its data, not just present in the markup', () =
|
|
|
796
811
|
assert.match(SCRIPT, /cardFact\('cardRuns', runsOf\(p\)\)/, 'runs is the address on the poster');
|
|
797
812
|
const runs = (SCRIPT.match(/function runsOf\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
798
813
|
assert.match(runs, /hostOf\(p\.origin\)/, 'the address is the catalog origin\'s host');
|
|
799
|
-
assert.match(SCRIPT, /'<
|
|
814
|
+
assert.match(SCRIPT, /'<div class="runs"><span class="rLbl">runs<\/span>' \+ tagsHtml\(p\.tags\)/, 'the table row keeps the module list, under its own label');
|
|
800
815
|
assert.match(SCRIPT, /cardFact\('cardOwner', rel === 'owner' \? '' : ownerHtml\(p\), true\)/,
|
|
801
816
|
'owner comes from the enrichment and yields to the rel chip saying "yours"');
|
|
802
817
|
const oh = (SCRIPT.match(/function ownerHtml\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
@@ -907,21 +922,21 @@ test('the card metadata block lives inside the card, not a hideable sub-view', (
|
|
|
907
922
|
ignores clicks inside an <a> so real links keep their jobs — but this link
|
|
908
923
|
had no job, so the click was swallowed AND went nowhere. ── */
|
|
909
924
|
|
|
910
|
-
test('the self row
|
|
911
|
-
const selfRow = (HUB.match(/<
|
|
912
|
-
assert.match(selfRow, /<a[^>]*href="\/"[^>]*>
|
|
913
|
-
'the platform project IS this site — its
|
|
925
|
+
test('the self row\'s door is a real link home, not an unwired hook', () => {
|
|
926
|
+
const selfRow = (HUB.match(/<li class="tblRow" id="selfRow">([\s\S]*?)\r?\n\s*<\/li>/) || [, ''])[1];
|
|
927
|
+
assert.match(selfRow, /<a[^>]*href="\/"[^>]*>view/,
|
|
928
|
+
'the platform project IS this site — its door navigates to /');
|
|
914
929
|
assert.doesNotMatch(selfRow, /target=/,
|
|
915
930
|
'a new tab for the page you are already on is a bug, not a courtesy');
|
|
916
931
|
assert.doesNotMatch(HUB, /data-self-open/, 'the never-wired hook must not survive');
|
|
917
932
|
});
|
|
918
933
|
|
|
919
|
-
test('no anchor in the
|
|
934
|
+
test('no anchor in the every-project list ships a placeholder href', () => {
|
|
920
935
|
// href="#" is the shape of this bug: a dead link wearing button clothes
|
|
921
|
-
const
|
|
922
|
-
assert.ok(
|
|
923
|
-
assert.doesNotMatch(
|
|
924
|
-
const generated = (SCRIPT.match(/return '<
|
|
936
|
+
const rows = (HUB.match(/<ul class="tbl" id="mapRows">[\s\S]*?<\/ul>/) || [''])[0];
|
|
937
|
+
assert.ok(rows, 'the list must be findable');
|
|
938
|
+
assert.doesNotMatch(rows, /href="#"/, 'no anchor in the list may ship href="#"');
|
|
939
|
+
const generated = (SCRIPT.match(/return '<li class="tblRow" data-generated([\s\S]*?)'<\/li>';/) || [, ''])[1];
|
|
925
940
|
assert.doesNotMatch(generated, /href="#"/, 'generated rows must not ship placeholders either');
|
|
926
941
|
});
|
|
927
942
|
|
|
@@ -958,11 +973,11 @@ test('a project both featured and owned renders once, marked owned', () => {
|
|
|
958
973
|
test('owned truth reaches the TABLE too — same list, same chip, same manage job', () => {
|
|
959
974
|
// the count, the sky and the table read ONE merged browse list; an owned row
|
|
960
975
|
// carries chipFor()'s truth in Status and routes its click to manage
|
|
961
|
-
const generated = (SCRIPT.match(/return '<
|
|
976
|
+
const generated = (SCRIPT.match(/return '<li class="tblRow" data-generated([\s\S]*?)'<\/li>';/) || [, ''])[1];
|
|
962
977
|
assert.ok(generated, 'the generated-row template must keep its extractable shape');
|
|
963
978
|
assert.match(generated, /data-manage/, 'an owned row carries the manage route key');
|
|
964
|
-
assert.match(generated, /p\._inst \? chipFor\(p\._inst\)/, 'an owned row\'s
|
|
965
|
-
assert.match(generated, /chip calm/, 'an unprobed catalog row
|
|
979
|
+
assert.match(generated, /p\._inst \? chipFor\(p\._inst\)/, 'an owned row\'s chip is the overlay truth');
|
|
980
|
+
assert.match(generated, /chip calm[\s\S]{0,80}not checked/, 'an unprobed catalog row says so in words beside the name');
|
|
966
981
|
assert.match(SCRIPT, /tr\.hasAttribute\('data-manage'\)\) return goToManage/,
|
|
967
982
|
'an owned row click goes to manage, exactly like its orb');
|
|
968
983
|
});
|
|
@@ -1783,25 +1798,35 @@ test('the feed carries the owner\'s choice through a seam: platform-identity res
|
|
|
1783
1798
|
|
|
1784
1799
|
/* ── the table's columns on FIXED tracks (task 1003348; the owner, 2026-08-29: "the
|
|
1785
1800
|
columns in that projects table view dont look aligned") ── */
|
|
1786
|
-
test('one
|
|
1787
|
-
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
// halves of the grammar can never drift apart: fixed planet + doors, ratio tracks with no content minimum
|
|
1801
|
+
test('one row grammar: the every-project row and the Your-projects row lay the SAME five tracks', () => {
|
|
1802
|
+
// task 1003348 aligned the two views on one set of fixed tracks while one was
|
|
1803
|
+
// still a table; task 1003356 gave the table the mine row's whole grammar, so
|
|
1804
|
+
// the tracks must now match line for line or the two ledgers drift apart
|
|
1791
1805
|
assert.match(COSMOS, /#view-mine \.proj\{display:grid;grid-template-columns:72px minmax\(0,1fr\);/, 'the mine row: the planet track is a width');
|
|
1792
1806
|
assert.match(COSMOS, /#view-mine \.projBody\{display:grid;grid-template-columns:minmax\(0,1\.1fr\) minmax\(0,1\.3fr\) minmax\(0,1fr\) 232px;/, 'the mine row: name, facts and rail by ratio with no content minimum, the doors a width');
|
|
1793
|
-
assert.match(COSMOS,
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
assert.doesNotMatch(COSMOS, /\.tbl \.col\w+\{width:1%/, 'no column hugs its widest cell any more');
|
|
1798
|
-
assert.match(COSMOS, /\.tbl th\.colName\{padding-left:22px;\}/, 'the Project header sits over the name, past the dot and its gap');
|
|
1799
|
-
assert.match(COSMOS, /\.tbl th\.right\{text-align:right;\}/, 'a right-aligned column\'s header is right-aligned too');
|
|
1800
|
-
assert.match(HUB, /<th class="right colBuilders" data-sort="builders">/, 'the builders header names its column so its width can be set');
|
|
1801
|
-
assert.match(COSMOS, /@media \(max-width:780px\)\{\.tbl \.colTags,\.tbl \.colType\{display:none;\}\.tbl\{min-width:600px;\}\}/, 'a phone scrolls the table inside its wrap');
|
|
1807
|
+
assert.match(COSMOS, /\.tbl \.tblRow\{display:grid;\r?\n\s*grid-template-columns:72px minmax\(0,1\.1fr\) minmax\(0,1\.3fr\) minmax\(0,1fr\) 232px;/, 'the table row: the same five tracks, composed into one grid');
|
|
1808
|
+
assert.match(COSMOS, /grid-template-areas:"orb who facts runs doors" "orb who tag runs doors";/, 'runs where the mine row draws its rail, the tagline where it puts its address');
|
|
1809
|
+
assert.match(COSMOS, /the SAME five tracks in its \S\.tbl \.tblRow\S rule/, 'the mine block names the table as its twin');
|
|
1810
|
+
assert.doesNotMatch(COSMOS, /table-layout:fixed|\.tbl th|\.tbl td/, 'no table rule outlived the table');
|
|
1802
1811
|
assert.match(COSMOS, /#sub-table-body\{overflow-x:auto;\}/, 'the wrap scrolls, never the page');
|
|
1803
1812
|
});
|
|
1804
1813
|
|
|
1814
|
+
test('the sort left the column head for a control the keyboard and a screen reader can both read (task 1003356)', () => {
|
|
1815
|
+
// aria-sort is only valid on a columnheader; with no header row the order has
|
|
1816
|
+
// to be stated by the control itself and by a sentence in a live region
|
|
1817
|
+
assert.doesNotMatch(HUB, /aria-sort=/, 'aria-sort has no host once the header row is gone');
|
|
1818
|
+
assert.match(HUB, /<div class="tblSort" role="group" aria-label="Sort the projects">/, 'the two orders are one group');
|
|
1819
|
+
for (const k of ['name', 'builders']) {
|
|
1820
|
+
assert.match(HUB, new RegExp(`<button type="button" class="sortPill" data-sort="${k}"`), `${k} is a real button`);
|
|
1821
|
+
}
|
|
1822
|
+
assert.match(HUB, /<p class="finePrint" id="mapSorted" role="status" aria-live="polite">/, 'the live order is announced, not only drawn');
|
|
1823
|
+
assert.match(SCRIPT, /b\.setAttribute\('aria-pressed', on \? 'true' : 'false'\)/, 'pressed IS the live order');
|
|
1824
|
+
assert.match(SCRIPT, /mapSort\.dir = \(mapSort\.key === k\) \? -mapSort\.dir : 1;/, 'pressing the pressed one reverses, exactly as the header did');
|
|
1825
|
+
assert.match(SCRIPT, /\$\('mapSorted'\)\.textContent = 'Sorted by '/, 'the sentence is written from the same state the pills paint from');
|
|
1826
|
+
assert.match(COSMOS, /\.tblSort button\[aria-pressed="true"\]\{background:var\(--ink\);border-color:var\(--ink\);color:var\(--bg\);\}/, 'the pressed pill inverts to ink, the mine filters\' rule');
|
|
1827
|
+
assert.match(COSMOS, /\.tblSort button:focus-visible\{outline:2px solid var\(--accent\);outline-offset:2px;\}/, 'keyboard focus on the sort is visible');
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1805
1830
|
/* ── the create wizard in the world (task 1003346; direction record §15, the pick: v1 the
|
|
1806
1831
|
rail above, the bongo pair kept). Two sheets carry it: cosmos.css draws the FRAME by scope
|
|
1807
1832
|
(#view-new), projects.html's own wizard block draws the wizard's structures. ── */
|