@bongos/core 1.19.393 → 1.19.394
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 +16 -16
- package/docs/copy-inventory.md +248 -235
- package/docs/copy-registry.json +542 -323
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +235 -103
- package/public-landing/index.html +228 -75
- package/public-landing/projects.html +256 -140
- package/src/module-api.js +3 -1
- package/tests/landing_page.mjs +72 -2
- package/tests/projects_hub.mjs +52 -73
- package/tests/projects_hub_pre_uat.mjs +10 -8
|
@@ -324,11 +324,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
324
324
|
used (860) — the two-up grids and the bongo cell fold together */
|
|
325
325
|
@media (max-width:840px){.row2,.choices{grid-template-columns:1fr;}}
|
|
326
326
|
|
|
327
|
-
/* cosmos's global reduced-motion kill switch zeroes every animation, which
|
|
328
|
-
leaves the mark's blinking caret parked at FULL opacity — a solid bar beside
|
|
329
|
-
the wordmark forever. Page-local on purpose: a second reduce block in
|
|
330
|
-
cosmos.css would shadow the first one tests/landing_page.mjs extracts. */
|
|
331
|
-
@media (prefers-reduced-motion:reduce){.shell .caret{display:none;}}
|
|
332
327
|
</style>
|
|
333
328
|
</head>
|
|
334
329
|
<body>
|
|
@@ -350,54 +345,120 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
350
345
|
<span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
|
|
351
346
|
</div>
|
|
352
347
|
|
|
353
|
-
<!-- ══ THE
|
|
348
|
+
<!-- ══ THE BAR ═══════════════════════════════════════════════════════════════
|
|
354
349
|
A header of the PAGE, not of the orb field, so it never drifts, scales or
|
|
355
|
-
pans with the sky — and it renders in EVERY view including the table.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
here
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
350
|
+
pans with the sky — and it renders in EVERY view including the table.
|
|
351
|
+
v2 "the hairline" (task 1003300; the owner's pick, docs/design/
|
|
352
|
+
projects-hub-direction-v2.md §7): no pill on this bar. Its ground IS the
|
|
353
|
+
page ground, closed by one hairline, and the only filled shape it can hold
|
|
354
|
+
is the sign-in door. The mark is static (the self-typing wordmark lives on
|
|
355
|
+
the landing only, §7) and it is the only way home. The map's search lives
|
|
356
|
+
here so it stays PUBLIC — a signed-out stranger must be able to search the
|
|
357
|
+
map — and setView hides it off the map view. Sky, Table, Your projects and
|
|
358
|
+
New project are the Projects band; the identity chip is the profile band's
|
|
359
|
+
trigger. No Home, no hall pill, no nav row, no toolbar. ── -->
|
|
360
|
+
<!-- keyboard skip: the chrome above every view is the bar and the two bands
|
|
361
|
+
that unfold under it. Hidden until focused (.vh), it becomes a real pill
|
|
362
|
+
on first Tab and jumps to the #hubMain landmark. -->
|
|
366
363
|
<a class="skip vh" href="#hubMain">Skip to content</a>
|
|
367
|
-
<header class="shell">
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
364
|
+
<header class="shell bar">
|
|
365
|
+
<div class="barLeft">
|
|
366
|
+
<a class="barMark" id="barMark" href="/">
|
|
367
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
368
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
369
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
370
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
371
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
372
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
373
|
+
</svg>
|
|
374
|
+
<span data-brand-product-inline>Cloud Bongos</span>
|
|
375
|
+
</a>
|
|
376
|
+
<!-- under 480 the search folds into this magnifier, which drops one
|
|
377
|
+
hairline row of the page ground under the bar; above 480 it is never
|
|
378
|
+
shown and the field sits in the bar. -->
|
|
379
|
+
<button class="qToggle" type="button" id="qToggle" aria-expanded="false" aria-controls="qWrap" aria-label="Search projects">
|
|
380
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
381
|
+
</button>
|
|
382
|
+
<!-- the map's search: a bare field with one bottom hairline. Every id is
|
|
383
|
+
kept (#mapQ, #mapCount, #onlyMine) so the filter path is untouched. -->
|
|
384
|
+
<div class="qWrap" id="qWrap">
|
|
385
|
+
<div class="qField">
|
|
386
|
+
<svg class="qIcon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
387
|
+
<input class="qInput" type="text" id="mapQ" placeholder="Find a project" aria-label="Search projects">
|
|
388
|
+
</div>
|
|
389
|
+
<span class="qCount" id="mapCount"></span>
|
|
390
|
+
<!-- the only-mine filter rides with the search (§7): it filters the same
|
|
391
|
+
map, folds with the field under 480 and hides with it off the map.
|
|
392
|
+
Member-gated by paintShellIdentity, as before. -->
|
|
393
|
+
<label class="switch" title="Show only projects you own or build on">
|
|
394
|
+
<input type="checkbox" id="onlyMine"><span class="track"></span> Only mine
|
|
395
|
+
</label>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="barRight">
|
|
399
|
+
<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
400
|
+
<!-- the glyph shows the mode you would GET: the moon in light, the sun in
|
|
401
|
+
dark, swapped by cosmos.css on data-mode; the script labels it -->
|
|
402
|
+
<button type="button" class="modeBtn" id="modeToggle" aria-label="Switch light or dark mode">
|
|
403
|
+
<svg class="moon" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
|
|
404
|
+
stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
405
|
+
<path d="M16.2 12.4A6.8 6.8 0 0 1 7.6 3.8a6.8 6.8 0 1 0 8.6 8.6z"/>
|
|
406
|
+
</svg>
|
|
407
|
+
<svg class="sun" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
|
|
408
|
+
stroke-width="1.4" stroke-linecap="round">
|
|
409
|
+
<circle cx="10" cy="10" r="3.9"/>
|
|
410
|
+
<path d="M10 1.6v2.1M10 16.3v2.1M1.6 10h2.1M16.3 10h2.1M4.1 4.1l1.5 1.5M14.4 14.4l1.5 1.5M15.9 4.1l-1.5 1.5M5.6 14.4l-1.5 1.5"/>
|
|
411
|
+
</svg>
|
|
412
|
+
</button>
|
|
413
|
+
<!-- the identity chip IS the profile trigger; renderWho fills it. Signed
|
|
414
|
+
out it does not exist and the sign-in door takes its slot. -->
|
|
415
|
+
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden></button>
|
|
416
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/projects">Sign in</a>
|
|
398
417
|
</div>
|
|
399
418
|
</header>
|
|
400
419
|
|
|
420
|
+
<!-- ══ THE BANDS ══ The menus are not floating cards. Each is a full-width band
|
|
421
|
+
that unfolds under the bar's edge and PUSHES the page down, so a
|
|
422
|
+
destination is a place the page opens into rather than a list that hovers
|
|
423
|
+
over it. One component, two instances (see the bands block in the script):
|
|
424
|
+
Escape closes and returns focus to the trigger, the arrows walk the items,
|
|
425
|
+
a click outside closes, opening one closes the other. Plain links in a
|
|
426
|
+
disclosure (aria-expanded + aria-controls, no menu role), so what is
|
|
427
|
+
announced is what ships. ── -->
|
|
428
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
429
|
+
<div class="bandIn">
|
|
430
|
+
<ul class="bandRow">
|
|
431
|
+
<!-- Sky and Table are two VIEWS of the map (§7): both deep-link
|
|
432
|
+
?view=map and carry the sub-view; setSub keeps the current one lit -->
|
|
433
|
+
<li class="bandCell"><a class="bandT" href="?view=map" data-nav="map" data-sub="sky" aria-current="page">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
434
|
+
<li class="bandCell"><a class="bandT" href="?view=map&sub=table" data-nav="map" data-sub="table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
435
|
+
<li class="bandCell"><a class="bandT" href="?view=mine" data-nav="mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
436
|
+
<li class="bandCell"><a class="bandT" href="?view=new" data-nav="new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
437
|
+
</ul>
|
|
438
|
+
<!-- under a hairline (§5's About cell): what this is, and the docs. About is the landing's
|
|
439
|
+
own explanation until goal 1000063's About page takes the anchor over. -->
|
|
440
|
+
<ul class="bandRow bandMore">
|
|
441
|
+
<li class="bandCell"><a class="bandT" href="/#about">About</a></li>
|
|
442
|
+
<li class="bandCell"><a class="bandT" href="/docs">Docs</a></li>
|
|
443
|
+
</ul>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
447
|
+
<div class="bandIn bandProfile">
|
|
448
|
+
<p class="bandWho" id="bandWho"></p>
|
|
449
|
+
<ul class="bandRow">
|
|
450
|
+
<li class="bandCell"><a class="bandT" href="?view=mine" data-nav-to="mine">My projects</a></li>
|
|
451
|
+
<!-- the deferred Settings slot: a place it would go, rendering nothing at
|
|
452
|
+
all — not even the hairline that declares a list item. Never a
|
|
453
|
+
disabled item: the apex has no settings page, and on a child with no
|
|
454
|
+
hall the buildersOrigin token falls back to the page you are on. -->
|
|
455
|
+
<li class="bandSlot" aria-hidden="true"></li>
|
|
456
|
+
<!-- the honest hover target; the delegated click upgrades it to the POST -->
|
|
457
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
458
|
+
</ul>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
401
462
|
<noscript>
|
|
402
463
|
<div class="viewWrap wrap"><div class="fieldNote">The project hub needs JavaScript — the map, your
|
|
403
464
|
projects, and starting a new one all live here. You can read what this is on the <a href="/">home page</a>.</div></div>
|
|
@@ -410,23 +471,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
410
471
|
<main id="hubMain">
|
|
411
472
|
<section id="view-map">
|
|
412
473
|
<h1 class="vh">The project map</h1>
|
|
413
|
-
<div class="toolbar">
|
|
414
|
-
<div class="seg" role="group" aria-label="Map display">
|
|
415
|
-
<button type="button" id="sub-sky" aria-pressed="true">Sky</button>
|
|
416
|
-
<button type="button" id="sub-table" aria-pressed="false">Table</button>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
<label class="switch" title="Show only projects you own or build on">
|
|
420
|
-
<input type="checkbox" id="onlyMine"><span class="track"></span> Only mine
|
|
421
|
-
</label>
|
|
422
|
-
|
|
423
|
-
<div class="search">
|
|
424
|
-
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
425
|
-
<input type="text" id="mapQ" placeholder="Search projects by name or description…" aria-label="Search projects">
|
|
426
|
-
<span class="count" id="mapCount"></span>
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
|
|
430
474
|
<div class="fieldWrap" id="sub-sky-body">
|
|
431
475
|
<div class="field" id="orbField">
|
|
432
476
|
<div class="ground"></div>
|
|
@@ -1026,7 +1070,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1026
1070
|
|
|
1027
1071
|
</main>
|
|
1028
1072
|
|
|
1029
|
-
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud Bongos</span>. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a
|
|
1073
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud Bongos</span>. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><!--apex-only:start--><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a><!--apex-only:end--></nav></footer>
|
|
1030
1074
|
|
|
1031
1075
|
<script>
|
|
1032
1076
|
/* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
|
|
@@ -1057,13 +1101,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1057
1101
|
if (m === 'light' || m === 'dark') return m;
|
|
1058
1102
|
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
1059
1103
|
}
|
|
1060
|
-
/* the glyph shows the mode you would GET
|
|
1061
|
-
|
|
1104
|
+
/* the glyph shows the mode you would GET (the moon in light, the sun in
|
|
1105
|
+
dark — cosmos.css swaps them on data-mode), so the label must say the same.
|
|
1062
1106
|
No aria-pressed — this swaps a mode, it is not a two-state toggle button. */
|
|
1063
1107
|
function paintModeToggle() {
|
|
1064
1108
|
var dark = resolvedMode() === 'dark';
|
|
1065
1109
|
var el = $('modeToggle');
|
|
1066
|
-
el.textContent = dark ? '☀' : '☾';
|
|
1067
1110
|
el.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
|
|
1068
1111
|
}
|
|
1069
1112
|
$('modeToggle').addEventListener('click', function () {
|
|
@@ -1105,6 +1148,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1105
1148
|
|
|
1106
1149
|
var VIEWS = { map: 1, mine: 1, new: 1, manage: 1 };
|
|
1107
1150
|
var currentView = 'map';
|
|
1151
|
+
var currentSub = 'sky'; /* the map's sub-view on stage — sky or table (task 1003300) */
|
|
1108
1152
|
/* the manage view's target project (task 1003041) — set by goToManage() and
|
|
1109
1153
|
the ?view=manage&id=N deep link, read by loadManage() */
|
|
1110
1154
|
var manageId = null;
|
|
@@ -1129,12 +1173,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1129
1173
|
currentView = v;
|
|
1130
1174
|
Object.keys(VIEWS).forEach(function (k) { $('view-' + k).hidden = (k !== v); });
|
|
1131
1175
|
document.querySelectorAll('[data-nav]').forEach(function (a) {
|
|
1132
|
-
/*
|
|
1133
|
-
|
|
1134
|
-
|
|
1176
|
+
/* Sky and Table are both the map view: only the one whose sub-view is on
|
|
1177
|
+
stage is current (setSub keeps the pair in step) */
|
|
1178
|
+
if (a.dataset.nav === v && (!a.dataset.sub || a.dataset.sub === currentSub)) a.setAttribute('aria-current', 'page');
|
|
1179
|
+
/* the manage view is a page OF your projects — that item stays lit there */
|
|
1135
1180
|
else if (v === 'manage' && a.dataset.nav === 'mine') a.setAttribute('aria-current', 'page');
|
|
1136
1181
|
else a.removeAttribute('aria-current');
|
|
1137
1182
|
});
|
|
1183
|
+
/* the map's search (and the only-mine filter riding with it) is meaningless
|
|
1184
|
+
off the map: HIDE, never disable, and never clear the value — returning to
|
|
1185
|
+
the map restores the filter. It is PUBLIC, so this is not in
|
|
1186
|
+
paintShellIdentity. The folded row (under 480) closes with it. */
|
|
1187
|
+
$('qWrap').hidden = $('qToggle').hidden = (v !== 'map');
|
|
1188
|
+
if (v !== 'map') foldSearch(false);
|
|
1138
1189
|
/* the manage view's URL names its project — the deep-link contract grew
|
|
1139
1190
|
an id in task 1003041, so a manage tab is shareable and reload-safe */
|
|
1140
1191
|
var idPart = (v === 'manage' && manageId) ? '&id=' + encodeURIComponent(manageId) : '';
|
|
@@ -1173,8 +1224,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1173
1224
|
}
|
|
1174
1225
|
|
|
1175
1226
|
document.querySelectorAll('[data-nav]').forEach(function (a) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1227
|
+
a.addEventListener('click', function (ev) {
|
|
1228
|
+
if (!isPlainClick(ev)) return; /* ctrl/cmd/shift/middle mean "new tab" everywhere else */
|
|
1229
|
+
ev.preventDefault();
|
|
1230
|
+
setView(a.dataset.nav, true);
|
|
1231
|
+
if (a.dataset.sub) setSub(a.dataset.sub);
|
|
1232
|
+
});
|
|
1178
1233
|
});
|
|
1179
1234
|
/* delegated, not swept: a data-nav-to link injected into a card or the manage
|
|
1180
1235
|
page after boot (addressRow, renderDelete) would never be upgraded by a
|
|
@@ -1190,54 +1245,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1190
1245
|
manage id too, so a manage entry restores its project */
|
|
1191
1246
|
window.addEventListener('popstate', function () { closeCard(); setView(viewFromUrl()); });
|
|
1192
1247
|
|
|
1193
|
-
/*
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
(function typeMark() {
|
|
1199
|
-
var parts = productName.toLowerCase().split(/\s+/);
|
|
1200
|
-
var word = parts.pop();
|
|
1201
|
-
var prefix = parts.join(' ');
|
|
1202
|
-
$('markWord').textContent = word;
|
|
1203
|
-
$('markPrefix').textContent = prefix;
|
|
1204
|
-
/* the link's name is its two spans, so mid-type it announces "gem bongos".
|
|
1205
|
-
Label the LINK instead — before the early return, so a re-branded or
|
|
1206
|
-
reduced-motion visitor gets it too. #markLeft stays visible to AT:
|
|
1207
|
-
tests/projects_hub.mjs pins that markup literal. */
|
|
1208
|
-
$('markLeft').parentNode.setAttribute('aria-label', productName + ' — home');
|
|
1209
|
-
if (REDUCED || prefix !== 'cloud' || word !== 'bongos') return;
|
|
1210
|
-
var PREFIXES = ['cloud', 'claude', 'chatgpt', 'gemini'];
|
|
1211
|
-
|
|
1212
|
-
/* Type, HOLD, erase — the landing hero's cadence (index.html), not a
|
|
1213
|
-
symmetric ping-pong. The single reversing interval this replaced turned
|
|
1214
|
-
around on the very tick it completed a word, so `claude bongos` finished
|
|
1215
|
-
and was already being deleted: the reader never got to see the word the
|
|
1216
|
-
whole animation exists to show. A hold is the only part of the cycle that
|
|
1217
|
-
carries meaning; the typing is just how it arrives.
|
|
1218
|
-
|
|
1219
|
-
`cloud` rests longest because it is the real wordmark and the state the
|
|
1220
|
-
mark should sit in most — the others are the joke, and the joke lands
|
|
1221
|
-
once. Same two numbers the hero uses, so the two surfaces read as one
|
|
1222
|
-
system rather than two animations that happen to share a font. */
|
|
1223
|
-
var TYPE = 72, ERASE = 38, HOLD = 900, HOLD_HOME = 2100;
|
|
1224
|
-
var el = $('markPrefix'), box = $('markLeft'), meas = $('markMeasure'), wi = 0;
|
|
1225
|
-
(function cycle() {
|
|
1226
|
-
var w = PREFIXES[wi++ % PREFIXES.length], i = 0;
|
|
1227
|
-
/* pin the box to the coming word's rendered width BEFORE typing: bongos
|
|
1228
|
-
must not move while letters come and go (the owner's call, 2026-08-14).
|
|
1229
|
-
Between words the CSS width transition glides instead of jumping. */
|
|
1230
|
-
meas.textContent = w;
|
|
1231
|
-
box.style.width = (meas.offsetWidth + 4) + 'px';
|
|
1232
|
-
(function type() {
|
|
1233
|
-
if (i <= w.length) { el.textContent = w.slice(0, i++); setTimeout(type, TYPE); }
|
|
1234
|
-
else setTimeout(function erase() {
|
|
1235
|
-
if (el.textContent.length) { el.textContent = el.textContent.slice(0, -1); setTimeout(erase, ERASE); }
|
|
1236
|
-
else cycle();
|
|
1237
|
-
}, w === 'cloud' ? HOLD_HOME : HOLD);
|
|
1238
|
-
})();
|
|
1239
|
-
})();
|
|
1240
|
-
})();
|
|
1248
|
+
/* the static mark (task 1003300, direction record §7): the self-typing
|
|
1249
|
+
wordmark lives on the landing only. Here the glyph and the product name are
|
|
1250
|
+
plain type, and the mark is the only way home — so it is labelled from the
|
|
1251
|
+
product name a re-branded instance stamps, not from its visible text. */
|
|
1252
|
+
$('barMark').setAttribute('aria-label', productName + ' — home');
|
|
1241
1253
|
|
|
1242
1254
|
/* ══ THE MAP ═══════════════════════════════════════════════════════════════
|
|
1243
1255
|
The public catalog: GET /projects/featured is the PUBLIC projection
|
|
@@ -1643,6 +1655,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1643
1655
|
(p.isSelf ? '' : ' target="_blank" rel="noopener"') + '>' +
|
|
1644
1656
|
(p.isSelf ? 'Home →' : 'Open site →') + '</a>'
|
|
1645
1657
|
: '';
|
|
1658
|
+
closeMenus();
|
|
1646
1659
|
cardReturn = returnTo || null;
|
|
1647
1660
|
$('cardScrim').hidden = false;
|
|
1648
1661
|
pageInert(true);
|
|
@@ -1731,7 +1744,95 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1731
1744
|
if (ev.target === $('cardScrim')) closeCard(); /* backdrop only, never the card */
|
|
1732
1745
|
});
|
|
1733
1746
|
document.addEventListener('keydown', function (ev) {
|
|
1734
|
-
|
|
1747
|
+
/* a band claims Escape while it is open — its own listener, registered
|
|
1748
|
+
below this one, closes it; the card's Escape is the page default */
|
|
1749
|
+
if (ev.key === 'Escape' && !openBand()) closeCard();
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
/* ══ THE BANDS (task 1003300) — one disclosure component, two instances: the
|
|
1753
|
+
Projects band and the profile band. Not palette.js's single-control focus
|
|
1754
|
+
trap (a multi-item menu is keyboard-unusable under it); the card's physics
|
|
1755
|
+
instead. Escape closes and RETURNS FOCUS to the trigger, a click outside
|
|
1756
|
+
closes, Tab walks the items in natural order and leaving them closes, the
|
|
1757
|
+
arrows move between items, opening one closes the other, and an item
|
|
1758
|
+
activated closes its band. Plain links in a disclosure — aria-expanded +
|
|
1759
|
+
aria-controls, no menu role — so what is announced is what ships. The open
|
|
1760
|
+
move is CSS (bandIn), so the kill switch owns it. Every read of the profile
|
|
1761
|
+
trigger is by class, and signed out it is simply hidden: nothing here can
|
|
1762
|
+
throw with no session, which is the public map's default state. ══ */
|
|
1763
|
+
function openBand() { return document.querySelector('.menuBand:not([hidden])'); }
|
|
1764
|
+
function closeMenus() {
|
|
1765
|
+
var open = openBand();
|
|
1766
|
+
if (!open) return;
|
|
1767
|
+
var trig = document.querySelector('.trig[aria-expanded="true"]');
|
|
1768
|
+
var inside = open.contains(document.activeElement);
|
|
1769
|
+
open.hidden = true;
|
|
1770
|
+
if (trig) trig.setAttribute('aria-expanded', 'false');
|
|
1771
|
+
/* focus restoration is a property of the close, not of one caller */
|
|
1772
|
+
if (inside && trig) trig.focus();
|
|
1773
|
+
}
|
|
1774
|
+
function openMenu(trig, byKeyboard) {
|
|
1775
|
+
closeMenus();
|
|
1776
|
+
foldSearch(false);
|
|
1777
|
+
trig.setAttribute('aria-expanded', 'true');
|
|
1778
|
+
var band = $(trig.getAttribute('aria-controls'));
|
|
1779
|
+
band.hidden = false;
|
|
1780
|
+
/* Enter/Space on the trigger lands on the first item; a pointer stays put */
|
|
1781
|
+
if (byKeyboard) { var first = band.querySelector('.bandCell:not([hidden]) a'); if (first) first.focus(); }
|
|
1782
|
+
}
|
|
1783
|
+
document.querySelectorAll('.trig').forEach(function (t) {
|
|
1784
|
+
t.addEventListener('click', function (ev) {
|
|
1785
|
+
if (t.getAttribute('aria-expanded') === 'true') closeMenus();
|
|
1786
|
+
else openMenu(t, ev.detail === 0);
|
|
1787
|
+
});
|
|
1788
|
+
});
|
|
1789
|
+
document.addEventListener('click', function (ev) {
|
|
1790
|
+
if (!openBand()) return;
|
|
1791
|
+
if (ev.target.closest && (ev.target.closest('.menuBand') || ev.target.closest('.trig'))) return;
|
|
1792
|
+
closeMenus();
|
|
1793
|
+
});
|
|
1794
|
+
document.addEventListener('keydown', function (ev) {
|
|
1795
|
+
var open = openBand();
|
|
1796
|
+
if (!open) {
|
|
1797
|
+
/* Escape with the folded search open (under 480) folds it and hands focus back */
|
|
1798
|
+
if (ev.key === 'Escape' && $('qWrap').classList.contains('is-open')) { foldSearch(false); $('qToggle').focus(); }
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (ev.key === 'Escape') { closeMenus(); return; }
|
|
1802
|
+
if (ev.key !== 'ArrowDown' && ev.key !== 'ArrowUp') return;
|
|
1803
|
+
var items = Array.prototype.filter.call(open.querySelectorAll('a'), function (a) { return !a.closest('[hidden]'); });
|
|
1804
|
+
if (!items.length) return;
|
|
1805
|
+
var at = items.indexOf(document.activeElement);
|
|
1806
|
+
ev.preventDefault();
|
|
1807
|
+
items[(at + (ev.key === 'ArrowDown' ? 1 : items.length - 1) + items.length) % items.length].focus();
|
|
1808
|
+
});
|
|
1809
|
+
document.querySelectorAll('.menuBand').forEach(function (b) {
|
|
1810
|
+
/* an item is a real link or a view switch: either way its band closes */
|
|
1811
|
+
b.addEventListener('click', function (ev) {
|
|
1812
|
+
if (ev.target.closest && ev.target.closest('a')) closeMenus();
|
|
1813
|
+
});
|
|
1814
|
+
/* Tab out of the band closes it; a null relatedTarget is left alone (the
|
|
1815
|
+
window losing focus, a click on nothing) — the document click covers it */
|
|
1816
|
+
b.addEventListener('focusout', function (ev) {
|
|
1817
|
+
var to = ev.relatedTarget;
|
|
1818
|
+
if (!to || b.contains(to) || (to.closest && to.closest('.trig'))) return;
|
|
1819
|
+
closeMenus();
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
/* the folded search (under 480): the magnifier opens one hairline row under
|
|
1824
|
+
the bar and hands focus to the field; Escape folds it and returns focus; an
|
|
1825
|
+
opening menu folds it. Above 480 the class is inert — the field is in the bar. */
|
|
1826
|
+
function qFolded() { return getComputedStyle($('qToggle')).display !== 'none'; }
|
|
1827
|
+
function foldSearch(open) {
|
|
1828
|
+
$('qWrap').classList.toggle('is-open', !!open);
|
|
1829
|
+
$('qToggle').setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1830
|
+
}
|
|
1831
|
+
$('qToggle').addEventListener('click', function () {
|
|
1832
|
+
var open = $('qToggle').getAttribute('aria-expanded') !== 'true';
|
|
1833
|
+
closeMenus();
|
|
1834
|
+
foldSearch(open);
|
|
1835
|
+
if (open) $('mapQ').focus();
|
|
1735
1836
|
});
|
|
1736
1837
|
|
|
1737
1838
|
/* The platform's own row is not in the catalog (it is synthesized — task
|
|
@@ -2244,18 +2345,24 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2244
2345
|
document.addEventListener('keyup', function (e) {
|
|
2245
2346
|
if (e.key !== 'Shift' || !solo) return;
|
|
2246
2347
|
solo = false;
|
|
2247
|
-
/*
|
|
2248
|
-
|
|
2348
|
+
/* one source of truth — the search's own visibility (hidden off the
|
|
2349
|
+
map) — and never steal focus out of the open card or an open band */
|
|
2350
|
+
if ($('qWrap').hidden || !$('cardScrim').hidden || openBand()) return;
|
|
2249
2351
|
var t = document.activeElement;
|
|
2250
2352
|
if (t && /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) return;
|
|
2251
|
-
|
|
2252
|
-
|
|
2353
|
+
if (qFolded()) foldSearch(true); /* under 480 the field is behind the magnifier */
|
|
2354
|
+
$('mapQ').focus();
|
|
2253
2355
|
});
|
|
2254
2356
|
})();
|
|
2255
2357
|
|
|
2256
2358
|
function setSub(which) {
|
|
2257
|
-
|
|
2258
|
-
|
|
2359
|
+
currentSub = which;
|
|
2360
|
+
/* the Sky and Table items of the Projects band are the control (task
|
|
2361
|
+
1003300): the one on stage is current, the other a plain link */
|
|
2362
|
+
document.querySelectorAll('[data-nav="map"][data-sub]').forEach(function (a) {
|
|
2363
|
+
if (a.dataset.sub === which && currentView === 'map') a.setAttribute('aria-current', 'page');
|
|
2364
|
+
else a.removeAttribute('aria-current');
|
|
2365
|
+
});
|
|
2259
2366
|
$('sub-sky-body').hidden = which !== 'sky';
|
|
2260
2367
|
$('sub-table-body').hidden = which !== 'table';
|
|
2261
2368
|
/* the sim runs only while the sky is on stage (C8); anchors that were
|
|
@@ -2265,8 +2372,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2265
2372
|
startSky();
|
|
2266
2373
|
} else { stopSky(); hideHover(); }
|
|
2267
2374
|
}
|
|
2268
|
-
$('sub-sky').addEventListener('click', function () { setSub('sky'); });
|
|
2269
|
-
$('sub-table').addEventListener('click', function () { setSub('table'); });
|
|
2270
2375
|
|
|
2271
2376
|
/* the starfield only paints where --star-display is block (dark mode) */
|
|
2272
2377
|
(function stars() {
|
|
@@ -5240,16 +5345,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5240
5345
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
5241
5346
|
|
|
5242
5347
|
function renderWho() {
|
|
5243
|
-
var w = $('who');
|
|
5348
|
+
var w = $('who'), b = $('bandWho');
|
|
5349
|
+
var login = me ? (me.github_login || 'signed in') : '';
|
|
5244
5350
|
if (me) {
|
|
5245
|
-
/* the chip
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
'<
|
|
5351
|
+
/* the chip is the profile band's trigger (task 1003300): avatar + login +
|
|
5352
|
+
a chevron. The one session control this page owes its visitor, Sign
|
|
5353
|
+
out (task 1002952), is a static anchor in that band; the delegated
|
|
5354
|
+
click below upgrades it to the real POST. The band names the person. */
|
|
5355
|
+
var who = (me.avatar_url ? '<img src="' + esc(me.avatar_url) + '" alt="">' : '') +
|
|
5356
|
+
'<span class="whoName">' + esc(login) + '</span>';
|
|
5357
|
+
w.innerHTML = who + '<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg>';
|
|
5358
|
+
w.setAttribute('aria-label', 'Account, ' + login);
|
|
5359
|
+
b.innerHTML = who;
|
|
5251
5360
|
} else {
|
|
5252
5361
|
w.innerHTML = '';
|
|
5362
|
+
b.innerHTML = '';
|
|
5253
5363
|
}
|
|
5254
5364
|
}
|
|
5255
5365
|
|
|
@@ -5273,7 +5383,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5273
5383
|
};
|
|
5274
5384
|
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
5275
5385
|
}
|
|
5276
|
-
$('
|
|
5386
|
+
$('menuProfile').addEventListener('click', function (ev) {
|
|
5277
5387
|
var a = ev.target.closest && ev.target.closest('a.signout');
|
|
5278
5388
|
if (!a) return;
|
|
5279
5389
|
ev.preventDefault();
|
|
@@ -5304,8 +5414,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5304
5414
|
var signedIn = !!me;
|
|
5305
5415
|
$('signinTop').hidden = signedIn;
|
|
5306
5416
|
$('who').hidden = !signedIn;
|
|
5307
|
-
|
|
5417
|
+
/* every Your-projects item a band carries (§6 asked for querySelectorAll so
|
|
5418
|
+
a second data-nav="mine" item could never become a signed-out leak) */
|
|
5419
|
+
document.querySelectorAll('[data-nav="mine"]').forEach(function (a) { (a.closest('.bandCell') || a).hidden = !signedIn; });
|
|
5308
5420
|
$('onlyMine').closest('.switch').hidden = !signedIn;
|
|
5421
|
+
if (!signedIn) closeMenus();
|
|
5309
5422
|
if (!signedIn && $('onlyMine').checked) { $('onlyMine').checked = false; applyMapFilter(); }
|
|
5310
5423
|
}
|
|
5311
5424
|
|
|
@@ -5313,6 +5426,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5313
5426
|
any request resolves — /projects?view=new must land on the create view even
|
|
5314
5427
|
if the identity call is slow or fails. */
|
|
5315
5428
|
setView(viewFromUrl());
|
|
5429
|
+
/* the Table item's deep link (and the landing's) names the sub-view; a view change
|
|
5430
|
+
never writes it back, so a reload from the band returns to the sky as before */
|
|
5431
|
+
if (new URLSearchParams(location.search).get('sub') === 'table') setSub('table');
|
|
5316
5432
|
|
|
5317
5433
|
fetch(API + '/me', { credentials: 'same-origin' })
|
|
5318
5434
|
.then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
|
package/src/module-api.js
CHANGED
|
@@ -1226,8 +1226,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1226
1226
|
// landed since 1.19.391 with no explicit bump. run 33176032603. (task 1002620)
|
|
1227
1227
|
// 1.19.393 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1228
1228
|
// landed since 1.19.392 with no explicit bump. run 33177136260. (task 1002620)
|
|
1229
|
+
// 1.19.394 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1230
|
+
// landed since 1.19.393 with no explicit bump. run 33182535783. (task 1002620)
|
|
1229
1231
|
// ---------------------------------------------------------------------------
|
|
1230
|
-
const CORE_VERSION = '1.19.
|
|
1232
|
+
const CORE_VERSION = '1.19.394'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1231
1233
|
|
|
1232
1234
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1233
1235
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|