@bongos/core 1.19.402 → 1.19.404

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.
@@ -539,7 +539,7 @@ summary{min-height:24px;padding:3px 0;}
539
539
  <td class="right num" id="selfBuilders">—</td>
540
540
  <td class="colTags" id="selfTags">—</td>
541
541
  <td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>
542
- <td class="colGo"><a class="rowGo" href="/">Open →</a></td>
542
+ <td class="colGo"><a class="rowGo" href="/">Open →</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></td>
543
543
  </tr>
544
544
  <!--apex-only:end-->
545
545
  </tbody>
@@ -563,20 +563,30 @@ summary{min-height:24px;padding:3px 0;}
563
563
  <div class="cardScrim" id="cardScrim" hidden>
564
564
  <div class="projCard" id="projCard" role="dialog" aria-modal="true" aria-labelledby="cardName">
565
565
  <button type="button" class="cardX" id="cardClose" aria-label="Close">✕</button>
566
+ <!-- The poster (task 1003301, the v2 pick): every region a DIRECT child of
567
+ the card, on purpose. The C6 card test slices the card non-greedily up
568
+ to the first adjacent pair of closing wrapper tags, and a nested
569
+ wrapper would silently shorten what that test guards. So the banner is
570
+ a bare img, the figures and the faces are paragraphs, the facts a
571
+ definition list. Each region degrades by hiding: no art, no banner;
572
+ no faces, no row; one figure null, the other stands alone; both null,
573
+ no band; a fact with no answer hides label and value together,
574
+ because "Owner: dash" on the most authoritative-looking element of
575
+ the page reads as broken, not honest (task 1002778). Everything null
576
+ is the card that shipped before this one: disc, name, tagline,
577
+ relationship, the address, one door. -->
578
+ <img class="cardArt" id="cardArt" alt="" hidden>
579
+ <p class="cardFigs" id="cardFigs" hidden>
580
+ <span class="cardCall a" id="cardBuildersFig" hidden><svg class="lead" viewBox="0 0 68 26" preserveAspectRatio="none" aria-hidden="true"><line x1="0" y1="25" x2="67" y2="1" stroke="currentColor" stroke-width="1" vector-effect="non-scaling-stroke"/></svg><span class="fig" id="cardBuilders"></span><span class="cap">builders</span></span>
581
+ <span class="cardCall b" id="cardShippedFig" hidden><svg class="lead" viewBox="0 0 68 26" preserveAspectRatio="none" aria-hidden="true"><line x1="0" y1="25" x2="67" y2="1" stroke="currentColor" stroke-width="1" vector-effect="non-scaling-stroke"/></svg><span class="fig" id="cardShipped"></span><span class="cap">shipped in 30 days</span></span>
582
+ </p>
566
583
  <span class="cardDot" id="cardDot"></span>
567
584
  <h3 id="cardName"></h3>
568
585
  <p class="cardTag" id="cardTag"></p>
569
586
  <p class="cardRel" id="cardRel" hidden></p>
570
- <!-- The facts strip. A definition list, never nested block wrappers, on
571
- purpose: the C6 card test slices the card non-greedily up to the
572
- first adjacent pair of closing wrapper tags, and a nested wrapper
573
- would silently shorten what that test guards. A fact with no answer
574
- hides, label and value together, because "Owner: dash" on the most
575
- authoritative-looking element of the page reads as broken, not
576
- honest (task 1002778). -->
587
+ <p class="cardFaces" id="cardFaces" hidden></p>
577
588
  <dl class="cardMeta" id="cardMeta" hidden>
578
589
  <dt id="cardTypeL" hidden>Type</dt><dd id="cardType" hidden></dd>
579
- <dt id="cardBuildersL" hidden>Builders</dt><dd id="cardBuilders" hidden></dd>
580
590
  <dt id="cardRunsL" hidden>Runs</dt><dd id="cardRuns" hidden></dd>
581
591
  <dt id="cardOwnerL" hidden>Owner</dt><dd id="cardOwner" hidden></dd>
582
592
  <dt id="cardListedL" hidden>Listed</dt><dd id="cardListed" hidden></dd>
@@ -1492,7 +1502,7 @@ summary{min-height:24px;padding:3px 0;}
1492
1502
  : '<span class="chip calm"><span class="cdot"></span>—</span>') + '</td>' +
1493
1503
  '<td class="colGo">' + (safeHttpUrl(p.origin)
1494
1504
  ? '<a class="rowGo" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
1495
- : '') + '</td>' +
1505
+ : '') + hallGlyphHtml(p, rel) + '</td>' +
1496
1506
  '</tr>';
1497
1507
  }).join(''));
1498
1508
  }
@@ -1577,6 +1587,9 @@ summary{min-height:24px;padding:3px 0;}
1577
1587
  if (row) {
1578
1588
  row.hidden = !selfShown;
1579
1589
  $('selfPill').innerHTML = badge ? ' <span class="pill">' + badge + '</span>' : '';
1590
+ /* the hall glyph: member-gated (the badge says so) and address-gated */
1591
+ var sh = $('selfHall');
1592
+ if (sh) sh.hidden = !badge || !hallUrlOf(SELF_PROJECT);
1580
1593
  }
1581
1594
 
1582
1595
  var shown = list.length + (selfShown ? 1 : 0);
@@ -1639,32 +1652,111 @@ summary{min-height:24px;padding:3px 0;}
1639
1652
  return !!value;
1640
1653
  }
1641
1654
 
1655
+ /* Runs, on the poster, is the project's ADDRESS: where it answers. The table
1656
+ keeps the module list in its Runs column (the v2 pick, direction §7). */
1657
+ function runsOf(p) {
1658
+ return p.isSelf ? hostOf(location.host) : hostOf(p.origin);
1659
+ }
1660
+ /* shipped in the last 30 days, "N+" when the count sat at the feed's row cap;
1661
+ null is "we could not ask" and hides, 0 is a real answer and renders */
1662
+ function shippedFigure(p) {
1663
+ if (p.shipped_30d == null) return '';
1664
+ return String(p.shipped_30d) + (p.shipped_30d_capped ? '+' : '');
1665
+ }
1666
+ /* A figure either has an answer or takes no space: the callout hides whole,
1667
+ leader and caption with it, so the other figure stands alone. */
1668
+ function cardFig(id, value) {
1669
+ $(id).textContent = value || '';
1670
+ $(id + 'Fig').hidden = !value;
1671
+ return !!value;
1672
+ }
1673
+ /* The faces: up to five avatars from the project's own leaderboard, then +N
1674
+ for the builders beyond them. Third-party URLs, scheme-checked before they
1675
+ become a src. No faces, no row. */
1676
+ var FACES_MAX = 5;
1677
+ function cardFaces(p) {
1678
+ var faces = Array.isArray(p.faces) ? p.faces.map(safeHttpUrl).filter(Boolean).slice(0, FACES_MAX) : [];
1679
+ var more = p.builders != null && p.builders > faces.length ? p.builders - faces.length : 0;
1680
+ $('cardFaces').innerHTML = faces.map(function (u) {
1681
+ return '<img src="' + esc(u) + '" alt="" loading="lazy">';
1682
+ }).join('') + (more ? '<span class="facesMore">+' + more + '</span>' : '');
1683
+ $('cardFaces').hidden = !faces.length;
1684
+ }
1685
+ /* The banner. has-art switches the whole layout (the disc over the banner's
1686
+ edge, the figures as callouts on it); without art the card is the card
1687
+ that shipped before this one. A broken image demotes to no-art, so the
1688
+ most authoritative element of the page never shows a broken glyph. */
1689
+ function cardArt(url) {
1690
+ var img = $('cardArt');
1691
+ if (url) img.src = url; else img.removeAttribute('src');
1692
+ img.hidden = !url;
1693
+ $('projCard').classList.toggle('has-art', !!url);
1694
+ }
1695
+ $('cardArt').addEventListener('error', function () { cardArt(''); });
1696
+ /* EXACTLY ONE hall-URL derivation (task 1003257, owner decision a). The hub's
1697
+ own hall comes from the DOM: the server-stamped anchor in its own table row
1698
+ (dropped whole by the stamp when a pack names no hall; never assembled in
1699
+ script). A project's comes from the hall_origin its feed row carries. Both
1700
+ go through safeHttpUrl, and both count only where the hall is a different
1701
+ host from the site: a door that opens onto the room you are in is a dead door. */
1702
+ function hallUrlOf(p) {
1703
+ var hall, site;
1704
+ if (p.isSelf) {
1705
+ var a = $('selfHall');
1706
+ hall = a ? safeHttpUrl(a.getAttribute('href')) : '';
1707
+ site = location.host;
1708
+ } else {
1709
+ hall = safeHttpUrl(p.hall_origin);
1710
+ site = p.origin;
1711
+ }
1712
+ return hall && hostOf(hall) !== hostOf(site) ? hall : '';
1713
+ }
1714
+ var HALL_GLYPH = '<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>';
1715
+ function isMember(rel) { return rel === 'owner' || rel === 'builder'; }
1716
+ /* the row's hall glyph: a member's door, only where the hall is elsewhere */
1717
+ function hallGlyphHtml(p, rel) {
1718
+ var hall = isMember(rel) ? hallUrlOf(p) : '';
1719
+ return hall
1720
+ ? '<a class="rowHall" href="' + esc(hall) + '" target="_blank" rel="noopener" aria-label="Builders\' hall" title="Builders\' hall">' + HALL_GLYPH + '</a>'
1721
+ : '';
1722
+ }
1723
+
1642
1724
  function openCard(p, rel, returnTo) {
1643
1725
  var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
1726
+ /* the art: catalog text an Archon typed, scheme-checked server-side and
1727
+ again here before it becomes a src */
1728
+ cardArt(p.isSelf ? '' : safeHttpUrl(p.art_url));
1644
1729
  $('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
1645
1730
  $('cardName').textContent = p.name || '';
1646
1731
  $('cardTag').textContent = p.tagline || '';
1647
1732
  var label = rel === 'owner' ? 'yours' : rel === 'builder' ? 'you build here' : '';
1648
1733
  $('cardRel').textContent = label;
1649
1734
  $('cardRel').hidden = !label;
1650
- /* builders: 0 is a real answer ("nobody has joined yet"), null is "we could
1651
- not ask" and hides. runs: [] is a real answer too — the project declares
1652
- nothing beyond the core set, which the owner voiced as "core". */
1735
+ cardFaces(p);
1736
+ /* the two figures: builders (0 is a real answer, "nobody has joined yet";
1737
+ null is "we could not ask" and hides) and shipped in the last 30 days */
1738
+ var figs = cardFig('cardBuilders', p.builders == null ? '' : String(p.builders));
1739
+ figs = cardFig('cardShipped', shippedFigure(p)) || figs;
1740
+ $('cardFigs').hidden = !figs;
1653
1741
  var any = cardFact('cardType', typeLabel(p.type));
1654
- any = cardFact('cardBuilders', p.builders == null ? '' : String(p.builders)) || any;
1655
- any = cardFact('cardRuns', Array.isArray(p.tags) ? tagsHtml(p.tags) : '', true) || any;
1742
+ any = cardFact('cardRuns', runsOf(p)) || any;
1656
1743
  /* when the rel chip already says "yours", naming the viewer as Owner is
1657
1744
  noise — the fact answers strangers, not the person it describes */
1658
1745
  any = cardFact('cardOwner', rel === 'owner' ? '' : ownerHtml(p), true) || any;
1659
1746
  any = cardFact('cardListed', p.created_at ? listedDate(p.created_at) : '') || any;
1660
1747
  $('cardMeta').hidden = !any;
1661
1748
  /* the platform's door is same-tab: / is this site, and a new tab for the
1662
- page you are already on is a bug, not a courtesy */
1663
- $('cardActions').innerHTML = href
1664
- ? '<a class="btn small" href="' + esc(href) + '"' +
1749
+ page you are already on is a bug, not a courtesy. The hall door beside it
1750
+ is member-gated AND address-gated: a member's entry, only where the hall
1751
+ is somewhere other than the site the first door already opens. */
1752
+ var hall = isMember(rel) ? hallUrlOf(p) : '';
1753
+ $('cardActions').innerHTML = (href
1754
+ ? '<a class="cardOpen" id="cardOpen" href="' + esc(href) + '"' +
1665
1755
  (p.isSelf ? '' : ' target="_blank" rel="noopener"') + '>' +
1666
1756
  (p.isSelf ? 'Home →' : 'Open site →') + '</a>'
1667
- : '';
1757
+ : '') + (hall
1758
+ ? '<a class="cardHall" id="cardHall" href="' + esc(hall) + '" target="_blank" rel="noopener">' + HALL_GLYPH + 'Builders\' hall</a>'
1759
+ : '');
1668
1760
  closeMenus();
1669
1761
  cardReturn = returnTo || null;
1670
1762
  $('cardScrim').hidden = false;
@@ -1864,6 +1956,7 @@ summary{min-height:24px;padding:3px 0;}
1864
1956
  builder count and its owner — the archon row — by the same rule the
1865
1957
  server-side enrichment applies to every other project */
1866
1958
  SELF_PROJECT.builders = real.length;
1959
+ SELF_PROJECT.faces = real.map(function (b) { return safeHttpUrl(b.avatar_url); }).filter(Boolean).slice(0, FACES_MAX);
1867
1960
  var arch = real.filter(function (b) { return b.rank === 'archon'; })[0];
1868
1961
  if (arch && arch.github_login) {
1869
1962
  SELF_PROJECT.owner = String(arch.github_login);
@@ -1872,6 +1965,21 @@ summary{min-height:24px;padding:3px 0;}
1872
1965
  })
1873
1966
  .catch(function () { /* unreachable → the dash already in the markup stands */ });
1874
1967
  }
1968
+ /* the platform's shipped-in-30-days figure, by the rule the server applies
1969
+ to every other project (project-stats.js): rows inside the window from
1970
+ shipped_at, "N+" when the feed's row cap (200 with ?days honoured) was full */
1971
+ function loadSelfShipped() {
1972
+ return fetch(API + '/public/recent-shipped?days=30', { credentials: 'same-origin' })
1973
+ .then(function (res) { return res.ok ? res.json() : null; })
1974
+ .then(function (d) {
1975
+ if (!d || !Array.isArray(d.tasks)) return;
1976
+ var since = Date.now() - 30 * 24 * 60 * 60 * 1000;
1977
+ var n = d.tasks.filter(function (t) { return Date.parse(t && t.shipped_at) >= since; }).length;
1978
+ SELF_PROJECT.shipped_30d = n;
1979
+ SELF_PROJECT.shipped_30d_capped = d.tasks.length >= 200 && n === d.tasks.length;
1980
+ })
1981
+ .catch(function () { /* unknown stays unknown: the figure hides */ });
1982
+ }
1875
1983
 
1876
1984
  function loadCatalog() {
1877
1985
  fetch(API + '/projects/featured', { credentials: 'same-origin' })
@@ -5610,7 +5718,8 @@ summary{min-height:24px;padding:3px 0;}
5610
5718
  signed-out stranger lands on and it must not depend on identity */
5611
5719
  .then(loadCatalog, loadCatalog)
5612
5720
  /* the platform's own count is public too, so it loads either way */
5613
- .then(loadSelfStats, loadSelfStats);
5721
+ .then(loadSelfStats, loadSelfStats)
5722
+ .then(loadSelfShipped, loadSelfShipped);
5614
5723
  })();
5615
5724
  </script>
5616
5725
  </body>
@@ -51,6 +51,34 @@
51
51
  { "name": "Escape closes the card when no band is open", "continue": true,
52
52
  "steps": [["press", "Escape"], ["wait", 150]],
53
53
  "expect": { "dialog": false } },
54
+ { "name": "the poster (task 1003301): art, five faces and +N, two figures on the art, the address, a member's hall door", "continue": true,
55
+ "steps": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"], ["wait", 900]],
56
+ "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]],
57
+ "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"],
58
+ "count": [["#cardFaces img", 5], ["#projCard > div", 1]],
59
+ "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "amazonprimea.com"], ["#cardFaces .facesMore", "+6"], ["#cardHall", "Builders' hall"]],
60
+ "href": [["#cardHall", "https://builders.amazonprimea.com/"], ["#cardOpen", "https://amazonprimea.com/"]] } },
61
+ { "name": "the ladder: a project that answered nothing is the card that shipped before (disc, name, tagline, address, listed, one door)", "continue": true,
62
+ "steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]"], ["wait", 500]],
63
+ "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]],
64
+ "hidden": ["#cardArt", "#cardFaces", "#cardFigs", "#cardBuildersFig", "#cardShippedFig", "#cardHall", "#cardRel", "#cardTypeL", "#cardOwnerL"],
65
+ "visible": ["#cardDot", "#cardName", "#cardTag", "#cardRuns", "#cardListed", "#cardOpen"],
66
+ "text": [["#cardRuns", "demo.cloudbongos.com"]] } },
67
+ { "name": "the hall door is address-gated: a member of a project whose hall is its own site gets no door; the capped figure reads N+", "continue": true,
68
+ "steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 500]],
69
+ "expect": { "dialog": true, "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardFigs", "#cardBuildersFig", "#cardShippedFig", "#cardOpen"],
70
+ "text": [["#cardRel", "you build here"], ["#cardShipped", "30+"], ["#cardBuilders", "1"]] } },
71
+ { "name": "the platform's own card: figures and faces by the same rule, its hall door from the stamped DOM link", "continue": true,
72
+ "steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#selfOrb"], ["wait", 500]],
73
+ "expect": { "dialog": true, "hidden": ["#cardArt"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardFaces", "#cardOpen", "#cardHall"],
74
+ "text": [["#cardRel", "yours"]], "href": [["#cardOpen", "/"]] } },
75
+ { "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,
76
+ "steps": [["press", "Escape"], ["wait", 150], ["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"table\"]"], ["wait", 300]],
77
+ "expect": { "dialog": false, "visible": ["#selfHall"], "count": [["#mapRows tr[data-generated] .rowHall", 1], ["#mapRows tr[data-generated] td", 18]],
78
+ "href": [["#mapRows tr[data-generated] .rowHall", "https://builders.amazonprimea.com/"]] } },
79
+ { "name": "back to the sky for the rest of the sequence", "continue": true,
80
+ "steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"sky\"]"], ["wait", 300]],
81
+ "expect": { "hidden": ["#sub-table-body"] } },
54
82
  { "name": "a solo Shift tap focuses the search on the map", "continue": true,
55
83
  "steps": [["clickAt", 700, 600], ["press", "Shift"], ["wait", 100]],
56
84
  "expect": { "activeId": "mapQ" } },
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The hub's states for modules/ui-design/kit/render.js (task 1003320): name → url + actions + expectations. `auth` picks the signed-in stub. Rendered dark + light at 1440/390/320 and audited against the platform floors.",
2
+ "_": "The hub's states for modules/ui-design/kit/render.js (task 1003320): name → url + actions + expectations. `auth` picks the signed-in stub. Rendered dark + light at 1440/390/320 and audited against the platform floors. The card states (task 1003301) walk the poster's ladder: the full poster, the artless two-up band, the all-null floor, the platform's own card.",
3
3
  "page": "/projects",
4
4
  "surface": "public-landing",
5
5
  "modeQuery": "mode",
@@ -14,12 +14,15 @@
14
14
  "in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
15
15
  "in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
16
16
  "in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
17
- "in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj"], "closed": "#menuProjects" } },
17
+ "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 tr[data-generated] .rowHall", 1]] } },
18
18
  "in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap"] } },
19
19
  "in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
20
20
  "out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
21
21
  "out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
22
- "card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 600]], "expect": { "dialog": true } }
22
+ "card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"], ["wait", 900]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]], "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"], "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "amazonprimea.com"], ["#cardFaces .facesMore", "+6"]], "count": [["#cardFaces img", 5]], "href": [["#cardHall", "https://builders.amazonprimea.com/"]] } },
23
+ "card-noart": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]], "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardOpen"], "text": [["#cardShipped", "30+"], ["#cardRel", "you build here"]] } },
24
+ "card-floor": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hidden": ["#cardArt", "#cardFaces", "#cardFigs", "#cardHall", "#cardRel", "#cardTypeL", "#cardOwnerL"], "visible": ["#cardOpen", "#cardRuns", "#cardListed"] } },
25
+ "card-self": { "auth": true, "actions": [["dispatchClick", "#selfOrb"], ["wait", 600]], "expect": { "dialog": true, "hidden": ["#cardArt"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardFaces", "#cardOpen", "#cardHall"], "text": [["#cardRel", "yours"]] } }
23
26
  },
24
27
  "reducedMotion": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 100]] }
25
28
  }
package/src/module-api.js CHANGED
@@ -1244,8 +1244,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1244
1244
  // landed since 1.19.400 with no explicit bump. run 33205015717. (task 1002620)
1245
1245
  // 1.19.402 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1246
1246
  // landed since 1.19.401 with no explicit bump. run 33206226800. (task 1002620)
1247
+ // 1.19.403 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1248
+ // landed since 1.19.402 with no explicit bump. run 33208825538. (task 1002620)
1249
+ // 1.19.404 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1250
+ // landed since 1.19.403 with no explicit bump. run 33211074633. (task 1002620)
1247
1251
  // ---------------------------------------------------------------------------
1248
- const CORE_VERSION = '1.19.402'; // CI auto-patch carrier (ADR 0161); see changelog above
1252
+ const CORE_VERSION = '1.19.404'; // CI auto-patch carrier (ADR 0161); see changelog above
1249
1253
 
1250
1254
  // A namespaced logger so a module's log lines are attributable + consistent.
1251
1255
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -176,6 +176,8 @@ const PAIRS = [
176
176
  // shipped a fired-down ember where #ffffff measured 4.61:1 — that is history now,
177
177
  // and any prose below still naming those values is history too.)
178
178
  ['.btn label on its fill', 'var(--accent-on)', 'var(--accent)', 4.5],
179
+ // the poster card's door lifts to --accent-lift on hover (task 1003301)
180
+ ['.cardOpen:hover label on its lift', 'var(--accent-on)', 'var(--accent-lift)', 4.5],
179
181
  ['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
180
182
  ['.btn.danger label', 'var(--err)', CARD, 4.5],
181
183
  // Code chips, pills, the now-step number, the hover inks: --accent-deep is
@@ -205,8 +207,9 @@ const PAIRS = [
205
207
  ['.chip.live label', 'var(--ok)', 'var(--ok-soft)', 4.5],
206
208
  ['.chip.maybe label', 'var(--warn)', 'var(--warn-soft)', 4.5],
207
209
  ['.chip.down/.bad label', 'var(--err)', 'var(--err-soft)', 4.5],
208
- // .chip.busy + the .tbl .pill / .cardRel / builder-badge family: accent-deep
209
- // ink on the accent-soft tint, both modes
210
+ // .chip.busy + the builder-badge family: accent-deep ink on the accent-soft
211
+ // tint, both modes (.tbl .pill and .cardRel are mono labels in ink-faint since
212
+ // tasks 1003332 / 1003301 — a fact is not pressable, so the accent left them)
210
213
  ['.chip.busy label', 'var(--accent-deep)', 'var(--accent-soft)', 4.5],
211
214
  // lifecycle strip STATE INDICATORS (task 1002751): non-text at the WCAG
212
215
  // 1.4.11 bar of 3.0. --sea-ink derives brighter in dark exactly the way
@@ -19,10 +19,13 @@
19
19
  // is an accessibility constraint nobody sees fail; it holds through a
20
20
  // cross-FILE dependency (world.css carries ONE universal switch rather than
21
21
  // every page repeating it), which is the right shape but an invisible one;
22
- // * the live-data contract — every figure and row on the page is baked real
23
- // and then refreshed from five public feeds, with third-party strings in
24
- // the payload; the no-JS state, the XSS discipline and the definitions of
25
- // the figures are all invisible to a passing render.
22
+ // * the live-data contract — the ledger line, meter and rows are baked real
23
+ // and then refreshed from three public feeds, with third-party strings in
24
+ // the payload; the no-JS state, the XSS discipline and the definition of
25
+ // the ledger line are all invisible to a passing render;
26
+ // * the owner's review of the hero (task 1003333) — no figure or leader line
27
+ // on the landing, the doors below the blob's centre, the big plates on
28
+ // their own slow drifts: a regression here looks like a design choice.
26
29
  //
27
30
  // Deliberately NOT asserted here: that GET / returns the page (tests/platform_boot.mjs
28
31
  // owns that), and nothing about geometry or colour — the world's token layer is
@@ -61,9 +64,9 @@ test('the apex-only sentinels are balanced and each one wraps content', () => {
61
64
  const starts = HTML.match(/<!--apex-only:start-->/g) || [];
62
65
  const ends = HTML.match(/<!--apex-only:end-->/g) || [];
63
66
  assert.equal(starts.length, ends.length, 'every apex-only:start needs its end');
64
- // four sections plus every baked apex FIGURE (task 1003298: the page bakes real
65
- // numbers as its no-JS state, and a child must show an empty figure, not the
66
- // platform's count, until its own feed answers)
67
+ // four sections plus the baked ledger line, meter and rows (task 1003298: the
68
+ // page bakes real numbers as its no-JS state, and a child must show an empty
69
+ // ledger, not the platform's, until its own feed answers)
67
70
  assert.ok(starts.length >= 4, 'the direction marks at least four apex-only regions');
68
71
  // no start after an unclosed start — a nested pair would make the non-greedy
69
72
  // strip close on the wrong sentinel and leak the platform's claims.
@@ -95,8 +98,8 @@ test('a child instance is served none of the platform-only claims', () => {
95
98
  test("a child instance is served none of the platform's NUMBERS either", () => {
96
99
  // The v3 composition puts real ledger values in the markup as the no-JS state.
97
100
  // A claim is obvious when it leaks; a number is not — a fresh child instance
98
- // would silently advertise the hub's 2,107 shipped tasks and its V1 progress
99
- // line until its own feeds answered. Both are apex-only for exactly that reason.
101
+ // would silently advertise the hub's V1 progress line and its six shipped
102
+ // rows until its own feed answered. Both are apex-only for exactly that reason.
100
103
  // the MARKUP only: the page script names every one of these hooks in a
101
104
  // querySelector, so scanning the whole file would match its own selectors.
102
105
  const child = stripApexOnly(HTML, false)
@@ -107,7 +110,6 @@ test("a child instance is served none of the platform's NUMBERS either", () => {
107
110
  'a thousands-separated figure survived the strip',
108
111
  );
109
112
  for (const [what, re, expected] of [
110
- ['the three hero figures', /data-fig="[a-z]+"[^>]*>([\s\S]*?)<\/span>/g, 3],
111
113
  ['the ledger line', /data-ledger="line"[^>]*>([\s\S]*?)<\/p>/g, 1],
112
114
  ['the ledger meter', /data-ledger="meter"[^>]*>([\s\S]*?)<\/div>/g, 1],
113
115
  ]) {
@@ -134,7 +136,6 @@ test('a child instance keeps the whole portable spine', () => {
134
136
  'id="who"', // the identity slot
135
137
  'The six most recent, straight from the ledger.', // the ledger's own sub-line
136
138
  // the live hooks: a child's OWN feeds fill these, so the shells must survive
137
- 'data-fig="shipped"', 'data-fig="value"', 'data-fig="builders"',
138
139
  'data-ledger="line"', 'data-ledger="meter"', 'data-rows',
139
140
  ]) {
140
141
  assert.ok(child.includes(portable), `a child instance lost "${portable}"`);
@@ -314,8 +315,9 @@ test('nothing on the page marks an animation !important and escapes that switch'
314
315
 
315
316
  test('the signature animations are plain declarations, so the switch reaches them', () => {
316
317
  for (const name of [
317
- 'breathe', 'sheen', 'orbit', // the breath
318
- 'arrive-form', 'arrive-orb', 'arrive-bead', 'arrive-mark', // the authored moment
318
+ 'breathe', 'sheen', // the breath
319
+ 'drift-form', 'drift-orb', // the drift (task 1003333)
320
+ 'arrive-form', 'arrive-orb', 'arrive-mark', // the authored moment
319
321
  ]) {
320
322
  assert.match(PAGE_CSS, new RegExp(`@keyframes\\s+${name}\\s*\\{`), `@keyframes ${name} is gone`);
321
323
  assert.match(PAGE_CSS, new RegExp(`animation:[^;}]*\\b${name}\\b`), `nothing runs ${name}`);
@@ -334,22 +336,83 @@ test('the breath stays slow and the arrival stays brief', () => {
334
336
  };
335
337
  assert.ok(period('breathe', 's') >= 30, 'the blob breathes at 32s; under 30s it reads as a pulse');
336
338
  assert.ok(period('sheen', 's') >= 30, 'the hue drift is 40s; under 30s the iridescence shimmers');
337
- assert.ok(period('orbit', 's') >= 60, 'the bead walks its circle in 60s');
338
- for (const arrive of ['arrive-form', 'arrive-orb', 'arrive-bead', 'arrive-mark']) {
339
+ // the lava lamp (task 1003333): two long loops, and two DIFFERENT ones, so the
340
+ // plates never phase-lock into one animation
341
+ const form = period('drift-form', 's');
342
+ const orb = period('drift-orb', 's');
343
+ assert.ok(form >= 30, 'the sweeping form drifts over 74s; under 30s it paddles');
344
+ assert.ok(orb >= 30, 'the blob drifts over 58s; under 30s it paddles');
345
+ assert.notEqual(form, orb, 'equal periods would make the two plates read as one animation');
346
+ for (const arrive of ['arrive-form', 'arrive-orb', 'arrive-mark']) {
339
347
  assert.ok(period(arrive, 'ms') <= 800, `${arrive} holds the first paint too long`);
340
348
  }
341
349
  });
342
350
 
351
+ // ── the owner's review of the hero (task 1003333) ──────────────────────────
352
+ // Three corrections that a passing render cannot distinguish from taste: the
353
+ // figures with their leader lines were "random lines" on the landing (they live
354
+ // on the /projects poster card now), the doors sat too high and central, and
355
+ // the small bead's orbit was not the motion asked for — the big plates drift.
356
+
357
+ test('no figure, leader line or bead renders on the landing hero at any width (task 1003333)', () => {
358
+ const body = HTML.split('<body>')[1].split('<script>')[0];
359
+ assert.doesNotMatch(body, /class="callout|class="leader|data-fig=|callouts-row/, 'a hero callout came back');
360
+ assert.doesNotMatch(body, /class="plate bead"/, 'the orbiting bead came back');
361
+ // the narrow breakpoint has no three-up row to fall back to either
362
+ assert.doesNotMatch(PAGE_CSS, /\.callout|\.callouts-row|\.leader\b|\.fig\b|\.cap\b|\.bead\b/, 'callout or bead CSS survived');
363
+ assert.doesNotMatch(PAGE_CSS, /--c[123]-[xy]|--leader-len|--orbit-r|--bead-(w|cx|cy)/, 'a callout or bead token survived');
364
+ assert.doesNotMatch(PAGE_CSS, /@keyframes\s+(orbit|arrive-bead)\b/, 'the orbit keyframes survived');
365
+ // and the script no longer reads the two feeds that fed only those figures
366
+ assert.doesNotMatch(CODE, /setFig|data-fig|\/public\/cost-summary|\/public\/leaderboard/);
367
+ });
368
+
369
+ test('the doors hang below the blob\'s centre in both modes (task 1003333)', () => {
370
+ // the door pair is a stage token; the blob's centre is --orb-y + half its height. In dark the
371
+ // blob spans stage 24% to ~100% (centre ~62%); in light 32% to ~99% (centre ~66%). The doors
372
+ // used to sit at 53% / 60% — above centre and "too high and central" to the owner.
373
+ const cy = (block) => {
374
+ const m = block.match(/--doors-cy:calc\(var\(--vh\) \* (\d+)\)/);
375
+ assert.ok(m, 'the doors\' vertical token is no longer a stage percentage — update this test');
376
+ return Number(m[1]);
377
+ };
378
+ const light = PAGE_CSS.match(/\nbody\{([\s\S]*?)\n\}/);
379
+ // the wide blocks carry the stage percentage; the under-900 twins zero the token
380
+ const dark = [...PAGE_CSS.matchAll(/:root(?:\[data-mode="dark"\]|:not\(\[data-mode="light"\]\)) body\{([\s\S]*?)\n\s*\}/g)]
381
+ .filter((m) => /--doors-cy:calc\(/.test(m[1]));
382
+ assert.ok(light, 'the light token block is findable');
383
+ assert.equal(dark.length, 2, 'the dark token block and its prefers-color-scheme twin');
384
+ assert.ok(cy(light[1]) >= 70, 'light: the doors sit in the blob\'s lower half');
385
+ for (const d of dark) assert.ok(cy(d[1]) >= 68, 'dark: the doors sit in the blob\'s lower half');
386
+ assert.equal(cy(dark[0][1]), cy(dark[1][1]), 'the dark twins agree');
387
+ });
388
+
389
+ test('the big plates drift on transform alone, and the still object never does (task 1003333)', () => {
390
+ // transform is the one property the arrival (translate), the breath (scale) and the sheen
391
+ // (filter) do not use, so the drift composes with them instead of overriding one
392
+ for (const name of ['drift-form', 'drift-orb']) {
393
+ const kf = PAGE_CSS.match(new RegExp(`@keyframes\\s+${name}\\s*\\{([\\s\\S]*?)\\}\\s*\\}`));
394
+ assert.ok(kf, `@keyframes ${name} is gone`);
395
+ assert.doesNotMatch(kf[1], /translate:|scale:|filter:|opacity:|left:|top:/, `${name} must ride the transform property only`);
396
+ assert.match(kf[1], /0%\{transform:translate\(0,0\) rotate\(0deg\)\}/, `${name}'s rest frame is the graded composition`);
397
+ assert.match(kf[1], /100%\{transform:translate\(0,0\) rotate\(0deg\)/, `${name} must close its loop where it opened`);
398
+ }
399
+ const form = PAGE_CSS.match(/\n\.form\{[^}]*\}/);
400
+ const orb = PAGE_CSS.match(/\n\.orb\{[^}]*\}/);
401
+ const mark = PAGE_CSS.match(/\n\.mark\{[^}]*\}/);
402
+ assert.match(form[0], /animation:[^}]*\bdrift-form\b/, 'the sweeping form drifts');
403
+ assert.match(orb[0], /animation:[^}]*\bdrift-orb\b/, 'the big blob drifts');
404
+ assert.doesNotMatch(mark[0], /drift/, 'the bongo pair is the still object the moving ground is read against');
405
+ assert.doesNotMatch(PAGE_CSS, /\.doors\{[^}]*animation/, 'the doors never move with a plate');
406
+ });
407
+
343
408
  // ── the live-data contract ─────────────────────────────────────────────────
344
409
  // Five public feeds fill the page after it has already rendered a real, baked
345
410
  // state. Everything here is invisible to a passing render: a feed renamed, an
346
411
  // innerHTML crept in with third-party titles in it, a figure quietly redefined.
347
412
 
348
- test('the script reads the five public feeds the head comment documents', () => {
413
+ test('the script reads the three public feeds the head comment documents', () => {
349
414
  for (const feed of [
350
- '/public/progress', // the shipped figure + the ledger line
351
- '/public/cost-summary', // the value figure
352
- '/public/leaderboard', // the builders figure
415
+ '/public/progress', // the ledger line + meter
353
416
  '/public/recent-shipped?limit=6', // the ledger rows
354
417
  '/projects/featured', // living proof
355
418
  ]) {
@@ -381,17 +444,12 @@ test('a live task title is normalised to the front door\'s own punctuation', ()
381
444
  assert.ok(has('/,\\s*,/g'), 'the comma a dash leaves beside one collapses');
382
445
  });
383
446
 
384
- test('each figure is defined the way the head comment says it is', () => {
447
+ test('the ledger line is defined the way the head comment says it is', () => {
385
448
  // A figure whose definition drifts still renders a plausible number, so the
386
- // page would lie without ever looking wrong. These are the three definitions.
387
- assert.match(CODE, /total \+= Number\(r\.shipped_count\)/, 'shipped = the SUM over every progress row');
388
- assert.match(CODE, /setFig\('shipped'/);
389
- assert.match(CODE, /data\.total_value_credits/, 'value = total_value_credits from the cost summary');
390
- assert.match(CODE, /setFig\('value'/);
391
- assert.match(CODE, /Number\(r\.total_credits\) > 0/, 'builders = leaderboard entries HOLDING credits');
392
- assert.match(CODE, /setFig\('builders'/);
393
- // the ledger line + meter come off the first `building` row
449
+ // page would lie without ever looking wrong. The ledger line + meter come off
450
+ // the first `building` row (else the first row).
394
451
  assert.match(CODE, /r\.status === 'building'/);
452
+ assert.match(CODE, /if \(!live\) live = rows\[0\];/);
395
453
  assert.match(CODE, /' shipped on '/);
396
454
  // numbers are grouped, so the live value matches the baked one's shape
397
455
  assert.match(CODE, /toLocaleString\('en-US'\)/);