@bongos/core 1.19.538 → 1.19.540
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 +44 -29
- package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +115 -0
- package/docs/adr/README.md +2 -0
- package/docs/copy-inventory.md +168 -158
- package/docs/copy-registry.json +304 -202
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/hall-kit.js +105 -6
- package/modules/hall-ui/public/profile-charts.js +39 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +8 -1
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/public-landing/public/assets/cosmos.css +18 -0
- package/modules/public-landing/public/projects.html +220 -1
- package/modules/public-landing/public/projects.states.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/me.js +42 -0
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +4 -1
- package/tests/hall_kit_charts.mjs +75 -0
- package/tests/hall_profile_world.mjs +4 -1
- package/tests/me_ship_days_bound.mjs +76 -0
- package/tests/project_invite_ui.mjs +502 -0
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +2 -2
|
@@ -1525,5 +1525,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1525
1525
|
landed since 1.19.536 with no explicit bump. run 33888812605. (task 1002620)
|
|
1526
1526
|
1.19.538 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1527
1527
|
landed since 1.19.537 with no explicit bump. run 33890191596. (task 1002620)
|
|
1528
|
+
1.19.539 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1529
|
+
landed since 1.19.538 with no explicit bump. run 33892948885. (task 1002620)
|
|
1530
|
+
1.19.540 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1531
|
+
landed since 1.19.539 with no explicit bump. run 33895602533. (task 1002620)
|
|
1528
1532
|
---------------------------------------------------------------------------
|
|
1529
1533
|
```
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// modules/hall-ui/public/hall-kit.js — the hall's shared UI component kit
|
|
2
2
|
// (task 2166, the hall UX-coherence program's foundation).
|
|
3
3
|
//
|
|
4
|
-
// One dependency-free IIFE exposing `window.OTBKit`: the SIX primitives
|
|
5
|
-
// page redesign (tasks 2169–2176) adopts instead of re-implementing —
|
|
6
|
-
// ledger, filter bar, tabs, section rail, empty/skeleton/collapsible
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// One dependency-free IIFE exposing `window.OTBKit`: the SIX layout primitives
|
|
5
|
+
// every page redesign (tasks 2169–2176) adopts instead of re-implementing —
|
|
6
|
+
// pager, ledger, filter bar, tabs, section rail, empty/skeleton/collapsible —
|
|
7
|
+
// plus, since task 1003445, the hall's CHART families: a horizontal bar series
|
|
8
|
+
// and a sparkline, the plotted time-axis charts, and (task 1003572) the
|
|
9
|
+
// year-grid contribution calendar. Before this file, the ledger+pager block
|
|
10
|
+
// existed in ~10 drifted copies across work.js / goals-page.js / builders.js;
|
|
11
|
+
// the section rail existed twice with different mechanics (Standing scrollspy vs
|
|
12
|
+
// Settings panel-switch), and the same bar series was hand-drawn three times.
|
|
10
13
|
//
|
|
11
14
|
// Contract:
|
|
12
15
|
// - Dependency-free like shell.js (own esc(), no window.OTB use) so load order
|
|
@@ -1030,6 +1033,100 @@
|
|
|
1030
1033
|
+ '</div>';
|
|
1031
1034
|
}
|
|
1032
1035
|
|
|
1036
|
+
// calendarGridHtml(days, opts) — a year of ship-days as a contribution calendar:
|
|
1037
|
+
// one cell per calendar day, BANDED by count rather than scaled, its reading a
|
|
1038
|
+
// per-cell title (plus an aria summary on the whole grid), not a crosshair. The
|
|
1039
|
+
// hall's THIRD chart family (task 1003572, criterion legibility-series-are-drawn):
|
|
1040
|
+
// OTBKit's other two shapes are a horizontal bar series and a plot on a time axis;
|
|
1041
|
+
// a 2-D year grid is neither, so it earns its own primitive rather than a hand-roll
|
|
1042
|
+
// at the page (modules/hall-ui/CLAUDE.md; tests/hall_audit.mjs section 6b).
|
|
1043
|
+
//
|
|
1044
|
+
// THE READING, decided and written down (the task asked which): a NON-picture reader
|
|
1045
|
+
// gets the exact figure two ways — each cell carries a `title` ("2026-09-03 · 3
|
|
1046
|
+
// ships" / "… · no ships"), and the grid carries an aria-label summarising the
|
|
1047
|
+
// window (active days + total ships). No table twin: unlike a line, a calendar's
|
|
1048
|
+
// value IS its position, so a per-cell title reads the same fact the picture shows,
|
|
1049
|
+
// and a 366-row table beneath would be longer than the thing it explains.
|
|
1050
|
+
//
|
|
1051
|
+
// `days`: [{ local_date:'YYYY-MM-DD', count }] — SPARSE (only days that had ships),
|
|
1052
|
+
// already resolved in the BUILDER's timezone server-side (GET /me). Empty → '' so
|
|
1053
|
+
// the caller renders its own empty state, never an empty grid.
|
|
1054
|
+
//
|
|
1055
|
+
// THE TIMEZONE TRAP the task names: local_date is already the builder's own day, so
|
|
1056
|
+
// the grid is built by ORDINAL arithmetic on the 'YYYY-MM-DD' parts (Date.UTC on the
|
|
1057
|
+
// parsed integers, floored to an epoch day) — never `new Date('2026-09-03')`, which
|
|
1058
|
+
// is UTC-midnight and would shift a cell a day west of Greenwich. Same epoch-day
|
|
1059
|
+
// floor cumulateByDay uses, so no cell moves relative to the other charts.
|
|
1060
|
+
function calendarGridHtml(days, opts = {}) {
|
|
1061
|
+
const list = (days || []).filter((d) => d && typeof d.local_date === 'string');
|
|
1062
|
+
const ymdToOrd = (ymd) => {
|
|
1063
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
1064
|
+
if (!m) return null;
|
|
1065
|
+
return Math.floor(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / 86400000);
|
|
1066
|
+
};
|
|
1067
|
+
const ordToYmd = (ord) => {
|
|
1068
|
+
const dt = new Date(ord * 86400000);
|
|
1069
|
+
return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(dt.getUTCDate()).padStart(2, '0')}`;
|
|
1070
|
+
};
|
|
1071
|
+
const counts = new Map();
|
|
1072
|
+
let maxOrd = null;
|
|
1073
|
+
let activeDays = 0;
|
|
1074
|
+
let totalShips = 0;
|
|
1075
|
+
for (const d of list) {
|
|
1076
|
+
const o = ymdToOrd(d.local_date);
|
|
1077
|
+
if (o == null) continue;
|
|
1078
|
+
const c = Math.max(0, Math.round(Number(d.count) || 0));
|
|
1079
|
+
if (c <= 0) continue;
|
|
1080
|
+
counts.set(o, (counts.get(o) || 0) + c);
|
|
1081
|
+
if (maxOrd == null || o > maxOrd) maxOrd = o;
|
|
1082
|
+
}
|
|
1083
|
+
if (maxOrd == null) return ''; // nothing real to draw → caller's empty state
|
|
1084
|
+
for (const [, c] of counts) { activeDays += 1; totalShips += c; }
|
|
1085
|
+
|
|
1086
|
+
// The grid ENDS at `today` (the builder's, when given) so a quiet recent week still
|
|
1087
|
+
// reads as time that passed, not a truncated chart; falls back to the last ship day.
|
|
1088
|
+
const todayOrd = (opts.today && ymdToOrd(opts.today)) || maxOrd;
|
|
1089
|
+
const endOrd = Math.max(todayOrd, maxOrd);
|
|
1090
|
+
const weeks = Math.max(1, Math.min(53, Math.round(Number(opts.weeks)) || 53));
|
|
1091
|
+
|
|
1092
|
+
// Columns are weeks, rows Sun..Sat. Epoch day 0 (1970-01-01) is a Thursday, so the
|
|
1093
|
+
// Sunday-indexed weekday is (ord + 4) mod 7. The last column is the week holding
|
|
1094
|
+
// endOrd; walk back `weeks` Sundays to the first cell.
|
|
1095
|
+
const dow = (o) => (((o + 4) % 7) + 7) % 7;
|
|
1096
|
+
const endSun = endOrd - dow(endOrd);
|
|
1097
|
+
const startSun = endSun - (weeks - 1) * 7;
|
|
1098
|
+
|
|
1099
|
+
// level 0 = empty; 1..5 map to the anti-gaming daily cap (streak.js caps a day at
|
|
1100
|
+
// 5), so the darkest band means "hit the day's max", not "over some threshold".
|
|
1101
|
+
const level = (c) => (c <= 0 ? 0 : Math.min(5, c));
|
|
1102
|
+
|
|
1103
|
+
let cells = '';
|
|
1104
|
+
for (let col = 0; col < weeks; col++) {
|
|
1105
|
+
for (let row = 0; row < 7; row++) {
|
|
1106
|
+
const o = startSun + col * 7 + row;
|
|
1107
|
+
if (o > endOrd) { cells += '<span class="kit-cal__cell kit-cal__cell--void" aria-hidden="true"></span>'; continue; }
|
|
1108
|
+
const c = counts.get(o) || 0;
|
|
1109
|
+
const ymd = ordToYmd(o);
|
|
1110
|
+
const title = c > 0 ? `${ymd} · ${c} ship${c === 1 ? '' : 's'}` : `${ymd} · no ships`;
|
|
1111
|
+
cells += `<span class="kit-cal__cell" data-level="${level(c)}" title="${esc(title)}"></span>`;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const summary = opts.ariaLabel
|
|
1116
|
+
|| `Ships per day over the last year: ${totalShips} ship${totalShips === 1 ? '' : 's'} across ${activeDays} day${activeDays === 1 ? '' : 's'}.`;
|
|
1117
|
+
// The legend is aria-hidden: it decorates the bands, it carries no fact the cell
|
|
1118
|
+
// titles and the summary do not already state in words.
|
|
1119
|
+
const legend = '<div class="kit-cal__key" aria-hidden="true"><span class="kit-cal__key-label">less</span>'
|
|
1120
|
+
+ [0, 1, 2, 3, 5].map((lv) => `<span class="kit-cal__cell" data-level="${lv}"></span>`).join('')
|
|
1121
|
+
+ '<span class="kit-cal__key-label">more</span></div>';
|
|
1122
|
+
// The grid is wide, so it gets its OWN horizontal strip (the section-rail idiom) —
|
|
1123
|
+
// it never widens the document (modules/hall-ui/CLAUDE.md "no sideways scroll").
|
|
1124
|
+
return `<div class="kit-cal" role="img" aria-label="${esc(summary)}">`
|
|
1125
|
+
+ `<div class="kit-cal__scroll"><div class="kit-cal__grid">${cells}</div></div>`
|
|
1126
|
+
+ legend
|
|
1127
|
+
+ '</div>';
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1033
1130
|
const api = {
|
|
1034
1131
|
esc,
|
|
1035
1132
|
// pager (low-level exports so pages can adopt incrementally)
|
|
@@ -1042,6 +1139,8 @@
|
|
|
1042
1139
|
// linePoints/cumulate are the maths on their own, for a chart that wants
|
|
1043
1140
|
// the geometry without this SVG (the profile's timeChart).
|
|
1044
1141
|
sparklineHtml, barSeriesHtml, chartCardHtml, linePoints, cumulate, cumulateByDay,
|
|
1142
|
+
// the year-grid contribution calendar (task 1003572) — the hall's third chart family
|
|
1143
|
+
calendarGridHtml,
|
|
1045
1144
|
// plotted charts: the axis frame + the two series shapes that ride it
|
|
1046
1145
|
plotSvgHtml, plotValueLabelHtml, timeStepSeriesHtml, timeStepGeometry, columnSeriesHtml,
|
|
1047
1146
|
// plotBox is exported so a caller mapping pointer x back into viewBox
|
|
@@ -258,6 +258,35 @@
|
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// The contribution calendar (task 1003572, criterion legibility-series-are-drawn):
|
|
262
|
+
// a year of qualifying ship-days as OTBKit's third chart family, with the streak
|
|
263
|
+
// scalars read beside it. SELF-ONLY and range-INDEPENDENT — the data is streak.
|
|
264
|
+
// ship_days off the viewer's own /me (task added it there), and a calendar is a
|
|
265
|
+
// year by definition, so it ignores the lookback the other figures clip to. Returns
|
|
266
|
+
// null on an empty history so draw() simply omits it — never an empty grid (the
|
|
267
|
+
// module doc's honest-exit rule; DONE WHEN #4).
|
|
268
|
+
function calendarChart({ id, streak }) {
|
|
269
|
+
const days = (streak && Array.isArray(streak.ship_days)) ? streak.ship_days : [];
|
|
270
|
+
const grid = window.OTBKit.calendarGridHtml(days, { today: streak && streak.today });
|
|
271
|
+
if (!grid) return null;
|
|
272
|
+
const active = days.filter((d) => Number(d.count) > 0).length;
|
|
273
|
+
const cur = Number(streak && streak.current) || 0;
|
|
274
|
+
const lng = Number(streak && streak.longest) || 0;
|
|
275
|
+
// The scalars in words, in the .pchart__read slot the other figures use for their
|
|
276
|
+
// crosshair readout — here it is static because the calendar's reading is per-cell.
|
|
277
|
+
const read = `Current streak ${cur} day${cur === 1 ? '' : 's'} \u00b7 longest ${lng} \u00b7 `
|
|
278
|
+
+ `${active} day${active === 1 ? '' : 's'} shipped this year`;
|
|
279
|
+
const html = `<figure class="pchart" id="${esc(id)}">
|
|
280
|
+
<figcaption class="pchart__title">Your shipping year</figcaption>
|
|
281
|
+
<p class="pchart__read">${esc(read)}</p>
|
|
282
|
+
${grid}
|
|
283
|
+
<p class="pchart__foot">a year of qualifying ships, in your timezone \u00b7 only you see this one</p>
|
|
284
|
+
</figure>`;
|
|
285
|
+
// No wire: the reading is the per-cell title + the grid's aria summary, not a
|
|
286
|
+
// crosshair, so there is no pointer handler to attach.
|
|
287
|
+
return { id, html, wire() {} };
|
|
288
|
+
}
|
|
289
|
+
|
|
261
290
|
// Weekly columns: at most 24px thick, a 4px rounded cap on a square base, a
|
|
262
291
|
// 2px surface gap between neighbours, the tallest column labelled.
|
|
263
292
|
function columnChart({ id, title, buckets, footer }) {
|
|
@@ -400,7 +429,7 @@
|
|
|
400
429
|
|
|
401
430
|
// Draw every figure for one lookback window into #charts-grid. Split from
|
|
402
431
|
// render() so the range buttons can re-draw without re-fetching the record.
|
|
403
|
-
async function draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId }) {
|
|
432
|
+
async function draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId, selfStreak }) {
|
|
404
433
|
const grid = $('#charts-grid');
|
|
405
434
|
if (!grid) return;
|
|
406
435
|
const range = RANGES.find((r) => r.id === rangeId) || RANGES[RANGES.length - 1];
|
|
@@ -431,6 +460,12 @@
|
|
|
431
460
|
footer: `${fmtNum(inWindow.length)} work${inWindow.length === 1 ? '' : 's'} · ${span}`,
|
|
432
461
|
}),
|
|
433
462
|
];
|
|
463
|
+
if (isOwn && selfStreak) {
|
|
464
|
+
// The calendar leads the viewer's own overview — the year-at-a-glance the goal's
|
|
465
|
+
// thesis is about. Null (no qualifying ships yet) → simply not shown.
|
|
466
|
+
const cal = calendarChart({ id: 'chart-calendar', streak: selfStreak });
|
|
467
|
+
if (cal) figs.unshift(cal);
|
|
468
|
+
}
|
|
434
469
|
if (isOwn) {
|
|
435
470
|
const paceDays = Math.min(range.days || PACE_MAX_DAYS, PACE_MAX_DAYS);
|
|
436
471
|
const own = await fetchOwnRate(api, API, builderId, paceDays);
|
|
@@ -448,7 +483,7 @@
|
|
|
448
483
|
figs.forEach((f) => { const el = grid.querySelector(`#${f.id}`); if (el) f.wire(el); });
|
|
449
484
|
}
|
|
450
485
|
|
|
451
|
-
async function render({ api, API, builderId, selfId, signedIn, now = Date.now() }) {
|
|
486
|
+
async function render({ api, API, builderId, selfId, signedIn, selfStreak = null, now = Date.now() }) {
|
|
452
487
|
const mount = $('#charts-grid');
|
|
453
488
|
const controls = $('#charts-controls');
|
|
454
489
|
if (!mount) return;
|
|
@@ -500,10 +535,10 @@
|
|
|
500
535
|
b.classList.toggle('is-active', on);
|
|
501
536
|
b.setAttribute('aria-pressed', String(on));
|
|
502
537
|
});
|
|
503
|
-
draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId });
|
|
538
|
+
draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId, selfStreak });
|
|
504
539
|
});
|
|
505
540
|
}
|
|
506
|
-
await draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId });
|
|
541
|
+
await draw({ api, API, builderId, isOwn, rows, paidFull, estimated, now, rangeId, selfStreak });
|
|
507
542
|
}
|
|
508
543
|
|
|
509
544
|
window.OTBProfileCharts = { render, cumulativeByDay, craftMix, weeklyBuckets, estimateOf, clipToWindow };
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
<!-- The charts (task 1003489). Loaded BEFORE profile.js so window.OTBProfileCharts
|
|
209
209
|
exists when renderProfile runs; profile.js guards on it, so a page that
|
|
210
210
|
omits this script simply has no charts section content. -->
|
|
211
|
-
<script src="/builders/profile-charts.js?v=2026-09-
|
|
211
|
+
<script src="/builders/profile-charts.js?v=2026-09-04-calendar"></script>
|
|
212
212
|
<script src="/builders/profile.js?v=2026-09-02-rail"></script>
|
|
213
213
|
</body>
|
|
214
214
|
</html>
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
// ({ status, ok, data }) preserves control flow 1:1. throwOnError:false so a
|
|
26
26
|
// 401/404 is a value to branch on, not an exception.
|
|
27
27
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
28
|
+
// The viewer's own /me payload, captured wherever it is fetched so the Overview's
|
|
29
|
+
// contribution calendar (task 1003572) can read streak.ship_days without a second
|
|
30
|
+
// /me round-trip. Null until /me resolves; only ever the VIEWER's own, never a
|
|
31
|
+
// profile being viewed by id.
|
|
32
|
+
let selfMe = null;
|
|
28
33
|
|
|
29
34
|
const { escapeHtml, $, fmtNum, fmtDate } = window.OTB;
|
|
30
35
|
const rankLabel = (rank) => window.OTB.rankLabel(rank);
|
|
@@ -656,7 +661,7 @@
|
|
|
656
661
|
// The charts (task 1003489) read the activity route and the credits ledger
|
|
657
662
|
// (task 1003491), so they need the same session the ledgers do; on the
|
|
658
663
|
// viewer's own profile they add the pace.
|
|
659
|
-
if (window.OTBProfileCharts) window.OTBProfileCharts.render({ api, API, builderId: builder.id, selfId, signedIn: selfId != null });
|
|
664
|
+
if (window.OTBProfileCharts) window.OTBProfileCharts.render({ api, API, builderId: builder.id, selfId, signedIn: selfId != null, selfStreak: (selfMe && selfMe.streak) || null });
|
|
660
665
|
renderCrossProject(data, selfId);
|
|
661
666
|
renderAchievements(data.achievements);
|
|
662
667
|
buildWorksLedger(builder.id, selfId != null, data.recent_ships);
|
|
@@ -722,6 +727,7 @@
|
|
|
722
727
|
try {
|
|
723
728
|
const me = await api.request('GET', `${API}/me`);
|
|
724
729
|
if (!me.ok) return null;
|
|
730
|
+
selfMe = me.data || null;
|
|
725
731
|
const builder = (me.data && me.data.builder) ? me.data.builder : me.data;
|
|
726
732
|
return (builder && builder.id) || null;
|
|
727
733
|
} catch (err) {
|
|
@@ -746,6 +752,7 @@
|
|
|
746
752
|
return;
|
|
747
753
|
}
|
|
748
754
|
if (!me.ok) throw new Error(`me → ${me.status}`);
|
|
755
|
+
selfMe = me.data || null;
|
|
749
756
|
const builder = (me.data && me.data.builder) ? me.data.builder : me.data;
|
|
750
757
|
const selfId = builder && builder.id;
|
|
751
758
|
if (!selfId) {
|
|
@@ -1988,6 +1988,37 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1988
1988
|
.kit-chart__empty { font-size: 12px; color: var(--ink-faint); }
|
|
1989
1989
|
.kit-chart__foot { font-size: 11.5px; color: var(--ink-faint); margin-top: 4px; }
|
|
1990
1990
|
|
|
1991
|
+
/* The contribution calendar — OTBKit.calendarGridHtml (task 1003572), the hall's
|
|
1992
|
+
THIRD chart family. A year grid of small cells, banded by ship count 0..5. Theme-
|
|
1993
|
+
aware by construction: every band is a color-mix off --accent and --surface-2, both
|
|
1994
|
+
theme tokens, so light and dark read without a second palette and level 5 is the
|
|
1995
|
+
full accent in either. The grid is wide, so .kit-cal__scroll holds it in its OWN
|
|
1996
|
+
horizontal strip — the document never scrolls sideways (hall-ui/CLAUDE.md). */
|
|
1997
|
+
.kit-cal { display: flex; flex-direction: column; gap: 6px; --kit-cal-cell: 11px; }
|
|
1998
|
+
.kit-cal__scroll { overflow-x: auto; padding-bottom: 2px; }
|
|
1999
|
+
.kit-cal__grid {
|
|
2000
|
+
display: grid;
|
|
2001
|
+
grid-template-rows: repeat(7, var(--kit-cal-cell));
|
|
2002
|
+
grid-auto-flow: column;
|
|
2003
|
+
grid-auto-columns: var(--kit-cal-cell);
|
|
2004
|
+
gap: 3px;
|
|
2005
|
+
width: max-content; /* intrinsic width; the scroll strip, not the document, holds it */
|
|
2006
|
+
}
|
|
2007
|
+
.kit-cal__cell {
|
|
2008
|
+
width: var(--kit-cal-cell); height: var(--kit-cal-cell);
|
|
2009
|
+
border-radius: 2px;
|
|
2010
|
+
background: var(--surface-2);
|
|
2011
|
+
}
|
|
2012
|
+
.kit-cal__cell[data-level="1"] { background: color-mix(in srgb, var(--accent) 25%, var(--surface-2)); }
|
|
2013
|
+
.kit-cal__cell[data-level="2"] { background: color-mix(in srgb, var(--accent) 45%, var(--surface-2)); }
|
|
2014
|
+
.kit-cal__cell[data-level="3"] { background: color-mix(in srgb, var(--accent) 65%, var(--surface-2)); }
|
|
2015
|
+
.kit-cal__cell[data-level="4"] { background: color-mix(in srgb, var(--accent) 82%, var(--surface-2)); }
|
|
2016
|
+
.kit-cal__cell[data-level="5"] { background: var(--accent); }
|
|
2017
|
+
.kit-cal__cell--void { background: transparent; }
|
|
2018
|
+
.kit-cal__key { display: flex; align-items: center; gap: 3px; font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-faint); }
|
|
2019
|
+
.kit-cal__key .kit-cal__cell { --kit-cal-cell: 10px; }
|
|
2020
|
+
.kit-cal__key-label { margin: 0 2px; }
|
|
2021
|
+
|
|
1991
2022
|
.kit-spark { width: 100%; height: 44px; display: block; }
|
|
1992
2023
|
/* The stroke/fill here are the FALLBACK: sparklineHtml writes the caller's
|
|
1993
2024
|
colour inline, because the Curve spends a different accent per chart. */
|
|
@@ -411,6 +411,16 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
411
411
|
task 1003355 moved it to the project's own page, where the section's heading
|
|
412
412
|
is the disclosure and the row it left carries only its two doors. */
|
|
413
413
|
.attachNote{margin:8px 0 0;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
414
|
+
/* invites filed in this session (task 1002346): a hairline ledger under the pair
|
|
415
|
+
above — one line per person, the name in the body voice, what happened next to
|
|
416
|
+
it in the faint ink. Shared by the wizard's step and the project's own page. */
|
|
417
|
+
.inviteList{list-style:none;display:grid;margin:12px 0 0;padding:0;}
|
|
418
|
+
.inviteList:empty{display:none;}
|
|
419
|
+
.inviteList>li{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;padding:10px 0 11px;border-top:1px solid var(--rule-soft);}
|
|
420
|
+
.inviteList>li:first-child{border-top:0;}
|
|
421
|
+
.inviteList>li::before{content:"";grid-column:1;grid-row:1;width:7px;height:7px;margin-top:7px;border-radius:50%;border:1px solid var(--accent);background:var(--bg);}
|
|
422
|
+
.inviteList b{grid-column:2;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
|
|
423
|
+
.inviteList small{grid-column:2;font-family:var(--font-body);font-size:13px;font-weight:400;line-height:1.45;color:var(--ink-faint);}
|
|
414
424
|
.attachNote code{font-family:var(--font-mono);}
|
|
415
425
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
416
426
|
.projMsg.ok{color:var(--ok);}
|
|
@@ -1516,6 +1526,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1516
1526
|
#view-new #wizSignin:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));}
|
|
1517
1527
|
/* the error alert: the error ink between two hairlines, no plate */
|
|
1518
1528
|
#view-new .alert{background:none;border:0;border-top:1px solid var(--err);border-bottom:1px solid var(--err);border-radius:0;padding:12px 0;font-weight:400;}
|
|
1529
|
+
/* the invite step (task 1002346): the page's own input+button pair, wearing the
|
|
1530
|
+
wizard's hairline field rather than the plated one — the .attach width has to
|
|
1531
|
+
give way to the row's flex, or #view-new's own width:100% overflows it */
|
|
1532
|
+
#view-new .attach{gap:10px 14px;margin-top:14px;align-items:center;}
|
|
1533
|
+
#view-new .attach input[type=text]{width:auto;flex:1;min-width:220px;}
|
|
1534
|
+
#view-new .inviteBox{margin-top:34px;}
|
|
1535
|
+
#view-new .inviteBox .finePrint{margin-top:10px;}
|
|
1536
|
+
#view-new #wizInviteLater{margin-top:14px;}
|
|
1519
1537
|
/* one fold for the whole wizard: 840 is where the page's bands and ledger stack
|
|
1520
1538
|
(cosmos's createSplit collapse), so the rail folds with them, not before them */
|
|
1521
1539
|
@media (max-width:840px){
|
|
@@ -849,6 +849,27 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
849
849
|
</details>
|
|
850
850
|
</div>
|
|
851
851
|
|
|
852
|
+
<!-- live since task 1002346 (BV1.R27): the way to invite a builder AFTER
|
|
853
|
+
creation. The wizard's invite step is skippable, and — the mModules
|
|
854
|
+
argument beside it — an optional step is a default rather than a dead end
|
|
855
|
+
only if there is a way back. The SAME widget on the SAME route
|
|
856
|
+
(POST /projects/invite), admitted here by the project's own ownership
|
|
857
|
+
(ADR 0248); like its neighbour above, nothing is pushed to the project and
|
|
858
|
+
nothing restarts, so there is no "what your project runs" read-back line. -->
|
|
859
|
+
<div class="card mSec" id="mInvite">
|
|
860
|
+
<h2>Invite a builder</h2>
|
|
861
|
+
<div id="mInv">
|
|
862
|
+
<p class="finePrint">By GitHub username. They get an invitation to accept from their own projects page, and they need to have signed in here at least once.</p>
|
|
863
|
+
<div class="attach">
|
|
864
|
+
<label class="vh" for="mInviteWho">GitHub username</label>
|
|
865
|
+
<input type="text" id="mInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
866
|
+
<button type="button" class="btn small" data-invite-send="mInvite">Invite</button>
|
|
867
|
+
</div>
|
|
868
|
+
<ul class="inviteList" id="mInviteList"></ul>
|
|
869
|
+
<div class="projMsg" id="mInviteMsg" role="status" aria-live="polite"></div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
852
873
|
<!-- the planet picker (task 1003347; direction record §16, v1 the bands): the
|
|
853
874
|
owner's own choice of how the platform draws this project — the sky,
|
|
854
875
|
the list, this page's head and the card read the same row. The pick is
|
|
@@ -1203,7 +1224,29 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1203
1224
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
1204
1225
|
and a live region that is itself replaced never announces -->
|
|
1205
1226
|
<div id="liveProgress" role="status" aria-live="polite"></div>
|
|
1206
|
-
|
|
1227
|
+
<!-- the invite step (task 1002346, BV1.R27) — OPTIONAL, and it stands HERE
|
|
1228
|
+
rather than in the numbered rail because an invite needs a project that
|
|
1229
|
+
already exists: the catalog row a client_id resolves is projected at
|
|
1230
|
+
create (ADR 0246), so before the Create step there is nothing to invite
|
|
1231
|
+
anyone TO. The whole reasoning is ADR 0249. Hidden until the created
|
|
1232
|
+
record carries an id and a slug; the script is the only thing that
|
|
1233
|
+
shows it. -->
|
|
1234
|
+
<div class="inviteBox" id="wizInvite" hidden>
|
|
1235
|
+
<h2 class="stepsHead">Invite builders</h2>
|
|
1236
|
+
<p class="finePrint">Optional — invite people by GitHub username. Each one gets an invitation to accept from their own projects page, and they need to have signed in here at least once. Skip this and invite anyone later; nothing about your project waits on it.</p>
|
|
1237
|
+
<div class="attach">
|
|
1238
|
+
<label class="vh" for="wizInviteWho">GitHub username</label>
|
|
1239
|
+
<input type="text" id="wizInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
1240
|
+
<button type="button" class="btn small" data-invite-send="wizInvite">Invite</button>
|
|
1241
|
+
<button type="button" class="btn small ghost" data-invite-skip="wizInvite">Skip</button>
|
|
1242
|
+
</div>
|
|
1243
|
+
<ul class="inviteList" id="wizInviteList"></ul>
|
|
1244
|
+
<div class="projMsg" id="wizInviteMsg" role="status" aria-live="polite"></div>
|
|
1245
|
+
</div>
|
|
1246
|
+
<!-- the way back, the doneModules idiom: an optional step is a default
|
|
1247
|
+
rather than a dead end only if the panel names where it lives after -->
|
|
1248
|
+
<p class="samePath" id="wizInviteLater"></p>
|
|
1249
|
+
<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play</h2>
|
|
1207
1250
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
1208
1251
|
<!-- the modules step was optional (task 1002342, BV1.R23) — one line
|
|
1209
1252
|
saying where it lives afterwards, so a skip is never a dead end -->
|
|
@@ -4098,6 +4141,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4098
4141
|
skippable, so this panel names where it lives afterwards rather than
|
|
4099
4142
|
leaving an owner to go looking for it. */
|
|
4100
4143
|
$('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
|
|
4144
|
+
/* the invite step (task 1002346, BV1.R27). The CREATED RECORD is the proof
|
|
4145
|
+
the widget needs and the only one this panel has: no signed-in identity,
|
|
4146
|
+
no filed instance or no slug ⇒ nothing to invite to, and the step stays
|
|
4147
|
+
hidden. It is deliberately NOT keyed on instanceId, which startProgress
|
|
4148
|
+
sets only AFTER this runs. */
|
|
4149
|
+
var created = createdRecord();
|
|
4150
|
+
setInviteSlug('wizInvite', (me && created && created.id && created.slug) ? created.slug : '');
|
|
4151
|
+
$('wizInviteLater').innerHTML = 'You can invite builders any time from ' + projectPageLink('the project’s own page') + '.';
|
|
4101
4152
|
renderSteps();
|
|
4102
4153
|
}
|
|
4103
4154
|
|
|
@@ -5280,6 +5331,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5280
5331
|
progress survives the tick because only a different project re-renders it */
|
|
5281
5332
|
renderPick(inst);
|
|
5282
5333
|
renderCardPick(inst);
|
|
5334
|
+
/* invite a builder (task 1002346): fetch-free too — the widget needs only the
|
|
5335
|
+
slug, and the API is the wall. This read is own-scoped and 404s for anyone
|
|
5336
|
+
who is not the owner, so reaching renderManage at all IS the ownership
|
|
5337
|
+
answer the widget does not ask for itself. */
|
|
5338
|
+
setInviteSlug('mInvite', inst.slug || '');
|
|
5283
5339
|
/* the page names its project — the heading holds #mName; and in the tab */
|
|
5284
5340
|
document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
|
|
5285
5341
|
/* the card's own story, on the project's own page (task 1003232): the
|
|
@@ -6692,6 +6748,169 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6692
6748
|
goToManage(m.getAttribute('data-manage'));
|
|
6693
6749
|
});
|
|
6694
6750
|
|
|
6751
|
+
/* ══ invite a builder (task 1002346, BV1.R27; goal 1000046 criterion C6, ADR 0249)
|
|
6752
|
+
ONE widget, mounted twice: the creation wizard's optional invite step (on the
|
|
6753
|
+
done panel — an invite needs a project that already exists) and the project's
|
|
6754
|
+
own page, so a skipped step is a default rather than a dead end.
|
|
6755
|
+
|
|
6756
|
+
THE ROUTE IS USED AS IT STANDS: POST /projects/invite { client_id, github_login },
|
|
6757
|
+
where client_id IS the project's slug — ADR 0246 projects a control-plane project
|
|
6758
|
+
onto the catalog under its slug — and the project's owner is admitted by ADR 0248's
|
|
6759
|
+
ownership arm. Nothing here is a second gate; the server owns the answer.
|
|
6760
|
+
|
|
6761
|
+
TWO QUESTIONS THIS WIDGET DELIBERATELY DOES NOT ASK: "is anyone signed in" and
|
|
6762
|
+
"is this their project". Each caller already knows, and hands over a slug or
|
|
6763
|
+
nothing — which is the ONE door in (setInviteSlug). A widget that re-derived
|
|
6764
|
+
ownership on the page would be a second, weaker copy of a server rule.
|
|
6765
|
+
|
|
6766
|
+
THE REFUSALS ARE SAID IN THE ROUTE'S OWN VOCABULARY, and the 403 is said
|
|
6767
|
+
CAREFULLY: it is byte-identical whether the project is real, stealth, or absent
|
|
6768
|
+
(ADR 0248), so this page may never word it as "it exists but you may not" — that
|
|
6769
|
+
would hand back the membership oracle the route equalised away.
|
|
6770
|
+
|
|
6771
|
+
IT NEVER BLOCKS CREATION. The project is filed before the step is shown at all;
|
|
6772
|
+
a refusal leaves the box exactly as it was, ready to try again.
|
|
6773
|
+
|
|
6774
|
+
WHAT IT PUBLISHES OF AN INVITEE is the 201 row's own membership_kind and the
|
|
6775
|
+
login the inviter typed — never the github_id the row carries, which belongs to
|
|
6776
|
+
someone who has accepted nothing yet.
|
|
6777
|
+
Proof: ../../../tests/project_invite_ui.mjs ══ */
|
|
6778
|
+
|
|
6779
|
+
var INVITE_MOUNTS = {
|
|
6780
|
+
wizInvite: { who: 'wizInviteWho', list: 'wizInviteList', msg: 'wizInviteMsg', after: 'doneStepsHead' },
|
|
6781
|
+
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null }
|
|
6782
|
+
};
|
|
6783
|
+
var inviteSlug = {}; /* mount id → the slug it invites to; '' means no step at all */
|
|
6784
|
+
var inviteRows = {}; /* mount id → the invites filed on THIS project this session */
|
|
6785
|
+
var inviteBusy = {}; /* a send is in flight — the mount is inert until it answers */
|
|
6786
|
+
|
|
6787
|
+
/* setInviteSlug(id, slug) — the only door in. A falsy slug closes the mount, which
|
|
6788
|
+
is how "signed out", "not the owner" and "no project yet" all reach this widget.
|
|
6789
|
+
A DIFFERENT slug starts clean: the pending lines belonged to the project they
|
|
6790
|
+
were filed on, and following them to another one would be a lie. */
|
|
6791
|
+
function setInviteSlug(id, slug) {
|
|
6792
|
+
var m = INVITE_MOUNTS[id];
|
|
6793
|
+
if (!m) return;
|
|
6794
|
+
var next = slug || '';
|
|
6795
|
+
if (inviteSlug[id] !== next) {
|
|
6796
|
+
inviteSlug[id] = next;
|
|
6797
|
+
inviteRows[id] = [];
|
|
6798
|
+
inviteSay(id, '', '');
|
|
6799
|
+
}
|
|
6800
|
+
var box = $(id);
|
|
6801
|
+
if (box) box.hidden = !next;
|
|
6802
|
+
renderInviteRows(id);
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6805
|
+
/* One line per invite this session filed: the login the inviter typed, and the
|
|
6806
|
+
WORD THE ROW ITSELF CARRIES — so a membership_kind other than 'pending' shows
|
|
6807
|
+
up here rather than being papered over as one. */
|
|
6808
|
+
function renderInviteRows(id) {
|
|
6809
|
+
var m = INVITE_MOUNTS[id];
|
|
6810
|
+
var list = m && $(m.list);
|
|
6811
|
+
if (!list) return;
|
|
6812
|
+
list.innerHTML = (inviteRows[id] || []).map(function (r) {
|
|
6813
|
+
var said = r.kind === 'pending' ? 'invited — waiting for them to accept.' : r.kind;
|
|
6814
|
+
return '<li><b>' + esc(r.login) + '</b><small>' + esc(said) + '</small></li>';
|
|
6815
|
+
}).join('');
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
function inviteSay(id, text, cls, isHtml) {
|
|
6819
|
+
var m = INVITE_MOUNTS[id];
|
|
6820
|
+
var el = m && $(m.msg);
|
|
6821
|
+
if (!el) return;
|
|
6822
|
+
el.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
6823
|
+
if (isHtml) el.innerHTML = text; else el.textContent = text;
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
/* inviteRefusal(code, status, message) — the route's refusals as one sentence
|
|
6827
|
+
each. PURE, so the harness reads it directly. A code this page does not know
|
|
6828
|
+
falls through to the SERVER'S own words rather than an invented reason. */
|
|
6829
|
+
function inviteRefusal(code, status, message) {
|
|
6830
|
+
if (status === 404 && code === 'account_not_found') return 'That builder hasn’t signed in here yet — ask them to sign in once, then invite them.';
|
|
6831
|
+
if (status === 404 && code === 'unknown_project') return 'This project isn’t on the map yet — give it a moment and try again.';
|
|
6832
|
+
/* deliberately says nothing about the project: see the header */
|
|
6833
|
+
if (status === 403) return 'This account can’t invite people here.';
|
|
6834
|
+
return message || 'The invitation couldn’t be sent — try again in a moment.';
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
/* doInvite(id) — one builder, one request. */
|
|
6838
|
+
function doInvite(id) {
|
|
6839
|
+
var m = INVITE_MOUNTS[id];
|
|
6840
|
+
var slug = inviteSlug[id] || '';
|
|
6841
|
+
if (!m || !slug || inviteBusy[id]) return;
|
|
6842
|
+
var field = $(m.who);
|
|
6843
|
+
var login = String((field && field.value) || '').replace(/^\s+|\s+$/g, '');
|
|
6844
|
+
if (!login) {
|
|
6845
|
+
inviteSay(id, 'Type the GitHub username of the person you want to invite.', 'err');
|
|
6846
|
+
if (field && field.focus) field.focus();
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
inviteBusy[id] = true;
|
|
6850
|
+
inviteSay(id, 'Sending the invitation…', '');
|
|
6851
|
+
fetch(API + '/projects/invite', {
|
|
6852
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
6853
|
+
body: JSON.stringify({ client_id: slug, github_login: login })
|
|
6854
|
+
}).then(function (res) {
|
|
6855
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
6856
|
+
inviteBusy[id] = false;
|
|
6857
|
+
if (res.ok) {
|
|
6858
|
+
if (data && data.invited && data.row) {
|
|
6859
|
+
inviteRows[id] = (inviteRows[id] || []).concat([{ login: login, kind: String(data.row.membership_kind || 'pending') }]);
|
|
6860
|
+
renderInviteRows(id);
|
|
6861
|
+
if (field) { field.value = ''; if (field.focus) field.focus(); }
|
|
6862
|
+
inviteSay(id, login + ' is invited — they accept it from their own projects page.', 'ok');
|
|
6863
|
+
return;
|
|
6864
|
+
}
|
|
6865
|
+
/* 200 { invited: false, alreadyMember: true } — not a failure, and not a
|
|
6866
|
+
pending invite either: they are already on the project */
|
|
6867
|
+
if (field) field.value = '';
|
|
6868
|
+
inviteSay(id, login + ' is already on this project.', 'ok');
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
if (res.status === 401) { inviteSay(id, signInAgain(), 'err', true); return; }
|
|
6872
|
+
var e = apiError(data, res.status);
|
|
6873
|
+
inviteSay(id, inviteRefusal(e.code, res.status, e.message), 'err');
|
|
6874
|
+
});
|
|
6875
|
+
}).catch(function () {
|
|
6876
|
+
inviteBusy[id] = false;
|
|
6877
|
+
inviteSay(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
6878
|
+
});
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
/* skipInvite(id) — the step steps aside having sent NOTHING. Focus moves on
|
|
6882
|
+
rather than being stranded inside the box that just went away. */
|
|
6883
|
+
function skipInvite(id) {
|
|
6884
|
+
var m = INVITE_MOUNTS[id];
|
|
6885
|
+
if (!m || inviteBusy[id]) return;
|
|
6886
|
+
var box = $(id);
|
|
6887
|
+
if (box) box.hidden = true;
|
|
6888
|
+
inviteSay(id, '', '');
|
|
6889
|
+
var after = m.after && $(m.after);
|
|
6890
|
+
if (after && after.focus) { after.setAttribute('tabindex', '-1'); after.focus(); }
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
/* One delegated pair per mount: the buttons name their own mount, so the two
|
|
6894
|
+
surfaces share every line of behaviour above without sharing state. */
|
|
6895
|
+
Object.keys(INVITE_MOUNTS).forEach(function (id) {
|
|
6896
|
+
var box = $(id);
|
|
6897
|
+
if (box) {
|
|
6898
|
+
box.addEventListener('click', function (ev) {
|
|
6899
|
+
var t = ev.target.closest && ev.target.closest('[data-invite-send],[data-invite-skip]');
|
|
6900
|
+
if (!t) return;
|
|
6901
|
+
if (t.hasAttribute('data-invite-skip')) skipInvite(id); else doInvite(id);
|
|
6902
|
+
});
|
|
6903
|
+
}
|
|
6904
|
+
var field = $(INVITE_MOUNTS[id].who);
|
|
6905
|
+
if (field) {
|
|
6906
|
+
field.addEventListener('keydown', function (ev) {
|
|
6907
|
+
if (ev.key !== 'Enter') return;
|
|
6908
|
+
ev.preventDefault();
|
|
6909
|
+
doInvite(id);
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
});
|
|
6913
|
+
|
|
6695
6914
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
6696
6915
|
|
|
6697
6916
|
function renderWho() {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
|
|
23
23
|
"in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
|
|
24
24
|
"in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
|
|
25
|
+
"in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
|
|
25
26
|
"in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
|
|
26
27
|
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
|
|
27
28
|
"in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.540",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.540",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.540",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|