@bongos/core 1.21.50 → 1.21.52
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 +41 -31
- package/docs/copy-inventory.md +173 -140
- package/docs/copy-registry.json +601 -271
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +8 -4
- package/docs/page-readings.json +499 -495
- package/modules/government/docket.js +3 -1
- package/modules/hall-ui/public/street.css +6 -2
- package/modules/hall-ui/public/street.js +42 -20
- package/modules/hall-ui/records/governor-street.md +21 -0
- package/modules/public-landing/public/account.html +12 -101
- package/modules/public-landing/public/assets/hub-bar.js +127 -0
- package/modules/public-landing/public/contact.html +60 -9
- package/modules/public-landing/public/privacy.html +60 -9
- package/modules/public-landing/public/terms.html +60 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/scrub-identity.js +9 -1
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +2 -0
- package/tests/hall_preview_governor_fixtures.mjs +75 -0
- package/tests/hall_preview_scrub.mjs +5 -0
- package/tests/hall_street_amend.mjs +21 -8
- package/tests/hall_street_desk.mjs +41 -0
- package/tests/hub_account_page.mjs +22 -5
- package/tests/landing_legal_pages.mjs +275 -0
|
@@ -338,7 +338,9 @@ function countStates(matters) {
|
|
|
338
338
|
function homeLine(counts) {
|
|
339
339
|
if (!counts.needs && !counts.moving) return 'nothing waits on you.';
|
|
340
340
|
const parts = [];
|
|
341
|
-
|
|
341
|
+
// with nothing on you but work still moving, the design says so first
|
|
342
|
+
// ("nothing waits on you · 2 moving"), task 1004531
|
|
343
|
+
parts.push(counts.needs ? `${counts.needs} ${counts.needs === 1 ? 'matter waits' : 'matters wait'} on you` : 'nothing waits on you');
|
|
342
344
|
if (counts.moving) parts.push(`${counts.moving} moving`);
|
|
343
345
|
return parts.join(' · ');
|
|
344
346
|
}
|
|
@@ -272,7 +272,12 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
272
272
|
.inside-draft { display: flex; flex-direction: column; gap: 12px; padding: 14px 0 4px; border-top: 1px solid var(--street-rule); }
|
|
273
273
|
.inside-draft[hidden] { display: none; }
|
|
274
274
|
.inside-draft__change { gap: 6px; }
|
|
275
|
-
.inside-draft__picks { display: grid; grid-template-columns: 1fr
|
|
275
|
+
.inside-draft__picks { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
|
276
|
+
/* who and how are one-tap chips (task 1004531); a long choice wraps inside its
|
|
277
|
+
chip. Scoped under .inside-draft so they outrank the desk chip rules below. */
|
|
278
|
+
.inside-draft__chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
279
|
+
.inside-draft .inside-draft__chip { height: auto; min-height: 30px; padding: 5px 14px; white-space: normal; text-align: left; line-height: 1.3; }
|
|
280
|
+
.inside-draft .inside-draft__chip--on { background: var(--ink); border-color: var(--ink); color: var(--bg); }
|
|
276
281
|
.inside-draft__fixed { font-size: 14px; line-height: 1.45; color: var(--ink-soft); min-height: 36px; display: flex; align-items: center; }
|
|
277
282
|
.inside-meter { display: flex; flex-direction: column; gap: 6px; }
|
|
278
283
|
.inside-meter__track { height: 6px; border-radius: 999px; background: var(--street-wash); overflow: hidden; }
|
|
@@ -382,7 +387,6 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
382
387
|
|
|
383
388
|
/* narrow: the drawing in its own sideways strip, the panel below it */
|
|
384
389
|
@media (max-width: 899px) {
|
|
385
|
-
.inside-draft__picks { grid-template-columns: 1fr; }
|
|
386
390
|
.inside { display: block; overflow-x: hidden; overflow-y: auto; }
|
|
387
391
|
.inside-sizer { width: auto !important; height: auto !important; margin: 0; overflow: visible; }
|
|
388
392
|
.inside-board { position: static; width: auto; height: auto; transform: none; display: flex; flex-direction: column; gap: 16px; padding: 8px 16px 24px; }
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
var desk = { done: {}, mine: {}, cleared: {}, order: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
|
|
91
91
|
var TODAY_KEY = 'gov-desk-today';
|
|
92
92
|
var today = { day: '', n: 0 };
|
|
93
|
+
var todayDrawn = null; // the count the meter's pips last drew: they are rebuilt only when it moves
|
|
93
94
|
|
|
94
95
|
// ---------- light / dark ----------
|
|
95
96
|
var chosen = null;
|
|
@@ -811,9 +812,13 @@
|
|
|
811
812
|
var D = window.GovDesk, pipsEl = $('streetTodayPips');
|
|
812
813
|
if (!D || !pipsEl) return;
|
|
813
814
|
var t = D.todayPips(today.n);
|
|
815
|
+
setText('streetTodayN', t.text);
|
|
816
|
+
// the pips are rebuilt only when the count moves, so the newest pops once
|
|
817
|
+
// and a redraw neither replays nor cuts its pop short (task 1004531)
|
|
818
|
+
if (todayDrawn === today.n && pipsEl.children && pipsEl.children.length) return;
|
|
819
|
+
todayDrawn = today.n;
|
|
814
820
|
clear(pipsEl);
|
|
815
821
|
t.pips.forEach(function (p) { pipsEl.appendChild(node('span', 'street-today__pip' + (p.on ? ' street-today__pip--on' : '') + (p.fresh ? ' desk-pop' : ''))); });
|
|
816
|
-
setText('streetTodayN', t.text);
|
|
817
822
|
}
|
|
818
823
|
|
|
819
824
|
function pickFloor(i) {
|
|
@@ -850,8 +855,7 @@
|
|
|
850
855
|
dr.d = A ? A.draftFor(st.key, c && c.board ? c : null) : { ok: false, reason: 'unread' };
|
|
851
856
|
dr.rev++;
|
|
852
857
|
drawDraft(true);
|
|
853
|
-
|
|
854
|
-
if (who && who.focus) who.focus();
|
|
858
|
+
focusDraft('insideDraftWho');
|
|
855
859
|
return !!(dr.d && dr.d.ok);
|
|
856
860
|
});
|
|
857
861
|
}
|
|
@@ -868,26 +872,42 @@
|
|
|
868
872
|
if (!dr) return;
|
|
869
873
|
dr.rev++;
|
|
870
874
|
drawDraft(true);
|
|
875
|
+
focusDraft(focusId);
|
|
876
|
+
}
|
|
877
|
+
// Focus one control of the draft; a group of chips takes it on its picked chip.
|
|
878
|
+
function focusDraft(focusId) {
|
|
871
879
|
var el = focusId ? $(focusId) : null;
|
|
880
|
+
if (el && el.getAttribute && el.getAttribute('role') === 'radiogroup') {
|
|
881
|
+
var kids = el.children || [], pick = kids[0] || null;
|
|
882
|
+
for (var i = 0; i < kids.length; i++) if (kids[i].getAttribute('aria-checked') === 'true') { pick = kids[i]; break; }
|
|
883
|
+
el = pick;
|
|
884
|
+
}
|
|
872
885
|
if (el && el.focus) el.focus();
|
|
873
886
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
887
|
+
// A draft choice is one tap (the owner's line rule: choices are one-tap
|
|
888
|
+
// chips, task 1004531): a radio group of chips, the picked one filled.
|
|
889
|
+
function draftChips(id, list, value, onPick) {
|
|
890
|
+
var g = node('div', 'inside-draft__chips');
|
|
891
|
+
g.setAttribute('id', id);
|
|
892
|
+
g.setAttribute('role', 'radiogroup');
|
|
893
|
+
g.setAttribute('aria-labelledby', id + 'Label');
|
|
877
894
|
list.forEach(function (o) {
|
|
878
|
-
var
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
895
|
+
var on = o.value === value;
|
|
896
|
+
var b = node('button', 'desk-chip desk-chip--choice inside-draft__chip' + (on ? ' inside-draft__chip--on' : ''), o.words);
|
|
897
|
+
b.setAttribute('type', 'button');
|
|
898
|
+
b.setAttribute('role', 'radio');
|
|
899
|
+
b.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
900
|
+
b.setAttribute('data-value', o.value);
|
|
901
|
+
b.addEventListener('click', function () { if (!on) onPick(o.value); });
|
|
902
|
+
g.appendChild(b);
|
|
882
903
|
});
|
|
883
|
-
|
|
884
|
-
sel.addEventListener('change', function () { onPick(sel.value); });
|
|
885
|
-
return sel;
|
|
904
|
+
return g;
|
|
886
905
|
}
|
|
887
|
-
function draftField(labelText, forId, control) {
|
|
906
|
+
function draftField(labelText, forId, control, group) {
|
|
888
907
|
var f = node('div', 'mcard-form');
|
|
889
|
-
var l = node(forId ? 'label' : 'span', 'mcard-form__label', labelText);
|
|
890
|
-
if (forId) l.setAttribute('for', forId);
|
|
908
|
+
var l = node(forId && !group ? 'label' : 'span', 'mcard-form__label', labelText);
|
|
909
|
+
if (forId && !group) l.setAttribute('for', forId);
|
|
910
|
+
if (group) l.setAttribute('id', forId + 'Label');
|
|
891
911
|
f.appendChild(l);
|
|
892
912
|
f.appendChild(control);
|
|
893
913
|
return f;
|
|
@@ -925,13 +945,13 @@
|
|
|
925
945
|
} else ch.appendChild(node('p', 'inside-note', 'This is how it works today. Pick who decides, or how, to change it.'));
|
|
926
946
|
box.appendChild(ch);
|
|
927
947
|
var picks = node('div', 'inside-draft__picks');
|
|
928
|
-
picks.appendChild(draftField('who decides', 'insideDraftWho',
|
|
948
|
+
picks.appendChild(draftField('who decides', 'insideDraftWho', draftChips('insideDraftWho', d.whoOptions, d.decided_by, function (v) {
|
|
929
949
|
dr.d = A.pick(dr.d, 'who', v); dr.error = ''; bumpDraft('insideDraftWho');
|
|
930
|
-
})));
|
|
950
|
+
}), true));
|
|
931
951
|
if (d.ruleOptions.length) {
|
|
932
|
-
picks.appendChild(draftField('how they decide', 'insideDraftRule',
|
|
952
|
+
picks.appendChild(draftField('how they decide', 'insideDraftRule', draftChips('insideDraftRule', d.ruleOptions, d.rule, function (v) {
|
|
933
953
|
dr.d = A.pick(dr.d, 'rule', v); dr.error = ''; bumpDraft('insideDraftRule');
|
|
934
|
-
})));
|
|
954
|
+
}), true));
|
|
935
955
|
} else {
|
|
936
956
|
picks.appendChild(draftField('how they decide', null, node('span', 'inside-draft__fixed', d.decided_by === 'nobody' ? 'nothing is decided until someone holds it' : 'decides alone, as today')));
|
|
937
957
|
}
|
|
@@ -1210,6 +1230,8 @@
|
|
|
1210
1230
|
card.pick = null;
|
|
1211
1231
|
card.fresh = key === 'for' || key === 'object' ? 'you' : null;
|
|
1212
1232
|
if (a.building && a.kind) card.answer = { building: a.building, matter: cv.key, kind: a.kind, at: Date.now() };
|
|
1233
|
+
// a decision from a card is a decision today, as one from a desk row is (task 1004531)
|
|
1234
|
+
if (recorded) { bumpToday(); drawToday(); }
|
|
1213
1235
|
if (a.close) closeCard(true);
|
|
1214
1236
|
else if (view.open) { fillCard(); var c = $('streetCardClose'); if (c && c.focus) c.focus(); }
|
|
1215
1237
|
if (!recorded) return true;
|
|
@@ -47,3 +47,24 @@ Every building and every floor desk was walked in light, dark and at phone width
|
|
|
47
47
|
- **The desk's toast is the board's**: an ink pill with the orange dot, low over the desk. The street's own toast is centred again (its enter animation had undone the centring).
|
|
48
48
|
|
|
49
49
|
Left as found, on purpose: the six readings stay under a building (task 1004526 marks the building's own reading), the old month drawers stay under the desk, and the recording sign-off still goes to its page (an open owner choice).
|
|
50
|
+
|
|
51
|
+
## The whole city, walked against the prototype (task 1004531)
|
|
52
|
+
|
|
53
|
+
The UAT walkthrough of goal 1000125: open the home city, start governing, find what needs you, enter every building and district, decide a vote from its card and watch the city change, and draft an amendment from a building, in light and dark at 1440 and at phone width (390 px), side by side with `Street.dc.html` (the prototype), `Desks.dc.html` and `Ground.dc.html`, judged by the owner's line rule. The live site is behind its password gate, so the walk ran on the real pages in the hall preview harness (`--fixture-me`). **The real-vote step was done in the harness only**: the vote went to the harness's canned answer for `POST /government/board/items/:id/votes`, and the docket read after it was stubbed in the page to show what the server would now say; no vote was cast on the live site. The amendment was filed the same way (the page's `POST /government/board/decision-rules` answered in the page, the harness refuses writes). The district interior was walked over twelve stand-in tasks answered in the page, since the harness serves one task list for every goal. Screenshots stay out of the repo: they are in the session scratchpad, `walk-1004531/` (53 shots).
|
|
54
|
+
|
|
55
|
+
What it found and fixed:
|
|
56
|
+
|
|
57
|
+
- **The preview fixtures disagreed with themselves.** The docket fixture's counts, home line, per-building counts and fairness reading had been written for the first seven matters and never re-derived as later tasks added more, so the city said "3 matters wait on you", the street header 4, its pins 13, and Disputes read "0 open" under two matters that need you. `buildDocket` derives all four from the matters, so live they cannot disagree; the fixture now holds the same derivation, and `tests/hall_preview_governor_fixtures.mjs` holds it to it.
|
|
58
|
+
- **Change how this works could not be walked.** The constitution fixture had no `decision_rules` block, so the drafter said it could not read how a building decides. It now carries the block the route serves (every building as today), and the same test drafts every building from it. The fixture scrubber had read a building's decided_by (gate, board, nobody) as a login; it keeps those words now.
|
|
59
|
+
- **The drafter's choices were dropdowns.** Who decides and how they decide are one-tap chips now (the line rule: choices are one-tap chips): a labelled radio group each, the picked chip filled in ink, a long choice wrapping inside its chip, stacked one under the other so the chips have the panel's width. Focus lands on the picked chip after each pick.
|
|
60
|
+
- **A vote from a street card did not count today.** The today meter only took a pip for an act on a desk row; a decision from a card is a decision today too, so a recorded card act now fills a pip (an act that records nothing, or is refused, does not).
|
|
61
|
+
- **The meter's newest pip popped on every redraw** (any pick, the card closing, the docket read after an act), and the read after an act cut the pop short. The pips are drawn again only when the count moves, so the newest pops once, as on the board.
|
|
62
|
+
- **The home line with nothing on you.** With nothing waiting on you but work still moving, the server said "2 moving"; the prototype says "nothing waits on you · 2 moving", and now so does the server (`homeLine`).
|
|
63
|
+
|
|
64
|
+
Walked and matching, left as found: the city (pins, the one button, its line), the street (the order, the group brackets, one pin per building in its tier, the crates and the tester count pin, the six readings), every building's section, lift and panel, the card (its pager, the change, the seats and tally, counsel, the yes/no acts under the desk's words from task 1004566), the toast, and a district's road of work.
|
|
65
|
+
|
|
66
|
+
Left on purpose, not counted as differences:
|
|
67
|
+
|
|
68
|
+
- **The city sits inside the hall's shell** (sidebar and top bar). The prototype's home page carries only the wordmark, the switch, one button and one line; standalone hall pages were retired by task 1002868, and the city page adopted the shell after its record was written.
|
|
69
|
+
- **The home line never says "pieces of work stuck at Review".** The prototype adds it while Review is a vacant lot; the city draws Review as held in this project (the header of `city-draw.js`), so the clause has nothing to stand on.
|
|
70
|
+
- **Open owner choices met on the walk, untouched:** the "1" on a single-item pin, the light-mode legend hint wording, the recording sign-off going to its page, failed passed-vote actions on the record, a Discord update on withdraw, and the Work categories link.
|
|
@@ -84,7 +84,8 @@
|
|
|
84
84
|
<!-- THE BAR and its two bands are the hub's own (projects.html, task 1004455):
|
|
85
85
|
same markup, same cosmos.css rules, so /projects → /account keeps the chrome.
|
|
86
86
|
No map search (it only filters the map), and the band links name /projects
|
|
87
|
-
in full because this page has no views of its own.
|
|
87
|
+
in full because this page has no views of its own. assets/hub-bar.js runs it,
|
|
88
|
+
shared with the three footer doors (task 1004489). -->
|
|
88
89
|
<a class="skip vh" href="#acct">Skip to content</a>
|
|
89
90
|
<header class="shell bar">
|
|
90
91
|
<div class="barLeft">
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
</div>
|
|
102
103
|
<div class="barRight">
|
|
103
104
|
<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>
|
|
104
|
-
<!-- the identity chip IS the profile trigger;
|
|
105
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from GET /me.
|
|
105
106
|
Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
106
107
|
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
107
108
|
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/account&repos=1">Sign in</a>
|
|
@@ -249,11 +250,17 @@
|
|
|
249
250
|
|
|
250
251
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">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><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
251
252
|
|
|
253
|
+
<script src="/assets/hub-bar.js"></script>
|
|
252
254
|
<script>
|
|
253
255
|
(function () {
|
|
254
256
|
'use strict';
|
|
255
257
|
var API = '/api/bongos';
|
|
256
258
|
function $(id) { return document.getElementById(id); }
|
|
259
|
+
// The bar's chip, bands and sign out are assets/hub-bar.js (task 1004489), loaded
|
|
260
|
+
// just above. This page only repaints the chip: signed out when its own read
|
|
261
|
+
// 401s, and named from the account when GET /me failed. Without the asset the
|
|
262
|
+
// page still works; it just has no chip to repaint.
|
|
263
|
+
var bar = window.HubBar || { paint: function () {}, ready: Promise.resolve(null) };
|
|
257
264
|
|
|
258
265
|
// The server's own view of the account — the last GET /me/cross-project
|
|
259
266
|
// answer. Every save re-renders from it, never from what was typed.
|
|
@@ -298,7 +305,7 @@
|
|
|
298
305
|
function showDoor() {
|
|
299
306
|
$('loading').hidden = true;
|
|
300
307
|
$('acctBody').hidden = true;
|
|
301
|
-
|
|
308
|
+
bar.paint(null);
|
|
302
309
|
$('door').hidden = false;
|
|
303
310
|
}
|
|
304
311
|
function showAlert(text) {
|
|
@@ -523,106 +530,10 @@
|
|
|
523
530
|
: 'Cross-project record shown on your public profile.');
|
|
524
531
|
});
|
|
525
532
|
|
|
526
|
-
// ---- the bar: who is signed in, from the hub's own identity read. Signed
|
|
527
|
-
// out there is no chip and no Your-projects item; the sign-in door takes the slot ----
|
|
528
|
-
function paintBar(me) {
|
|
529
|
-
var login = me ? (me.github_login || 'signed in') : '';
|
|
530
|
-
var avatar = me && me.avatar_url;
|
|
531
|
-
$('who').hidden = !me;
|
|
532
|
-
$('signinTop').hidden = !!me;
|
|
533
|
-
$('mineCell').hidden = !me;
|
|
534
|
-
[['whoImg', 'whoLogin'], ['bandWhoImg', 'bandWhoLogin']].forEach(function (ids) {
|
|
535
|
-
$(ids[0]).hidden = !avatar;
|
|
536
|
-
if (avatar) $(ids[0]).src = avatar;
|
|
537
|
-
$(ids[1]).textContent = login;
|
|
538
|
-
});
|
|
539
|
-
if (me) $('who').setAttribute('aria-label', 'Account, ' + login);
|
|
540
|
-
else closeMenus();
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// ---- the bands: the hub's disclosure (projects.html "THE BANDS") without
|
|
544
|
-
// the map search. Escape closes and returns focus to the trigger, a click
|
|
545
|
-
// outside closes, the arrows walk the items, opening one closes the other ----
|
|
546
|
-
function openBand() { return document.querySelector('.menuBand:not([hidden])'); }
|
|
547
|
-
function closeMenus() {
|
|
548
|
-
var open = openBand();
|
|
549
|
-
if (!open) return;
|
|
550
|
-
var trig = document.querySelector('.trig[aria-expanded="true"]');
|
|
551
|
-
var inside = open.contains(document.activeElement);
|
|
552
|
-
open.hidden = true;
|
|
553
|
-
if (trig) trig.setAttribute('aria-expanded', 'false');
|
|
554
|
-
if (inside && trig) trig.focus();
|
|
555
|
-
}
|
|
556
|
-
function openMenu(trig, byKeyboard) {
|
|
557
|
-
closeMenus();
|
|
558
|
-
trig.setAttribute('aria-expanded', 'true');
|
|
559
|
-
var band = $(trig.getAttribute('aria-controls'));
|
|
560
|
-
band.hidden = false;
|
|
561
|
-
// Enter/Space on the trigger lands on the first item; a pointer stays put
|
|
562
|
-
if (byKeyboard) { var first = band.querySelector('.bandCell:not([hidden]) a'); if (first) first.focus(); }
|
|
563
|
-
}
|
|
564
|
-
document.querySelectorAll('.trig').forEach(function (t) {
|
|
565
|
-
t.addEventListener('click', function (ev) {
|
|
566
|
-
if (t.getAttribute('aria-expanded') === 'true') closeMenus();
|
|
567
|
-
else openMenu(t, ev.detail === 0);
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
document.addEventListener('click', function (ev) {
|
|
571
|
-
if (!openBand()) return;
|
|
572
|
-
if (ev.target.closest && (ev.target.closest('.menuBand') || ev.target.closest('.trig'))) return;
|
|
573
|
-
closeMenus();
|
|
574
|
-
});
|
|
575
|
-
document.addEventListener('keydown', function (ev) {
|
|
576
|
-
var open = openBand();
|
|
577
|
-
if (!open) return;
|
|
578
|
-
if (ev.key === 'Escape') { closeMenus(); return; }
|
|
579
|
-
if (ev.key !== 'ArrowDown' && ev.key !== 'ArrowUp') return;
|
|
580
|
-
var items = Array.prototype.filter.call(open.querySelectorAll('a'), function (a) { return !a.closest('[hidden]'); });
|
|
581
|
-
if (!items.length) return;
|
|
582
|
-
var at = items.indexOf(document.activeElement);
|
|
583
|
-
ev.preventDefault();
|
|
584
|
-
items[(at + (ev.key === 'ArrowDown' ? 1 : items.length - 1) + items.length) % items.length].focus();
|
|
585
|
-
});
|
|
586
|
-
document.querySelectorAll('.menuBand').forEach(function (b) {
|
|
587
|
-
b.addEventListener('click', function (ev) {
|
|
588
|
-
if (ev.target.closest && ev.target.closest('a')) closeMenus();
|
|
589
|
-
});
|
|
590
|
-
// Tab out of the band closes it; a null relatedTarget is the window losing
|
|
591
|
-
// focus or a click on nothing, which the document click covers
|
|
592
|
-
b.addEventListener('focusout', function (ev) {
|
|
593
|
-
var to = ev.relatedTarget;
|
|
594
|
-
if (!to || b.contains(to) || (to.closest && to.closest('.trig'))) return;
|
|
595
|
-
closeMenus();
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
// ---- sign out: the hub's own logout (projects.html doLogout), delegated on
|
|
600
|
-
// the profile band's plain anchor ----
|
|
601
|
-
$('menuProfile').addEventListener('click', function (ev) {
|
|
602
|
-
if (!ev.target.closest) return;
|
|
603
|
-
// Account is this page: a reload would only throw away what was typed
|
|
604
|
-
if (ev.target.closest('[aria-current="page"]')) { ev.preventDefault(); return; }
|
|
605
|
-
if (!ev.target.closest('a.signout')) return;
|
|
606
|
-
ev.preventDefault();
|
|
607
|
-
var finish = function () {
|
|
608
|
-
var apex = location.hostname.split('.').slice(-2).join('.');
|
|
609
|
-
['bongos_session', 'gds_session', 'pms_session'].forEach(function (name) {
|
|
610
|
-
document.cookie = name + '=; Max-Age=0; path=/';
|
|
611
|
-
document.cookie = name + '=; Max-Age=0; path=/; Domain=.' + apex;
|
|
612
|
-
});
|
|
613
|
-
window.location.assign(location.pathname);
|
|
614
|
-
};
|
|
615
|
-
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
var whoRead = request('GET', '/me').then(function (res) {
|
|
619
|
-
var d = res.ok && res.data;
|
|
620
|
-
paintBar(d ? (d.builder || d.me || d) : null);
|
|
621
|
-
}, function () { paintBar(null); });
|
|
622
533
|
// The identity read failing is not a sign-out. If the account itself was
|
|
623
534
|
// read, the chip names it from that answer, so the way out is never lost.
|
|
624
|
-
load().then(function () { return
|
|
625
|
-
if (acct && $('who').hidden)
|
|
535
|
+
load().then(function () { return bar.ready; }).then(function () {
|
|
536
|
+
if (acct && $('who').hidden) bar.paint({ github_login: acct.handle || acct.display_name });
|
|
626
537
|
});
|
|
627
538
|
})();
|
|
628
539
|
</script>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* assets/hub-bar.js — THE HUB'S BAR on the pages that have no views of their own
|
|
2
|
+
(task 1004489, after task 1004455).
|
|
3
|
+
|
|
4
|
+
/projects draws the bar and its two bands — the Projects band and the profile
|
|
5
|
+
band behind the identity chip. The apex pages a person reaches from it wear the
|
|
6
|
+
same bar so the chrome does not swap under them: the account page (/account) and
|
|
7
|
+
the three footer doors (/privacy, /terms, /contact). Each page keeps the bar's
|
|
8
|
+
MARKUP (real markup, pinned byte-equal to the hub's); this file is the one copy
|
|
9
|
+
of what makes it work there:
|
|
10
|
+
- the chip, filled from GET /me — signed out there is no chip and no
|
|
11
|
+
Your-projects item, and the sign-in door takes the chip's slot;
|
|
12
|
+
- the bands' disclosure: a trigger toggles its band, opening one closes the
|
|
13
|
+
other, Escape closes and returns focus to the trigger, a click outside or
|
|
14
|
+
Tab out closes, the arrows walk the items, an item activated closes it;
|
|
15
|
+
- sign out: the profile band's plain anchor upgraded to the hub's logout POST.
|
|
16
|
+
|
|
17
|
+
projects.html keeps its own copy (its bands also fold the map search and switch
|
|
18
|
+
views), so a change to the disclosure is made there AND here.
|
|
19
|
+
tests/hub_account_page.mjs and tests/landing_legal_pages.mjs execute this file.
|
|
20
|
+
|
|
21
|
+
A page that knows more about who is signed in than GET /me says paints the chip
|
|
22
|
+
itself through window.HubBar.paint(me); window.HubBar.ready settles with what
|
|
23
|
+
GET /me answered (null when nobody is signed in or the read failed). */
|
|
24
|
+
(function () {
|
|
25
|
+
'use strict';
|
|
26
|
+
var API = '/api/bongos';
|
|
27
|
+
function $(id) { return document.getElementById(id); }
|
|
28
|
+
|
|
29
|
+
// ---- the chip: who is signed in (null = nobody) ----
|
|
30
|
+
function paint(me) {
|
|
31
|
+
var login = me ? (me.github_login || 'signed in') : '';
|
|
32
|
+
var avatar = me && me.avatar_url;
|
|
33
|
+
$('who').hidden = !me;
|
|
34
|
+
$('signinTop').hidden = !!me;
|
|
35
|
+
$('mineCell').hidden = !me;
|
|
36
|
+
[['whoImg', 'whoLogin'], ['bandWhoImg', 'bandWhoLogin']].forEach(function (ids) {
|
|
37
|
+
$(ids[0]).hidden = !avatar;
|
|
38
|
+
if (avatar) $(ids[0]).src = avatar;
|
|
39
|
+
$(ids[1]).textContent = login;
|
|
40
|
+
});
|
|
41
|
+
if (me) $('who').setAttribute('aria-label', 'Account, ' + login);
|
|
42
|
+
else closeMenus();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- the bands: the hub's disclosure (projects.html "THE BANDS") without the map search ----
|
|
46
|
+
function openBand() { return document.querySelector('.menuBand:not([hidden])'); }
|
|
47
|
+
function closeMenus() {
|
|
48
|
+
var open = openBand();
|
|
49
|
+
if (!open) return;
|
|
50
|
+
var trig = document.querySelector('.trig[aria-expanded="true"]');
|
|
51
|
+
var inside = open.contains(document.activeElement);
|
|
52
|
+
open.hidden = true;
|
|
53
|
+
if (trig) trig.setAttribute('aria-expanded', 'false');
|
|
54
|
+
if (inside && trig) trig.focus();
|
|
55
|
+
}
|
|
56
|
+
function openMenu(trig, byKeyboard) {
|
|
57
|
+
closeMenus();
|
|
58
|
+
trig.setAttribute('aria-expanded', 'true');
|
|
59
|
+
var band = $(trig.getAttribute('aria-controls'));
|
|
60
|
+
band.hidden = false;
|
|
61
|
+
// Enter/Space on the trigger lands on the first item; a pointer stays put
|
|
62
|
+
if (byKeyboard) { var first = band.querySelector('.bandCell:not([hidden]) a'); if (first) first.focus(); }
|
|
63
|
+
}
|
|
64
|
+
document.querySelectorAll('.trig').forEach(function (t) {
|
|
65
|
+
t.addEventListener('click', function (ev) {
|
|
66
|
+
if (t.getAttribute('aria-expanded') === 'true') closeMenus();
|
|
67
|
+
else openMenu(t, ev.detail === 0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
document.addEventListener('click', function (ev) {
|
|
71
|
+
if (!openBand()) return;
|
|
72
|
+
if (ev.target.closest && (ev.target.closest('.menuBand') || ev.target.closest('.trig'))) return;
|
|
73
|
+
closeMenus();
|
|
74
|
+
});
|
|
75
|
+
document.addEventListener('keydown', function (ev) {
|
|
76
|
+
var open = openBand();
|
|
77
|
+
if (!open) return;
|
|
78
|
+
if (ev.key === 'Escape') { closeMenus(); return; }
|
|
79
|
+
if (ev.key !== 'ArrowDown' && ev.key !== 'ArrowUp') return;
|
|
80
|
+
var items = Array.prototype.filter.call(open.querySelectorAll('a'), function (a) { return !a.closest('[hidden]'); });
|
|
81
|
+
if (!items.length) return;
|
|
82
|
+
var at = items.indexOf(document.activeElement);
|
|
83
|
+
ev.preventDefault();
|
|
84
|
+
items[(at + (ev.key === 'ArrowDown' ? 1 : items.length - 1) + items.length) % items.length].focus();
|
|
85
|
+
});
|
|
86
|
+
document.querySelectorAll('.menuBand').forEach(function (b) {
|
|
87
|
+
b.addEventListener('click', function (ev) {
|
|
88
|
+
if (ev.target.closest && ev.target.closest('a')) closeMenus();
|
|
89
|
+
});
|
|
90
|
+
// Tab out of the band closes it; a null relatedTarget is the window losing
|
|
91
|
+
// focus or a click on nothing, which the document click covers
|
|
92
|
+
b.addEventListener('focusout', function (ev) {
|
|
93
|
+
var to = ev.relatedTarget;
|
|
94
|
+
if (!to || b.contains(to) || (to.closest && to.closest('.trig'))) return;
|
|
95
|
+
closeMenus();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ---- the profile band: the current place, and sign out (projects.html doLogout) ----
|
|
100
|
+
$('menuProfile').addEventListener('click', function (ev) {
|
|
101
|
+
if (!ev.target.closest) return;
|
|
102
|
+
// an item marked as this page: a reload would only throw away what was typed
|
|
103
|
+
if (ev.target.closest('[aria-current="page"]')) { ev.preventDefault(); return; }
|
|
104
|
+
if (!ev.target.closest('a.signout')) return;
|
|
105
|
+
ev.preventDefault();
|
|
106
|
+
var finish = function () {
|
|
107
|
+
var apex = location.hostname.split('.').slice(-2).join('.');
|
|
108
|
+
['bongos_session', 'gds_session', 'pms_session'].forEach(function (name) {
|
|
109
|
+
document.cookie = name + '=; Max-Age=0; path=/';
|
|
110
|
+
document.cookie = name + '=; Max-Age=0; path=/; Domain=.' + apex;
|
|
111
|
+
});
|
|
112
|
+
window.location.assign(location.pathname);
|
|
113
|
+
};
|
|
114
|
+
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---- the identity read: the hub's own GET /me ----
|
|
118
|
+
var ready = fetch(API + '/me', { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
119
|
+
.then(function (res) { return res.ok ? res.json().catch(function () { return null; }) : null; })
|
|
120
|
+
.then(function (d) {
|
|
121
|
+
var me = d ? (d.builder || d.me || d) : null;
|
|
122
|
+
paint(me);
|
|
123
|
+
return me;
|
|
124
|
+
}, function () { paint(null); return null; });
|
|
125
|
+
|
|
126
|
+
window.HubBar = { paint: paint, ready: ready };
|
|
127
|
+
})();
|
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
<!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
21
21
|
<style>
|
|
22
22
|
/* The reading column only. No :root block — the pack's injected theme owns the
|
|
23
|
-
tokens (landing direction C1); everything below reads them.
|
|
23
|
+
tokens (landing direction C1); everything below reads them. The bar and the
|
|
24
|
+
bands are cosmos.css's own rules, not restyled here. */
|
|
25
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
26
|
+
.skip:focus{position:fixed;top:8px;left:8px;z-index:100;width:auto;height:auto;margin:0;overflow:visible;clip-path:none;white-space:normal;
|
|
27
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
28
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
24
29
|
.legal{position:relative;z-index:1;max-width:720px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 48px;}
|
|
25
|
-
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin
|
|
30
|
+
.legal h1{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.8vw,36px);letter-spacing:-0.5px;color:var(--chrome-ink);text-shadow:var(--chrome-glow);margin:18px 0 6px;}
|
|
26
31
|
.legal .lede{color:var(--hero-lede);text-shadow:var(--chrome-glow);margin-bottom:18px;}
|
|
27
32
|
.legal h2{font-family:var(--font-display);font-weight:700;font-size:var(--fs-h3);letter-spacing:-0.3px;line-height:1.25;margin:22px 0 8px;}
|
|
28
33
|
.legal .card h2:first-child{margin-top:0;}
|
|
@@ -34,15 +39,60 @@
|
|
|
34
39
|
</style>
|
|
35
40
|
</head>
|
|
36
41
|
<body>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html; task 1004489,
|
|
43
|
+
after the account page's task 1004455): same markup, same cosmos.css rules,
|
|
44
|
+
so following a footer link from /projects or /account keeps the chrome. No
|
|
45
|
+
map search (it only filters the map), the band links name /projects in full
|
|
46
|
+
because this page has no views of its own, and no item is the current place.
|
|
47
|
+
assets/hub-bar.js fills the chip from GET /me and runs the bands. -->
|
|
48
|
+
<a class="skip vh" href="#legal">Skip to content</a>
|
|
49
|
+
<header class="shell bar">
|
|
50
|
+
<div class="barLeft">
|
|
51
|
+
<a class="barMark" id="barMark" href="/">
|
|
52
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
53
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
54
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
55
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
56
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
57
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
58
|
+
</svg>
|
|
59
|
+
<span>{{productName}}</span>
|
|
60
|
+
</a>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="barRight">
|
|
63
|
+
<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>
|
|
64
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from
|
|
65
|
+
GET /me. Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
66
|
+
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
67
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/contact&repos=1">Sign in</a>
|
|
42
68
|
</div>
|
|
43
69
|
</header>
|
|
44
70
|
|
|
45
|
-
<
|
|
71
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
72
|
+
<div class="bandIn">
|
|
73
|
+
<ul class="bandRow">
|
|
74
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
75
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
76
|
+
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
77
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
82
|
+
<div class="bandIn bandProfile">
|
|
83
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
84
|
+
<ul class="bandRow">
|
|
85
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
86
|
+
<!-- apex-only because /account is: it is served only where platform-identity
|
|
87
|
+
runs, so a child instance loses the item whole (as on projects.html) -->
|
|
88
|
+
<!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
|
|
89
|
+
<!-- the honest hover target; assets/hub-bar.js upgrades the click to the POST -->
|
|
90
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
91
|
+
</ul>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<main class="legal" id="legal">
|
|
46
96
|
<h1>Contact</h1>
|
|
47
97
|
<p class="lede">Two doors. Both are public, and both are how {{productName}} itself works day to day.</p>
|
|
48
98
|
|
|
@@ -68,6 +118,7 @@
|
|
|
68
118
|
</section>
|
|
69
119
|
</main>
|
|
70
120
|
|
|
71
|
-
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact" aria-current="page">Contact</a></nav></footer>
|
|
121
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact" aria-current="page">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
122
|
+
<script src="/assets/hub-bar.js"></script>
|
|
72
123
|
</body>
|
|
73
124
|
</html>
|