@bongos/core 1.23.8 → 1.23.10

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.
Files changed (42) hide show
  1. package/.bongos-core.json +92 -37
  2. package/docs/copy-inventory.md +858 -724
  3. package/docs/copy-registry.json +2538 -1155
  4. package/docs/file-map.md +1 -0
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +41 -4
  7. package/docs/page-readings.json +1240 -1156
  8. package/modules/public-landing/public/account.html +11 -0
  9. package/modules/public-landing/public/assets/cosmos.css +9 -1
  10. package/modules/public-landing/public/assets/hub-bar.js +1 -1
  11. package/modules/public-landing/public/community-empty.states.json +15 -0
  12. package/modules/public-landing/public/community-recruiter.states.json +15 -0
  13. package/modules/public-landing/public/community.html +469 -0
  14. package/modules/public-landing/public/community.probes.json +92 -0
  15. package/modules/public-landing/public/community.states.json +23 -0
  16. package/modules/public-landing/public/connections.html +11 -0
  17. package/modules/public-landing/public/contact.html +11 -0
  18. package/modules/public-landing/public/guild.html +11 -0
  19. package/modules/public-landing/public/index.html +18 -1
  20. package/modules/public-landing/public/index.probes.json +1 -1
  21. package/modules/public-landing/public/invitations.html +11 -0
  22. package/modules/public-landing/public/privacy.html +11 -0
  23. package/modules/public-landing/public/projects.html +12 -1
  24. package/modules/public-landing/public/terms.html +11 -0
  25. package/modules/ui-design/kit/fixtures/community-leaderboard-empty.json +4 -0
  26. package/modules/ui-design/kit/fixtures/community-leaderboard.json +28 -0
  27. package/modules/ui-design/kit/fixtures/community-search.json +10 -0
  28. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +11 -0
  29. package/modules/ui-design/kit/fixtures/scouting.json +4 -0
  30. package/modules/ui-design/kit/serve.js +55 -0
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/release-notes.json +12 -0
  34. package/src/bongos/serve-internal.js +4 -2
  35. package/src/module-api.js +1 -1
  36. package/src/platform-server.js +11 -0
  37. package/tests/hub_account_page.mjs +2 -0
  38. package/tests/hub_community_page.mjs +475 -0
  39. package/tests/landing_legal_pages.mjs +3 -0
  40. package/tests/landing_page.mjs +1 -1
  41. package/tests/render_check.mjs +2 -2
  42. package/tests/ui_design_kit.mjs +2 -2
@@ -105,6 +105,7 @@
105
105
  </a>
106
106
  </div>
107
107
  <div class="barRight">
108
+ <button class="trig commTrig" type="button" id="commTrig" data-menu="community" aria-expanded="false" aria-controls="menuCommunity">Community<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
108
109
  <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>
109
110
  <!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from GET /me.
110
111
  Signed out it stays hidden and the sign-in door takes its slot. -->
@@ -113,6 +114,15 @@
113
114
  </div>
114
115
  </header>
115
116
 
117
+ <div class="menuBand" id="menuCommunity" data-menu="community" hidden>
118
+ <div class="bandIn">
119
+ <ul class="bandRow">
120
+ <li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
121
+ <li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
122
+ <li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
123
+ </ul>
124
+ </div>
125
+ </div>
116
126
  <div class="menuBand" id="menuProjects" data-menu="projects" hidden>
117
127
  <div class="bandIn">
118
128
  <ul class="bandRow">
@@ -120,6 +130,7 @@
120
130
  <li class="bandCell"><a class="bandT" href="/projects?view=map&amp;sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
121
131
  <li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
122
132
  <li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
133
+ <li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
123
134
  </ul>
124
135
  </div>
125
136
  </div>
@@ -617,12 +617,18 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
617
617
  font-family:var(--font-body);font-size:15px;font-weight:400;text-transform:lowercase;
618
618
  color:var(--ink-soft);transition:color .2s cubic-bezier(.2,.7,.3,1);}
619
619
  .trig:hover{color:var(--ink);}
620
- .trig[aria-expanded="true"]{color:var(--ink);text-decoration:underline;text-decoration-color:var(--accent);
620
+ .trig[aria-expanded="true"],.trig[aria-current="true"]{color:var(--ink);text-decoration:underline;text-decoration-color:var(--accent);
621
621
  text-decoration-thickness:2px;text-underline-offset:7px;}
622
622
  /* the chevron — NOT .caret, which was the typing wordmark's blinking bar */
623
623
  .trig .cv{width:13px;height:13px;flex:none;fill:none;stroke:currentColor;stroke-width:1.4;
624
624
  stroke-linecap:round;stroke-linejoin:round;transition:transform .2s cubic-bezier(.2,.7,.3,1);}
625
625
  .trig[aria-expanded="true"] .cv{transform:rotate(180deg);}
626
+ /* the Community menu (task 1003363) is a trigger like Projects; on its own pages it is the
627
+ current section (aria-current="true"), underlined like an open menu. Under 480 the bar has
628
+ no room for it beside the map search, Projects and the chip, so its three items fold into
629
+ the Projects band as that band's last cells; above 480 those cells are not drawn (the
630
+ arrows skip them: hub-bar.js asks offsetParent) */
631
+ .bandFold{display:none;}
626
632
  /* the identity chip is the profile trigger: avatar + login + the chevron */
627
633
  .who{gap:9px;}
628
634
  .who img{width:26px;height:26px;border-radius:var(--radius-pill);object-fit:cover;flex:none;
@@ -756,6 +762,8 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
756
762
  .barLeft{gap:8px;}
757
763
  .barRight{gap:6px;}
758
764
  .signinDoor{padding:0 13px;}
765
+ .commTrig{display:none;}
766
+ .bandFold{display:block;}
759
767
  }
760
768
  @media (max-width:360px){
761
769
  /* the field measured 95px with the count and the switch beside it; the
@@ -77,7 +77,7 @@
77
77
  if (!open) return;
78
78
  if (ev.key === 'Escape') { closeMenus(); return; }
79
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]'); });
80
+ var items = Array.prototype.filter.call(open.querySelectorAll('a'), function (a) { return !a.closest('[hidden]') && a.offsetParent !== null; });
81
81
  if (!items.length) return;
82
82
  var at = items.indexOf(document.activeElement);
83
83
  ev.preventDefault();
@@ -0,0 +1,15 @@
1
+ {
2
+ "_": "The community page with nobody on any board (task 1003363): a hub on its first day (fixtures/community-leaderboard-empty.json). The board says so in one line instead of drawing empty rows.",
3
+ "page": "/community",
4
+ "surface": "public-landing",
5
+ "modes": ["dark"],
6
+ "modeQuery": false,
7
+ "stub": { "fixtures": { "community-leaderboard": "community-leaderboard-empty" } },
8
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/me/guild-", "/scouting$"],
9
+ "ignoreConsole": ["/api/(gds|bongos)/me", "/scouting", "\\b40[13]\\b", "Unauthorized", "Forbidden"],
10
+ "ignoreControls": [".vh"],
11
+ "states": {
12
+ "out-empty": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#boardEmpty", "#boardWindow"], "hidden": ["#boardFailed", "#boardLoading", "#find", "#recruit"], "count": [["#boardList .commRow", 0]], "text": [["#boardEmpty", "No builders on the leaderboard yet."]] } }
13
+ },
14
+ "reducedMotion": { "auth": false, "actions": [["wait", 400]] }
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "_": "The recruiter list for a viewer the recruiting door admits — a project owner (task 1003363, ADR 0337 D4.1): GET /scouting answers 200 (fixtures/scouting-recruiter.json), so /community/recruiters lists the builders who hide their numbers but are open to recruiters, by name, face and GitHub login only. The ranked `builders` half of the answer is never drawn.",
3
+ "page": "/community",
4
+ "surface": "public-landing",
5
+ "modes": ["dark"],
6
+ "modeQuery": false,
7
+ "stub": { "fixtures": { "scouting": "scouting-recruiter" } },
8
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/me/guild-"],
9
+ "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
10
+ "ignoreControls": [".vh"],
11
+ "states": {
12
+ "in-recruiter": { "auth": true, "url": "/community/recruiters", "actions": [["wait", 600]], "expect": { "visible": ["#recruit", "#recruitList"], "hidden": ["#board", "#find", "#recruitDoor", "#recruitClosed", "#recruitLoading", "#recruitEmpty"], "count": [["#recruitList .mineRow", 3], ["#recruitList .mineRow__hint", 0]], "text": [["#recruitList .mineRow:first-child .mineRow__name", "Kemi Adeyemi"], ["#recruitList .mineRow:first-child .mineRow__host", "GitHub: kemi-a"], ["#recruitList .mineRow:last-child .mineRow__name", "yuki-t"]] } }
13
+ },
14
+ "reducedMotion": { "auth": true, "url": "/community/recruiters", "actions": [["wait", 600]] }
15
+ }
@@ -0,0 +1,469 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-mode="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <!-- THE COMMUNITY PAGES (task 1003363; ADR 0217 D1, ADR 0337 D1/D3/D4.1, the community
7
+ spec docs/specs/bongos-v1-community-tab.md D1/D3/D4/D8). Who is building, across every
8
+ project, as three sub-pages of the bar's Community menu (owner, 2026-10-27), all served
9
+ from this one file: /community is the leaderboard, /community/find the builder search,
10
+ /community/recruiters the builders who hide their numbers but are open to recruiters.
11
+ The script shows the section the path names and marks it current in the menu.
12
+ VIEWING IS UNGATED (ADR 0217 D1): the leaderboard and the search are the same for every
13
+ viewer, signed in or not. Both reads are public, viewer-independent routes
14
+ (modules/platform-identity/routes/community-leaderboard.js, community-search.js),
15
+ which decide WHO appears through account-visibility.js; these pages add no rule of
16
+ their own. The one viewer-relative page is the recruiter list: drawn only from a 200 of
17
+ GET /scouting, whose door (a project owner, or project.curate) is the server's, never
18
+ this page's (scouting-authz.js). A 401 or 403 draws no row.
19
+ Hub-only, like /account: platform-server.js serves /community only where
20
+ platform-identity is on. noindex until the SEO goal decides (owner, 2026-10-27).
21
+ Server-stamped like the footer doors: {{…}} tokens filled by landingPage(). -->
22
+ <title>{{productName}} — Community</title>
23
+ <meta name="description" content="Who is building on {{productName}}: the leaderboard across every project, and a search for builders by name.">
24
+ <meta name="robots" content="noindex">
25
+ <link rel="preconnect" href="https://fonts.googleapis.com">
26
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
27
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
28
+ <link rel="stylesheet" href="/assets/cosmos.css">
29
+ <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
30
+ <style>
31
+ /* The pages' own frame. No top block (the /design floor, owner 2026-10-23): the bar's
32
+ Community menu names the section and marks the sub-page, so each page starts at its one
33
+ section and keeps a visually hidden h1. Rows, fields and buttons are cosmos.css's own
34
+ rules (.mineRows, .mineJoinRow, .btn); no :root block — the pack's theme owns the tokens. */
35
+ /* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
36
+ .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;
37
+ font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
38
+ border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
39
+ .acct{position:relative;z-index:2;max-width:1060px;margin:0 auto;padding:0 clamp(16px,4vw,40px) 56px;}
40
+ .acct .lede{margin:0 0 14px;max-width:62ch;font-size:15px;line-height:1.55;color:var(--ink-soft);}
41
+ /* one section per sub-page, right under the bar's hairline, so it draws no second one */
42
+ .mineExtra>section{margin-top:0;border-top:0;padding-top:clamp(18px,3vw,30px);}
43
+ /* the leaderboard's filters on one line: the craft as lowercase text, the pressed one
44
+ underlined in the accent (the bar's open-trigger grammar), and the window as a dropdown */
45
+ .commFilters{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 24px;margin:0 0 2px;}
46
+ .commTabs{display:flex;flex-wrap:wrap;gap:0 18px;}
47
+ .commTab{min-height:44px;padding:0;cursor:pointer;background:none;border:0;border-radius:var(--radius-sm);
48
+ font-family:var(--font-body);font-size:14.5px;text-transform:lowercase;color:var(--ink-soft);
49
+ transition:color .2s cubic-bezier(.2,.7,.3,1);}
50
+ .commTab:hover{color:var(--ink);}
51
+ .commTab[aria-pressed="true"]{color:var(--ink);text-decoration:underline;text-decoration-color:var(--accent);
52
+ text-decoration-thickness:2px;text-underline-offset:7px;}
53
+ .commTab:focus-visible{outline:2px solid var(--focus);outline-offset:3px;box-shadow:0 0 0 8px var(--bg);}
54
+ .commNote{margin:6px 0 4px;max-width:62ch;font-size:13.5px;line-height:1.5;color:var(--ink-soft);}
55
+ .commMsg{margin:10px 0 0;max-width:62ch;font-size:14.5px;line-height:1.55;color:var(--ink-soft);}
56
+ /* one place on the board: the place, the face, the name over the @handle and the
57
+ works and karma, then the credits — the board's order — big on the right */
58
+ .commRows{list-style:none;margin:8px 0 0;padding:0;}
59
+ .commRow{display:grid;grid-template-columns:3ch 36px minmax(0,1fr) auto;align-items:center;gap:0 14px;
60
+ padding:14px 0;border-top:1px solid var(--rule-soft);}
61
+ .commRows>.commRow:first-child{border-top:0;}
62
+ .commPlace{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-faint);font-variant-numeric:tabular-nums;text-align:right;}
63
+ .commCredits{text-align:right;}
64
+ .commNum{display:block;font-family:var(--font-display);font-weight:300;font-size:22px;letter-spacing:-.02em;line-height:1.1;
65
+ color:var(--ink);font-variant-numeric:tabular-nums;}
66
+ .commUnit{display:block;margin-top:2px;font-family:var(--label-face);font-size:11.5px;text-transform:lowercase;color:var(--ink-faint);}
67
+ /* a builder as a row (the connections page's voice): avatar, then the name over the handle */
68
+ .connWho{display:flex;align-items:center;gap:12px;min-width:0;flex:1 1 auto;}
69
+ .connAv{width:36px;height:36px;border-radius:50%;flex:none;object-fit:cover;background:color-mix(in srgb,var(--ink) 12%,transparent);}
70
+ .commRow .mineRow__name,.connWho .mineRow__name{overflow-wrap:anywhere;}
71
+ /* the two dropdowns (the window, the search's craft): the field's pill */
72
+ .commWhen select,.commFind select{flex:0 1 auto;min-height:44px;padding:0 14px;font:inherit;font-size:14.5px;color:var(--ink);
73
+ background:var(--bg-card);border:1px solid var(--ctl-line);border-radius:var(--radius-pill);cursor:pointer;}
74
+ .commWhen select:focus-visible,.commFind input:focus-visible,.commFind select:focus-visible{outline:2px solid var(--focus);outline-offset:3px;box-shadow:0 0 0 8px var(--bg);}
75
+ #findStatus{margin-top:8px;}
76
+ /* the recruiter list, signed out: the account page's door */
77
+ .commDoor{padding:8px 0 10px;max-width:62ch;}
78
+ .commDoor p{margin:0 0 18px;font-size:15px;line-height:1.55;color:var(--ink-soft);}
79
+ .commDoor .gh{width:18px;height:18px;fill:currentColor;}
80
+ .commDoor .btn{text-transform:none;}
81
+ @media (max-width:480px){
82
+ .commRow{grid-template-columns:3ch 32px minmax(0,1fr) auto;gap:0 10px;}
83
+ .commRow .connAv{width:32px;height:32px;}
84
+ .commNum{font-size:19px;}
85
+ }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <!-- THE BAR and its two bands are the hub's own (projects.html, task 1004455):
90
+ same markup, same cosmos.css rules. No map search (it only filters the map), and
91
+ the band links name /projects in full because this page has no views of its own.
92
+ assets/hub-bar.js runs it, shared with /account, /invitations, /connections and
93
+ the three footer doors. -->
94
+ <a class="skip vh" href="#comm">Skip to content</a>
95
+ <header class="shell bar">
96
+ <div class="barLeft">
97
+ <a class="barMark" id="barMark" href="/">
98
+ <svg class="bongosMark" viewBox="0 0 200 200" aria-hidden="true">
99
+ <path class="mr" d="M137 31 A78 78 0 1 0 170 66" fill="none" stroke="#17181a" stroke-width="15" stroke-linecap="round"/>
100
+ <circle class="mp" cx="100" cy="100" r="55" fill="#17181a"/>
101
+ <circle cx="156" cy="46" r="12" style="fill:var(--accent)"/>
102
+ </svg>
103
+ <span>{{productName}}</span>
104
+ </a>
105
+ </div>
106
+ <div class="barRight">
107
+ <button class="trig commTrig" type="button" id="commTrig" data-menu="community" aria-expanded="false" aria-controls="menuCommunity" aria-current="true">Community<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
108
+ <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>
109
+ <!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from GET /me.
110
+ Signed out it stays hidden and the sign-in door takes its slot. -->
111
+ <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>
112
+ <a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/community&amp;repos=1">Sign in</a>
113
+ </div>
114
+ </header>
115
+
116
+ <div class="menuBand" id="menuCommunity" data-menu="community" hidden>
117
+ <div class="bandIn">
118
+ <ul class="bandRow">
119
+ <li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
120
+ <li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
121
+ <li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
122
+ </ul>
123
+ </div>
124
+ </div>
125
+ <div class="menuBand" id="menuProjects" data-menu="projects" hidden>
126
+ <div class="bandIn">
127
+ <ul class="bandRow">
128
+ <li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
129
+ <li class="bandCell"><a class="bandT" href="/projects?view=map&amp;sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
130
+ <li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
131
+ <li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
132
+ <li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
133
+ </ul>
134
+ </div>
135
+ </div>
136
+ <div class="menuBand" id="menuProfile" data-menu="profile" hidden>
137
+ <div class="bandIn bandProfile">
138
+ <p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
139
+ <ul class="bandRow">
140
+ <li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
141
+ <li class="bandCell"><a class="bandT" href="/invitations">Invitations</a></li><li class="bandCell"><a class="bandT" href="/connections">Connections</a></li><li class="bandCell"><a class="bandT" href="/account">Account</a></li>
142
+ <!-- the honest hover target; the delegated click upgrades it to the POST -->
143
+ <li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
144
+ </ul>
145
+ </div>
146
+ </div>
147
+
148
+ <main class="acct" id="comm">
149
+ <h1 class="vh">Community</h1>
150
+
151
+ <!-- THREE SUB-PAGES, ONE FILE: every section is real markup; the script un-hides the one
152
+ the path names (/community, /community/find, /community/recruiters). -->
153
+ <div class="mineExtra">
154
+ <!-- THE LEADERBOARD (spec D3): GET /community/leaderboard, one window and one craft
155
+ at a time. The server ranks; this page draws its rows in the order and with the
156
+ `place` it was given, and every count it prints is the length of a list. -->
157
+ <section id="board" data-view="leaderboard" aria-labelledby="boardH">
158
+ <h2 id="boardH">Leaderboard</h2>
159
+ <div class="commFilters">
160
+ <div class="commTabs" id="craftTabs" role="group" aria-label="Role">
161
+ <button class="commTab" type="button" data-craft="" aria-pressed="true">Everyone</button>
162
+ <button class="commTab" type="button" data-craft="engineer" aria-pressed="false">Engineers</button>
163
+ <button class="commTab" type="button" data-craft="artist" aria-pressed="false">Artists</button>
164
+ <button class="commTab" type="button" data-craft="ideator" aria-pressed="false">Ideators</button>
165
+ <button class="commTab" type="button" data-craft="ui" aria-pressed="false">Designers</button>
166
+ </div>
167
+ <div class="commWhen">
168
+ <label class="vh" for="boardWindow">Over</label>
169
+ <select id="boardWindow">
170
+ <option value="7d">Last 7 days</option>
171
+ <option value="30d">Last 30 days</option>
172
+ <option value="365d">Past year</option>
173
+ <option value="all" selected>All time</option>
174
+ </select>
175
+ </div>
176
+ </div>
177
+ <p class="commNote" id="boardNote">{{Currency}} earned across every project, since each builder started.</p>
178
+ <p class="commMsg" id="boardLoading" role="status">Reading the leaderboard…</p>
179
+ <p class="projMsg err" id="boardFailed" role="alert" hidden></p>
180
+ <p class="commMsg" id="boardEmpty" hidden></p>
181
+ <ol class="commRows" id="boardList" data-unit="{{currency}}" aria-labelledby="boardH"></ol>
182
+ <p class="commNote" id="boardFine">A project reports a builder’s numbers when they sign in or ship there, so a window counts work when it was reported, not when it was done.</p>
183
+ </section>
184
+
185
+ <!-- THE SEARCH (spec D4): GET /community/search, a name and an optional craft. A
186
+ builder who hides their numbers is still found by name, and drops out once a
187
+ craft is chosen — the server's rule (builder-search.js), never this page's. -->
188
+ <section id="find" data-view="find" aria-labelledby="findH" hidden>
189
+ <h2 id="findH">Find a builder</h2>
190
+ <form class="mineJoinRow commFind" id="findForm" role="search" aria-labelledby="findH">
191
+ <label class="vh" for="findQ">Their name or handle</label>
192
+ <input id="findQ" type="search" placeholder="Their name or handle" maxlength="100" autocomplete="off" autocapitalize="off" spellcheck="false">
193
+ <label class="vh" for="findCraft">Role</label>
194
+ <select id="findCraft">
195
+ <option value="">Any role</option>
196
+ <option value="engineer">Engineers</option>
197
+ <option value="artist">Artists</option>
198
+ <option value="ideator">Ideators</option>
199
+ <option value="ui">Designers</option>
200
+ </select>
201
+ <button id="findBtn" type="submit" class="btn small">Search</button>
202
+ </form>
203
+ <p class="projMsg" id="findStatus" role="status" aria-live="polite"></p>
204
+ <ul class="mineRows" id="findList"></ul>
205
+ </section>
206
+
207
+ <!-- THE RECRUITER LIST (ADR 0337 D4.1, ADR 0334): builders who hide their numbers but
208
+ are open to recruiters, by name, face and login only — GET /scouting's `name_only`,
209
+ alphabetical as the server sends it. Rows come ONLY from a 200: the door is the
210
+ route's. Signed out it is a sign-in door, a viewer the route refuses is told the
211
+ list is for project owners, and neither is told who is on it. The ranked
212
+ `builders` half of the answer is never drawn; the leaderboard shows those numbers
213
+ to everyone. -->
214
+ <section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>
215
+ <h2 id="recruitH">Open to recruiters</h2>
216
+ <p class="lede">Builders who keep their numbers hidden but are open to hearing from projects. Only project owners see who they are, and only by name.</p>
217
+ <p class="commMsg" id="recruitLoading" role="status">Checking whether you can see this list…</p>
218
+ <div class="commDoor" id="recruitDoor" hidden>
219
+ <p>Sign in to see whether this list is open to you.</p>
220
+ <a class="btn" id="recruitSignin" href="/api/bongos/auth/web/start?return=/community/recruiters&amp;repos=1">
221
+ <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23 .82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2 .82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12 .51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29 .25.54.73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15 .46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
222
+ Sign in with GitHub
223
+ </a>
224
+ </div>
225
+ <p class="commMsg" id="recruitClosed" hidden>Only project owners can see this list.</p>
226
+ <p class="projMsg err" id="recruitFailed" role="alert" hidden></p>
227
+ <p class="commMsg" id="recruitEmpty" hidden>Nobody is on this list right now.</p>
228
+ <ul class="mineRows" id="recruitList"></ul>
229
+ </section>
230
+ </div>
231
+ </main>
232
+
233
+ <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;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>
234
+
235
+ <script src="/assets/hub-bar.js"></script>
236
+ <script>
237
+ (function () {
238
+ 'use strict';
239
+ var API = '/api/bongos';
240
+ // Boundary-forced copies of the routes' own vocabulary and limits
241
+ // (modules/platform-identity/community-leaderboard.js WINDOWS, builder-search.js
242
+ // MIN_QUERY_CHARS / MAX_QUERY_CHARS); tests/hub_community_page.mjs holds them equal.
243
+ var WINDOWS = ['7d', '30d', '365d', 'all'];
244
+ var QUERY_MIN = 2;
245
+ var QUERY_MAX = 100;
246
+ var CRAFT_WORD = { engineer: 'engineer', artist: 'artist', ideator: 'ideator', ui: 'designer' };
247
+ // The three sub-pages this file is served as (platform-server.js's /community routes);
248
+ // the path picks one, and its menu items are marked current.
249
+ var VIEWS = {
250
+ leaderboard: { path: '/community', section: 'board', title: 'Leaderboard' },
251
+ find: { path: '/community/find', section: 'find', title: 'Find a builder' },
252
+ recruiters: { path: '/community/recruiters', section: 'recruit', title: 'Open to recruiters' },
253
+ };
254
+ function $(id) { return document.getElementById(id); }
255
+ // The bar's chip, bands and sign out are assets/hub-bar.js (task 1004489); its
256
+ // `ready` settles with who GET /me says is signed in. Only the recruiter list asks.
257
+ var bar = window.HubBar || { paint: function () {}, ready: Promise.resolve(null) };
258
+ var UNIT = $('boardList').getAttribute('data-unit') || 'credits';
259
+
260
+ /* THREE THINGS THESE PAGES MUST NOT DO (ADR 0217 D1, ADR 0337 D4.1, ADR 0255):
261
+ 1. differ for a viewer anywhere but the recruiter list — the leaderboard and the
262
+ search are drawn from viewer-independent reads, and neither asks /me;
263
+ 2. draw a recruiter row from anything but a 200 of GET /scouting, or draw more of a
264
+ name-only row than its name, face and login;
265
+ 3. re-rank, re-filter or re-count what the server sent — rows are drawn in the
266
+ order and with the `place` they came with, and a count is a list's length. */
267
+
268
+ function el(tag, cls, text) {
269
+ var e = document.createElement(tag);
270
+ if (cls) e.className = cls;
271
+ if (text != null) e.textContent = text;
272
+ return e;
273
+ }
274
+ function arr(v) { return Array.isArray(v) ? v : []; }
275
+ function num(v) { var n = Number(v); return isFinite(n) ? n : 0; }
276
+ function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
277
+ function safeHttpUrl(raw) {
278
+ try {
279
+ var u = new URL(String(raw));
280
+ return (u.protocol === 'https:' || u.protocol === 'http:') ? u.href : '';
281
+ } catch (e) { return ''; }
282
+ }
283
+ function read(path) {
284
+ return fetch(API + path, { credentials: 'same-origin' }).then(function (res) {
285
+ return res.json().catch(function () { return {}; }).then(function (data) {
286
+ return { ok: res.ok, status: res.status, data: data || {} };
287
+ });
288
+ }).catch(function () { return { ok: false, status: 0, data: {} }; });
289
+ }
290
+ function failText(status, what) {
291
+ if (status === 429) return 'Too many requests from your network just now. Wait a minute, then try again.';
292
+ return what + ' could not be read. Reload the page to try again.';
293
+ }
294
+ function viewOf(pathname) {
295
+ var m = String(pathname || '').replace(/\/+$/, '').match(/^\/community(?:\/(find|recruiters))?$/);
296
+ return m ? (m[1] || 'leaderboard') : 'leaderboard';
297
+ }
298
+
299
+ // ── one builder: the avatar, then the name over the @handle ─────────────────
300
+ // No link: there is no public profile page to point at yet (/u/<handle> is
301
+ // unbuilt, canonical-profile.js).
302
+ function face(p) {
303
+ var src = safeHttpUrl(p.avatar_url);
304
+ var av;
305
+ if (src) { av = el('img', 'connAv'); av.src = src; av.alt = ''; }
306
+ else { av = el('span', 'connAv'); av.setAttribute('aria-hidden', 'true'); }
307
+ return av;
308
+ }
309
+ function nameBlock(p, sub) {
310
+ var main = el('div', 'mineRow__main');
311
+ main.appendChild(el('div', 'mineRow__name', p.display_name || p.handle || p.github_login || 'Builder'));
312
+ if (sub) main.appendChild(el('span', 'mineRow__host', sub));
313
+ return main;
314
+ }
315
+ function crafts(list) {
316
+ return arr(list).map(function (d) { return CRAFT_WORD[d] || String(d); }).join(' · ');
317
+ }
318
+
319
+ // ── the leaderboard ─────────────────────────────────────────────────────────
320
+ var board = { window: 'all', craft: '' };
321
+ var boardSeq = 0;
322
+ var NOTE = {
323
+ '7d': ' earned in the last 7 days, across every project.',
324
+ '30d': ' earned in the last 30 days, across every project.',
325
+ '365d': ' earned in the past year, across every project.',
326
+ all: ' earned across every project, since each builder started.',
327
+ };
328
+ function capital(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; }
329
+ function boardRow(r) {
330
+ var li = el('li', 'commRow');
331
+ li.appendChild(el('span', 'commPlace', String(num(r.place))));
332
+ li.appendChild(face(r));
333
+ var main = nameBlock(r, r.handle ? '@' + r.handle : '');
334
+ main.appendChild(el('p', 'mineRow__hint', plural(num(r.tasks_shipped), 'work') + ' shipped · ' + num(r.karma) + ' karma'));
335
+ li.appendChild(main);
336
+ var credits = el('div', 'commCredits');
337
+ credits.appendChild(el('span', 'commNum', num(r.credits).toLocaleString()));
338
+ credits.appendChild(el('span', 'commUnit', UNIT));
339
+ li.appendChild(credits);
340
+ return li;
341
+ }
342
+ function emptyText() {
343
+ var who = (board.craft ? CRAFT_WORD[board.craft] || board.craft : 'builder') + 's';
344
+ if (board.window === 'all') return 'No ' + who + ' on the leaderboard yet.';
345
+ return 'No ' + who + ' earned ' + UNIT + ' in this window yet.';
346
+ }
347
+ function paintBoard(results) {
348
+ var list = $('boardList');
349
+ list.textContent = '';
350
+ results.forEach(function (r) { if (r) list.appendChild(boardRow(r)); });
351
+ $('boardEmpty').textContent = list.children.length ? '' : emptyText();
352
+ $('boardEmpty').hidden = !!list.children.length;
353
+ }
354
+ function loadBoard() {
355
+ var seq = ++boardSeq;
356
+ Array.prototype.forEach.call($('craftTabs').querySelectorAll('.commTab'), function (b) {
357
+ b.setAttribute('aria-pressed', String(b.getAttribute('data-craft') === board.craft));
358
+ });
359
+ $('boardWindow').value = board.window;
360
+ $('boardNote').textContent = capital(UNIT) + NOTE[board.window];
361
+ $('boardLoading').hidden = false;
362
+ $('boardFailed').hidden = true;
363
+ var q = '?window=' + encodeURIComponent(board.window) + (board.craft ? '&discipline=' + encodeURIComponent(board.craft) : '');
364
+ return read('/community/leaderboard' + q).then(function (r) {
365
+ if (seq !== boardSeq) return; // a later pick answered first; its rows win
366
+ $('boardLoading').hidden = true;
367
+ if (!r.ok) {
368
+ $('boardList').textContent = '';
369
+ $('boardEmpty').hidden = true;
370
+ $('boardFailed').textContent = failText(r.status, 'The leaderboard');
371
+ $('boardFailed').hidden = false;
372
+ return;
373
+ }
374
+ paintBoard(arr(r.data.results));
375
+ });
376
+ }
377
+ $('boardWindow').addEventListener('change', function () {
378
+ var w = $('boardWindow').value;
379
+ if (WINDOWS.indexOf(w) < 0 || w === board.window) return;
380
+ board.window = w;
381
+ loadBoard();
382
+ });
383
+ $('craftTabs').addEventListener('click', function (ev) {
384
+ var b = ev.target.closest && ev.target.closest('[data-craft]');
385
+ if (!b || b.getAttribute('data-craft') === board.craft) return;
386
+ board.craft = b.getAttribute('data-craft');
387
+ loadBoard();
388
+ });
389
+
390
+ // ── the search ──────────────────────────────────────────────────────────────
391
+ function say(text, kind) {
392
+ var s = $('findStatus');
393
+ s.textContent = text || '';
394
+ s.className = 'projMsg' + (kind ? ' ' + kind : '');
395
+ }
396
+ function findRow(r) {
397
+ var li = el('li', 'mineRow');
398
+ var who = el('div', 'connWho');
399
+ who.appendChild(face(r));
400
+ var main = nameBlock(r, r.handle ? '@' + r.handle : '');
401
+ var c = crafts(r.disciplines);
402
+ if (c) main.appendChild(el('p', 'mineRow__hint', c));
403
+ who.appendChild(main);
404
+ li.appendChild(who);
405
+ return li;
406
+ }
407
+ function find() {
408
+ var q = String($('findQ').value || '').trim();
409
+ var craft = $('findCraft').value || '';
410
+ var list = $('findList');
411
+ if (q.length < QUERY_MIN) { list.textContent = ''; say('Type at least ' + QUERY_MIN + ' letters of a name.', 'err'); return Promise.resolve(); }
412
+ if (q.length > QUERY_MAX) { list.textContent = ''; say('A search is at most ' + QUERY_MAX + ' characters.', 'err'); return Promise.resolve(); }
413
+ var btn = $('findBtn');
414
+ btn.disabled = true;
415
+ say('Searching…');
416
+ var path = '/community/search?q=' + encodeURIComponent(q) + (craft ? '&discipline=' + encodeURIComponent(craft) : '');
417
+ return read(path).then(function (r) {
418
+ btn.disabled = false;
419
+ list.textContent = '';
420
+ if (!r.ok) { say(failText(r.status, 'The search'), 'err'); return; }
421
+ arr(r.data.results).forEach(function (p) { if (p) list.appendChild(findRow(p)); });
422
+ var n = list.children.length;
423
+ say(n ? plural(n, 'builder') + ' found.' : 'No builder by that name' + (craft ? ' in that role' : '') + '.');
424
+ });
425
+ }
426
+ $('findForm').addEventListener('submit', function (ev) { ev.preventDefault(); find(); });
427
+
428
+ // ── the recruiter list: signed in, and admitted by GET /scouting's own door ──
429
+ var RECRUIT_STATES = ['recruitLoading', 'recruitDoor', 'recruitClosed', 'recruitFailed', 'recruitEmpty'];
430
+ function showRecruit(id) { RECRUIT_STATES.forEach(function (s) { $(s).hidden = s !== id; }); }
431
+ function recruitRow(p) {
432
+ var li = el('li', 'mineRow');
433
+ var who = el('div', 'connWho');
434
+ who.appendChild(face(p));
435
+ who.appendChild(nameBlock({ display_name: p.display_name, github_login: p.github_login }, p.github_login ? 'GitHub: ' + p.github_login : ''));
436
+ li.appendChild(who);
437
+ return li;
438
+ }
439
+ function loadRecruit() {
440
+ return read('/scouting').then(function (r) {
441
+ var list = $('recruitList');
442
+ list.textContent = '';
443
+ if (r.status === 401) { showRecruit('recruitDoor'); return; }
444
+ if (r.status === 403) { showRecruit('recruitClosed'); return; }
445
+ if (!r.ok) { $('recruitFailed').textContent = failText(r.status, 'This list'); showRecruit('recruitFailed'); return; }
446
+ arr(r.data.name_only).forEach(function (p) { if (p) list.appendChild(recruitRow(p)); });
447
+ showRecruit(list.children.length ? '' : 'recruitEmpty');
448
+ });
449
+ }
450
+
451
+ // ── the sub-page the path names: shown, named, marked current, and read ──────
452
+ var VIEW = viewOf(window.location.pathname);
453
+ var view = VIEWS[VIEW];
454
+ Object.keys(VIEWS).forEach(function (k) { $(VIEWS[k].section).hidden = k !== VIEW; });
455
+ // its item in the Community band and its folded twin in the Projects band
456
+ Array.prototype.forEach.call(document.querySelectorAll('a[href="' + view.path + '"]'), function (a) {
457
+ a.setAttribute('aria-current', 'page');
458
+ });
459
+ document.title = document.title + ' · ' + view.title;
460
+ $('signinTop').setAttribute('href', '/api/bongos/auth/web/start?return=' + view.path + '&repos=1');
461
+ if (VIEW === 'leaderboard') loadBoard();
462
+ if (VIEW === 'recruiters') bar.ready.then(function (me) { if (me) loadRecruit(); else showRecruit('recruitDoor'); });
463
+ })();
464
+ </script>
465
+ <script src="/assets/invite-badge.js"></script>
466
+ <!-- the Artist review button: signed-in builders only (task 1004499) -->
467
+ <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
468
+ </body>
469
+ </html>